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

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.

Makes sense. I agree.

Thanks for all the insights everyone. Learned a lot from this.

I wonder if the development of a Julia equivalent of pypet shouldn’t be considered What is pypet all about? — pypet 0.5.0 documentation