HW 9¶
ISE 530 Optimization for Analytics Homework IX: Integer Programming. Due 11:59 PM Tuesday November 24, 2020
You, as the leader of an oil exploration drilling venture, must determine the best selection of five out of ten possible sites. Label the sites as \(s_1, s_2, \cdots , s_{10}\) and the expected profits associated with each as \(p_1, p_2, \cdots , p_{10}\).
If site \(s_2\) is explored, then site \(s_3\) must also be explored.
Exploring sites \(s_1\) and \(s_7\) will disallow the exploration of site s8.
Exploring sites \(s_3\) or \(s_4\) will disallow the exploration of site \(s_5\).
You have \\(14,000 to invest among four different investment opportunities. Investment 1 requires an investment of \\\)7,000 and has a net present value of \\(11,000; investment 2 requires an investment of \\\)5,000 and has a net present value of \\(8,000; investment 3 requires an investment of \\\)4,000 and 4 has a net present value of \\(6,000; investment 4 requires an investment of \\\)3,000 and has a net present value of \$4,000. These are all “take it or leave it” opportunities and you are not allowed to invest partially in any of the projects. The objective is to maximize the total value given the budget constraint. Formulate this problem as a binary integer program and solve it by the branch-and-bound technique.
Solve the following two integer programs by the branch-and-bound algorithm:
Consider the integer programming problem:
Use a figure to answer the following questions.
(a) What is the optimal objective value of the linear programming relaxation? What is the optimal objective value of the integer program?
(b) What is the convex hull of the set of all feasible solution to the integer program?
(c) Construct a Gomory cutting plane using the optimal linear programming solution.
(d) Starting with the optimal linear programming solution, solve the integer program using the branch-and-bound method.
(e) Suppose you relax the first constraint. What is the optimal value \(Z_{\text{dual}}\) of the Lagrangian dual?
(f) Repeat part (e) by relaxing the third constraint.
Consider the integer program:
(a) Construct the convex hull of the feasible integer solutions and derive an optimal solution graphically.
(b) Solve the linear program relaxation.
(c) Solve by Lagrangian relaxation of the first constraint.
%load_ext autotime
%load_ext nb_black
%matplotlib inline
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
plt.rcParams["figure.dpi"] = 300
plt.rcParams["figure.figsize"] = (16, 12)
import operator
import pandas as pd
import numpy as np
import cvxpy as cp
import scipy as sp
from graphviz import Digraph
import dot2tex
from IPython.display import Latex
from sympy import *
import re
You, as the leader of an oil exploration drilling venture, must determine the best selection of five out of ten possible sites. Label the sites as \(s_1, s_2, \cdots , s_{10}\) and the expected profits associated with each as \(p_1, p_2, \cdots , p_{10}\).
If site \(s_2\) is explored, then site \(s_3\) must also be explored.
Exploring sites \(s_1\) and \(s_7\) will disallow the exploration of site \(s_8\).
Exploring sites \(s_3\) or \(s_4\) will disallow the exploration of site \(s_5\).
You have \\(14,000 to invest among four different investment opportunities. Investment 1 requires an investment of \\\)7,000 and has a net present value of \\(11,000; investment 2 requires an investment of \\\)5,000 and has a net present value of \\(8,000; investment 3 requires an investment of \\\)4,000 and has a net present value of \\(6,000; investment 4 requires an investment of \\\)3,000 and has a net present value of \$4,000. These are all “take it or leave it” opportunities and you are not allowed to invest partially in any of the projects. The objective is to maximize the total value given the budget constraint. Formulate this problem as a binary integer program and solve it by the branch-and-bound technique.
# Converts the optimization problem parameters into the mathematical representation
def solve_lp(x, constraints, obj_coeff, problem_type):
"""Solves the standard LP maximization problem and creates the
mathematical formulation of optimization label for graphviz
"""
obj = cp.Maximize(obj_coeff @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
label = (
f"{problem_type} {' + '.join([str(coeff) + 'x' + str(i+1) for coeff, i in zip(obj_coeff, range(x.shape[0]))])} "
+ "\nsubject to: \n"
+ "{}".format(
"\n".join(
[
re.sub(
r"x\[([0-9]*)\]",
"x"
+ str(
int(
re.search(
r"\[[0-9]*\]",
re.sub(r"var[0-9]*", "x", str(constraint)),
)
.group()
.replace("]", "")
.replace("[", "")
)
+ 1
),
re.sub(r"var[0-9]*", "x", str(constraint)),
)
if re.search(
r"x\[([0-9]*)\]", re.sub(r"var[0-9]*", "x", str(constraint))
)
is not None
else re.sub(r"var[0-9]*", "x", str(constraint))
for constraint in constraints
]
)
)
+ (
"\n\nObjective Value: "
+ (
str(np.round(prob.value, 2))
if prob.status == "optimal"
else str(prob.value)
)
+ "\nSolution: "
+ (
str([np.round(x_i, 2) for x_i in x.value])
if prob.status == "optimal"
else prob.status
)
)
)
return label, prob
# Checks if the number is an integer within specified decimal rounding
is_integer = lambda x, decimals=3: np.round(x, decimals).is_integer()
# Checks if all elements in array are integers
is_integer_solution = lambda x: np.alltrue([is_integer(x_i) for x_i in x])
def branch_and_bound_dfs(root, x, constraints, obj_coeff, problem_type):
"""Iterative DFS traversal of branch and bound tree"""
node_id = "root" # ID to map the node of branch and bound tree to the constraints
stack = (
[]
) # To simulate depth first traversal by appending onto stack the node's children
node_constraints = (
{}
) # A map of the node_id to constraints associated with the node
visited = [] # A list to keep the nodes already visited
stack.append((node_id, None))
node_constraints[node_id] = constraints
lb, ub = None, None # Lower bound and upper bound of ILP
while len(stack) > 0:
# Visit node
node_id, parent_node_id = stack.pop()
visited.append(node_id)
# Process Root
label, prob = solve_lp(x, node_constraints[node_id], obj_coeff, problem_type)
root.node(name=str(node_id), label=label)
# Add Edge
if node_id != "root":
root.edge(
tail_name=str(parent_node_id),
head_name=str(node_id),
label=re.sub(
r"x\[([0-9]*)\]",
"x"
+ str(
int(
re.search(
r"\[[0-9]*\]",
re.sub(
r"var[0-9]*",
"x",
str(node_constraints[node_id][-1]),
),
)
.group()
.replace("]", "")
.replace("[", "")
)
+ 1
),
re.sub(r"var[0-9]*", "x", str(node_constraints[node_id][-1])),
),
len="5",
)
if prob.status == "optimal":
# If current solution is integer
is_integer_current_solution = is_integer_solution(
np.array(list(prob.solution.primal_vars.values())).flatten()
)
# Break Conditions
if is_integer_current_solution and (
(ub is None) or (prob.value == np.floor(ub.value))
): # Optimal Integer solution found
return root, prob
# Set lower bound of the ilp
if is_integer_current_solution and (
(lb is None) or (prob.value > lb.value)
):
lb = prob
# Set upper bound of the ilp
if (ub is None) or (prob.value > ub.value):
ub = prob
if (
prob.value > lb.value if lb is not None else True
) and not is_integer_current_solution:
# Find non-integer value of solution
largest_nonint_idx, largest_nonint_val = sorted(
[
(idx, x_i)
for idx, x_i in enumerate(prob.variables()[0].value)
if not is_integer(x_i)
],
key=lambda x: x[1],
reverse=True,
)[0]
# Add left and right children nodes for processing
stack.append((node_id + " -> left", node_id))
node_constraints[node_id + " -> left"] = node_constraints[node_id] + [
x[largest_nonint_idx] >= np.ceil(largest_nonint_val)
]
stack.append((node_id + " -> right", node_id))
node_constraints[node_id + " -> right"] = node_constraints[node_id] + [
x[largest_nonint_idx] <= np.floor(largest_nonint_val)
]
return root, lb
def branch_and_bound_ilp(x, constraints, obj_coeff, problem_type):
"""Solves Pure Integer Linear Program using
Branch and Bound method.
Args:
x (cvxpy.Variable): The integer variables in our problem
constraints (List[Inequality]): List of inequality constraints to impose on problem
obj_coeff (numpy.array): The objective coefficients for a linear program
problem_type (str): Either min, or max (default - min)
Returns:
"""
assert (problem_type == "min") or (problem_type == "max"), print(
"problem_type must be either `min` or `max`."
)
# Standardize the optimization problem to be maximization
if problem_type == "min":
obj_coeff = -obj_coeff
# Run the recursive depth-first branch and bound algo
root = Digraph(
name="Branch and Bound Tree",
filename="btree.gv",
node_attr={"shape": "record", "height": ".1"},
)
root.attr("node", shape="box")
root, prob = branch_and_bound_dfs(root, x, constraints, obj_coeff, problem_type)
print(
f"Optimal Objective Value: {np.round(prob.value, 3)} | Integer Solution: {np.round(np.array(list(prob.solution.primal_vars.values())).flatten(), 3)}"
)
root.attr(label=r"\n\nBranch and Bound Tree")
root.attr(fontsize="20")
display(root)
time: 8.97 ms
x = cp.Variable((4,), integer=False)
branch_and_bound_ilp(
x=x,
constraints=[np.array([[7, 5, 4, 3]]) @ x <= np.array([14]), x >= 0, x <= 1,],
obj_coeff=np.array([11, 8, 6, 4]),
problem_type="max",
)
Optimal Objective Value: 21.0 | Integer Solution: [1. 0. 1. 1.]
time: 447 ms
# Sanity Check
x = cp.Variable((4,), integer=True)
constraints = [
np.array([[7, 5, 4, 3]]) @ x <= np.array([14]),
x >= 0,
x <= 1,
]
obj = cp.Maximize(np.array([11, 8, 6, 4]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 21.0
The optimal solution is: x = [1.0, 0.0, 1.0, 1.0]
time: 10.4 ms
Solve the following two integer programs by the branch-and-bound algorithm:
x = cp.Variable((2,), integer=False)
branch_and_bound_ilp(
x=x,
constraints=[
np.array([[1, 1], [-1, 1], [6, 2]]) @ x <= np.array([5, 0, 21]),
x >= 0,
],
obj_coeff=np.array([2, 1]),
problem_type="max",
)
Optimal Objective Value: 7.0 | Integer Solution: [3. 1.]
time: 375 ms
# Sanity Check
x = cp.Variable((2,), integer=True)
constraints = [
np.array([[1, 1], [-1, 1], [6, 2]]) @ x <= np.array([5, 0, 21]),
x >= 0,
]
obj = cp.Maximize(np.array([2, 1]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 7.0
The optimal solution is: x = [3.0, 1.0]
time: 9.45 ms
x = cp.Variable((4,), integer=False)
branch_and_bound_ilp(
x=x,
constraints=[
np.array([[1, 3, 2, 5], [3, -1, -5, 10]]) @ x <= np.array([10, -4]),
x >= 0,
],
obj_coeff=np.array([2, 1, 4, 5]),
problem_type="max",
)
Optimal Objective Value: 20.0 | Integer Solution: [2. 0. 4. 0.]
time: 419 ms
# Sanity Check
x = cp.Variable((4,), integer=True)
constraints = [
np.array([[1, 3, 2, 5], [3, -1, -5, 10]]) @ x <= np.array([10, -4]),
x >= 0,
]
obj = cp.Maximize(np.array([2, 1, 4, 5]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 20.0
The optimal solution is: x = [0.0, 0.0, 5.0, 0.0]
time: 9.07 ms
Consider the integer programming problem:
Use a figure to answer the following questions.
# Sanity Check
x = cp.Variable((2,), integer=True)
constraints = [
np.array([[-3, 4], [3, 2], [2, -1]]) @ x <= np.array([4, 11, 5]),
x >= 0,
]
obj = cp.Maximize(np.array([1, 1]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 4.0
The optimal solution is: x = [2.0, 2.0]
time: 7.59 ms
# Construct lines
x1 = np.linspace(0, 6, 2000) # x_1 >= 0
x2_1 = lambda x1: (3 * x1 + 4) / 4 # constraint 1: −3𝑥1+4𝑥2≤4
x2_2 = lambda x1: (-3 * x1 + 11) / 2 # constraint 2: 3𝑥1+2𝑥2≤11
x2_3 = lambda x1: 2 * x1 - 5 # constraint 3: 2𝑥1−𝑥2≤5
# Make plot
plt.plot(x1, x2_1(x1), label=r"$-3x_1 + 4x_2 \leq 4$")
plt.plot(x1, x2_2(x1), label=r"$3x_1 + 2x_2 \leq 11$")
plt.plot(x1, x2_3(x1), label=r"$2x_1 - x_2 \leq 5$")
plt.xlim((0, 6))
plt.ylim((0, 6))
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")
# Fill feasible region when Integer constraints are relaxed
ub = np.minimum(x2_1(x1), x2_2(x1))
lb = np.maximum(np.zeros(len(x1)), x2_3(x1))
plt.fill_between(
x1,
lb,
ub,
where=ub > lb,
color="grey",
alpha=0.5,
label="Convex Hull [feasible region (relaxed int constraints)]",
)
# Get Feasible Integer Points
feasible = lambda x: np.alltrue(
[
np.alltrue(np.array([[-3, 4], [3, 2], [2, -1]]) @ x <= np.array([4, 11, 5])),
np.alltrue(x >= 0),
]
)
feasible_integer_points = [
(i, j) for i in range(6) for j in range(6) if feasible(np.array([i, j]))
]
plt.scatter(*list(zip(*feasible_integer_points)), marker="X", c="r", s=100)
for x, y in feasible_integer_points:
label = f"({x},{y})"
plt.annotate(label, (x, y), textcoords="offset points", xytext=(0, 10), ha="center")
plt.grid(True)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0)
plt.show()
time: 1.86 s
(a) What is the optimal objective value of the linear programming relaxation? What is the optimal objective value of the integer program?
# Sanity Check
x = cp.Variable((2,), integer=False)
constraints = [
np.array([[-3, 4], [3, 2], [2, -1]]) @ x <= np.array([4, 11, 5]),
x >= 0,
]
obj = cp.Maximize(np.array([1, 1]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 4.5
The optimal solution is: x = [2.0, 2.5]
time: 10.4 ms
print(
"Optimal Objective value of Integer Program: ",
max([np.array([1, 1]) @ np.array([x1, x2]) for x1, x2 in feasible_integer_points]),
)
feasible_integer_points[
np.argmax(
[np.array([1, 1]) @ np.array([x1, x2]) for x1, x2 in feasible_integer_points]
)
]
Optimal Objective value of Integer Program: 4
(2, 2)
time: 3.23 ms
(b) What is the convex hull of the set of all feasible solution to the integer program?
print(
f"Integer points in Convex Hull of the set of all feasible solution to the integer program: {feasible_integer_points}"
)
Integer points in Convex Hull of the set of all feasible solution to the integer program: [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (3, 1)]
time: 447 µs
(c) Construct a Gomory cutting plane using the optimal linear programming solution.
A = np.array([[-3, 4, 1, 0, 0], [3, 2, 0, 1, 0], [2, -1, 0, 0, 1]])
b = np.array([4, 11, 5])
c = np.array([1, 1, 0, 0, 0])
x = cp.Variable((5,), integer=False)
constraints = [
A @ x == b,
x >= 0,
]
obj = cp.Maximize(c.T @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 4.5
The optimal solution is: x = [2.0, 2.5, 0.0, -0.0, 3.5]
time: 10.1 ms
basic_idxs = [0, 1, 4]
non_basic_idxs = [2, 3]
time: 507 µs
\({c_B}^\top B^{-1} N - c_N\)
(c[basic_idxs].T @ np.linalg.inv(A[:, basic_idxs]) @ A[:, non_basic_idxs]) - c[
non_basic_idxs
]
array([0.05555556, 0.38888889])
time: 2.7 ms
Since the negative reduced cost coefficients are >= 0 for maximization problem, solution is optimal.
\(B^{-1}N\)
(np.linalg.inv(A[:, basic_idxs]) @ A[:, non_basic_idxs])
array([[-0.11111111, 0.22222222],
[ 0.16666667, 0.16666667],
[ 0.38888889, -0.27777778]])
time: 2.59 ms
\(B^{-1}b\)
np.linalg.inv(A[:, basic_idxs]) @ b
array([2. , 2.5, 3.5])
time: 2.25 ms
\({c_B}^\top B^{-1}b\)
c[basic_idxs].T @ np.linalg.inv(A[:, basic_idxs]) @ b
4.499999999999999
time: 2.56 ms
Optimal Solution Simplex Tableau:
From the 2nd row:
LHS is always \(\geq 0\) since \(s_1\) and \(s_2\) are non-negative, hence, we add a new constraint (Gomory Cutting plane):
A = np.array(
[
[-3, 4, 1, 0, 0, 0],
[3, 2, 0, 1, 0, 0],
[2, -1, 0, 0, 1, 0],
[0, 0, *-(np.linalg.inv(A[:, basic_idxs]) @ A[:, non_basic_idxs])[1], 0, 1],
]
)
b = np.array([4, 11, 5, -0.5])
c = np.array([1, 1, 0, 0, 0, 0])
x = cp.Variable((6,), integer=False)
constraints = [
A @ x == b,
x >= 0,
]
obj = cp.Maximize(c.T @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 4.33
The optimal solution is: x = [2.33, 2.0, 3.0, 0.0, 2.33, -0.0]
time: 11.6 ms
(d) Starting with the optimal linear programming solution, solve the integer program using the branch-and-bound method.
x = cp.Variable((2,), integer=False)
branch_and_bound_ilp(
x=x,
constraints=[
np.array([[-3, 4], [3, 2], [2, -1]]) @ x <= np.array([4, 11, 5]),
x >= 0,
],
obj_coeff=np.array([1, 1]),
problem_type="max",
)
Optimal Objective Value: 4.0 | Integer Solution: [2. 2.]
time: 389 ms
(e) Suppose you relax the first constraint. What is the optimal value \(Z_{\text{dual}}\) of the Lagrangian dual?
# Construct lines
x1 = np.linspace(0, 6, 2000) # x_1 >= 0
x2_2 = lambda x1: (-3 * x1 + 11) / 2 # constraint 2: 3𝑥1+2𝑥2≤11
x2_3 = lambda x1: 2 * x1 - 5 # constraint 3: 2𝑥1−𝑥2≤5
# Make plot
plt.plot(x1, x2_2(x1), label=r"$3x_1 + 2x_2 \leq 11$")
plt.plot(x1, x2_3(x1), label=r"$2x_1 - x_2 \leq 5$")
plt.xlim((0, 6))
plt.ylim((0, 6))
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")
# Fill feasible region when Integer constraints are relaxed
ub = x2_2(x1)
lb = np.maximum(np.zeros(len(x1)), x2_3(x1))
plt.fill_between(
x1,
lb,
ub,
where=ub > lb,
color="grey",
alpha=0.5,
label="Convex Hull [feasible region (relaxed int constraints)]",
)
# Get Feasible Integer Points
feasible = lambda x: np.alltrue(
[
np.alltrue(np.array([[3, 2], [2, -1]]) @ x <= np.array([11, 5])),
np.alltrue(x >= 0),
]
)
feasible_integer_points = [
(i, j) for i in range(6) for j in range(6) if feasible(np.array([i, j]))
]
plt.scatter(*list(zip(*feasible_integer_points)), marker="X", c="r", s=100)
for x, y in feasible_integer_points:
label = f"({x},{y})"
plt.annotate(label, (x, y), textcoords="offset points", xytext=(0, 10), ha="center")
plt.grid(True)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0)
plt.show()
time: 1.63 s
print(
f"Integer points in Convex Hull X_hat of Dual Function: {feasible_integer_points}"
)
Integer points in Convex Hull X_hat of Dual Function: [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (2, 0), (2, 1), (2, 2), (3, 1)]
time: 364 µs
expr = lambda x1, x2: -x1 - x2 + symbols("λ") * (-3 * x1 + 4 * x2 - 4)
[expr(x1, x2) for x1, x2 in feasible_integer_points]
[-4*λ,
-1,
4*λ - 2,
8*λ - 3,
12*λ - 4,
16*λ - 5,
-7*λ - 1,
-3*λ - 2,
λ - 3,
5*λ - 4,
9*λ - 5,
-10*λ - 2,
-6*λ - 3,
-2*λ - 4,
-9*λ - 4]
time: 49.3 ms
t = cp.Variable((1,), integer=False)
λ = cp.Variable((1,), integer=False)
constraints = [
*[
t <= lambdify(symbols("λ"), expr(x1, x2), "numpy")(λ)
for x1, x2 in feasible_integer_points
],
λ >= 0,
]
obj = cp.Maximize(t)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Optimal Solution for Lagrangian Dual Relaxation")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value Z_dual is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: λ = {np.round(λ.value[0], 2)}")
Optimal Solution for Lagrangian Dual Relaxation
==============================
Status: optimal
The optimal value Z_dual is: -4.5
The optimal solution is: λ = 0.06
time: 36.8 ms
(f) Repeat part (e) by relaxing the third constraint.
# Construct lines
x1 = np.linspace(0, 6, 2000) # x_1 >= 0
x2_1 = lambda x1: (3 * x1 + 4) / 4 # constraint 1: −3𝑥1+4𝑥2≤4
x2_2 = lambda x1: (-3 * x1 + 11) / 2 # constraint 2: 3𝑥1+2𝑥2≤11
# Make plot
plt.plot(x1, x2_1(x1), label=r"$-3x_1 + 4x_2 \leq 4$")
plt.plot(x1, x2_2(x1), label=r"$3x_1 + 2x_2 \leq 11$")
plt.xlim((0, 6))
plt.ylim((0, 6))
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")
# Fill feasible region when Integer constraints are relaxed
ub = np.minimum(x2_1(x1), x2_2(x1))
lb = np.zeros(len(x1))
plt.fill_between(
x1,
lb,
ub,
where=ub > lb,
color="grey",
alpha=0.5,
label="Convex Hull [feasible region (relaxed int constraints)]",
)
# Get Feasible Integer Points
feasible = lambda x: np.alltrue(
[
np.alltrue(np.array([[-3, 4], [3, 2]]) @ x <= np.array([4, 11])),
np.alltrue(x >= 0),
]
)
feasible_integer_points = [
(i, j) for i in range(6) for j in range(6) if feasible(np.array([i, j]))
]
plt.scatter(*list(zip(*feasible_integer_points)), marker="X", c="r", s=100)
for x, y in feasible_integer_points:
label = f"({x},{y})"
plt.annotate(label, (x, y), textcoords="offset points", xytext=(0, 10), ha="center")
plt.grid(True)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0)
plt.show()
time: 1.64 s
print(
f"Integer points in Convex Hull X_hat of Dual Function: {feasible_integer_points}"
)
Integer points in Convex Hull X_hat of Dual Function: [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2), (3, 0), (3, 1)]
time: 367 µs
expr = lambda x1, x2: -x1 - x2 + symbols("λ") * (2 * x1 - x2 - 5)
[expr(x1, x2) for x1, x2 in feasible_integer_points]
[-5*λ, -6*λ - 1, -3*λ - 1, -4*λ - 2, -λ - 2, -2*λ - 3, -3*λ - 4, λ - 3, -4]
time: 3.95 ms
t = cp.Variable((1,), integer=False)
λ = cp.Variable((1,), integer=False)
constraints = [
*[
t <= lambdify(symbols("λ"), expr(x1, x2), "numpy")(λ)
for x1, x2 in feasible_integer_points
],
λ >= 0,
]
obj = cp.Maximize(t)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Optimal Solution for Lagrangian Dual Relaxation")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value Z_dual is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: λ = {np.round(λ.value[0], 2)}")
Optimal Solution for Lagrangian Dual Relaxation
==============================
Status: optimal
The optimal value Z_dual is: -4.0
The optimal solution is: λ = -0.0
time: 24.9 ms
Consider the integer program:
# Sanity Check
x = cp.Variable((2,), integer=True)
constraints = [
np.array([[1, 1], [1, -3], [-1, 0], [1, -1]]) @ x <= np.array([5, -4, -1, 0]),
x >= 0,
]
obj = cp.Maximize(np.array([1, 2]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 9.0
The optimal solution is: x = [1.0, 4.0]
time: 9.27 ms
# Construct lines
x1 = np.linspace(0, 5, 2000) # x_1 >= 0
x2_1 = lambda x1: -x1 + 5 # constraint 1: 𝑥1+𝑥2≤5
x2_2 = lambda x1: (x1 + 4) / 3 # constraint 2: 𝑥1−3𝑥2≤−4
x2_3 = lambda x1: [1] * len(x1) # constraint 3: x_1 >= 1
x2_4 = lambda x1: x1 # constraint 4: 𝑥1−𝑥2≤0
# Make plot
plt.plot(x1, x2_1(x1), label=r"$x_1 + x_2 \leq 5$")
plt.plot(x1, x2_2(x1), label=r"$x_1 - 3x_2 \leq -4$")
plt.plot([1] * len(x1), x1, label=r"$x_1 \geq 1$")
plt.plot(x1, x2_4(x1), label=r"$x_1 - x_2 \leq 0$")
plt.xlim((0, 5))
plt.ylim((0, 5))
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")
# Fill feasible region
ub = x2_1(np.linspace(1, 5, 2000))
lb = np.maximum(x2_2(np.linspace(1, 5, 2000)), x2_4(np.linspace(1, 5, 2000)))
plt.fill_between(
np.linspace(1, 5, 2000), lb, ub, where=ub > lb, color="grey", alpha=0.5
)
# Get Feasible Integer Points
feasible = lambda x: np.alltrue(
[
np.alltrue(
np.array([[1, 1], [1, -3], [-1, 0], [1, -1]]) @ x
<= np.array([5, -4, -1, 0])
),
np.alltrue(x >= 0),
]
)
feasible_integer_points = [
(i, j) for i in range(5) for j in range(5) if feasible(np.array([i, j]))
]
plt.scatter(*list(zip(*feasible_integer_points)), marker="X", c="r", s=100)
for x, y in feasible_integer_points:
label = f"({x},{y})"
plt.annotate(label, (x, y), textcoords="offset points", xytext=(0, 10), ha="center")
plt.grid(True)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0)
plt.show()
time: 1.55 s
(a) Construct the convex hull of the feasible integer solutions and derive an optimal solution graphically.
print(
f"Integer points in Convex Hull of the feasible integer solutions: {feasible_integer_points}"
)
Integer points in Convex Hull of the feasible integer solutions: [(1, 2), (1, 3), (1, 4), (2, 2), (2, 3)]
time: 376 µs
feasible_integer_points[
np.argmax(
[np.array([1, 2]) @ np.array([x1, x2]) for x1, x2 in feasible_integer_points]
)
]
(1, 4)
time: 1.68 ms
Going through each integer feasible solution and passing the values through the objective function, we get that the optimal objective value for the integer linear program is 9 and the solution is \(x_1 = 1, x_2 = 4\).
(b) Solve the linear program relaxation.
x = cp.Variable((2,), integer=False)
constraints = [
np.array([[1, 1], [1, -3], [-1, 0], [1, -1]]) @ x <= np.array([5, -4, -1, 0]),
x >= 0,
]
obj = cp.Maximize(np.array([1, 2]) @ x)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Linear Programming Solution")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: x = {[np.round(x_i, 2) for x_i in x.value]}")
Linear Programming Solution
==============================
Status: optimal
The optimal value is: 9.0
The optimal solution is: x = [1.0, 4.0]
time: 8.31 ms
The Linear Program gives the same solution as the Integer Linear Program because the optimal solution exists on the vertex of the convex hull of the linear program and is also integral.
(c) Solve by Lagrangian relaxation of the first constraint.
# Construct lines
x1 = np.linspace(0, 5, 2000) # x_1 >= 0
x2_2 = lambda x1: (x1 + 4) / 3 # constraint 2: 𝑥1−3𝑥2≤−4
x2_3 = lambda x1: [1] * len(x1) # constraint 3: x_1 >= 1
x2_4 = lambda x1: x1 # constraint 4: 𝑥1−𝑥2≤0
# Make plot
plt.plot(x1, x2_2(x1), label=r"$x_1 - 3x_2 \leq -4$")
plt.plot([1] * len(x1), x1, label=r"$x_1 \geq 1$")
plt.plot(x1, x2_4(x1), label=r"$x_1 - x_2 \leq 0$")
plt.xlim((0, 5))
plt.ylim((0, 5))
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")
# Fill feasible region
ub = np.array([5] * 2000)
lb = np.maximum(x2_2(np.linspace(1, 5, 2000)), x2_4(np.linspace(1, 5, 2000)))
plt.fill_between(
np.linspace(1, 5, 2000), lb, ub, where=ub > lb, color="grey", alpha=0.5
)
# Get Feasible Integer Points
feasible = lambda x: np.alltrue(
[
np.alltrue(np.array([[1, -3], [-1, 0], [1, -1]]) @ x <= np.array([-4, -1, 0])),
np.alltrue(x >= 0),
]
)
feasible_integer_points = [
(i, j) for i in range(6) for j in range(6) if feasible(np.array([i, j]))
]
plt.scatter(*list(zip(*feasible_integer_points)), marker="X", c="r", s=100)
for x, y in feasible_integer_points:
label = f"({x},{y})"
plt.annotate(label, (x, y), textcoords="offset points", xytext=(0, 10), ha="center")
plt.grid(True)
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.0)
plt.show()
time: 1.64 s
print(
f"Integer points in Convex Hull X_hat of Dual Function: {feasible_integer_points}"
)
Integer points in Convex Hull X_hat of Dual Function: [(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 3), (3, 4), (3, 5), (4, 4), (4, 5), (5, 5)]
time: 409 µs
expr = lambda x1, x2: -x1 - 2 * x2 + symbols("λ") * (x1 + x2 - 5)
[expr(x1, x2) for x1, x2 in feasible_integer_points]
[-2*λ - 5,
-λ - 7,
-9,
λ - 11,
-λ - 6,
-8,
λ - 10,
2*λ - 12,
λ - 9,
2*λ - 11,
3*λ - 13,
3*λ - 12,
4*λ - 14,
5*λ - 15]
time: 7.85 ms
t = cp.Variable((1,), integer=False)
λ = cp.Variable((1,), integer=False)
constraints = [
*[
t <= lambdify(symbols("λ"), expr(x1, x2), "numpy")(λ)
for x1, x2 in feasible_integer_points
],
λ >= 0,
]
obj = cp.Maximize(t)
prob = cp.Problem(obj, constraints)
prob.solve()
print("Optimal Solution for Lagrangian Dual Relaxation")
print("=" * 30)
print(f"Status: {prob.status}")
print(f"The optimal value Z_dual is: {np.round(prob.value, 2)}")
print(f"The optimal solution is: λ = {np.round(λ.value[0], 2)}")
Optimal Solution for Lagrangian Dual Relaxation
==============================
Status: optimal
The optimal value Z_dual is: -9.0
The optimal solution is: λ = 2.0
time: 40.9 ms
print(
"Integer Points associated with Optimal Dual Function in Convex Hull of Feasible Set of Lagrangian-relaxed problem: "
)
np.array(feasible_integer_points)[
list(
np.argwhere(
np.isclose(
[
lambdify(symbols("λ"), expr(x1, x2), "numpy")(2)
for x1, x2 in feasible_integer_points
],
np.array([-9] * len(feasible_integer_points)),
)
).flatten()
)
]
Integer Points associated with Optimal Dual Function in Convex Hull of Feasible Set of Lagrangian-relaxed problem:
array([[1, 2],
[1, 3],
[1, 4],
[1, 5]])
time: 12.1 ms