Handling solver parameters elegantly in Julia

question

#1

Consider a simple solver type:

using Parameters: @with_kw

@with_kw struct Param
  mean     = 0.
  variance = 1.
end

struct Solver
  params::Dict{Symbol,Param}

  function Solver(params...)
    new(Dict(params...))
  end
end

Based on this interface, the user can construct solvers like the following:

Solver(:var1=>Param(mean=2.), :var2=>Param(variance=3.))

The idea is that for each variable of the problem one can specify a set of parameters that will trigger a different method. The code is working fine, but I guess there is a cleaner way of achieving the same result without this ugly Param type in front of the parameters?

Solver(:var1=>(mean=2.), :var2=>(variance=3.), :var3=>(mean=1.,var=2.))

Do you have suggestions on how to refactor the code to make it look nicer? The actual implementation can be found here: https://github.com/juliohm/GeoStats.jl/blob/master/src/kriging_solver.jl

More generally, is there any effort in a package to generalize this process? Are macros useful here?


#2

Something along the lines of GetPot for C++ would be interesting. Reading algorithm parameters from files or dealing with them in the code directly.


#3

@mauro3 since you are the Parameters master :slight_smile: do you have suggestions on how to improve the interface?


#4

You could use NamedTuples.jl to get something closer to your “cleaner” option. When named tuples come to Julia 0.7, the syntax will be even nicer.

You could also use a TOML string/file for this type of thing. That’s more like your GetPot option. (It’s also coming to more use in Julia as part of Pkg3.)


#5

Thank you @tshort NamedTuples.jl seem like a very good fit. From what I understood, the interface would look something like:

Solver(:var1 => @NT(mean=1.), :var2 => @NT(variance=3.))

What is the syntax for Julia v0.7 that you mentioned?


#6

I tried to set some default solver parameters an ran into this issue:

Will wait until Julia v0.7 is out.


#7

Haha! But no, I have no answers only a few comments:

The deeper you nest your options, the more organized they are but also the more painful to deal with.

With scripting languages there is rarely a need to use separate configuration files: just use a script instead, saves you the hassle to deal with another package and the user the hassle of learning/context-switching to another mini-language.

Have a look at how DiffEq handles options, mostly through kwargs I think.


#8

I’d use keyword args for Solver. With 0.7 syntax, that’d be:

Solver(var1 = (mean=1.), var2 = (variance=3.))

You may need an extra comma in there.

https://github.com/JuliaLang/julia/pull/22194

You might also define a shorter name for Param. You can do this now:

Solver(var1 = P(mean=1.), var2 = P(variance=3.))

#9

Keyword args to the solver. Keyword arguments currently have an overhead, but that should get fixed soon enough so I wouldn’t worry about it. The cost isn’t too high though, so if it’s just in the setup phase you won’t even notice it. If you do it via splatting positional args and then assuming the args are in order, you can at least make it like

Solver(Params(mean=2.), Params(variance=3.), Params(mean=1.,var=2.))

which is a little nicer.


#10

I thought of using positional arguments and assume order, but the interface looks more obscure. I like explicit pairs var1=>param1, var2=>…, but I may change this point of view in the future.


#11

@tshort, I am back to this issue after a while. I am giving NamedTuple a try, but couldn’t make the outer constructor below work:

using Parameters: @with_kw

@with_kw struct Param
 mean     = 0.
 variance = 1.
end

struct Solver
 params::Dict{Symbol,Param}

 # inner ctor to be called from outside
 Solver(params::Dict{Symbol,Param}) = new(params)
end

# outer ctor with named tuples
function Solver(params...)
 dict = Dict{Symbol,Param}()
 for (varname, varparams) in params
   push!(dict, varname => Param(varparams...)) # doesn't work
 end

 Kriging(dict)
end

Could you please give a hand? The goal is to have the Solver working with named tuples as you described:

Solver(:var1 => @NT(mean=1), :var2 => @NT(variance=2.))

How to loop over the key/value pairs of a NamedTuple?
#12

Could you more explicitly say why normal keywords arguments don’t work here.


#13

Hi @kristoffer.carlsson, it is because I want to give the user a very explicit API. I have an example of it in the README: https://github.com/juliohm/GeoStats.jl#examples

If the user loads a spreadsheet with column names, I want him to be explicit about the variables by name, not by position.

In that example, the user would be able to call the solver with:

Kriging(:precipitation => @NT(mean=1., variogram=GaussianVariogram()))

With more variables one would list them explicitly, no matter the order they were defined in the problem:

Kriging(
  :precipitation => @NT(mean=1.),
  :co2_concentration => @NT(variogram=GaussianVariogram()),
...
)

This would become even more clear in Julia v0.7 without the @NT boilerplate.


#14

Keyword arguments are by name and not position so not sure what you mean here.

If you need to give many key-values for each variable then you could just use a Dict for each variable?


#15

The problem is that I have many variables in the problem. So if I say mean=1, what variable it refers to? That is why keyword arguments don’t solve the problem. I need to have pairs variable_name => (mean=1) to disambiguate instead of assuming that the user will remember the order of the variables when he defined the EstimationProblem.

To be more precise:

problem = EstimationProblem(data, domain, [:var1,:var2])
solve(problem, Kriging(:var1 => (params...), :var2 => (params...))

#16

You could just use a dictionary instead of a named tuple. If you really want to use named tuples you could convert the named tuple to a dictionary inside the Solver function:

julia> a = @NT(mean = 0.0, variance = 0.0)
(mean = 0.0, variance = 0.0)

julia> d = Dict(k=>v for (k,v) in zip(fieldnames(typeof(a)), a))
Dict{Symbol,Float64} with 2 entries:
  :mean     => 0.0
  :variance => 0.0

julia> Param(d...)

Or @kwdef could, in addition, generate a constructor for a named tuple.


#17

That is exactly my question, how to write an outer constructor that converts named tuples to dictionaries. I have tried it in my previous comment, but failed. I will try with your code snippet now.


#18

@kristoffer.carlsson this is my second attempt, now with your code snippet, it didn’t work unfortunately:

using Parameters: @with_kw
using NamedTuples

@with_kw struct Param
 mean     = 0.
 variance = 1.
end

struct Solver
 params::Dict{Symbol,Param}

 # inner ctor to be called from outside
 Solver(params::Dict{Symbol,Param}) = new(params)
end

# outer ctor with named tuples
function Solver(params...)
 # build Dict for inner ctor
 dict = Dict{Symbol,Param}()

 for (varname, varparams) in params
   # varparams is a NamedTuple
   d = Dict(k => v for (k,v) in zip(fieldnames(typeof(varparams)), varparams))

   push!(dict, varname => Param(d...))
 end

 Solver(dict) # call inner ctor
end

The conversion from NamedTuple to Dict works, but what I really need is to pass the NamedTuple entries as keyword arguments to the Param type, which takes care of all the checks like valid parameter names, types, etc.


#19

Posting an MWE (which includes the way you want to call this function) would increase the probability that you get help on this.

I am not sure why you need an inner constructor for Solver, the default one would do the same as above.


#20

@Tamas_Papp, sorry, I thought the MWE was clear from my previous comments. I am trying to have a solver type that I can create with:

solver = Solver(
  :var1 => @NT(mean=1.),
  :var2 => @NT(variogram=GaussianVariogram())
)

Each pair in this list would be converted to a pair of Symbol and Param, where Param is defined above. The Param type has keyword constructors from the Parameters.jl package. This way I am enforcing meaningful parameters for each variable even though the user types arbitrary NamedTuples like @NT(not_valid=2.).

My issue is in defining the outer constructor that does this conversion Dict{Symbol,NamedTuple} to Dict{Symbol,Param}. This outer constructor is vararg.

Please let me know if is still not clear, all the code from my previous comment is correct, except for the outer constructor, which doesn’t compile.

The reason I am doing this now is because I hope that in Julia v0.7, the @NT will go away and then the syntax will be super clean:

Solver(
  :var1 => (mean=1.),
  :var2 => (variogram=GaussianVariogram())
)