Looping over structure elements by their names

I am wondering how to loop over the elements of a structure by their names?

Big-picture, I have a structure which contains the parameters of an economic model, some of which are functions of other parameters. I would like to see how the model output changes when I vary a subset of parameters one at a time. Importantly, I want to make sure the parameter dependencies remain. For instance, in the MWE below, I’d like to have c and d also update when I update a and b. I believe this precludes me using something like SetField. I think it’d be easy to do this if I put the parameters in an array, but I’d like to be able to access them by name.

Concretely, I’m trying to see how particular simulated moments change and how the difference between the data moments and simulated moments changes.I haven’t figured out a way to do this which doesn’t involve copying the same block of code for each parameter, which is roughly what I’m doing in the testfunc() example below.

What I’d like is to have something like testfunc2(), which errors out because I cannot get the iterator to be recognized as a valid input to the myparams() structure. Per some previous threads in the discord I’ve tried experimenting with using metaprogramming in testfunc3(). This works in the REPL, but not otherwise, and I’m not sure if something like this is overkill for this type of problem.

@with_kw struct myparams
    a::Float64 = 0.03
    b::Float64 = 0.95
    c::Float64 = 1 / (1 + a)
    d::Float64 = a * b
end

function myobjfunc(p::myparams)
    @unpack a, b, c, d = p
    return (a + b) * (c + d)
end


function testfunc()

    p0 = myparams()
    steps = collect(0.5:0.1:1.5)
    vals = Array{Float64}(undef, 2, length(steps))

    for i = eachindex(steps)
        p = myparams(a=p0.a * steps[i])
        vals[1, i] = myobjfunc(p)

        p = myparams(b=p0.b * steps[i])
        vals[2, i] = myobjfunc(p)
    end

    return vals
end



function testfunc2()

    p0 = myparams()
    steps = collect(0.5:0.1:1.5)
    vals = Array{Float64}(undef, 2, length(steps))
    iter = 0
    for symb in (:a, :b)
        iter += 1
        for i = eachindex(steps)
            p = myparams(symb=p0.symb * steps[i])
            vals[iter, i] = myobjfunc(p)
        end
    end

    return vals
end



function testfunc3()

    p0 = myparams()
    steps = collect(0.5:0.1:1.5)
    vals = Array{Float64}(undef, 2, length(steps))
    iter = 0
    for symb in (:a, :b)
        iter += 1
        for i = eachindex(steps)
            ex = :(myparams($symb=p0.$symb * steps[i]))
            p = eval(ex)
            vals[iter, i] = myobjfunc(p)
        end
    end

    return vals
end

testfunc()
2×11 Matrix{Float64}:
 0.96449   0.967437  0.9704    0.973379  0.976375  0.979386  0.982414  0.985459  0.988519  0.991596  0.994689
 0.497488  0.592784  0.688623  0.785002  0.881924  0.979386  1.07739   1.17594   1.27502   1.37465   1.47482

testfunc2()
ERROR: type myparams has no field symb

testfunc3()
ERROR: UndefVarError: p0 not defined

Any advice would be greatly appreciated! This seems it should be simple to implement, so I’ve been a bit frustrated by how it has stumped me…

I’m not sure if I understand what you want, but have you thought about overloading getindex for your type?

You can use Setfield.setproperties

julia> using Setfield, Parameters

julia> @with_kw struct myparams
           a::Float64 = 0.03
           b::Float64 = 0.95
           c::Float64 = 1 / (1 + a)
           d::Float64 = a * b
       end
myparams

julia> m = myparams()
myparams
  a: Float64 0.03
  b: Float64 0.95
  c: Float64 0.970873786407767
  d: Float64 0.028499999999999998


julia> symb = :c
:c

julia> setproperties(m, NamedTuple{(symb,)}(1))
myparams
  a: Float64 0.03
  b: Float64 0.95
  c: Float64 1.0
  d: Float64 0.028499999999999998

There might be a more elegant solution, though.

  • Maybe a NamedTuple is a good type for you to use.
params(;a = 0.03, b = 0.95, c = 1/(1+a), d = a * b) = (;a, b, c, d) 

You can use it like this

p = params(a=1.0)
p = params(a=1.0; c = 2.0) # if you want to overwrite c with another value
p = params(a=1.0, b=2.0) 

To access the values, you can use

@unpack a, b, c, d = p
# or 
x = p.c

And code like this should be fairly efficient, since Julia can optimize it well

function myobjfunc(p)
    @unpack a, b, c, d = p
    return (a + b) * (c + d)
end

vals_a = myobjfunc.( ( params(a=a) for a in 0.5:0.1:1.5 ) )
vals_b = myobjfunc.( ( params(b=b) for b in 0.5:0.1:1.5 ) )
vals_ab = myobjfunc.( ( params(; sym => x ) for sym in (:a,:b), x in 0.5:0.1:1.5 ) )

EDIT: Used nicer syntax to create named tuple, see Essentials · The Julia Language

1 Like

I think the issue with this is that it won’t update the parameters that are functions of the parameter being updated. For instance, if I change :a I want :c to change as well.


julia> @with_kw struct myparams
                  a::Float64 = 0.03
                  b::Float64 = 0.95
                  c::Float64 = 1 / (1 + a)
                  d::Float64 = a * b
              end
myparams

julia> myparams
myparams

julia> m = myparams()
myparams
  a: Float64 0.03
  b: Float64 0.95
  c: Float64 0.970873786407767
  d: Float64 0.028499999999999998


julia> symb = :a
:a

julia> setproperties(m, NamedTuple{(symb,)}(1))
myparams
  a: Float64 1.0
  b: Float64 0.95
  c: Float64 0.970873786407767
  d: Float64 0.028499999999999998

c is the same as before here, but I’d want it to be 1/(1+a) = 1/2.

This definitely seems promising! To be clear, I think what I was looking for was a way to iterate over symbols in the struct I created, as you do here:

vals_ab = myobjfunc.( ( params(; NamedTuple{(sym,)}((x,))... ) for sym in (:a,:b), x in 0.5:0.1:1.5 ) )

In non-MWE-world, I hadn’t been sure whether using a struct to hold the parameter values was necessary for this problem, but also hadn’t seen a reason to switch to using NamedTuples. This certainly seems like enough of a reason to switch.

Actually, the same code probably works with params replaced by your type myparams :smiley:

vals_ab = myobjfunc.( ( myparams(; sym =>x ) for sym in (:a,:b), x in 0.5:0.1:1.5 ) )

Actually, I’m realizing that this doesn’t do quite what I want. The purpose of the iterator from .5 to 1.5 is to scale the original parameter guess from 50% of it’s original value to 150% of the original value, so I’d need something like

vals_ab = myobjfunc.( ( myparams(; sym =>x*p0[sym] ) for sym in (:a,:b), x in 0.5:0.1:1.5 ) )

but then I’m back to the same original problem. Sorry if I’m being dense…

Mh, I think it is not completely clear what exactly the goal is. And in the end the solution should be not too complicated as well.

This for example would create a 10 by 10 matrix with all the values a and b scaled between 50% (helf) and 200% (double):

p_def = myparams()
a_factors = p_def.a * 2 .^ LinRange(-1,1,10)
b_factors = p_def.b * 2 .^ LinRange(-1,1,10)

vals_ab = myobjfunc.( myparams(; a, b ) for a in a_factors, b in b_factors )

Of course, if you really want to scale in one line, you could also define

function scale_params(p_def = myparams(); factors...)
    scaled_params = [sym => p_def[sym] * s for (sym, s) in factors]
    return myparams(; scaled_params... )
end

and then use

vals_ab = myobjfunc.( scale_params(; a, b ) for a in 2 .^ LinRange(-1,1,10), b in 2 .^ LinRange(-1,1,10) )

Would the idea here to be to associate each parameter with an integer value? Something like

Base.getindex(p::myparams, i::Int) = p.a*(i == 1) + p.b*(i == 2) + p.c*(i == 3) + p.d*(i==4)

then something like

function testfunc4()

    p0 = myparams()
    steps = collect(0.5:0.1:1.5)
    vals = Array{Float64}(undef, 2, length(steps))

    iter = 0
    for sym in (:a, :b)
        iter += 1
        for i in eachindex(steps)
            p = myparams(; sym => p0[iter] * steps[i])
            vals[iter, i] = myobjfunc(p)
        end
    end

    return vals
end

using some of the syntax from @SteffenPL would work? This gives the right answer but seems pretty inelegant, which I’d take at this point. I suppose I could create a tuple that associates the symbols with their integer value, which eliminates the need for updating the “iter”.

Not sure if I’m reading this problem really wrong, but fieldnames(myparams) gets you (:a, :b, :c, :d) if you need to iterate over the field names. Core.getfield can accept either integers or symbols to get to the fields:

julia> getfield.(Ref(myparams()), Tuple(1:4))
(0.03, 0.95, 0.970873786407767, 0.028499999999999998)

julia> fieldnames(myparams)
(:a, :b, :c, :d)

julia> getfield.(Ref(myparams()), fieldnames(myparams))
(0.03, 0.95, 0.970873786407767, 0.028499999999999998)

The Ref is to wrap the instance in a 1-element iterable to act as a scalar, myparams() isn’t iterable so broadcasting would’ve just failed.

4 Likes

@SteffenPL
I definitely agree that the solution should not be too complicated :slight_smile:
The big-picture goal is to see how sensitive the model outputs are to their inputs, and how smooth that relationship is. In non-MWE world I’ve got ~20 parameters which end up affecting behavior in a simulated economy. I want to see, for instance, how the employment rate in the simulated economy changes as I vary the parameters one at a time over some range. I’d want to do this for a pretty large number of model outputs, ~60. The main goal of posting was to figure out how to get the correct syntax so this could scale up without copy-pasting a bunch :slight_smile:

@Benny I think this, in combination with one of @SteffenPL’s earlier answers, gets me what I want, although the code could probably be written more compactly.

function testfunc()

    p0 = myparams()
    steps = collect(0.5:0.1:1.5)
    vals = Array{Float64}(undef, 2, length(steps))

    iter = 0
    for sym in (:a, :b)
        iter += 1
        for i in eachindex(steps)
            p = myparams(; sym => getfield.(Ref(p0), sym) * steps[i])
            vals[iter, i] = myobjfunc(p)
        end
    end

    return vals
end

Thanks everyone, this was very helpful!

:+1:
By the way, in your example getfield(p0,sym) works as well. In the post from Benny the Ref was just needed to get multiple fields at the same time :wink:

2 Likes