{
"cells": [
{
"cell_type": "markdown",
"id": "f3abfd48",
"metadata": {},
"source": [
"\n",
""
]
},
{
"cell_type": "markdown",
"id": "6b70f722",
"metadata": {},
"source": [
"# Linear Programming"
]
},
{
"cell_type": "markdown",
"id": "b5330a37",
"metadata": {},
"source": [
"# Migrated lecture\n",
"\n",
"This lecture has moved from our [Intermediate Quantitative Economics with Python](https://python.quantecon.org/intro.html) lecture series and is now a part of [A First Course in Quantitative Economics](https://intro.quantecon.org/intro.html).\n",
"\n",
"In this lecture, we will need the following library. Install [ortools](https://developers.google.com/optimization) using `pip`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a07397c3",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"!pip install ortools"
]
},
{
"cell_type": "markdown",
"id": "872b8c13",
"metadata": {},
"source": [
"## Overview\n",
"\n",
"**Linear programming** problems either maximize or minimize\n",
"a linear objective function subject to a set of linear equality and/or inequality constraints.\n",
"\n",
"Linear programs come in pairs:\n",
"\n",
"- an original **primal** problem, and \n",
"- an associated **dual** problem. \n",
"\n",
"\n",
"If a primal problem involves **maximization**, the dual problem involves **minimization**.\n",
"\n",
"If a primal problem involves **minimization**, the dual problem involves **maximization**.\n",
"\n",
"We provide a standard form of a linear program and methods to transform other forms of linear programming problems into a standard form.\n",
"\n",
"We tell how to solve a linear programming problem using [SciPy](https://scipy.org/) and [Google OR-Tools](https://developers.google.com/optimization).\n",
"\n",
"We describe the important concept of complementary slackness and how it relates to the dual problem.\n",
"\n",
"Let’s start with some standard imports."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2ea1874e",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"import numpy as np\n",
"from ortools.linear_solver import pywraplp\n",
"from scipy.optimize import linprog\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.patches import Polygon\n",
"%matplotlib inline"
]
},
{
"cell_type": "markdown",
"id": "55f04ef7",
"metadata": {},
"source": [
"Let’s start with some examples of linear programming problem."
]
},
{
"cell_type": "markdown",
"id": "4c3aeee6",
"metadata": {},
"source": [
"## Example 1: Production Problem\n",
"\n",
"This example was created by [[Bertsimas, 1997](https://intro.quantecon.org/zreferences.html#id58)]\n",
"\n",
"Suppose that a factory can produce two goods called Product $ 1 $ and Product $ 2 $.\n",
"\n",
"To produce each product requires both material and labor.\n",
"\n",
"Selling each product generates revenue.\n",
"\n",
"Required per unit material and labor inputs and revenues are shown in table below:\n",
"\n",
"||Product 1|Product 2|\n",
"|:-------------------------------:|:-------------------------------:|:-------------------------------:|\n",
"|Material|2|5|\n",
"|Labor|4|2|\n",
"|Revenue|3|4|\n",
"30 units of material and 20 units of labor available.\n",
"\n",
"A firm’s problem is to construct a production plan that uses its 30 units of materials and 20 units of labor to maximize its revenue.\n",
"\n",
"Let $ x_i $ denote the quantity of Product $ i $ that the firm produces.\n",
"\n",
"This problem can be formulated as:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\max_{x_1,x_2} \\ & z = 3 x_1 + 4 x_2 \\\\\n",
"\\mbox{subject to } \\ & 2 x_1 + 5 x_2 \\le 30 \\\\\n",
"& 4 x_1 + 2 x_2 \\le 20 \\\\\n",
"& x_1, x_2 \\ge 0 \\\\\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"The following graph illustrates the firm’s constraints and iso-revenue lines."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1a35d598",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"fig, ax = plt.subplots()\n",
"ax.grid()\n",
"\n",
"# Draw constraint lines\n",
"ax.hlines(0, -1, 17.5)\n",
"ax.vlines(0, -1, 12)\n",
"ax.plot(np.linspace(-1, 17.5, 100), 6-0.4*np.linspace(-1, 17.5, 100), color=\"r\")\n",
"ax.plot(np.linspace(-1, 5.5, 100), 10-2*np.linspace(-1, 5.5, 100), color=\"r\")\n",
"ax.text(1.5, 8, \"$2x_1 + 5x_2 \\leq 30$\", size=12)\n",
"ax.text(10, 2.5, \"$4x_1 + 2x_2 \\leq 20$\", size=12)\n",
"ax.text(-2, 2, \"$x_2 \\geq 0$\", size=12)\n",
"ax.text(2.5, -0.7, \"$x_1 \\geq 0$\", size=12)\n",
"\n",
"# Draw the feasible region\n",
"feasible_set = Polygon(np.array([[0, 0],\n",
" [0, 6],\n",
" [2.5, 5],\n",
" [5, 0]]),\n",
" color=\"cyan\")\n",
"ax.add_patch(feasible_set)\n",
"\n",
"# Draw the objective function\n",
"ax.plot(np.linspace(-1, 5.5, 100), 3.875-0.75*np.linspace(-1, 5.5, 100), color=\"orange\")\n",
"ax.plot(np.linspace(-1, 5.5, 100), 5.375-0.75*np.linspace(-1, 5.5, 100), color=\"orange\")\n",
"ax.plot(np.linspace(-1, 5.5, 100), 6.875-0.75*np.linspace(-1, 5.5, 100), color=\"orange\")\n",
"ax.arrow(-1.6, 5, 0, 2, width = 0.05, head_width=0.2, head_length=0.5, color=\"orange\")\n",
"ax.text(5.7, 1, \"$z = 3x_1 + 4x_2$\", size=12)\n",
"\n",
"# Draw the optimal solution\n",
"ax.plot(2.5, 5, \"*\", color=\"black\")\n",
"ax.text(2.7, 5.2, \"Optimal Solution\", size=12)\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "5511abc4",
"metadata": {},
"source": [
"The blue region is the feasible set within which all constraints are satisfied.\n",
"\n",
"Parallel orange lines are iso-revenue lines.\n",
"\n",
"The firm’s objective is to find the parallel orange lines to the upper boundary of the feasible set.\n",
"\n",
"The intersection of the feasible set and the highest orange line delineates the optimal set.\n",
"\n",
"In this example, the optimal set is the point $ (2.5, 5) $."
]
},
{
"cell_type": "markdown",
"id": "9df8815f",
"metadata": {},
"source": [
"### Computation: Using OR-Tools\n",
"\n",
"Let’s try to solve the same problem using the package *ortools.linear_solver*\n",
"\n",
"The following cell instantiates a solver and creates two variables specifying the range of values that they can have."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "895ab465",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Instantiate a GLOP(Google Linear Optimization Package) solver\n",
"solver = pywraplp.Solver.CreateSolver('GLOP')"
]
},
{
"cell_type": "markdown",
"id": "4328034c",
"metadata": {},
"source": [
"Let’s us create two variables $ x_1 $ and $ x_2 $ such that they can only have nonnegative values."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "34680dc5",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Create the two variables and let them take on any non-negative value.\n",
"x1 = solver.NumVar(0, solver.infinity(), 'x1')\n",
"x2 = solver.NumVar(0, solver.infinity(), 'x2')"
]
},
{
"cell_type": "markdown",
"id": "4451d796",
"metadata": {},
"source": [
"Add the constraints to the problem."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a3dd6fa4",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Constraint 1: 2x_1 + 5x_2 <= 30.0\n",
"solver.Add(2 * x1 + 5 * x2 <= 30.0)\n",
"\n",
"# Constraint 2: 4x_1 + 2x_2 <= 20.0\n",
"solver.Add(4 * x1 + 2 * x2 <= 20.0)"
]
},
{
"cell_type": "markdown",
"id": "97403eb4",
"metadata": {},
"source": [
"Let’s specify the objective function. We use `solver.Maximize` method in the case when we want to maximize the objective function and in the case of minimization we can use `solver.Minimize`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4074dce3",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Objective function: 3x_1 + 4x_2\n",
"solver.Maximize(3 * x1 + 4 * x2)"
]
},
{
"cell_type": "markdown",
"id": "53047508",
"metadata": {},
"source": [
"Once we solve the problem, we can check whether the solver was successful in solving the problem using it’s status. If it’s successful, then the status will be equal to `pywraplp.Solver.OPTIMAL`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d56dce81",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Solve the system.\n",
"status = solver.Solve()\n",
"\n",
"if status == pywraplp.Solver.OPTIMAL:\n",
" print('Objective value =', solver.Objective().Value())\n",
" x1_sol = round(x1.solution_value(), 2)\n",
" x2_sol = round(x2.solution_value(), 2)\n",
" print(f'(x1, x2): ({x1_sol}, {x2_sol})')\n",
"else:\n",
" print('The problem does not have an optimal solution.')"
]
},
{
"cell_type": "markdown",
"id": "d7e1f014",
"metadata": {},
"source": [
"## Example 2: Investment Problem\n",
"\n",
"We now consider a problem posed and solved by [[Hu, 2018](https://intro.quantecon.org/zreferences.html#id59)].\n",
"\n",
"A mutual fund has $ \\\\$ 100,000 $ to be invested over a three year horizon.\n",
"\n",
"Three investment options are available:\n",
"\n",
"1. **Annuity:** the fund can pay a same amount of new capital at the beginning of each of three years and receive a payoff of 130% of **total capital** invested at the end of the third year. Once the mutual fund decides to invest in this annuity, it has to keep investing in all subsequent years in the three year horizon. \n",
"1. **Bank account:** the fund can deposit any amount into a bank at the beginning of each year and receive its capital plus 6% interest at the end of that year. In addition, the mutual fund is permitted to borrow no more than \\$20,000 at the beginning of each year and is asked to pay back the amount borrowed plus 6% interest at the end of the year. The mutual fund can choose whether to deposit or borrow at the beginning of each year. \n",
"1. **Corporate bond:** At the beginning of the second year, a corporate bond becomes available.\n",
" The fund can buy an amount\n",
" that is no more than $ \\\\$ $50,000 of this bond at the beginning of the second year and at the end of the third year receive a payout of 130% of the amount invested in the bond. \n",
"\n",
"\n",
"The mutual fund’s objective is to maximize total payout that it owns at the end of the third year.\n",
"\n",
"We can formulate this as a linear programming problem.\n",
"\n",
"Let $ x_1 $ be the amount of put in the annuity, $ x_2, x_3, x_4 $ be bank deposit balances at the beginning of the three years, and $ x_5 $ be the amount invested in the corporate bond.\n",
"\n",
"When $ x_2, x_3, x_4 $ are negative, it means that the mutual fund has borrowed from bank.\n",
"\n",
"The table below shows the mutual fund’s decision variables together with the timing protocol described above:\n",
"\n",
"||Year 1|Year 2|Year 3|\n",
"|:-----------------------:|:-----------------------:|:-----------------------:|:-----------------------:|\n",
"|Annuity|$ x_1 $|$ x_1 $|$ x_1 $|\n",
"|Bank account|$ x_2 $|$ x_3 $|$ x_4 $|\n",
"|Corporate bond|0|$ x_5 $|0|\n",
"The mutual fund’s decision making proceeds according to the following timing protocol:\n",
"\n",
"1. At the beginning of the first year, the mutual fund decides how much to invest in the annuity and\n",
" how much to deposit in the bank. This decision is subject to the constraint: \n",
" $$\n",
" x_1 + x_2 = 100,000\n",
" $$\n",
"1. At the beginning of the second year, the mutual fund has a bank balance of $ 1.06 x_2 $.\n",
" It must keep $ x_1 $ in the annuity. It can choose to put $ x_5 $ into the corporate bond,\n",
" and put $ x_3 $ in the bank. These decisions are restricted by \n",
" $$\n",
" x_1 + x_5 = 1.06 x_2 - x_3\n",
" $$\n",
"1. At the beginning of the third year, the mutual fund has a bank account balance equal\n",
" to $ 1.06 x_3 $. It must again invest $ x_1 $ in the annuity,\n",
" leaving it with a bank account balance equal to $ x_4 $. This situation is summarized by the restriction: \n",
" $$\n",
" x_1 = 1.06 x_3 - x_4\n",
" $$\n",
"\n",
"\n",
"The mutual fund’s objective function, i.e., its wealth at the end of the third year is:\n",
"\n",
"$$\n",
"1.30 \\cdot 3x_1 + 1.06 x_4 + 1.30 x_5\n",
"$$\n",
"\n",
"Thus, the mutual fund confronts the linear program:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\max_{x} \\ & 1.30 \\cdot 3x_1 + 1.06 x_4 + 1.30 x_5 \\\\\n",
"\\mbox{subject to } \\ & x_1 + x_2 = 100,000\\\\\n",
" & x_1 - 1.06 x_2 + x_3 + x_5 = 0\\\\\n",
" & x_1 - 1.06 x_3 + x_4 = 0\\\\\n",
" & x_2 \\ge -20,000\\\\\n",
" & x_3 \\ge -20,000\\\\\n",
" & x_4 \\ge -20,000\\\\\n",
" & x_5 \\le 50,000\\\\\n",
" & x_j \\ge 0, \\quad j = 1,5\\\\\n",
" & x_j \\ \\text{unrestricted}, \\quad j = 2,3,4\\\\\n",
"\\end{aligned}\n",
"$$"
]
},
{
"cell_type": "markdown",
"id": "16e5e027",
"metadata": {},
"source": [
"### Computation: Using OR-Tools\n",
"\n",
"Let’s try to solve the above problem using the package *ortools.linear_solver*.\n",
"\n",
"The following cell instantiates a solver and creates two variables specifying the range of values that they can have."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "20c937ff",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Instantiate a GLOP(Google Linear Optimization Package) solver\n",
"solver = pywraplp.Solver.CreateSolver('GLOP')"
]
},
{
"cell_type": "markdown",
"id": "d5f84c10",
"metadata": {},
"source": [
"Let’s us create five variables $ x_1, x_2, x_3, x_4, $ and $ x_5 $ such that they can only have the values defined in the above constraints."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ce7fe7dd",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Create the variables using the ranges available from constraints\n",
"x1 = solver.NumVar(0, solver.infinity(), 'x1')\n",
"x2 = solver.NumVar(-20_000, solver.infinity(), 'x2')\n",
"x3 = solver.NumVar(-20_000, solver.infinity(), 'x3')\n",
"x4 = solver.NumVar(-20_000, solver.infinity(), 'x4')\n",
"x5 = solver.NumVar(0, 50_000, 'x5')"
]
},
{
"cell_type": "markdown",
"id": "655a7e23",
"metadata": {},
"source": [
"Add the constraints to the problem."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ec659ee2",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Constraint 1: x_1 + x_2 = 100,000\n",
"solver.Add(x1 + x2 == 100_000.0)\n",
"\n",
"# Constraint 2: x_1 - 1.06 * x_2 + x_3 + x_5 = 0\n",
"solver.Add(x1 - 1.06 * x2 + x3 + x5 == 0.0)\n",
"\n",
"# Constraint 3: x_1 - 1.06 * x_3 + x_4 = 0\n",
"solver.Add(x1 - 1.06 * x3 + x4 == 0.0)"
]
},
{
"cell_type": "markdown",
"id": "7dbc3e7d",
"metadata": {},
"source": [
"Let’s specify the objective function."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2366a7ce",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Objective function: 1.30 * 3 * x_1 + 1.06 * x_4 + 1.30 * x_5\n",
"solver.Maximize(1.30 * 3 * x1 + 1.06 * x4 + 1.30 * x5)"
]
},
{
"cell_type": "markdown",
"id": "e89ec34d",
"metadata": {},
"source": [
"Let’s solve the problem and check the status using `pywraplp.Solver.OPTIMAL`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "00389b27",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Solve the system.\n",
"status = solver.Solve()\n",
"\n",
"if status == pywraplp.Solver.OPTIMAL:\n",
" print('Objective value =', solver.Objective().Value())\n",
" x1_sol = round(x1.solution_value(), 3)\n",
" x2_sol = round(x2.solution_value(), 3)\n",
" x3_sol = round(x1.solution_value(), 3)\n",
" x4_sol = round(x2.solution_value(), 3)\n",
" x5_sol = round(x1.solution_value(), 3)\n",
" print(f'(x1, x2, x3, x4, x5): ({x1_sol}, {x2_sol}, {x3_sol}, {x4_sol}, {x5_sol})')\n",
"else:\n",
" print('The problem does not have an optimal solution.')"
]
},
{
"cell_type": "markdown",
"id": "4ffc3482",
"metadata": {},
"source": [
"OR-Tools tells us that the best investment strategy is:\n",
"\n",
"1. At the beginning of the first year, the mutual fund should buy $ \\\\$24,927.755 $ of the annuity. Its bank account balance should be $ \\\\$75,072.245 $. \n",
"1. At the beginning of the second year, the mutual fund should buy $ \\\\$24,927.755 $ of the corporate bond and keep invest in the annuity. Its bank balance should be $ \\\\$24,927.755 $. \n",
"1. At the beginning of the third year, the bank balance should be $ \\\\$75,072.245 $. \n",
"1. At the end of the third year, the mutual fund will get payouts from the annuity and corporate bond and repay its loan from the bank. At the end it will own $ \\\\$141018.24 $, so that it’s total net rate of return over the three periods is $ 41.02\\% $. "
]
},
{
"cell_type": "markdown",
"id": "c14ebc14",
"metadata": {},
"source": [
"## Standard form\n",
"\n",
"For purposes of\n",
"\n",
"- unifying linear programs that are initially stated in superficially different forms, and \n",
"- having a form that is convenient to put into black-box software packages, \n",
"\n",
"\n",
"it is useful to devote some effort to describe a **standard form**.\n",
"\n",
"Our standard form is:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\min_{x} \\ & c_1 x_1 + c_2 x_2 + \\dots + c_n x_n \\\\\n",
"\\mbox{subject to } \\ & a_{11} x_1 + a_{12} x_2 + \\dots + a_{1n} x_n = b_1 \\\\\n",
" & a_{21} x_1 + a_{22} x_2 + \\dots + a_{2n} x_n = b_2 \\\\\n",
" & \\quad \\vdots \\\\\n",
" & a_{m1} x_1 + a_{m2} x_2 + \\dots + a_{mn} x_n = b_m \\\\\n",
" & x_1, x_2, \\dots, x_n \\ge 0 \\\\\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"Let\n",
"\n",
"$$\n",
"A = \\begin{bmatrix}\n",
"a_{11} & a_{12} & \\dots & a_{1n} \\\\\n",
"a_{21} & a_{22} & \\dots & a_{2n} \\\\\n",
" & & \\vdots & \\\\\n",
"a_{m1} & a_{m2} & \\dots & a_{mn} \\\\\n",
"\\end{bmatrix}, \\quad\n",
"b = \\begin{bmatrix} b_1 \\\\ b_2 \\\\ \\vdots \\\\ b_m \\\\ \\end{bmatrix}, \\quad\n",
"c = \\begin{bmatrix} c_1 \\\\ c_2 \\\\ \\vdots \\\\ c_n \\\\ \\end{bmatrix}, \\quad\n",
"x = \\begin{bmatrix} x_1 \\\\ x_2 \\\\ \\vdots \\\\ x_n \\\\ \\end{bmatrix}. \\quad\n",
"$$\n",
"\n",
"The standard form LP problem can be expressed concisely as:\n",
"\n",
"\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\min_{x} \\ & c'x \\\\\n",
"\\mbox{subject to } \\ & Ax = b\\\\\n",
" & x \\geq 0\\\\\n",
"\\end{aligned} \\tag{34.1}\n",
"$$\n",
"\n",
"Here, $ Ax = b $ means that the $ i $-th entry of $ Ax $ equals the $ i $-th entry of $ b $ for every $ i $.\n",
"\n",
"Similarly, $ x \\geq 0 $ means that $ x_j $ is greater than equal to $ 0 $ for every $ j $."
]
},
{
"cell_type": "markdown",
"id": "70de8982",
"metadata": {},
"source": [
"### Useful transformations\n",
"\n",
"It is useful to know how to transform a problem that initially is not stated in the standard form into one that is.\n",
"\n",
"By deploying the following steps, any linear programming problem can be transformed into an equivalent standard form linear programming problem.\n",
"\n",
"1. **Objective Function:** If a problem is originally a constrained **maximization** problem, we can construct a new objective function that is the additive inverse of the original objective function. The transformed problem is then a **minimization** problem. \n",
"1. **Decision Variables:** Given a variable $ x_j $ satisfying $ x_j \\le 0 $, we can introduce a new variable $ x_j' = - x_j $ and substitute it into original problem. Given a free variable $ x_i $ with no restriction on its sign, we can introduce two new variables $ x_j^+ $ and $ x_j^- $ satisfying $ x_j^+, x_j^- \\ge 0 $ and replace $ x_j $ by $ x_j^+ - x_j^- $. \n",
"1. **Inequality constraints:** Given an inequality constraint $ \\sum_{j=1}^n a_{ij}x_j \\le 0 $, we can introduce a new variable $ s_i $, called a **slack variable** that satisfies $ s_i \\ge 0 $ and replace the original constraint by $ \\sum_{j=1}^n a_{ij}x_j + s_i = 0 $. \n",
"\n",
"\n",
"Let’s apply the above steps to the two examples described above."
]
},
{
"cell_type": "markdown",
"id": "92c1d169",
"metadata": {},
"source": [
"### Example 1: Production Problem\n",
"\n",
"The original problem is:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\max_{x_1,x_2} \\ & 3 x_1 + 4 x_2 \\\\\n",
"\\mbox{subject to } \\ & 2 x_1 + 5 x_2 \\le 30 \\\\\n",
"& 4 x_1 + 2 x_2 \\le 20 \\\\\n",
"& x_1, x_2 \\ge 0 \\\\\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"This problem is equivalent to the following problem with a standard form:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\min_{x_1,x_2} \\ & -(3 x_1 + 4 x_2) \\\\\n",
"\\mbox{subject to } \\ & 2 x_1 + 5 x_2 + s_1 = 30 \\\\\n",
"& 4 x_1 + 2 x_2 + s_2 = 20 \\\\\n",
"& x_1, x_2, s_1, s_2 \\ge 0 \\\\\n",
"\\end{aligned}\n",
"$$"
]
},
{
"cell_type": "markdown",
"id": "3140d43f",
"metadata": {},
"source": [
"### Computation: Using SciPy\n",
"\n",
"The package *scipy.optimize* provides a function ***linprog*** to solve linear programming problems with a form below:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\min_{x} \\ & c' x \\\\\n",
"\\mbox{subject to } \\ & A_{ub}x \\le b_{ub} \\\\\n",
" & A_{eq}x = b_{eq} \\\\\n",
" & l \\le x \\le u \\\\\n",
"\\end{aligned}\n",
"$$\n",
"\n",
">**Note**\n",
">\n",
">By default $ l = 0 $ and $ u = \\text{None} $ unless explicitly specified with the argument ‘bounds’.\n",
"\n",
"Let’s now try to solve the Problem 1 using SciPy."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "72b5f94c",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Construct parameters\n",
"c_ex1 = np.array([3, 4])\n",
"\n",
"# Inequality constraints\n",
"A_ex1 = np.array([[2, 5],\n",
" [4, 2]])\n",
"b_ex1 = np.array([30,20])"
]
},
{
"cell_type": "markdown",
"id": "3d5f6fb1",
"metadata": {},
"source": [
"Once we solve the problem, we can check whether the solver was successful in solving the problem using the boolean attribute `success`. If it’s successful, then the `success` attribute is set to `True`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4f6718cc",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Solve the problem\n",
"# we put a negative sign on the objective as linprog does minimization\n",
"res_ex1 = linprog(-c_ex1, A_ub=A_ex1, b_ub=b_ex1)\n",
"\n",
"if res_ex1.success:\n",
" # We use negative sign to get the optimal value (maximized value)\n",
" print('Optimal Value:', -res_ex1.fun)\n",
" print(f'(x1, x2): {res_ex1.x[0], res_ex1.x[1]}')\n",
"else:\n",
" print('The problem does not have an optimal solution.')"
]
},
{
"cell_type": "markdown",
"id": "832a2184",
"metadata": {},
"source": [
"The optimal plan tells the factory to produce $ 2.5 $ units of Product 1 and $ 5 $ units of Product 2; that generates a maximizing value of revenue of $ 27.5 $.\n",
"\n",
"We are using the *linprog* function as a **black box**.\n",
"\n",
"Inside it, Python first transforms the problem into standard form.\n",
"\n",
"To do that, for each inequality constraint it generates one slack variable.\n",
"\n",
"Here the vector of slack variables is a two-dimensional NumPy array that equals $ b_{ub} - A_{ub}x $.\n",
"\n",
"See the [official documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html#scipy.optimize.linprog) for more details.\n",
"\n",
">**Note**\n",
">\n",
">This problem is to maximize the objective, so that we need to put a minus sign in front of parameter vector c."
]
},
{
"cell_type": "markdown",
"id": "30634550",
"metadata": {},
"source": [
"### Example 2: Investment Problem\n",
"\n",
"The original problem is:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\max_{x} \\ & 1.30 \\cdot 3x_1 + 1.06 x_4 + 1.30 x_5 \\\\\n",
"\\mbox{subject to } \\ & x_1 + x_2 = 100,000\\\\\n",
" & x_1 - 1.06 x_2 + x_3 + x_5 = 0\\\\\n",
" & x_1 - 1.06 x_3 + x_4 = 0\\\\\n",
" & x_2 \\ge -20,000\\\\\n",
" & x_3 \\ge -20,000\\\\\n",
" & x_4 \\ge -20,000\\\\\n",
" & x_5 \\le 50,000\\\\\n",
" & x_j \\ge 0, \\quad j = 1,5\\\\\n",
" & x_j \\ \\text{unrestricted}, \\quad j = 2,3,4\\\\\n",
"\\end{aligned}\n",
"$$\n",
"\n",
"This problem is equivalent to the following problem with a standard form:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\min_{x} \\ & -(1.30 \\cdot 3x_1 + 1.06 x_4^+ - 1.06 x_4^- + 1.30 x_5) \\\\\n",
"\\mbox{subject to } \\ & x_1 + x_2^+ - x_2^- = 100,000\\\\\n",
" & x_1 - 1.06 (x_2^+ - x_2^-) + x_3^+ - x_3^- + x_5 = 0\\\\\n",
" & x_1 - 1.06 (x_3^+ - x_3^-) + x_4^+ - x_4^- = 0\\\\\n",
" & x_2^- - x_2^+ + s_1 = 20,000\\\\\n",
" & x_3^- - x_3^+ + s_2 = 20,000\\\\\n",
" & x_4^- - x_4^+ + s_3 = 20,000\\\\\n",
" & x_5 + s_4 = 50,000\\\\\n",
" & x_j \\ge 0, \\quad j = 1,5\\\\\n",
" & x_j^+, x_j^- \\ge 0, \\quad j = 2,3,4\\\\\n",
" & s_j \\ge 0, \\quad j = 1,2,3,4\\\\\n",
"\\end{aligned}\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0c402f89",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Construct parameters\n",
"rate = 1.06\n",
"\n",
"# Objective function parameters\n",
"c_ex2 = np.array([1.30*3, 0, 0, 1.06, 1.30])\n",
"\n",
"# Inequality constraints\n",
"A_ex2 = np.array([[1, 1, 0, 0, 0],\n",
" [1, -rate, 1, 0, 1],\n",
" [1, 0, -rate, 1, 0]])\n",
"b_ex2 = np.array([100000, 0, 0])\n",
"\n",
"# Bounds on decision variables\n",
"bounds_ex2 = [( 0, None),\n",
" (-20000, None),\n",
" (-20000, None),\n",
" (-20000, None),\n",
" ( 0, 50000)]"
]
},
{
"cell_type": "markdown",
"id": "eb5dd00d",
"metadata": {},
"source": [
"Let’s solve the problem and check the status using `success` attribute."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e261b0a1",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Solve the problem\n",
"res_ex2 = linprog(-c_ex2, A_eq=A_ex2, b_eq=b_ex2,\n",
" bounds=bounds_ex2)\n",
"\n",
"if res_ex2.success:\n",
" # We use negative sign to get the optimal value (maximized value)\n",
" print('Optimal Value:', -res_ex2.fun)\n",
" x1_sol = round(res_ex2.x[0], 3)\n",
" x2_sol = round(res_ex2.x[1], 3)\n",
" x3_sol = round(res_ex2.x[2], 3)\n",
" x4_sol = round(res_ex2.x[3], 3)\n",
" x5_sol = round(res_ex2.x[4], 3)\n",
" print(f'(x1, x2, x3, x4, x5): {x1_sol, x2_sol, x3_sol, x4_sol, x5_sol}')\n",
"else:\n",
" print('The problem does not have an optimal solution.')"
]
},
{
"cell_type": "markdown",
"id": "500dced8",
"metadata": {},
"source": [
"SciPy tells us that the best investment strategy is:\n",
"\n",
"1. At the beginning of the first year, the mutual fund should buy $ \\\\$24,927.75 $ of the annuity. Its bank account balance should be $ \\\\$75,072.25 $. \n",
"1. At the beginning of the second year, the mutual fund should buy $ \\\\$50,000 $ of the corporate bond and keep invest in the annuity. Its bank account balance should be $ \\\\$ 4,648.83 $. \n",
"1. At the beginning of the third year, the mutual fund should borrow $ \\\\$20,000 $ from the bank and invest in the annuity. \n",
"1. At the end of the third year, the mutual fund will get payouts from the annuity and corporate bond and repay its loan from the bank. At the end it will own $ \\\\$141018.24 $, so that it’s total net rate of return over the three periods is $ 41.02\\% $. \n",
"\n",
"\n",
">**Note**\n",
">\n",
">You might notice the difference in the values of optimal solution using OR-Tools and SciPy but the optimal value is the same. It is because there can be many optimal solutions for the same problem."
]
},
{
"cell_type": "markdown",
"id": "d6953acf",
"metadata": {},
"source": [
"## Exercises"
]
},
{
"cell_type": "markdown",
"id": "fa261b71",
"metadata": {},
"source": [
"## Exercise 34.1\n",
"\n",
"Implement a new extended solution for the Problem 1 where in the factory owner decides that number of units of Product 1 should not be less than the number of units of Product 2."
]
},
{
"cell_type": "markdown",
"id": "1f866f54",
"metadata": {},
"source": [
"## Solution to[ Exercise 34.1](https://intro.quantecon.org/#lp_intro_ex1)\n",
"\n",
"So we can reformulate the problem as:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\max_{x_1,x_2} \\ & z = 3 x_1 + 4 x_2 \\\\\n",
"\\mbox{subject to } \\ & 2 x_1 + 5 x_2 \\le 30 \\\\\n",
"& 4 x_1 + 2 x_2 \\le 20 \\\\\n",
"& x_1 \\ge x_2 \\\\\n",
"& x_1, x_2 \\ge 0 \\\\\n",
"\\end{aligned}\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d9da81de",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Instantiate a GLOP(Google Linear Optimization Package) solver\n",
"solver = pywraplp.Solver.CreateSolver('GLOP')\n",
"\n",
"# Create the two variables and let them take on any non-negative value.\n",
"x1 = solver.NumVar(0, solver.infinity(), 'x1')\n",
"x2 = solver.NumVar(0, solver.infinity(), 'x2')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9375c265",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Constraint 1: 2x_1 + 5x_2 <= 30.0\n",
"solver.Add(2 * x1 + 5 * x2 <= 30.0)\n",
"\n",
"# Constraint 2: 4x_1 + 2x_2 <= 20.0\n",
"solver.Add(4 * x1 + 2 * x2 <= 20.0)\n",
"\n",
"# Constraint 3: x_1 >= x_2\n",
"solver.Add(x1 >= x2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "13771b04",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Objective function: 3x_1 + 4x_2\n",
"solver.Maximize(3 * x1 + 4 * x2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e49a23a",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Solve the system.\n",
"status = solver.Solve()\n",
"\n",
"if status == pywraplp.Solver.OPTIMAL:\n",
" print('Objective value =', solver.Objective().Value())\n",
" x1_sol = round(x1.solution_value(), 2)\n",
" x2_sol = round(x2.solution_value(), 2)\n",
" print(f'(x1, x2): ({x1_sol}, {x2_sol})')\n",
"else:\n",
" print('The problem does not have an optimal solution.')"
]
},
{
"cell_type": "markdown",
"id": "baabe8e2",
"metadata": {},
"source": [
"## Exercise 34.2\n",
"\n",
"A carpenter manufactures $ 2 $ products - $ A $ and $ B $.\n",
"\n",
"Product $ A $ generates a profit of $ 23 $ and product $ B $ generates a profit of $ 10 $.\n",
"\n",
"It takes $ 2 $ hours for the carpenter to produce $ A $ and $ 0.8 $ hours to produce $ B $.\n",
"\n",
"Moreover, he can’t spend more than $ 25 $ hours per week and the total number of units of $ A $ and $ B $ should not be greater than $ 20 $.\n",
"\n",
"Find the number of units of $ A $ and product $ B $ that he should manufacture in order to maximise his profit."
]
},
{
"cell_type": "markdown",
"id": "6a9bc22f",
"metadata": {},
"source": [
"## Solution to[ Exercise 34.2](https://intro.quantecon.org/#lp_intro_ex2)\n",
"\n",
"Let us assume the carpenter produces $ x $ units of $ A $ and $ y $ units of $ B $.\n",
"\n",
"So we can formulate the problem as:\n",
"\n",
"$$\n",
"\\begin{aligned}\n",
"\\max_{x,y} \\ & z = 23 x + 10 y \\\\\n",
"\\mbox{subject to } \\ & x + y \\le 20 \\\\\n",
"& 2 x + 0.8 y \\le 25 \\\\\n",
"\\end{aligned}\n",
"$$"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "901e1a2f",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Instantiate a GLOP(Google Linear Optimization Package) solver\n",
"solver = pywraplp.Solver.CreateSolver('GLOP')"
]
},
{
"cell_type": "markdown",
"id": "56c828d8",
"metadata": {},
"source": [
"Let’s us create two variables $ x_1 $ and $ x_2 $ such that they can only have nonnegative values."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5610bf6e",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Create the two variables and let them take on any non-negative value.\n",
"x = solver.NumVar(0, solver.infinity(), 'x')\n",
"y = solver.NumVar(0, solver.infinity(), 'y')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10b7de7d",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Constraint 1: x + y <= 20.0\n",
"solver.Add(x + y <= 20.0)\n",
"\n",
"# Constraint 2: 2x + 0.8y <= 25.0\n",
"solver.Add(2 * x + 0.8 * y <= 25.0)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c0f01ae6",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Objective function: 23x + 10y\n",
"solver.Maximize(23 * x + 10 * y)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "453e03d9",
"metadata": {
"hide-output": false
},
"outputs": [],
"source": [
"# Solve the system.\n",
"status = solver.Solve()\n",
"\n",
"if status == pywraplp.Solver.OPTIMAL:\n",
" print('Maximum Profit =', solver.Objective().Value())\n",
" x_sol = round(x.solution_value(), 3)\n",
" y_sol = round(y.solution_value(), 3)\n",
" print(f'(x, y): ({x_sol}, {y_sol})')\n",
"else:\n",
" print('The problem does not have an optimal solution.')"
]
}
],
"metadata": {
"date": 1712983676.7976134,
"filename": "lp_intro.md",
"kernelspec": {
"display_name": "Python",
"language": "python3",
"name": "python3"
},
"title": "Linear Programming"
},
"nbformat": 4,
"nbformat_minor": 5
}