Best practice for storing, passing, and managing parameters in economic models

I am working on a discrete time HANK model in Julia.

The model has a very large number of parameters. Most of these are related to the theoretical structure of the model (e.g., production function parameters, utility function parameters, policy parameters, stochastic process parameters, etc.). Some are related to numerical methods (e.g., numbers of grid points, state space bounds, convergence tolerances, dampening parameters for fixed point iterations, etc.).

The code will make use of many functions, some will only require a small subset of the parameters (e.g., a function for the marginal utility of consumption), others will require all of them as they will call many of the other functions (e.g., a function for computing the steady-state of the model).

I will also write some calibration routines that will alter the parameters to achieve certain calibration targets for the model (e.g., matching some features of the empirical income or wealth distribution for example). These are likely to be iterative (e.g., solving the model, updating parameters, solve again, etc.)

What is the best practice for storing and passing these parameters to functions and why? I have often seen examples where modelers set up a structure to hold all the model parameters and then simply pass the structure to functions. But it seems like named tuples could also work.

The model is large and thus performance will be a top consideration.

Thank you in advance for any insights.

2 Likes

I have been doing more reading on this issue. For anyone else that has the same question, there is a highly relevant thread here:

https://discourse.julialang.org/t/macro-challenge-factory-for-creating-named-tuples/9748

As it turns out, thinking about this issue seems to have led to the development of features in Parameters.jl

For convenience, using Parameters.jl seems to be the way to go, but I’m still not entirely clear on the advantages/disadvantages of structs versus named tuples. Particularly as it relates to calibrating the model where I will want to make numerous changes to the parameter values and possibly use a numerical root-finding algorithm to alter them.

One of the advantages is the possibilities introduced for construction, particularly if you use the Parameters package. You can just set a bunch of parameters as default and only set the other ones each time:

julia> using Parameters

julia> @with_kw struct Options
         n::Int = 1
         x::Float64 = 1.0
       end
Options

julia> opt = Options(n=2)
Options
  n: Int64 2
  x: Float64 1.0

I don’t see how that could be done with named tuples readily. Also, note that all functions that you define in your code can be dispatched to the Options type specifically:

julia> function f(y,opt::Options) 
         @unpack n, x = opt # unpack is from Parameters.jl
         return (x*y)^n
       end
f (generic function with 1 method)

julia> f(1.0,Options(n=3,x=2.0))
8.0

With named tuples that would be more cumbersome, for sure, and the “user” could face many more sources of error by providing the functions with tuples with wrong argument names or types.

In this case, you can use a mutable struct or, more likely, store arrays in the struct, and just calibrate the parameters that are defined in these arrays, that mutable anyway, even if the struct is not mutable (this would be the same for a named tuple containing an array):

julia> using Parameters

julia> @with_kw struct Options
         n::Int = 1
         x::Vector{Float64} = zeros(2)
       end
Options

julia> opt = Options()
Options
  n: Int64 1
  x: Array{Float64}((2,)) [0.0, 0.0]


julia> opt.x[1] = 1.0
1.0

julia> opt
Options
  n: Int64 1
  x: Array{Float64}((2,)) [1.0, 0.0]

3 Likes

I don’t see how that could be done with named tuples readily.

I thought that @with_kw and @unpack worked with named tuples as well. Using a bit of code from the Parameters.jl site and expanding on it:

julia> using Parameters

julia> MyNT = @with_kw (x = 1, y = "foo", z = :(bar))
##NamedTuple_kw#263 (generic function with 2 methods)

julia> tupl1 = MyNT(x=2)
(x = 2, y = "foo", z = :bar)

julia> @unpack x=tupl1
(x = 2, y = "foo", z = :bar)

julia> x
2

In this case, you can use a mutable struct or, more likely, store arrays in the struct, and just calibrate the parameters that are defined in these arrays, that mutable anyway, even if the struct is not mutable (this would be the same for a named tuple containing an array):

I was reading here that immutable types have significant performance advantages and that using Setfield.jl to modify them has performance advantages compared to using mutables. Not sure whether those advantages hold when you store arrays in an immutable struct and then alter those arrays. But that seems like more hoops to jump through than just using Setfield.jl, particularly since many of the elements in the parameters container would be of the form: alpha=0.5, beta=0.9, nk=50 , etc.

Right now I’m leaning a bit towards named tuples because of the fact that they are type inferred and I therefore would not need to explicitly set out the type of each parameter.

I don’t do anything as complicated as a HANK model, but 100% @unpack and @set are your best friends. I have like 3 medium-ish structs that are parametrically typed that I just pass to every function, whether I need it or not. Saves a lot of mental overhead.

Then I just use @unpack as needed. Sure it’s annoying to write @unpack everywhere, but it beats using getproperty a lot, passing each parameter as a separate argument, or globals, or namespace issues.

@set will be fast as long as the types are isbits, which they will be if they are just non-mutable structs full of numbers. Using @set a lot definitely not going to be close to a bottleneck in your code.

I think you are on the right track.

1 Like

If you are not intending to use these for dispatching (which you probably aren’t for this sort of use), then they are roughly equivalent. In both cases, use the tools to define defaults (e.g. the @with_kw for the named tuples) and use @unpack where applicable. With named tuples, it tends to be a little bit easier to to iterate on your design as you can add new things to it as you code and you don’t need to restart the repl as much to compile the new structs, but that is a 2nd order concern.

The most important thing for pararmeters if ensuring that the structures remain immutable and that you properly type any struct. Named tuples will figure out the concrete types for you. Here are a few hints, but the julia docs are more complete: 5. Introduction to Types and Generic Programming — Quantitative Economics with Julia

3 Likes

I was not aware of that syntax. But note that it does not limit you in defining anything as any of the parameters:

julia> MyNT = @with_kw (x = 1, y = "foo", z = :(bar))
##NamedTuple_kw#257 (generic function with 2 methods)

julia> tupl1 = MyNT(x=1)
(x = 1, y = "foo", z = :bar)

julia> tupl1 = MyNT(x="abc")
(x = "abc", y = "foo", z = :bar)

Thus, you cannot really dispatch on that MyNT type, which is not really a type.

Setfield.jl copies the complete structure each time. Note, for instance:

julia> using Setfield

julia> struct A
         x
       end

julia> f(x::A) = @set! x.x = 5
f (generic function with 1 method)

julia> a = A(1)
A(1)

julia> f(a)
A(5)

julia> a
A(1)

Note that a was not mutated really. This can be convenient if your structure does not have many parameters, but for large enough structures it become better to really write mutable structures.

At the same time, immutable structures containing mutable fields is a very common thing. You guarantee with that the the type o the field is constant, and that allows for many compiler optimizations, even if the content of the field changes.

1 Like

With named tuples, it tends to be a little bit easier to to iterate on your design as you can add new things to it as you code and you don’t need to restart the repl as much to compile the new structs

Are you referring to the merge() function?

At the same time, immutable structures containing mutable fields is a very common thing. You guarantee with that the the type o the field is constant, and that allows for many compiler optimizations, even if the content of the field changes.

I find the syntax with that approach is a bit tricky. For example, if I want to have a simple parameter, β=0.5, I need to remember to declare it’s type as an Array and use square brackets when entering the value even though it will always be a scalar:

#this doesn't work
julia> @with_kw struct MParams

                         α::Array{Float64} = 0.5
                         β::Array{Float64} = 0.9
                     end
MParams

julia> p = MParams()

ERROR: MethodError: Cannot `convert` an object of type
  Float64 to an object of type
  Array{Float64, N} where N

#This does
julia> @with_kw struct MParams

                         α::Array{Float64} = [0.5]
                         β::Array{Float64} = [0.9]
                     end
MParams

julia> p = MParams()
MParams
  α: Array{Float64}((1,)) [0.5]
  β: Array{Float64}((1,)) [0.9]

And then when I want to mutate it, I need to remember to use square brackets again:

#this doesn't work
julia> p.α = 0.6
ERROR: MethodError: Cannot `convert` an object of type
  Float64 to an object of type
  Array{Float64, N} where N

#this does
julia> p.α[1] = 0.6
0.6

I suppose if I use that technique enough, it would become habit and I wouldn’t make those errors anymore. At the same time, I’m the early adopter of Julia at my institution and I’d like to reduce the chances of errors for other potential users. Especially since they may at some point need to learn how to do simulations with my model without having to learn a lot about Julia itself.

There is some confusion in this post that could be alleviated by re-reading the manual on Julia’s types.

struct MParams
    α::Array{Float64} = 0.5
    β::Array{Float64} = 0.9
end

is not the appropriate type declaration. I’m not sure what this is intended to be. A vector where every value of .5?

struct MParams
    α::Array{Float64} = 0.5
    β::Array{Float64} = 0.9
end

If you rad the manual, you will see that declaring types in this way will lead to type instabilities. You want to declare the types in the same line as struct. That way Julia can “see” the types of an object at compile time and create more optimized code.

Here is what I would recomment (without @with_kw) for simplicity

julia> struct MParams{T <:AbstractVector{<:Real}}
           α::T
           β::T
       end;

julia> function MParams()
           α = fill(.5, 10)
           β = fill(.9, 10)
           MParams(α, β)
       end;

julia> MParams()
MParams{Vector{Float64}}([0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], [0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9])

EDIT: I re-read your question and I think I have better advice.

There is nothing wrong with a fully-types mutable struct. The following will be faster than having mutable types in a non-mutable struct, as you are currently doing with your one element array trick above.

julia> mutable struct MParamsMutable{T<:Real}
           α::T
           β::T
       end;

julia> function MParamsMutable()
           MParamsMutable(.5, .9)
       end;

julia> m = MParamsMutable()
MParamsMutable{Float64}(0.5, 0.9)

julia> m.α = .2
0.2

julia> m
MParamsMutable{Float64}(0.2, 0.9)

Overall, the distinction between a mutable struct and using @set is really not about speed for small to medium structs.

  1. If you have a persistent object you update across time, like during solving the model, use a mutable struct
  2. If, in the process of solving the model, parameters are constant, then I would use a non-mutable struct and then use @set to analyze comparative statics, making a new object as needed. This will be very cheap for medium-sized structs of numbers, so don’t worry about it.
2 Likes

Probably you meant:

@with_kw struct Params
  x::Vector{Float64} = [0.5]
end

I agree that having one value in an immutable struct that must be mutable lacks a beautiful syntax. I have even asked that here before: Mutable scalar in immutable object: the best alternative?

In the cases I needed that I finally keep using a vector with a single element.

None of the alternatives is completely clean, IMO. To have a clean syntax use a mutable struct,

julia> @with_kw mutable struct Params
         α::Float64 = 1.0
       end
Params

julia> a = Params()
Params
  α: Float64 1.0


julia> a.α = 2.0
2.0

julia> a
Params
  α: Float64 2.0

But that may have its performance penalties.

(now I see that a great part of this was already mentioned before)

1 Like

Yes sorry should have written Array{Float64,1} or better yet, your suggestion Vector{Float64}.

When I fix that, the other potential syntax errors (e.g., forgetting square brackets when creating the struct or the .[1] when mutating the value) still persist and produce the same errors.

My error inadvertently demonstrated my point which is that one has to be more careful with syntax with this approach to avoid errors compared to just using an immutable struct and @set…or even easier, a named tuple.

Still, it’s a technique worthwhile keeping in the quiver. My parameters struct will maybe have about 40-50 elements, mostly scalars or very small arrays (e.g., 2x2 or 4x1). Not sure if that qualifies as big or not…guess I’ll just have to test it both ways (mutable, vs. arrays, vs. @set) to see if there’s a significant performance advantage with one approach over the other.

I think with 40-50 elements that qualifies as big, to the extent that a parametric struct might not be the best solution. Not that I have any concrete examples for where that might fail, only that it seems rare for me to see structs of that size.

I would be interested to hear of other peoples’s solutions to heterogenous containers of this size.

EDIT: One solution might be to have a nested struct, but where you overload methods for UnPack.unpack to access the properties easily, see here.

Using @set is of course one of the options. On the other side, how can tuple help there? (If there is a better syntax using a tuple for that I will be glad to know).

Ps: not sure if you are aware of it, but for the small arrays, use StaticArrays. (These are recommended for sizes of about 100 elements. Probably that size is more or less where using a mutable struct will start to be advantageous relative to an immutable with Setfield).

Yes you are correct. @set alone won’t work - the tuple doesn’t get changed when you call it again. I saw this thread explaining how to make it work with the BangBang package though. For example:

julia>using BangBang

julia> ParamTupl = @with_kw (α = 0.5, β = 0.9)
##NamedTuple_kw#257 (generic function with 2 methods)

julia> p = ParamTupl()
(α = 0.5, β = 0.9)

julia> @set p.β = 0.7
(α = 0.5, β = 0.7)

julia> p
(α = 0.5, β = 0.9)

# now with BangBang
julia> @set!! p.β = 0.7
(α = 0.5, β = 0.7)

julia> p
(α = 0.5, β = 0.7)

The main advantage with named tuples is that you don’t have to specify types to get optimizations to work so in that sense they may be a bit more user friendly. Not sure there are advantages beyond that.

declaring types in this way will lead to type instabilities. You want to declare the types in the same line as struct . That way Julia can “see” the types of an object at compile time and create more optimized code.

I’m new to Julia, so just to clarify, did changing my Array{Float64} to Array{Float64,1} fix the type stability issue you were referring to? I don’t think what I wanted to do was declare a parametric type…just a composite type I think.

The Julia manual seems to use similar syntax in the section on composite types.

Regarding the use-case. The parameters container, whatever that may be, will need to mutated infrequently - for example only when developing the model or making significant changes. The process of calibrating the model involves running the model many times (e.g., via some sort of numerical solver) while making adjustments to the parameters. Each run of the model would involve lots of calls to the parameters container without doing any mutations.

More typically, I will be just running the calibrated model to illustrate the effects of various shocks (e.g., monetary policy shocks, government spending shocks, tax policy changes, etc.)

So I think this probably argues for using an immutable container.

So I think this probably argues for using an immutable container.

I agree, an immutable container is the right choice.

I am pretty sure that Array{Float64} is not good because you aren’t specifying the dimension of the array. But I could be wrong about this and hope someone else chimes in.

struct MyType
    a::Vector{Float64} # alias for Array{Float64, 1}
end

will be perfectly performant.

But this will be limiting. When you optimize your code further, you may want to use Static Vectors from StaticArrays.jl. You can’t go back and forth between Static Arrays and Base Arrays when the type is declared like that. But you can if you do

julia> struct B{T <: AbstractVector{<:Real}}
           b::T
       end;

This is all pretty complicated, and you expressly said you didn’t want to get in the weeds with Julia’s type system. So I think the answer is

Yes, use something along the lines of

struct A
    a::Array{Float64, 1}
    b::...
    c::...
end

but make sure your declarations are fully specified, ie. don’t just write Array{Float64}. That will hurt performance.

If you feel like you will want more flexibility, then don’t just follow the syntax in the Composite types section, but also the Parametric types section.

1 Like

I think named tuples usually the better place to start then. You are less likely to make mistakes in setting the wrong parametric types (as you saw above) which could have catastrophic effects on performance. It isn’t worth the risk.

It will also be faster for you to code and design since you can just recreate and change your named tuples as you are playing around with it, while structures can’t be redefined so easily.

4 Likes

Yes. But it is also useful to leave things more generic, as the performance differences and design flexibility can be huge.

There are some hints here:

But the summary is that you want to leave everything as generic as possible, without specifying any more specific types than you need. That gives you maximum flexibility in the future.

For parameters, etc the easiest way to do that before you really know the details of Julia types is just to use named tuples. They are completely generic and the compiler figures the types out for you.

2 Likes

Interesting point of view. I think the struct will be useful, with a more restrictive type information, to avoid user errors. While you are exploring the parameters from a development perspective, certainly the tuples will be more convenient.