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:

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 function run_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 optional first and last 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 using update_param! two different times. The syntax is update_param!(model_name, :component_name, :parameter_name, value). Alternatively if these parameters are always meant to use the same value, one could use add_shared_param! to create a shared model parameter and add it to the model, and then use connect_param! to connect both. This syntax would use add_shared_param!(model_name, :model_param_name, value) followed by connect_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 the grosseconomy component and a parameter in the emissions component. The syntax is connect_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 either explore(model_name) to open the UI window, or use Mimi.plot(model_name, :component_name, :variable_name) or Mimi.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 into update_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.