Creating Parameters.jl struct samples using variable keywords

Hi, I’m trying to make a function that takes in a default struct which holds parameters for my model. I want to be able to make a sample list where some properties are kept constant, and some are sampled using a sampling algorithm. The properties/keywords I would like to change are defined in a vector of strings.

There is, as far as I know no simple way to input a string as key-word argument, so I used the Meta.parse and eval() function to convert a string to an expression, which when evaluated gives me the new object with the sampled parameters. I already felt that this is probably not ‘conventional’, but it worked.

A example can be seen below:

function create_samplelist(default::ParameterContainer, sample_params::Vector{String}, upper_bounds, lower_bounds; n_samples::Integer = 10)
    samples = typeof(default)[]
    # Make sure the lengths of parameters are all the same
    @assert length(sample_params) == length(upper_bounds) == length(lower_bounds)
    
    A = QuasiMonteCarlo.sample(n_samples, lower_bounds, upper_bounds, QuasiMonteCarlo.LatinHypercubeSample())

    for col in eachcol(A)
        #Build the expression string
        string = "$(typeof(default))(default, "
        for i in eachindex(col)
            string = string*"$(sample_params[i]) = $(col[i]),"
        end
        string = string*")"

        #Parse the string into an expression and add the evaluated expression to the sample list
        expr = Meta.parse(string)
        push!(samples, eval(expr))
    end

    return samples
end

# Define the variant, sample parameters to sample, and their upper&lower bounds
default = GridCell()
sample_params = ["growth_rate", "death_rate"]
upper_bounds = [0.1, 0.01]
lower_bounds = [0.0, 0.0]

# Create a sample list of GridCell to simulate for data fitting.
variant_samples = create_samplelist(default, sample_params, upper_bounds, lower_bounds)

This example works, until I put this function in a Module. Then I realized that eval() evaluates in the global scope, so it was using the default variable in the global scope the whole time instead of the one passed in the function (which is undefined in my Module, giving me an error)

I can always move the function back into the global scope to fix this problem, but I was wondering: Is there a better way, more conventional to do this?

I can’t quite follow all of this, as I’m not familiar with the packages you are using, but this is almost certainly, as you suspected, not a good way to this. If I understand you correctly, you would like to make something like the following call:

typeof(default)(default; growth_rate = A[j,1], death_rate = A[j,2])

or something similar to it, but you have growth_rate and death_rate as strings. If you instead declare

sample_params = [:growth_rate, :death_rate]

you could inside your function do something like

#[...]
for col in eachcol(A)
      nt = (; (sample_params[i] => col[i] for i in eachindex(col))...)
      res = typeof(default)(default; nt...)
      push!(samples, res)
end
#[...]

Here, nt is a NamedTuple, which can be splatted (using ...) into the keyword-argument slot of a function following a semicolon in the function call. If you need to use strings as inputs you can convert string to symbols using, e.g.,

sym_params = Symbol.(["growth_rate", "death_rate"])

I can’t check that the above works, but something along these lines should be better than what you’ve got.

Hopefully someone more familiar with the packages you’re using can come along and suggest an overall better design for your purposes.

Thank you so much, it works! Parameters.jl as far as I know works using named tuples, so this works perfectly!

Is there a specific reason why you define properties by a vector of strings?
IMO a clean solution is:

using AccessorsExtra

default = GridCell()
sample_params = [(@o _.growth_rate), (@o _.death_rate)]
params_combined = concat(sample_params...)

samples = map(eachcol(A)) do col
    setall(default, params_combined, col)
end

As a bonus, you can use lots of stuff in sample_params, not just plain properties. For example, all of @o log(_.growth_rate), @o _.optimizer.rate, @o _.death_rates[1] would work.

There was no specific reason, it was just convenient at the time. I will probably use a vector of symbols from now on, or something similar, as suggested by JonasWickman.

Thank you for your code suggestion, but I’m not familiar with the concept of optics. I will consider it if I ever need more functionality.