Closed Economy Model with Monopoly in the X Sector
July 23, 2025
This example is a conversion of the Markusen M51 model into Julia. This is a standard two-good, two-factor, closed-economy general-equilibrium model. Activities are X, Y and W (welfare or utility). Factors of production are called unskilled and skilled labor. Unskilled labor is typically called L with a price of PW or just W, and skilled labor is called S with a price of PZ or just Z. PU is the price of a unit of utility (the value of the unit expenditure function).
For a full description of this model, including the algebraic equation, see the original Markusen M51 model in the MPSGE documentation. I recommend searching the document for “M51” to find the relevant section.
The monopoly or oligopoly markup will be denoted MK or
MARKUP. There are two agents, the representative consumer
who receives all the factor incomes and tax revenue (if any) and pays
subsidies (if any). Then there may be an agent called ENTRE
who receives markup revenue and pays fixed costs (if any).
The data matrix for our model is as follows. Notes that negative values indicate a cost (or input).
| Markets | X | Y | W | CONS | ENTRE |
|---|---|---|---|---|---|
| PX | 100 | -100 | |||
| PY | 100 | -100 | |||
| PU | 200 | -180 | -20 | ||
| PW | -32 | -60 | 92 | ||
| PZ | -48 | -40 | 88 | ||
| MK | -20 | 20 |
At this point we are going to take a detour to set up a Julia environment. I encourage you to read through Markusen’s model and description.
Setting up a Julia Environment
Open VSCode and add a new folder to your workspace. You can do this
by either opening the Explorer side bar or using the
File menu. Start with an empty folder, we’ll build
everything we need.
Open a terminal in VSCode and start julia by typing
julia. You should see the Julia REPL start up. Enter the
package manager by typing ] and then add the required
packages:
pkg> add MPSGE@0.6.1This will install version 0.6.1 of the MPSGE package,
which is the current version as of the writing of this post. The
@0.6.1 is optional, if you omit it Julia will install the
latest version of the package.
If you have the explorer sidebar open you’ll see two files appear
Project.toml and Manifest.toml. You don’t need
to edit or worry about these files, they are used by Julia to manage
packages and dependencies. I’ll post an example someday going into depth
about these files.
And that’s it. You now have a Julia environment set up with the MPSGE package installed.
The Model
Create a new file in your workspace called main.jl, you
can just press Ctrl+N to create a new file and then save it
as main.jl. This is where we will write our model. We’ll
build it up step by step.
Package Imports
At the top of your main.jl file, you need to import the
MPSGE package. This is done with the following line:
using MPSGEIf there are more packages you need, you can add them here as well. It’s recommend to keep your imports organized at the top of the file.
Model Definition and Variables
The first step to any MPSGE model is to define the model object.
M51 = MPSGEModel()I’ve called the model M51 to match the original Markusen
model. You can name it whatever you like, but it’s good practice to use
a descriptive name that reflects the model’s purpose.
Next we define our variables,
@parameter(M51, sigma, 9, description = "Elasticity of substitution")
@sectors(M51, begin
X, (description = "Activity level for sector X",)
Y, (description = "Activity level for sector Y",)
W, (description = "Activity level for sector W (Hicksian welfare index)",)
end)
@commodities(M51, begin
PX, (description = "Price index for commodity X", start = 1.25)
PY, (description = "Price index for commodity Y",)
PW, (description = "Price index for primary factor L (net of tax)",)
PZ, (description = "Price index for primary factor K",)
PU, (description = "Price index for welfare (expenditure function)",)
end)
@consumers(M51, begin
CONS, (description = "Representative agent",)
ENTRE, (description = "Entreprenuer (monopolist)",)
end)
@auxiliaries(M51, begin
SHAREX, (description = "Value share of good X", start = .5)
MARKUP, (description = "X sector markup on marginal cost", start = .2)
end)Several things to note here. First, we define a parameter
sigma which is the elasticity of substitution between
goods. We do not need to make this a parameter as I doubt the value will
change after model creation. But this is an example and this is
possible.
Second, we define three sectors: X, Y, and
W with the @sectors macro. We used
@sectors because we want to define more than one sector.
Notice @parameter was singular, if we had more than one
parameter we would use @parameters instead. Each sector has
a description, which is optional. You should notice the parentheses
around the descriptions, this is an artifact of how julia parses
expressions you must have them. Final thing to point out, there are no
commas at the end of the lines. Each new line denotes a sector.
Third, the commodity PX is defined with a starting value
of 1.25. This value comes from the markup, refer to
Markusen’s model for more details. The default starting value for all
variables is 1.0.
Finally, the auxiliary variables SHAREX and
MARKUP are defined with starting values of 0.5
and 0.2, respectively. Auxiliary variables are always
optional but they provide a wide range of modeling possibilities. For
example, we can use an auxiliary variable as a tax so that the model can
set the tax rate endogenously. This is exactly what we will do with the
MARKUP variable.
Production Blocks
These contain all the information to make our cost functions. These are not production functions, they are cost functions.
Let’s examine the production block for sector X:
@production(M51, X, [s=1,t=0], begin
@output(PX, 80, t, taxes = [Tax(ENTRE, MARKUP)])
@input(PW, 32, s)
@input(PZ, 48, s)
end)The first two arguments to @production are the model and
the sector. The third argument describes the model’s nesting structure.
This model has only top-level nests, s=1 means the input
side will be Cobb-Douglas. Finally, we define our netputs. The
@output and @input macros take the commodity,
its reference quantity, and its parent nest. The taxes
argument is where we apply the MARKUP auxiliary variable as
a tax with the tax revenue assigned to the ENTRE agent.
The remaining production blocks are straightforward,
@production(M51, Y, [s=1,t=0], begin
@output(PY, 100, t)
@input(PW, 60, s)
@input(PZ, 40, s)
end)
@production(M51, W, [s=sigma,t=0], begin
@output(PU, 200, t)
@input(PX, 80, s, reference_price=1.25)
@input(PY, 100, s)
end)The only thing to point out is the reference_price=1.25
argument in the @input macro for sector W.
This is used to set the price of the input commodity PX to
its starting value of 1.25. This negates the starting value
of PX in this sector.
Demands
The structure of demands is similar to production blocks, except
there are no nestings and instead of @input and
@output we use @final_demand and
@endowment.
@demand(M51, CONS, begin
@final_demand(PU, 180)
@endowment(PW, 92)
@endowment(PZ, 88)
end)
@demand(M51, ENTRE, begin
@final_demand(PU, 20)
end)Auxiliary Constraints
Auxiliary constraints are taken to be equal to zero. They are used to define relationships between variables that are not directly modeled in the production or demand blocks.
@aux_constraint(M51, SHAREX,
SHAREX - (80*PX*X/ (80*PX*X + 100*PY*Y))
)
@aux_constraint(M51, MARKUP,
MARKUP - 1/(sigma - (sigma - 1)*SHAREX)
);Solving the Model
First, we set a numeraire,
fix(PU, 1)Then we can solve the model at the benchmark.
solve!(M51, cumulative_iteration_limit=0)If you model is balanced you’ll see
Postsolved residual: 0.0000e+00. If you do not see this,
try running generate_report(M51). Variables with non-zero
marginal values are where you want to start looking for errors in your
model.
Counterfactuals
Before diving into counterfactuals, let’s write a little function to extract how total income is divided between the representative factor owner and the monopolist.
function income_report(M::MPSGEModel)
factory_owner = value(M[:W]*M[:CONS]/(M[:ENTRE] + M[:CONS]))
monopolist = value(M[:W]*M[:ENTRE]/(M[:ENTRE] + M[:CONS]))
println("Factory Owner Income: $factory_owner")
println("Monopolist Income: $monopolist")
endThis isn’t too useful as it’s just printing the values, but it gives
you an idea of how to extract values from the model. You can use
value(M[:variable]) to get the value of a variable in the
model where M is your model.
Test the function at the benchmark:
income_report(M51)This should print
Factory Owner Income: 0.1
Monopolist Income: 0.9
The counter-factual experiment is to impose marginal-cost pricing, which we can do by simply fixing the markup at zero and solving the model:
fix(M51[:MARKUP], 0)
solve!(M51)
income_report(M51)The income report shows the monoplist’s income has fallen to zero,
while the factory owner’s relative income has gone up to 1.04 from 0.90.
Additionally, the value of welfare value(M51[:W]) is now
1.04, meaning welfare has increased by 4% from the
benchmark value of 1.0.