Mimi Framework

Behavior of Parameters in Different Timestep Length Models

I have a model that couples components with varying timestep lengths. If I create a connection between two of the shorter components, the array holding this parameter takes on the length of the longer component (with the timesteps the shorter component doesn’t cover = missing). If I also do a set_param!() within this shorter component for a time-varying parameter, the array holding it takes on the size of the shorter component.

Am I understanding this correctly, and if so is this the desired behavior? It was causing all kinds of errors in my model because I was inadvertently indexing into these components the wrong way and having a bunch of missing values pop up. I attached a working example of what I’m talking about below (In this case the error arises from using [1] to index the first value. I know there are ways around this, but the error could still occur in more complicated indexing cases).

Here’s a toy model where the period 15 temperature value is either calculated or missing depending on how I index.

using Mimi

# An economy component (the longer component).
@defcomp economy begin
	gdp = Variable(index=[time])

	function run_timestep(p, v, d, t)
    	if is_first(t)
    		v.gdp[t] = 10.0
    	else
    		v.gdp[t] = v.gdp[t-1] * 1.02
    	end
    end
end

# A shorter emissions component that switches on in period 15.
@defcomp co2_emissions begin
	gdp	= Parameter(index=[time])
	co2 = Variable(index=[time])

	function run_timestep(p, v, d, t)
		v.co2[t] = (0.1 * p.gdp[t])
    end
end

# A shorter climate component that switches on in period 15.
@defcomp climate begin
    co2           = Parameter(index=[time])
    other_ghg     = Parameter(index=[time])
    temperature   = Variable(index=[time])

	function run_timestep(p, v, d, t)
		if is_first(t)
			v.temperature[t] = (p.co2[1] + p.other_ghg[1]) ^ 2
		else
			v.temperature[t] = (p.co2[t] + p.other_ghg[t]) ^ 1.5
		end
	end
end


# Create the model for 20 time steps.
m = Model()
set_dimension!(m, :time, 20)

# Add components (emissions and climate turn on in period 15).
add_comp!(m, economy)
add_comp!(m, co2_emissions; first=15)
add_comp!(m, climate; first=15)

# Set exogenous gas scenario for periods 15-20.
set_param!(m, :climate, :other_ghg, ones(6))

# Create component connections.
connect_param!(m, :co2_emissions => :gdp, :economy => :gdp)
connect_param!(m, :climate, :co2, :co2_emissions, :co2)

run(m)

In this example, m[:climate, :temperature][15] is a missing value. But the temperature will be calculated If I change the initial temperature calculation to :

v.temperature[t] = (p.co2[15] + p.other_ghg[1]) ^ 2

because the connected co2 parameter ends up in a 20x1 array while the external other_ghg parameter is in a 6x1 array. This can get really confusing with more complicated set ups.

And I should say, I don’t think this is an error with Mimi, but more of an inconsistency with how the variable timestep models behave.

For instance, in the above example this would work:

if is_first(t)
    v.temperature[t] = (p.co2[t] + p.other_ghg[t]) ^ 2
else...

If you checked the values of t using the t.t syntax, it would come back as t=1 in this case. But as I mentioned, trying:

if is_first(t)
    v.temperature[t] = (p.co2[1] + p.other_ghg[1]) ^ 2
else...

results in a missing value because of the different sized parameter arrays. So I got confused because setting something[t] works, and in that case t.t = 1… but setting something[1] will cause an error.

I think the current set up for the different time step models just doesn’t have a consistent, intuitive approach for how to deal with the different couplings. I had to sort this out by messing with these toy models for two days until I was able to figure out what was going on.

Hi Frank,

Sorry you toiled over this for so long! It can be pretty confusing. You’ve correctly figured out though how it’s all currently implemented. A while ago we decided that it was much clearer (from an implementing Mimi standpoint) to have the data storage arrays be the whole length of the model’s time index, as you’ve discovered.

As you’ve also discovered, this makes it very confusing if you use integer indexing, which we are trying to discourage and will be discontinued in version 1.0. So for now, you should use:

v.temperature[t] = (p.co2[t] + p.other_ghg[t]) ^ 2

Because t in this timestep is t.t==1, which means it’s the first timestep for this component, so it will correctly index into the expected “first” position for this component. But since the data are being stored in a longer array, using the integer 1 returns a missing value.

@ckingdon95 Thanks for the helpful reply! And yes, I know integer indexing isn’t robust, but was just an example. I still just find the entire different array size vs. different timestep values within different length components quite confusing.

The immediate example that comes to mind is if I couple a longer climate model to a shorter IAM. There could be a case where I’d want the first temperature value (from 1765) and the first GDP value (from 2010) within the same component. I’m sure there’s a clean Mimi way to set this up, but it isn’t immediately clear.

Probably plenty of design issues I’m not thinking of, but for me it would be much easier if everything was just stored in an array whose length is that of the longest component (with missing values filled in for years without coverage). This would apply to external parameters too. And then just have a universal time step. So in the example above, the climate model would start at t=1, and the IAM would start at t=246.

I think what you’ve just proposed is very similar to something @plevin recently proposed for how we can re-design this functionality to be easier/cleaner. I’ll bring it up in our next meeting and we will let you know what we decide, and we may want to loop you in to a design discussion at some point!

Thanks again for working through this, it’s very helpful to have real life users try this out :slight_smile: and it seems like it can/will be a very important functionality that we want to get right!

@ckingdon95 thanks so much for working through this, that all looks right to me. @FrankErrickson we’ve also added some new types in the developing v0.9.5 version to add more flexible ways to index without using integers, although of course the discussion above is the wider question about different model lengths that we do need to work through.

In terms of indexing, 0.9.5 and beyond has two more types as described in the User Guide and copied below:


Indexing into a variable or parameter’s time dimension with an Integer is deprecated and will soon error. Instead, users should take advantage of the TimestepIndex and TimestepValue types. For examples we will refer back to our component definition above, and repeated below.

@defcomp MyComponentName begin
  regions = Index()

  A = Variable(index = [time])
  B = Variable(index = [time, regions])

  c = Parameter()
  d = Parameter(index = [time])
  e = Parameter(index = [time, regions])
  f = Parameter(index = [regions])

  function run_timestep(p, v, d, t)
    v.A[t] = p.c + p.d[t]
    for r in d.regions
      v.B[t, r] = p.f[r] * p.e[t, r]
    end
  end

end

TimestepIndex has one field, index, which refers to the absolute index in the parameter or variable array’s time dimension. Thus, constructing a TimestepIndex is done by simply writing TimestepIndex(index::Int). Looking back at our original component example
one could modify the first line of run_timestep to always refer to the first timestep of p.d with the following. One may index into the time dimension with a single TimestepIndex, or an Array of them.

v.A[t] = p.c + p.d[TimestepIndex(1)]

TimestepValue has two fields, value and offset, referring to the value within the time dimension and an optional offset from that value. Thus, constructing a TimestepValue is done either by writing TimestepValue(value), with an implied offset of 0, or TimestepValue(value, offset = i::Int), with an explicit offset of i. One may index into the time dimension with a single TimestepValue, or an Array of them. For example, you can use a TimestepValue to keep track of a baseline year.

v.A[t] = p.c + p.d[TimestepValue(2000)]

You may also use shorthand to create arrays of TimestepIndex using Colon syntax.

TimestepIndex(1):TimestepIndex(10) # implicit step size of 1
TimestepIndex(1):2:TimestepIndex(10) # explicit step of type Int 

Both TimestepIndex and TimestepArray have methods to support addition and subtraction of integers. Note that the addition or subtraction is relative to the definition of the time dimension, so while TimestepIndex(1) + 1 == TimestepIndex(2), TimestepValue(2000) + 1 could be equivalent to TimestepValue(2001) if 2001 is the next year in the time dimension, or TimestepValue(2005) if the array has a step size of 5. Hence adding or subtracting is relative to the definition of the time dimension.