Worksheet Chapter 12, Part 3#

Convex Quadratic Optimization#

Notebook Info#

Topics#

  1. Strictly Convex Quadratic Optimization

  2. Convex Quadratic Optimization

Learning goals#

  • Use cvxpy to code both the primal and dual formulations of a strictly convex quadratic optimization problem.

  • Obtain the primal and dual solutions from a single run.

  • Use cvxpy to solve a (non-strict) convex quadratic program.

import numpy as np
import matplotlib.pyplot as plt
import cvxpy as cp

%matplotlib inline
np.set_printoptions(precision=4, suppress=True)

1. Strictly Convex Quadratic Programming#

Problem Setup#

Consider the following strictly convex quadratic program:

\[\begin{split}\begin{aligned} \text{(P)} \quad & f^* = \min_{\mathbf{x}} \quad & \mathbf{x}^T Q \mathbf{x} + 2\mathbf{c}^T \mathbf{x} \\ & \text{such that} & A\mathbf{x} \leq \mathbf{b} \end{aligned}\end{split}\]

Data:

\[\begin{split}Q = \begin{pmatrix} 2 & 0 & 0 \\ 0 & 2 & 0 \\ 0 & 0 & 1 \end{pmatrix}, \quad \mathbf{c} = \begin{pmatrix} 1 \\ 0 \\ -1 \end{pmatrix}, \quad A = \begin{pmatrix} 1 & 1 & 0 \\ 0 & 1 & 1 \end{pmatrix}, \quad \mathbf{b} = \begin{pmatrix} 2 \\ 1 \end{pmatrix}\end{split}\]

Equivalently in scalar form:

\[\begin{split}\begin{aligned} \min \quad & 2x_1^2 + 2x_2^2 + x_3^2 + 2x_1 - 2x_3 \\ \text{such that} \quad & x_1 + x_2 \leq 2 \\ & x_2 + x_3 \leq 1 \end{aligned}\end{split}\]
# Set up the data for the problem
Q_prob = np.array([[2, 0, 0], 
                   [0, 2, 0], 
                   [0, 0, 1]], dtype=float)
c_prob = np.array([1, 0, -1], dtype=float)
A_prob = np.array([[1, 1, 0], 
                   [0, 1, 1]], dtype=float)
b_prob = np.array([2, 1], dtype=float)

❓❓❓ Question ❓❓❓: Check that this problem is strictly convex by ensuring that \(Q\) is positive definite.

# your code here

Hide code cell content

# Verify that Q is positive definite
# by checking all eigenvalues are strictly positive 
# (not equal to 0)
eigvals = np.linalg.eigvals(Q_prob)
print(f"Eigenvalues of Q: {eigvals}")
print(f"Q is positive definite: {np.all(eigvals > 0)}")
Eigenvalues of Q: [2. 2. 1.]
Q is positive definite: True

Solve the Primal Problem using CVXPY#

❓❓❓ Question ❓❓❓: Use CVXPY to solve the primal problem as written above. Store the optimal value in f_star_primal and the optimal solution in x_star.

Hints:

  • The objective is: x @ Q @ x + 2 * c @ x

  • The constraint is: A @ x <= b. This needs to get passed to cp.Problem as the only entry in a list, so save it as something like constraints = [A@x<=b].

  • Use cp.Problem(cp.Minimize(...), [...]) to formulate the problem.

  • Call .solve() on the problem to solve it

  • Access the optimal value with problem.value and the solution with variable.value

# Your code here

# x = 
# objective = 
# constraints = [...]
# primal_problem = 

# primal_problem.solve()

# f_star_primal = 
# x_star = 

Hide code cell content

# Part A: Solve the primal problem
x = cp.Variable(3)
objective = x @ Q_prob @ x + 2 * c_prob @ x
constraints = [A_prob @ x <= b_prob]
primal_problem = cp.Problem(cp.Minimize(objective), constraints)

primal_problem.solve()

f_star_primal = primal_problem.value
x_star = x.value

print(f"Primal optimal value f*: {f_star_primal:.6f}")
print(f"Primal optimal solution x*:\n{x_star}")
print(f"\nPrimal problem status: {primal_problem.status}")
Primal optimal value f*: -1.500000
Primal optimal solution x*:
[-0.5  0.   1. ]

Primal problem status: optimal

Solve the Dual Problem#

From the lecture, recall that for a strictly convex QP with positive definite \(Q\), the dual problem is:

\[\begin{split}\begin{aligned} \text{(D)} \quad & q^* = \max_{\boldsymbol{\lambda}} \, q(\boldsymbol{\lambda}) \\ & \text{such that} \quad \boldsymbol{\lambda} \geq 0 \end{aligned}\end{split}\]

where the dual objective is

\[q(\boldsymbol{\lambda}) = -\boldsymbol{\lambda}^T A Q^{-1} A^T \boldsymbol{\lambda} - 2(AQ^{-1}\mathbf{c} + \mathbf{b})^T \boldsymbol{\lambda} - \mathbf{c}^T Q^{-1} \mathbf{c}.\]

❓❓❓ Question ❓❓❓: We’re going to break up the dual objective functions into pieces.

  1. Compute \(Q^{-1}\) and store it in Q_inv.

  2. Compute the three components of the dual objective:

    • \(H = A Q^{-1} A^T\) (store as H_dual)

    • \(g = AQ^{-1}\mathbf{c} + \mathbf{b}\) (store as g_dual)

    • \(\mathbf{c}^T Q^{-1} \mathbf{c}\) (store as constant_term)

# Your code here 

Hide code cell content

# inverse of Q_prob
Q_inv = np.linalg.inv(Q_prob)

# H_dual = A Q^{-1} A^T
H_dual = A_prob @ Q_inv @ A_prob.T

# g_dual = A Q^{-1} c + b
g_dual = A_prob @ Q_inv @ c_prob + b_prob

# constant term = c^T Q^{-1} c
constant_term = c_prob @ Q_inv @ c_prob

print("Q^{-1}:")
print(Q_inv)
print(f"\nH_dual = A Q^{{-1}} A^T:\n{H_dual}")
print(f"\ng_dual = A Q^{{-1}} c + b:\n{g_dual}")
print(f"\nConstant term c^T Q^{{-1}} c: {constant_term:.6f}")
Q^{-1}:
[[0.5 0.  0. ]
 [0.  0.5 0. ]
 [0.  0.  1. ]]

H_dual = A Q^{-1} A^T:
[[1.  0.5]
 [0.5 1.5]]

g_dual = A Q^{-1} c + b:
[2.5 0. ]

Constant term c^T Q^{-1} c: 1.500000

❓❓❓ Question ❓❓❓: We will now set up the objective function, which with our replacements above is

\[q(\boldsymbol{\lambda}) = -\boldsymbol{\lambda}^T H \boldsymbol{\lambda} - 2 g^T \boldsymbol{\lambda} - e,\]

where \(H =\) H_dual, \(g =\) g_dual, and \(e = \) constant_term.

Setup and solve the problem using cvxpy. To do this:

  1. Set up the variables \(\lambda\) as lam. Remeber that Python reserves the word lambda so do not save the variables as this!

  2. Formulate the dual objective. Be sure to use cp.quad_form(lam, H_dual) to represent \(\boldsymbol{\lambda}^T H \boldsymbol{\lambda}\) for the first term so that cvxpy recognizes it as a convex function.

  3. Add the constraints \(\boldsymbol{\lambda} \geq 0\).

  4. Create and solve the dual problem using cp.Problem(cp.Maximize(...), [...]). Save the problem as dual_problem so we can use it later.

  5. Store the dual optimal value in q_star_dual and the optimal dual variables in lam_star

# your code here

Hide code cell content

# Solve the dual problem

# Set up variables
lam = cp.Variable(2)  # Lagrange multipliers for the inequality constraints

# Students will try to do this but it won't work: 
# dual_objective = -lam.T@H_dual@lam - 2 * g_dual @ lam - constant_term
# Use cp.quad_form() for proper quadratic form handling in CVXPY
dual_objective = -cp.quad_form(lam, H_dual) - 2 * g_dual @ lam - constant_term

constraints_dual = [lam >= 0]
dual_problem = cp.Problem(cp.Maximize(dual_objective), constraints_dual)

dual_problem.solve()

q_star_dual = dual_problem.value
lam_star = lam.value

print(f"Dual optimal value q*: {q_star_dual:.6f}")
print(f"Dual optimal solution λ*:\n{lam_star}")
print(f"\nDual problem status: {dual_problem.status}")
Dual optimal value q*: -1.500000
Dual optimal solution λ*:
[0. 0.]

Dual problem status: optimal

Comparison#

Strong duality tells us that for a strictly convex problem, the primal and dual optimal values must be equal (\(f^* = q^*\)).

❓❓❓ Question ❓❓❓: Check that your solutions from above, f_star_primal and q_star_dual are the same.

# Your code here 

Hide code cell content

print("="*60)
print("STRONG DUALITY CHECK")
print("="*60)
print(f"Primal optimal value f*:  {f_star_primal:.10f}")
print(f"Dual optimal value q*:    {q_star_dual:.10f}")
print(f"Difference |f* - q*|:     {abs(f_star_primal - q_star_dual):.2e}")
print(f"Match? {np.isclose(f_star_primal, q_star_dual)}\n")
============================================================
STRONG DUALITY CHECK
============================================================
Primal optimal value f*:  -1.5000000000
Dual optimal value q*:    -1.5000000000
Difference |f* - q*|:     0.00e+00
Match? True

It turns out that cvxpy was actually solving both the primal and dual for you in the background when you were solving the primal problem. From the primal problem, you saved your constraints as a list constraints = [...].

constraints
[Inequality(Expression(AFFINE, UNKNOWN, (2,)))]

These constraints save the dual value.

# Extract dual variables from primal solve
lam_primal = np.array([constraints[i].dual_value for i in range(len(constraints))])
print(f"λ from primal solve: {lam_primal}")
λ from primal solve: [[0. 0.]]

❓❓❓ Question ❓❓❓: Check that the \(\lambda\) found in the primal problem and the ones you calculated directly using the dual problem are the same.

# Your code or notes here

Hide code cell content

print("="*60)
print("DUAL VARIABLES FROM PRIMAL SOLVE vs. DUAL SOLVE")
print("="*60)
lam_primal = np.array([constraints[i].dual_value for i in range(len(constraints))])
print(f"λ from primal solve: {lam_primal}")
print(f"λ from dual solve:   {lam_star}")
print(f"Match? {np.allclose(lam_primal, lam_star)}\n")
============================================================
DUAL VARIABLES FROM PRIMAL SOLVE vs. DUAL SOLVE
============================================================
λ from primal solve: [[0. 0.]]
λ from dual solve:   [0. 0.]
Match? True

2. Convex Quadratic Programming#

Now consider a convex (not necessarily strictly convex) quadratic program in the same notation as the slides:

\[\begin{split}\begin{aligned} \text{(P2)}\quad & \min_{\mathbf{x}} \quad & \mathbf{x}^T Q\mathbf{x} + 2\mathbf{c}^T\mathbf{x} \\ & \text{such that} & A\mathbf{x} \leq \mathbf{b} \end{aligned}\end{split}\]

For this example, use

\[\begin{split}Q = \begin{pmatrix}2&0&0\\0&0&0\\0&0&1\end{pmatrix},\quad \mathbf{c} = \begin{pmatrix}-1\\1\\0\end{pmatrix},\end{split}\]
\[\begin{split}A = \begin{pmatrix}1&1&0\\-1&0&1\\0&1&1\\0&-1&0\end{pmatrix},\quad \mathbf{b}=\begin{pmatrix}2\\1\\2\\1\end{pmatrix}.\end{split}\]

Notice: \(Q\succeq 0\) (positive semidefinite), so this is convex, but not strictly convex.

# Data for convex QP (Q is PSD, not PD)
Q2_prob = np.array([[2, 0, 0],
                    [0, 0, 0],
                    [0, 0, 1]], dtype=float)
c2_prob = np.array([-1, 1, 0], dtype=float)
A2_prob = np.array([[1, 1, 0],
                    [-1, 0, 1],
                    [0, 1, 1],
                    [0, -1, 0]], dtype=float)
b2_prob = np.array([2, 1, 2, 1], dtype=float)

Solving original problem#

❓❓❓ Question ❓❓❓: Setup and solve this original convex QP using cvxpy.

\[\min_{\mathbf{x}}\ \mathbf{x}^TQ\mathbf{x}+2\mathbf{c}^T\mathbf{x}\quad\text{s.t. }A\mathbf{x}\le\mathbf{b}.\]

Save the value/optimizer as f_star_convex, x_star_convex. What is the dual solution?

#Your code here

Hide code cell content

# Original convex QP
x2 = cp.Variable(3)

# Note for this example, the following will work but it's better to do the quad_form
# objective2 = x2 @ Q2_prob @ x2 + 2 * c2_prob @ x2
objective2 = cp.quad_form(x2, Q2_prob) + 2 * c2_prob @ x2

constraints2 = [A2_prob @ x2 <= b2_prob]
convex_problem = cp.Problem(cp.Minimize(objective2), constraints2)
convex_problem.solve()

f_star_convex = convex_problem.value
x_star_convex = x2.value

print(f"Convex QP optimal value f*: {f_star_convex:.6f}")
print(f"Convex QP optimal solution x*:\n{x_star_convex}")
print(f"\nConvex QP problem status: {convex_problem.status}\n")

# Get the dual variables for the convex QP
lam_convex = np.array([constraints2[i].dual_value for i in range(len(constraints2))])
print(f"Dual variables (λ) from convex QP primal solve: {lam_convex}")
Convex QP optimal value f*: -2.500000
Convex QP optimal solution x*:
[ 0.5 -1.   0. ]

Convex QP problem status: optimal

Dual variables (λ) from convex QP primal solve: [[0. 0. 0. 2.]]

Solving the modified original problem#

Ok, so you already know from above that the dual problem has been solved, but let’s try to set up the dual problem directly to see the results. To do this, first we need to find a matrix \(D\) such that \(D^\top D = Q\) where we’ve saved \(Q =\) Q2_prob. We can do this using spectral factorization.

eigvals2 = np.linalg.eigvals(Q2_prob)
print(f"Eigenvalues of Q2: {eigvals2}")
print(f"Q2 is PSD (convex): {np.all(eigvals2 >= -1e-10)}")
print(f"Q2 is PD (strictly convex): {np.all(eigvals2 > 1e-10)}")

# Build D so that Q = D^T D using spectral factorization
w, V = np.linalg.eigh(Q2_prob)
w = np.clip(w, 0.0, None)
D2 = np.diag(np.sqrt(w)) @ V.T
Eigenvalues of Q2: [2. 0. 1.]
Q2 is PSD (convex): True
Q2 is PD (strictly convex): False

Check ||D^T D - Q||_F: 4.44e-16

❓❓❓ Question ❓❓❓: Check that \(Q=D^T D\).

# Your code here

Hide code cell content

print(f"D2^T D2:\n{D2.T @ D2}")
print(f"Q2_prob:\n{Q2_prob}")
D2^T D2:
[[2. 0. 0.]
 [0. 0. 0.]
 [0. 0. 1.]]
Q2_prob:
[[2. 0. 0.]
 [0. 0. 0.]
 [0. 0. 1.]]

❓❓❓ Question ❓❓❓: Now, set up and solve the reformulated primal problem

\[\begin{split} \begin{aligned} & \text{min} & & \|\mathbf{z}\|^2 + 2 \mathbf{c}^{\top} \mathbf{x} \\ & \text{such that} & & A \mathbf{x} \leq \mathbf{b}, \\ & & & \mathbf{z} = D \mathbf{x}. \end{aligned} \end{split}\]

Save the results as f_star_reform, x_star_reform, z_star_reform. Did you get the same answer as the original problem version?

# Your code here

Hide code cell content

# Reformulated primal with z = D x
x2_reform = cp.Variable(3)
z2_reform = cp.Variable(3)
objective2_reform = cp.sum_squares(z2_reform) + 2 * c2_prob @ x2_reform
constraints2_reform = [A2_prob @ x2_reform <= b2_prob, z2_reform == D2 @ x2_reform]
reform_problem = cp.Problem(cp.Minimize(objective2_reform), constraints2_reform)
reform_problem.solve()

f_star_reform = reform_problem.value
x_star_reform = x2_reform.value
z_star_reform = z2_reform.value

print(f"Reformulated primal optimal value f*: {f_star_reform:.6f}")
print(f"Reformulated primal optimal solution x*:\n{x_star_reform}")
print(f"Reformulated primal optimal solution z*:\n{z_star_reform}")
print(f"\nReformulated primal problem status: {reform_problem.status}\n")
Reformulated primal optimal value f*: -2.500000
Reformulated primal optimal solution x*:
[ 0.5 -1.   0. ]
Reformulated primal optimal solution z*:
[0.     0.     0.7071]

Reformulated primal problem status: optimal

Solving the dual problem#

Ok, so finally, we can solve the dual problem for the reformulated version. ❓❓❓ Question ❓❓❓: Set up and solve the convex-QP dual using cvxpy.

\[\max_{\lambda,\mu}\ -\|\mu\|^2-2\mathbf{b}^T\lambda\]
\[\text{s.t. }\mathbf{c}+A^T\lambda-D^T\mu=0,\ \lambda\ge0,\ \mu\in\mathbb{R}^n.\]

Save the dual value as q_star_convex_dual.

# Your code here

Hide code cell content

# Dual for convex QP
lam2 = cp.Variable(4, nonneg=True)
mu2 = cp.Variable(3)
dual_obj2 = -cp.sum_squares(mu2) - 2 * b2_prob @ lam2
dual_constraints2 = [c2_prob + A2_prob.T @ lam2 - D2.T @ mu2 == 0]
dual_problem2 = cp.Problem(cp.Maximize(dual_obj2), dual_constraints2)
dual_problem2.solve()

q_star_convex_dual = dual_problem2.value
print(f"Convex QP dual optimal value q*: {q_star_convex_dual:.6f}")
Convex QP dual optimal value q*: -2.500000

Compare#

Now that you’ve solved all the different versions, run the following code. Do you get the same answer every time?

print("="*60)
print("CONVEX QP VIA REFORMULATION + DUAL")
print("="*60)

print(f"\nOriginal primal value f*:   {f_star_convex:.8f}")
print(f"Reformulated value f*:      {f_star_reform:.8f}")
print(f"Dual value q*:              {q_star_convex_dual:.8f}")

print(f"\n|f_original - f_reform|:    {abs(f_star_convex - f_star_reform):.2e}")
print(f"|f_reform - q_dual|:        {abs(f_star_reform - q_star_convex_dual):.2e}")

print(f"\nOriginal x*:                {x_star_convex}")
print(f"Reformulated x*:            {x_star_reform}")
print(f"Reformulated z* (D x*):     {z_star_reform}")
print(f"D x* from reformulated x*:  {D2 @ x_star_reform}")
============================================================
CONVEX QP VIA REFORMULATION + DUAL
============================================================

Original primal value f*:   -2.50000000
Reformulated value f*:      -2.50000000
Dual value q*:              -2.50000000

|f_original - f_reform|:    0.00e+00
|f_reform - q_dual|:        0.00e+00

Original x*:                [ 0.5 -1.   0. ]
Reformulated x*:            [ 0.5 -1.   0. ]
Reformulated z* (D x*):     [0.     0.     0.7071]
D x* from reformulated x*:  [0.     0.     0.7071]

© Copyright 2026, The Department of Computational Mathematics, Science and Engineering at Michigan State University.