Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Your First Optimization Problem

Let’s solve your first optimization problem in Pyomo.

Cloud Computing with Google Colab

We will include the following code at the top of our notebooks to configure Google Colab.

## Tip: Please put code like this at the top of your notebook.
# We want all of the module/package installations to start up front

import sys

if "google.colab" in sys.modules:
    !wget "https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py"
    import helper

    helper.easy_install()
else:
    sys.path.insert(0, "../")
    import helper
helper.set_plotting_style()

# `helper` also provides the extract / archive / figure plumbing used below.

What does this code do? If we run it on Google Colab, the code first downloads helper.py. This small utility then helps us install Pyomo and the needed solvers (often via IDAES).

Mathematical Model

Let’s start with a purely mathematical example:

minxx12+2x22x3s.t.x1+x2=1x1+2x2x3=510x1,x2,x310\begin{align*} \min_{x} \quad & x_1^2 + 2 x_2^2 - x_3 \\ \mathrm{s.t.} \quad & x_1 + x_2 = 1 \\ & x_1 + 2 x_2 - x_3 = 5 \\ & -10 \leq x_1, x_2, x_3 \leq 10 \end{align*}

We want to solve the constrained optimization problem numerically.

Define the Model in Pyomo

Activity

Fill in the missing constraint.
import pyomo.environ as pyo

# Create instance of concrete Pyomo model.
# concrete means all of the sets and model data are specified at the time of model construction.
# In this class, you'll use a concrete model.
m = pyo.ConcreteModel()

## Declare variables with initial values with bounds
m.x1 = pyo.Var(initialize=1, bounds=(-10, 10))
m.x2 = pyo.Var(initialize=1, bounds=(-10, 10))
m.x3 = pyo.Var(initialize=1, bounds=(-10, 10))

## Declare objective
m.OBJ = pyo.Objective(expr=m.x1**2 + 2 * m.x2**2 - m.x3, sense=pyo.minimize)

## Declare equality constraints
m.h1 = pyo.Constraint(expr=m.x1 + m.x2 == 1)

# Add your solution here

## Display model
m.pprint()
Click to see the solution to the activity
m.h2 = pyo.Constraint(expr=m.x1 + 2 * m.x2 - m.x3 == 5)

Solve using Ipopt

Toward the end of the semester we will learn, in perhaps more detail than you care, what makes Ipopt work under the hood. For now, we’ll use it as a computational tool.

opt1 = pyo.SolverFactory("ipopt")
status1 = opt1.solve(m, tee=True)
assert pyo.check_optimal_termination(status1), (
    f"Solve failed: status={status1.solver.status}, "
    f"termination={status1.solver.termination_condition}"
)
Ipopt 3.14.19: 

******************************************************************************
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
******************************************************************************

This is Ipopt version 3.14.19, running with linear solver MUMPS 5.8.2.

Number of nonzeros in equality constraint Jacobian...:        5
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:        2

Total number of variables............................:        3
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        3
                     variables with only upper bounds:        0
Total number of equality constraints.................:        2
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  2.0000000e+00 3.00e+00 3.33e-01  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  4.3065612e+00 0.00e+00 2.89e-01  -1.0 4.36e+00    -  6.72e-01 1.00e+00h  1
   2  4.2501103e+00 8.88e-16 1.34e-16  -1.0 1.31e-01    -  1.00e+00 1.00e+00f  1
   3  4.2500000e+00 0.00e+00 4.32e-16  -2.5 6.02e-03    -  1.00e+00 1.00e+00f  1
   4  4.2500000e+00 0.00e+00 1.14e-16  -3.8 4.41e-05    -  1.00e+00 1.00e+00f  1
   5  4.2500000e+00 0.00e+00 1.31e-16  -5.7 1.98e-06    -  1.00e+00 1.00e+00f  1
   6  4.2500000e+00 0.00e+00 3.08e-16  -8.6 2.45e-08    -  1.00e+00 1.00e+00f  1

Number of Iterations....: 6

                                   (scaled)                 (unscaled)
Objective...............:   4.2500000000000000e+00    4.2500000000000000e+00
Dual infeasibility......:   3.0826231515081323e-16    3.0826231515081323e-16
Constraint violation....:   0.0000000000000000e+00    0.0000000000000000e+00
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   2.5059105039901454e-09    2.5059105039901454e-09
Overall NLP error.......:   2.5059105039901454e-09    2.5059105039901454e-09


Number of objective function evaluations             = 7
Number of objective gradient evaluations             = 7
Number of equality constraint evaluations            = 7
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 7
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 6
Total seconds in IPOPT                               = 0.022

EXIT: Optimal Solution Found.


Inspect the Solution

Now let’s inspect the solution. We’ll use the function value() to extract the numeric value from the Pyomo variable object.

## Return the solution
print("x1 = ", pyo.value(m.x1))
print("x2 = ", pyo.value(m.x2))
print("x3 = ", pyo.value(m.x3))
print("\n")
x1 =  0.49999999996668265
x2 =  0.5000000000333173
x3 =  -3.4999999999666827


Visualize the Solution

Is our answer correct?

We can solve this optimization problem with guess and check. If we guess x3x_3, we can then solve the constraints for x1x_1 and x2x_2:

Constraints:

x1+x2=1x1+2x2x3=5\begin{align*} & x_1 + x_2 = 1 \\ & x_1 + 2 x_2 - x_3 = 5 \end{align*}
[1112][x1x2]=[15+x3]\begin{bmatrix} 1 & 1 \\ 1 & 2 \end{bmatrix} \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} 1 \\ 5 + x_3 \end{bmatrix}

We can then evaluate the objective. Let’s see the graphical solution to our optimization problem.

Notice the shape of what we are about to draw. Three variables minus two equality constraints leaves exactly one degree of freedom, so the entire feasible set is a curve, and we can plot the objective along it. That is a luxury of a tiny problem: with 26,280 variables there is no curve to look at, which is why we need a solver at all.

Activity

Verify you agree with how to translate the two linear constraints into a linear system of equations.
# ---------- EXTRACT --------------------------------------------------------
# Guess and check: sweep x3, solve the constraints for (x1, x2), evaluate the
# objective. Nothing is plotted here -- this cell only produces numbers.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd


def constraints(x3):
    """Solve the linear constraints for x1 and x2, given a guess of x3.

    Args:
        x3: Value for the decision variable x3

    Returns:
        x1 and x2: Values calculated from the constraints
    """

    # Define the matrices in the above equations
    A = np.array([[1, 1], [1, 2]])
    b = np.array([1, 5 + x3])

    # Solve the linear system of equations
    z = np.linalg.solve(A, b)
    x1 = z[0]
    x2 = z[1]
    return x1, x2


def objective(x1, x2, x3):
    """Evaluate the objective function."""
    return x1**2 + 2 * x2**2 - x3


# Guess many values of x3.
x3_guesses = np.linspace(-10, 4, 29)
obj = [objective(*constraints(x3), x3) for x3 in x3_guesses]

# Everything the figure needs, as plain Python: the guess-and-check curve, and
# the point Pyomo/Ipopt found in the cells above.
results = {
    "sweep": helper.table(pd.DataFrame({"x3": x3_guesses, "obj": obj})),
    "solution": helper.extract(m, x1=m.x1, x2=m.x2, x3=m.x3, obj=m.OBJ),
}

# ---------- ARCHIVE --------------------------------------------------------
# figures/results/guess-and-check.json, committed to the repository, so the
# figure can be re-rendered without Pyomo or a solver. A no-op on Colab.
helper.save_results(
    "guess-and-check",
    results,
    notebook="notebooks/1-dev/Pyomo-Introduction.ipynb",
    description="Objective versus x3 along the one-dimensional feasible set of "
    "the first NLP, with the Pyomo/Ipopt solution marked.",
    solver="Ipopt via Pyomo",
)

print("x3 sweep:", len(x3_guesses), "points")
print("Pyomo solution:", results["solution"])
[pyomo_results] wrote figures/results/guess-and-check.json
x3 sweep: 29 points
Pyomo solution: {'x1': 0.49999999996668265, 'x2': 0.5000000000333173, 'x3': -3.4999999999666827, 'obj': 4.25}
# ---------- PLOT -----------------------------------------------------------
# The plotting function takes the EXTRACTED results, not the Pyomo model. It
# needs no solver, so you can re-run this one cell as many times as it takes to
# get the labels where you want them.
#
# This cell is tagged `figure:guess-and-check`, which makes it the single
# source of the figure that also appears in the lecture handout: there is no
# second script re-deriving the curve somewhere else. See figures/README.md.


def plot_guess_and_check(results):
    """Objective versus x3 along the feasible curve, with the solver's answer marked."""
    sweep = helper.as_dataframe(results["sweep"])
    sln = results["solution"]

    fig, ax = plt.subplots(figsize=(6.4, 4.6))

    ax.plot(sweep["x3"], sweep["obj"], zorder=2)
    ax.plot(
        [sln["x3"]],
        [sln["obj"]],
        marker="o",
        markersize=13,
        linestyle="",
        markerfacecolor="none",
        markeredgewidth=3,
        zorder=3,
    )

    # Direct labelling rather than a legend: a legend keyed only by colour is
    # unreadable in black and white, and both labels fit in empty parts of the
    # axes. See figures/README.md, "Course additions".
    ax.annotate("guess and check", xy=(-8.8, 122.0), fontsize=14, ha="left")
    ax.annotate(
        "solver\n" + f"$x_3 = {sln['x3']:.2f}$, $f = {sln['obj']:.2f}$",
        xy=(sln["x3"], sln["obj"]),
        xytext=(sln["x3"], 42.0),
        fontsize=14,
        ha="center",
        va="bottom",
        arrowprops=dict(arrowstyle="->", lw=1.5, color="black"),
    )

    ax.set_xlabel("$x_3$")
    ax.set_ylabel("$f(x)$")
    ax.set_xlim(-10.0, 4.0)
    ax.set_ylim(bottom=0.0)

    fig.tight_layout()
    return fig


fig = plot_guess_and_check(results)

# Writes media/figures/guess-and-check.{png,pdf} at 300 dpi. A no-op on Colab.
helper.save_figure(fig, "guess-and-check");
[pyomo_results] wrote media/figures/guess-and-check.png and .pdf
<Figure size 640x460 with 1 Axes>

An exact check: eliminate the constraints

The picture is convincing, but we can do better than convincing. With only one degree of freedom we can solve this problem in closed form and check the solver against algebra rather than against a plot.

Subtracting the first constraint from the second gives x2x3=4x_2 - x_3 = 4, so

x2=4+x3,x1=1x2=3x3.x_2 = 4 + x_3, \qquad x_1 = 1 - x_2 = -3 - x_3 .

Substituting into the objective leaves a function of x3x_3 alone:

F(x3)=(3+x3)2+2(4+x3)2x3=3x32+21x3+41.F(x_3) = (3 + x_3)^2 + 2 (4 + x_3)^2 - x_3 = 3 x_3^2 + 21 x_3 + 41 .

This is a parabola opening upward — the curve plotted above — so its single stationary point is the minimum:

F(x3)=6x3+21=0x3=72,F(72)=174=4.25,F'(x_3) = 6 x_3 + 21 = 0 \quad \Longrightarrow \quad x_3 = -\tfrac{7}{2}, \qquad F\left(-\tfrac{7}{2}\right) = \tfrac{17}{4} = 4.25 ,

with x1=x2=12x_1 = x_2 = \tfrac{1}{2}. All three values lie strictly inside [10,10][-10, 10], so no bound is active and this really is the constrained minimum.

Later in the semester we will see why this trick does not scale: eliminating the constraints requires solving them, which is easy for a 2×22 \times 2 linear system and impossible in general.

# Check the solver against the algebra above, not just against the picture.
x3_star = -7 / 2
assert abs(pyo.value(m.x3) - x3_star) < 1e-6, "x3 does not match the closed-form solution"
assert abs(pyo.value(m.x1) - 0.5) < 1e-6, "x1 does not match the closed-form solution"
assert abs(pyo.value(m.x2) - 0.5) < 1e-6, "x2 does not match the closed-form solution"
assert abs(pyo.value(m.OBJ) - 17 / 4) < 1e-6, "objective does not match 17/4"

print("Closed form:  x1 = 1/2, x2 = 1/2, x3 = -7/2, f = 17/4 = 4.25")
print(
    f"Ipopt:        x1 = {pyo.value(m.x1):.6f}, x2 = {pyo.value(m.x2):.6f}, "
    f"x3 = {pyo.value(m.x3):.6f}, f = {pyo.value(m.OBJ):.6f}"
)
Closed form:  x1 = 1/2, x2 = 1/2, x3 = -7/2, f = 17/4 = 4.25
Ipopt:        x1 = 0.500000, x2 = 0.500000, x3 = -3.500000, f = 4.250000