How to programmatically generate different functions?

Hi all,

I want to figure out how to generate different version of a function programmatically.
I beleive metaprogramming is what I need but wanted to ask for advice before implementing it.
Apologies if below is a bit confusing and happy to answer any clarifying questions.

Here’s what I want:
I have different versions of a similar function that I fit to data.
I have working code that does the fitting so I don’t need any advice about that.
Each function uses several inputs, produces one output depending on several parameters.

A very simple example can be a function like this:

function test_fun(input1, input2, parameter1, parameter2)
    output = input1/parameter1 + input2/parameter2
    return output
end

The actual function has 5 inputs, 10-25 parameters and produces one output.
I want to be able to generate different versions of this function that contain different number of parameters, make some parameter=1, others =Inf and others =0.
Then I’ll use a loop to generate different version of a function and fit it to data to figure out what is the minimal number of parameters that I need to achieve good fit.

What would be the best approach to do this?

I could be overthinking this but I plan to make a macro that will take a list of symbols of parameters that are undefined (i.e., the parameters that will be modified by fitting algorythm), a list of parameters that =1, a list of parameters that =0 and a list of parameters that =Inf. Then this marco will create a version of the function having undefined parameters as positional arguments and defined parameters as kwargs. Then I’ll use a loop through different lists of parameters to do the function generation + fitting.

Does this sound sensible? If yes, how should I go about writing such a macro?

I have read Julia Manual Metaprogramming and Introducing Julia/Metaprogramming - Wikibooks, open books for an open world and I think I could implement this but want to make sure I’m on the right track as it looks like it’ll take a bunch of hours/maybe days for me to do this as I’m still getting a hang of the metaprograming syntax.

This sounds too complicated.

One idea: make a struct containing all the parameters and make one function that accepts an instance of the struct. Then instantiate the struct many times with different parameter values and pass it into the function.

I don’t really understand what you want to do. But be aware that macros do not have access to any input or parameter values, or even type information. They do pure code transformations.

Perhaps you are looking for anonymous functions?

2 Likes

Can you write a function that takes an array instead of the many parameter?
Then using Combinatorics.jl you can generate the different possible inputs.

Another way is to splat the array to a function which slurps the arguments. That way you could still call the function with individual parameters, but inside the function you could use it as an array again.

The parameters you don’t want to use you could set to missing, 2, nothing, NA, or something appropriate. Depends a bit on the type you want to use.

Thanks for the suggestions @jar1, @DNF, @feanor12!

Sounds like my explanation of what I want is too confusing so let me try again.

I have a function:

function test_fun(input1, input2, parameter1; parameter2=Inf, parameter3=0)
    output = input1/parameter1 + input2/parameter2 + parameter3
    return output
end

I want to make a for loop that will automatically define different virsions of test_fun during each iterations like
test_fun(input1, input2, parameter1, parameter2, parameter3) during 1st iteration, test_fun(input1, input2, parameter1, parameter3; parameter2=Inf) during 2nd iteration, test_fun(input1, input2, parameter1, parameter2; parameter3=0) during 3rd iteration,
test_fun(input1, input2, parameter1; parameter2=Inf, parameter3=0) during 4th iteration.

What would be a way to achieve this?

My rational is that the actual example has 5 inputs and 25 parameters so there are many combinations and I want to automate this procedure.

A more elaborate explanation of what I am doing is below if anyone is curious but I think it is not necessary to know the details below.

Question:
I have data in this form: input1, input2, input3, input4, input5 => output
Specifically, the data describes activity of an enzyme (output) depending on concentrations of metabolites (inputs) but these details presumably don’t matter.
I have several models that describe the activity of this enzyme which basically means I have functions that take inputs and convert it to output using a set of parameters, where parameters are various binding constants.
The simplest model has 6 parameters and the most elaborate has 25 parameters.

Current implementation:
I can estimate parameters by fitting to data.
I have a function like below but with more inputs, more parameters and more complex math:

function test_fun(input1, input2, parameter1, parameter2, parameter3)
    output = input1/parameter1 + input2/parameter2 + parameter3
    return output
end

Each parameter has a specific role so they are not interchangeable.
I calculate loss function of the form:

function test_loss(parameter1, parameter2, parameter3)
    loss = abs2(Data(input1, input2) - test_fun(input1, input2, parameter1, parameter2, parameter3)
    return loss
end

I then estimate parameters using BlackBoxOptim.jl that takes test_loss(parameter1, parameter2, parameter3) and calculates parameter1, parameter2, parameter3 that minimize loss.

In order to remove various parameters from the model I set them to 1, 0 or Inf (there’s one specific default for each specific parameter) so an example of a reduced model without parameter2 will look like this:

function test_fun(input1, input2, parameter1, parameter3; parameter2=Inf)
    output = input1/parameter1 + input2/parameter2 + parameter3
    # input2/parameter2 term will become 0 as parameter2 in set to Inf
    return output
end

What I want to change:

Currently the model with 25 parameters is overfitting and the model with 6 parameters in underfitting and it seems that a model with about 12 parameters is enough to achieve best fit.

I want to write code that will loop through various versions of the model and calculate the fits for each version of the model so I can systematically find the model with least parameters that is sufficient to explain the data.

For example, the first iteration of the loop will use

test_fun(input1, input2, parameter1, parameter2, parameter3)

and calculate parameter1, parameter2, parameter 3 that yield best fit.

Another iteration will use

test_fun(input1, input2, parameter1, parameter3; parameter2=Inf)

and calculate parameter1, parameter 3 that yield best fit given that parameter2=Inf.

Another iteration will use

test_fun(input1, input2, parameter1, parameter2; parameter3=0)

and calculate parameter1, parameter2 that yield best fit given that parameter3=0.

So do you want a function gen_f to return a function that has a custom call pattern using the parameters that should be used in the optimization?

function testfun(in1,in2,p1,p2,p3)
   return something
end

gen_f(1) # return (in1,in2,p2,p3) -> testfun(in1,in2,Inf,p2,p3)
gen_f(2) # return (in1,in2,p1,p3) -> testfun(in1,in2,p1,Inf,p3)
gen_f(3) # return (in1,in2,p1,p2) -> testfun(in1,in2,p1,p2,Inf)

Yes, that sounds right.

Perhaps the following is more exactly what I want:

function testfun(in1,in2,p1,p2,p3)
   return something
end

gen_f(1) # return (in1,in2,p2,p3) -> testfun(in1,in2,p2,p3; p1=Inf)
gen_f(2) # return (in1,in2,p1,p3) -> testfun(in1,in2,p1,p3; p2=Inf)
gen_f(3) # return (in1,in2,p1,p2) -> testfun(in1,in2,p1,p2; p3=Inf)
gen_f(4) # return (in1,in2,p1,p2,p3) -> testfun(in1,in2,p1,p2, p3)
gen_f(4) # return (in1,in2,p1) -> testfun(in1,in2,p1; p2=Inf, p3=Inf)
...

I think calling f(a,b,c) with f(a,c;b=3) does not work. How should the function know the order of the first two arguments?

julia> f(a,b,c) = (a,b,c)
f (generic function with 1 method)

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

julia> f(1,2;b=3)
ERROR: MethodError: no method matching f(::Int64, ::Int64; b=3)
Closest candidates are:
  f(::Any, ::Any, ::type) at REPL[50]:1 got unsupported keyword argument "b"
Stacktrace:
 [1] top-level scope

This can for sure be written in a nicer way.

function testf(a,b,p1,p2,p3)
    @info "call with", (a,b,p1,p2,p3)
end

function genf(i)

    (a,b,p...) ->begin
        p_new = []
        param = collect(p)
        mask = Bool.(digits(i,base=2,pad=3))
        for i in mask
            if i
                push!(p_new,Inf)
            else
                push!(p_new,popfirst!(param))
            end
        end
        testf(a,b,p_new...)
    end

end
julia> for i in 0:7
           genf(i)(1,2,3,4,5)
       end
[ Info: ("call with", (1, 2, 3, 4, 5))
[ Info: ("call with", (1, 2, Inf, 3, 4))
[ Info: ("call with", (1, 2, 3, Inf, 4))
[ Info: ("call with", (1, 2, Inf, Inf, 3))
[ Info: ("call with", (1, 2, 3, 4, Inf))
[ Info: ("call with", (1, 2, Inf, 3, Inf))
[ Info: ("call with", (1, 2, 3, Inf, Inf))
[ Info: ("call with", (1, 2, Inf, Inf, Inf))

julia> genf(1)(1,2,3,4)
[ Info: ("call with", (1, 2, Inf, 3, 4))

julia> genf(3)(1,2,3)
[ Info: ("call with", (1, 2, Inf, Inf, 3))
1 Like

Beautiful, thanks @feanor12! I think this solution should work for me! Thanks for your time and help!