Solving Utility Maximisation Problems with Julia JuMP (Using Constructors)

JuMP
Julia
Published

April 30, 2025

Note: This article is translated from my Japanese article.

In my previous article, I introduced how to solve utility maximisation problems using JuMP, Julia’s mathematical optimisation library.

In that article, I introduced, as a simple example, how to solve a utility maximisation problem using the Cobb-Douglas utility function. However, economic models use a variety of utility functions besides the Cobb-Douglas type, such as the CES utility function. With the implementation approach used in the previous article, though, we would have needed to modify the optimisation problem’s implementation every time we changed the utility function.

So, in this article, I’ll introduce how to use Julia’s constructors to solve utility maximisation problems for different utility functions with the same code.

Implementing the Cobb-Douglas Utility Function

A utility function can be thought of as a function that takes a vector of quantities consumed as its argument and returns utility (a scalar value). On the other hand, when defining a utility function, we need to set parameters in advance, such as elasticity parameters, in addition to the quantity vector.

Since the parameters differ depending on the utility function, we define the utility function as the abstract type AbstractEconomicUtility, and define the Cobb-Douglas utility function CobbDouglasUtility as a subtype of it. We define CobbDouglasUtility as a type that holds the elasticity parameter weights.

abstract type AbstractEconomicUtility end

struct CobbDouglasUtility <: AbstractEconomicUtility
    weights::Vector{Float64}
end

Next, by defining function-like objects, we make it possible to compute utility by calling f(quantities) for a utility function type f. Note that since the utility function’s parameters can be obtained from the type f, the function’s only argument is quantities. This allows us to call the same code, f(quantities), for various utility function types f.

With this, we can define the Cobb-Douglas utility function.

function(f::CobbDouglasUtility)(quantities)
    return prod(quantities .^ f.weights)
end
# Define the utility function
cobb_douglas = CobbDouglasUtility([0.3, 0.4, 0.3])

# Compute utility
cobb_douglas([2., 3., 5.])
3.0963389922845703

Next, as in the previous article, we define the Marshallian demand function, demand_marshallian().

With demand_marshallian(), we can find the analytical solution to the utility maximisation problem for the Cobb-Douglas utility function.

function demand_marshallian(
  f::CobbDouglasUtility;
  prices,
  income
)
  return income * f.weights / sum(f.weights) ./ prices
end
demand_marshallian (generic function with 1 method)
quantities_analytical_cobb_douglas = demand_marshallian(
  cobb_douglas;
  prices = [1., 2., 3.],
  income = 100.
)
3-element Vector{Float64}:
 30.0
 20.0
 10.0

Numerical Solution to the Utility Maximisation Problem

By slightly modifying the code from the previous article, we can solve the utility maximisation problem numerically.

In the previous article, we defined code specific to the Cobb-Douglas utility function. However, the function demand_marshallian_numerical below takes the abstract type AbstractEconomicUtility as its argument, allowing the same code to be used for a variety of utility functions.

import Pkg
Pkg.add("JuMP")
Pkg.add("Ipopt")
using JuMP
using Ipopt

function demand_marshallian_numerical(
  f::AbstractEconomicUtility;
  prices::Vector{Float64},
  income::Float64
)
    n = length(prices)
    model = Model(Ipopt.Optimizer)
    set_silent(model)
    @variable(model, quantities[1:n] >= 0)
    @objective(model, Max, f(quantities))
    @constraint(model, sum(prices .* quantities) <= income)
    optimize!(model)
    return value.(quantities)
end
demand_marshallian_numerical (generic function with 1 method)

By applying demand_marshallian_numerical() to the cobb_douglas defined above, we can confirm, as in the previous article, that the analytical and numerical solutions to the Cobb-Douglas utility maximisation problem approximately agree.

quantities_numerical_cobb_douglas = demand_marshallian_numerical(
  cobb_douglas;
  prices = [1., 2., 3.],
  income = 100.
)

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************
3-element Vector{Float64}:
 30.000000297265974
 20.00000019590169
 10.000000099089569
quantities_analytical_cobb_douglas
3-element Vector{Float64}:
 30.0
 20.0
 10.0

Implementing the CES Utility Function

To get a feel for the benefits of using constructors, let’s implement a utility function other than the Cobb-Douglas type.

Below, let’s implement the CES utility function. The CES utility function is a generalisation of the Cobb-Douglas utility function and has a parameter substitution that represents the degree of substitutability. As with the Cobb-Douglas utility function, the CES utility function can be defined as follows.

I’ll skip the derivation of the Marshallian demand function, but by specifying the argument type f::CESUtility, we can change the behaviour of demand_marshallian() depending on the utility function. This mechanism is called multiple dispatch.

struct CESUtility <: AbstractEconomicUtility
    substitution::Float64
    weights::Vector{Float64}
end

function(f::CESUtility)(quantities)
    return sum(f.weights .* quantities .^ f.substitution) ^ (1 / f.substitution)
end

function demand_marshallian(
  f::CESUtility;
  prices,
  income
)
  return f.weights .^ (1 / (1 - f.substitution)) .* prices .^ (1 / (f.substitution - 1)) *
    income / sum(f.weights .^ (1 / (1 - f.substitution)) .* prices .^ (f.substitution / (f.substitution - 1)))
end
demand_marshallian (generic function with 2 methods)
ces = CESUtility(0.5, [0.3, 0.4, 0.3])
ces([2., 3., 5.])
3.196603520188051

By applying demand_marshallian() and demand_marshallian_numerical() to the ces defined above, we can find the analytical and numerical solutions to the CES utility maximisation problem.

quantities_analytical_ces = demand_marshallian(
  ces;
  prices = [1., 2., 3.],
  income = 100.
)

quantities_numerical_ces = demand_marshallian_numerical(
  ces;
  prices = [1., 2., 3.],
  income = 100.
)
3-element Vector{Float64}:
 45.00000043558223
 20.000000194987294
  5.000000053970371
quantities_analytical_ces
3-element Vector{Float64}:
 45.0
 20.000000000000004
  4.999999999999999

Summary

In this article, I showed that by extending the utility maximisation problem introduced in my previous article using Julia’s constructors, we can use the same code for a variety of utility functions.

By taking advantage of this mechanism, whenever we implement a new utility function, we can automatically check the consistency between the analytical and numerical solutions to the optimisation problem, which is expected to make it easier to implement tests for utility functions1.

Footnotes

  1. However, when I separately looked at the Leontief utility function, I was not able to successfully obtain a numerical solution. For special utility functions, it may be difficult to find a numerical solution, so it may be necessary to implement additional tests separately.↩︎