Skip to main content

A Few Examples of Dynamic Calibration

Mitch Phillipson September 19, 2025


This week was the MPSGE Monthly seminar, I presented a few examples calibrating a dynamic CGE model. I’ll use this blog post to share a few Julia specific tips and tricks I used in the process. The repository is available on GitHub.

Why is the data wrapped in a struct?

A struct is just a new data type. The dynamic models from my examples use 15 distinct variables, most of which are computed from the other variables. Wrapping the data in a struct allows me to pass around a single object instead of 15 different variables.

Let’s look at a small example. I want a function that requires two inputs x and y, and computes z as x * y. I could define the function like this:

function my_function(x::Float64, y::Float64)
    z = x*y
    return x + y + z
end

Or I can hold the data in a struct and define the function like this:

struct MyData
    x::Float64
    y::Float64
    z::Float64
    function MyData(x::Float64, y::Float64)
        z = x * y
        new(x, y, z)
    end
end

function my_function(data::MyData)
    return data.x + data.y + data.z
end

To run each function, we can do the following:

my_function(2, 3) # returns 11.0

data = MyData(2, 3)
my_function(data) # also returns 11.0

The first big advantage of using a struct is that I still have access to z using data.z. The second is I can use the MyData type to define other functions that require the same data. For example, I could define a function that computes x^2 + y^2 + z^2 like this:

function my_other_function(data::MyData)
    return data.x^2 + data.y^2 + data.z^2
end

The struct allows me to pass around a single object that contains all the data I need. If, for some reason, I need to change how z is computed, I only need to change it in one place (the constructor of the struct) instead of every function that uses z.

This example is quite small, but the same principle applies to larger models with more variables.

The second big advantage is that Julia runs faster and is more efficient when there are fewer global variables. Julia is a compiled language, and the compiler can not guarantee the type of a global variable will always be the same. For this reason Julia will create more complex and generic code to handle the possibility of different types. By wrapping the data in a struct, I can ensure that the type of the data is always the same, and Julia can generate more efficient code.

Why use Plotly?

Julia has a rich ecosystem of plotting packages, here is a breakdown from 2023. The package I am using, PlotlyJS, isn’t even on that list!

Why did I choose PlotlyJS? Because I wanted a package that integrated nicely with dataframes, was easy to use, and produced interactive plots. PlotlyJS checks all those boxes. As a bonus, PlotlyJS can export plots as HTML files that can be easily shared and embedded in web pages. For example, the final plot from the included code is saved as an HTML file and embedded below:

Plotly itself is also popular in the Python and R communities, so there is a lot of documentation and examples available online.

If you are just learning Julia it may be worthwhile to learn either Makie or AlgebraOfGraphics (which is powered by Makie). Both are very powerful and flexible, but they have a steeper learning curve than PlotlyJS. Do note that AlgebraOfGraphics is designed to act like ggplot2 in R, so if you are familiar with that package it may be easier to learn.

The difference between import and using

In my shared code I use both import and using to load packages. What is the difference? It has to do with namespace management.

When you use using PackageName, all the exported functions and types from that package are loaded into the current namespace. This means you can call those functions and types directly without prefixing them with the package name. For example:

using MPSGE

M = MPSGEModel()

The function MPSGEModel is exported by the MPSGE package, so I can call it directly.

When you use import PackageName, nothing is loaded into the current namespace. You have to prefix all functions and types from that package with the package name. For example:

import MPSGE

M = MPSGE.MPSGEModel()

You can also use this method to import specific functions or types from a package. For example:

import MPSGE: MPSGEModel

M = MPSGEModel()

In this case, only the MPSGEModel function is imported into the current namespace, so I can call it directly. But no other MPSGE functions or types are available without the MPSGE. prefix.

This is useful for avoiding name conflicts. If two packages export a function with the same name, using import allows you to specify which package’s function you want to use.