Skip to main content

Closed Economy Model with Monopoly in the X Sector

Mitch Phillipson 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.1

This 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 MPSGE

If 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")
end

This 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.