Tutorial 4: Create a Model
This tutorial walks through the steps to create a new model, first a one-region model and then a more complex multi-region model.
While we will walk through the code step by step below, the full code for implementation is also available in the examples/tutorial
folder in the Mimi github repository.
Working through the following tutorial will require:
- Julia v1.4.0 or higher
- Mimi v0.10.0 or higher
If you have not yet prepared these, go back to the first tutorial to set up your system.
Constructing A One-Region Model
In this example, we construct a stylized model of the global economy and its changing greenhouse gas emission levels through time. The overall strategy involves creating components for the economy and emissions separately, and then defining a model where the two components are coupled together.
There are two main steps to creating a component, both within the @defcomp
macro which defines a component:
- List the parameters and variables.
- Use the
run_timestep
functionrun_timestep(p, v, d, t)
to set the equations of that component.
Starting with the economy component, each variable and parameter is listed. If either variables or parameters have a time-dimension, that must be set with (index=[time])
.
Next, the run_timestep
function must be defined along with the various equations of the grosseconomy
component. In this step, the variables and parameters are linked to this component and must be identified as either a variable or a parameter in each equation. For this example, v
will refer to variables while p
refers to parameters.
It is important to note that t
below is an AbstractTimestep
, and the specific API for using this argument are described in detail in the how to guide How-to Guide 4: Work with Timesteps.
using Mimi # start by importing the Mimi package to your space
@defcomp grosseconomy begin
YGROSS = Variable(index=[time]) # Gross output
K = Variable(index=[time]) # Capital
l = Parameter(index=[time]) # Labor
tfp = Parameter(index=[time]) # Total factor productivity
s = Parameter(index=[time]) # Savings rate
depk = Parameter() # Depreciation rate on capital - Note that it has no time index
k0 = Parameter() # Initial level of capital
share = Parameter() # Capital share
function run_timestep(p, v, d, t)
# Define an equation for K
if is_first(t)
# Note the use of v. and p. to distinguish between variables and
# parameters
v.K[t] = p.k0
else
v.K[t] = (1 - p.depk)^5 * v.K[t-1] + v.YGROSS[t-1] * p.s[t-1] * 5
end
# Define an equation for YGROSS
v.YGROSS[t] = p.tfp[t] * v.K[t]^p.share * p.l[t]^(1-p.share)
end
end
Next, the component for greenhouse gas emissions must be created. Although the steps are the same as for the grosseconomy
component, there is one minor difference. While YGROSS
was a variable in the grosseconomy
component, it now enters the emissions
component as a parameter. This will be true for any variable that becomes a parameter for another component in the model.
@defcomp emissions begin
E = Variable(index=[time]) # Total greenhouse gas emissions
sigma = Parameter(index=[time]) # Emissions output ratio
YGROSS = Parameter(index=[time]) # Gross output - Note that YGROSS is now a parameter
function run_timestep(p, v, d, t)
# Define an equation for E
v.E[t] = p.YGROSS[t] * p.sigma[t] # Note the p. in front of YGROSS
end
end
We can now use Mimi to construct a model that binds the grosseconomy
and emissions
components together in order to solve for the emissions level of the global economy over time. In this example, we will run the model for twenty periods with a timestep of five years between each period.
- Once the model is defined,
set_dimension!
is used to set the length and interval of the time step. - We then use
add_comp!
to incorporate each component that we previously created into the model. It is important to note that the order in which the components are listed here matters. The model will run through each equation of the first component before moving onto the second component. One can also use the optionalfirst
andlast
keyword arguments to indicate a subset of the model's time dimension when the component should start and end. - Next,
update_param!
is used to assign values each component parameter with an external connection to an unshared model parameter. If population was a parameter for two different components, it must be assigned to each one usingupdate_param!
two different times. The syntax isupdate_param!(model_name, :component_name, :parameter_name, value)
. Alternatively if these parameters are always meant to use the same value, one could useadd_shared_param!
to create a shared model parameter and add it to the model, and then useconnect_param!
to connect both. This syntax would useadd_shared_param!(model_name, :model_param_name, value)
followed byconnect_param!(model_name, :component_name, :parameter_name, :model_param_name)
twice, once for each component. - If any variables of one component are parameters for another,
connect_param!
is used to couple the two components together. In this example, YGROSS is a variable in thegrosseconomy
component and a parameter in theemissions
component. The syntax isconnect_param!(model_name, :component_name_parameter, :parameter_name, :component_name_variable, :variable_name)
, where:component_name_variable
refers to the component where your parameter was initially calculated as a variable. - Finally, the model can be run using the command
run(model_name)
. - To access model results, use
model_name[:component, :variable_name]
. - To observe model results in a graphical form ,
explore
as eitherexplore(model_name)
to open the UI window, or useMimi.plot(model_name, :component_name, :variable_name)
orMimi.plot(model_name, :component_name, :parameter_name)
to plot a specific parameter or variable.
using Mimi
function construct_model()
m = Model()
set_dimension!(m, :time, collect(2015:5:2110))
# Order matters here. If the emissions component were defined first, the model would not run.
add_comp!(m, grosseconomy)
add_comp!(m, emissions)
# Update parameters for the grosseconomy component
update_param!(m, :grosseconomy, :l, [(1. + 0.015)^t *6404 for t in 1:20])
update_param!(m, :grosseconomy, :tfp, [(1 + 0.065)^t * 3.57 for t in 1:20])
update_param!(m, :grosseconomy, :s, ones(20).* 0.22)
update_param!(m, :grosseconomy, :depk, 0.1)
update_param!(m, :grosseconomy, :k0, 130.)
update_param!(m, :grosseconomy, :share, 0.3)
# Update parameters for the emissions component
update_param!(m, :emissions, :sigma, [(1. - 0.05)^t *0.58 for t in 1:20])
# connect parameters for the emissions component
connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS)
return m
end #end function
Note that as an alternative to using many of the update_param!
calls above, one may use the default
keyword argument in @defcomp
when first defining a Variable
or Parameter
, as shown in examples/tutorial/01-one-region-model/one-region-model-defaults.jl
.
Now we can run the model and examine the results:
# Run model
m = construct_model()
run(m)
# Check model results
getdataframe(m, :emissions, :E) # or m[:emissions, :E_Global] to return just the Array
Finally we can visualize the results via plotting and explorer:
# Plot model results
Mimi.plot(m, :emissions, :E);
# Observe all model result graphs in UI
explore(m)
Constructing A Multi-Region Model
We can now modify our two-component model of the globe to include multiple regional economies. Global greenhouse gas emissions will now be the sum of regional emissions. The modeling approach is the same, with a few minor adjustments:
- When using
@defcomp
, a regions index must be specified. In addition, for variables that have a regional index it is necessary to include(index=[regions])
. This can be combined with the time index as well,(index=[time, regions])
. - In the
run_timestep
function, unlike the time dimension, regions must be specified and looped through in any equations that contain a regional variable or parameter. set_dimension!
must be used to specify your regions in the same way that it is used to specify your timestep.- When using
update_param!
for values with a time and regional dimension, an array is used. Each row corresponds to a time step, while each column corresponds to a separate region. For regional values with no timestep, a vector can be used. It is often easier to create an array of parameter values before model construction. This way, the parameter name can be entered intoupdate_param!
rather than an entire equation. - When constructing regionalized models with multiple components, it is often easier to save each component as a separate file and to then write a function that constructs the model. When this is done,
using Mimi
must be speficied for each component. This approach will be used here.
To create a three-regional model, we will again start by constructing the grosseconomy and emissions components, making adjustments for the regional index as needed. Each component should be saved as a separate file.
As this model is also more complex and spread across several files, we will also take this as a chance to introduce the custom of using Modules to package Mimi models, as shown below.
using Mimi
@defcomp grosseconomy begin
regions = Index() #Note that a regional index is defined here
YGROSS = Variable(index=[time, regions]) #Gross output
K = Variable(index=[time, regions]) #Capital
l = Parameter(index=[time, regions]) #Labor
tfp = Parameter(index=[time, regions]) #Total factor productivity
s = Parameter(index=[time, regions]) #Savings rate
depk = Parameter(index=[regions]) #Depreciation rate on capital - Note that it only has a region index
k0 = Parameter(index=[regions]) #Initial level of capital
share = Parameter() #Capital share
function run_timestep(p, v, d, t)
# Note that the regional dimension is defined in d and parameters and variables are indexed by 'r'
# Define an equation for K
for r in d.regions
if is_first(t)
v.K[t,r] = p.k0[r]
else
v.K[t,r] = (1 - p.depk[r])^5 * v.K[t-1,r] + v.YGROSS[t-1,r] * p.s[t-1,r] * 5
end
end
# Define an equation for YGROSS
for r in d.regions
v.YGROSS[t,r] = p.tfp[t,r] * v.K[t,r]^p.share * p.l[t,r]^(1-p.share)
end
end
end
Save this component as gross_economy.jl
using Mimi #Make sure to call Mimi again
@defcomp emissions begin
regions = Index() # The regions index must be specified for each component
E = Variable(index=[time, regions]) # Total greenhouse gas emissions
E_Global = Variable(index=[time]) # Global emissions (sum of regional emissions)
sigma = Parameter(index=[time, regions]) # Emissions output ratio
YGROSS = Parameter(index=[time, regions]) # Gross output - Note that YGROSS is now a parameter
# function init(p, v, d)
# end
function run_timestep(p, v, d, t)
# Define an equation for E
for r in d.regions
v.E[t,r] = p.YGROSS[t,r] * p.sigma[t,r]
end
# Define an equation for E_Global
for r in d.regions
v.E_Global[t] = sum(v.E[t,:])
end
end
end
Save this component as emissions.jl
Let's create a file with all of our parameters that we can call into our model. This will help keep things organized as the number of components and regions increases. Each column refers to parameter values for a region, reflecting differences in initial parameter values and growth rates between the three regions.
l = Array{Float64}(undef, 20, 3)
for t in 1:20
l[t,1] = (1. + 0.015)^t *2000
l[t,2] = (1. + 0.02)^t * 1250
l[t,3] = (1. + 0.03)^t * 1700
end
tfp = Array{Float64}(undef, 20, 3)
for t in 1:20
tfp[t,1] = (1 + 0.06)^t * 3.2
tfp[t,2] = (1 + 0.03)^t * 1.8
tfp[t,3] = (1 + 0.05)^t * 2.5
end
s = Array{Float64}(undef, 20, 3)
for t in 1:20
s[t,1] = 0.21
s[t,2] = 0.15
s[t,3] = 0.28
end
depk = [0.11, 0.135 ,0.15]
k0 = [50.5, 22., 33.5]
sigma = Array{Float64}(undef, 20, 3)
for t in 1:20
sigma[t,1] = (1. - 0.05)^t * 0.58
sigma[t,2] = (1. - 0.04)^t * 0.5
sigma[t,3] = (1. - 0.045)^t * 0.6
end
Save this file as region_parameters.jl
The final step is to create a module:
module MyModel
using Mimi
include("region_parameters.jl")
include("gross_economy.jl")
include("emissions.jl")
export construct_MyModel
function construct_MyModel()
m = Model()
set_dimension!(m, :time, collect(2015:5:2110))
set_dimension!(m, :regions, [:Region1, :Region2, :Region3]) # Note that the regions of your model must be specified here
add_comp!(m, grosseconomy)
add_comp!(m, emissions)
update_param!(m, :grosseconomy, :l, l)
update_param!(m, :grosseconomy, :tfp, tfp)
update_param!(m, :grosseconomy, :s, s)
update_param!(m, :grosseconomy, :depk,depk)
update_param!(m, :grosseconomy, :k0, k0)
update_param!(m, :grosseconomy, :share, 0.3)
update_param!(m, :emissions, :sigma, sigma)
connect_param!(m, :emissions, :YGROSS, :grosseconomy, :YGROSS)
return m
end
end #module
Save this file as MyModel.jl
We can now run the model and evaluate the results.
using Mimi
include("MyModel.jl")
using .MyModel
m = construct_MyModel()
run(m)
# Check results
getdataframe(m, :emissions, :E_Global) # or m[:emissions, :E_Global] to return just the Array
# Observe model result graphs
explore(m)
Next, feel free to move on to the next tutorial, which will go into depth on how to run a sensitivity analysis on a own model.