Objectives, Decision Variables, Constraints, and Feasibility › Visualizing the Feasible Set and the Optimality Gap
Machine Learning from ScratchAll chapters Objectives, Decision Variables, Constraints, and Feasibility › Visualizing the Feasible Set and the Optimality GapSection view · Sections in this chapter5 / 10 ↓
Reading tools Definitions, links & notes
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
With two decision variables we can draw the entire problem, and the picture is a useful antidote to abstract notation. Each inequality defines a half-plane, its boundary line is where the constraint is tight, and the feasible set is the intersection of these half-planes with the non-negative quadrant. The objective is constant along lines perpendicular to , so we can draw several iso-profit lines and watch which one is the last to touch the region.
PythonIn [3]Expand
A_demo = np.array([[2.0, 1.0], [1.0, 2.0]])
b_demo = np.array([8.0, 8.0])
c_demo = np.array([40.0, 30.0])
x1_grid = np.linspace(0.0, 4.0, 400)
machine_line = 8.0 - 2.0 * x1_grid
labour_line = (8.0 - x1_grid) / 2.0
ceiling = np.minimum(machine_line, labour_line)
x_opt = np.linalg.solve(A_demo, b_demo)PythonIn [4]Expand
from book_plot_style import PALETTE, label_bars, polish_axes, use_book_style
use_book_style()
C = [PALETTE[key] for key in ("primary", "teal", "amber", "purple")]
fig = plt.figure(figsize=(6.0, 4.8))
ax = fig.add_subplot(111)
ax.fill_between(
x1_grid, 0.0, ceiling, color=C[0], alpha=0.15, label="feasible set"
)
ax.plot(
x1_grid,
machine_line,
color=C[0],
lw=1.1,
label=r"$2x_1 + x_2 = 8$ (machine)",
)
ax.plot(
x1_grid, labour_line, color=C[1], lw=1.1, label=r"$x_1 + 2x_2 = 8$ (labour)"
)
for level in (60.0, 120.0, 180.0, 240.0):
ax.plot(
x1_grid,
(level - c_demo[0] * x1_grid) / c_demo[1],
color=C[3],
lw=1.0,
ls=":",
label="iso-profit lines" if level == 60.0 else None,
)
ax.scatter(*x_opt, s=110, color=C[2], zorder=5, label=r"optimum $(8/3,\,8/3)$")
ax.set_xlim(0, 5)
ax.set_ylim(0, 5)
ax.set_xlabel(r"$x_1$ (pallets of product 1)")
ax.set_ylabel(r"$x_2$ (pallets of product 2)")
ax.set_title("Feasible set and iso-profit contours")
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.20), ncol=2)
polish_axes(ax)
plt.show()VisualizationOut [4]Expand

Two features of the figure carry most of the intuition. The feasible set is a polygon with four corners: the origin, the two axis intercepts at (4, 0) and (0, 4), and the intersection of the two resource lines. And the iso-profit lines all have the same slope, so pushing the profit level up slides the line northeast until it either exits the polygon through a corner or runs along an edge. Here it exits through the corner where both resources are exhausted, so this bounded polygon has an optimal vertex. A whole edge can tie in another linear example.
The next figure restricts production to whole pallets. The feasible decisions become the integer lattice points inside the continuous polygon. The best integer profit is 180, while the LP relaxation reaches . Their difference is this instance's additive integrality gap. It differs from a MILP solver's final search gap: after proving the integer optimum, the solver can report a zero search gap even though the initial LP relaxation remains fractional.
PythonIn [5]Expand
lattice = np.array(list(itertools.product(range(5), repeat=2)), dtype=float)
inside = np.all(lattice @ A_demo.T <= b_demo + 1e-12, axis=1)
lattice_profit = lattice @ c_demo
best_point = lattice[inside][np.argmax(lattice_profit[inside])]
best_integer_profit = float(lattice_profit[inside].max())PythonIn [6]Expand
from book_plot_style import PALETTE, label_bars, polish_axes, use_book_style
use_book_style()
C = [PALETTE[key] for key in ("primary", "teal", "amber", "purple")]
fig = plt.figure(figsize=(6.0, 4.8))
ax = fig.add_subplot(111)
ax.fill_between(
x1_grid, 0.0, ceiling, color=C[0], alpha=0.15, label="LP relaxation"
)
ax.plot(x1_grid, machine_line, color=C[0], lw=1.1)
ax.plot(x1_grid, labour_line, color=C[1], lw=1.1)
ax.scatter(
lattice[inside, 0],
lattice[inside, 1],
s=55,
color=C[1],
label="feasible integer plans",
zorder=4,
)
ax.scatter(
lattice[~inside, 0],
lattice[~inside, 1],
s=45,
marker="x",
color=C[3],
label="infeasible lattice points",
)
ax.scatter(
*best_point,
s=150,
facecolors="none",
edgecolors=C[2],
lw=1.1,
label=f"best integer {tuple(map(int, best_point))}, profit {best_integer_profit:.0f}",
)
ax.scatter(
*x_opt,
s=110,
marker="x",
color=C[2],
zorder=6,
label="LP optimum (fractional)",
)
ax.set_xlim(0, 5)
ax.set_ylim(0, 5)
ax.set_xlabel(r"$x_1$ (pallets of product 1)")
ax.set_ylabel(r"$x_2$ (pallets of product 2)")
ax.set_title("Integer decisions versus the LP relaxation")
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.20), ncol=2)
polish_axes(ax)
plt.show()VisualizationOut [6]Expand

The third figure isolates the difference between a hard constraint and a soft penalty on a one-dimensional problem: minimize subject to . The unconstrained minimizer is , which violates the rule by . Using the positive-part convention, the penalized objective is , where is the quadratic penalty weight on any violation of , and is the number of units by which exceeds the limit. For this equals , and for it is just . On the region the first-order condition solves to
where:
- : the penalized minimizer for penalty weight
- : the quadratic penalty weight on the violation of
- : the scalar decision variable in the one-dimensional example
- : the unconstrained minimizer of
- : the boundary of the hard constraint
The minimizer equals at , at , and at . The minimizer approaches the boundary from the wrong side and never crosses it.
PythonIn [7]Expand
target, limit = 1.5, 1.0
xs = np.linspace(0.3, 1.9, 500)
penalty_curves = []
for lam in (0.5, 2.0, 10.0):
star = (target + lam * limit) / (1.0 + lam)
star_val = (star - target) ** 2 + lam * max(0.0, star - limit) ** 2
curve = (xs - target) ** 2 + lam * np.maximum(0.0, xs - limit) ** 2
penalty_curves.append((lam, star, star_val, curve))PythonIn [8]Expand
from book_plot_style import PALETTE, label_bars, polish_axes, use_book_style
use_book_style()
C = [PALETTE[key] for key in ("primary", "teal", "amber", "purple")]
fig = plt.figure(figsize=(6.0, 4.8))
ax = fig.add_subplot(111)
ax.plot(
xs,
(xs - target) ** 2,
color=C[0],
lw=1.1,
label=r"unconstrained $(x-1.5)^2$",
)
for index, (lam, star, star_val, curve) in enumerate(penalty_curves):
col = C[index + 1]
ax.plot(
xs, curve, color=col, lw=1.1, label=rf"penalised, $\lambda={lam:g}$"
)
ax.scatter(star, star_val, color=col, s=60, zorder=5)
ax.axvline(
limit,
color=PALETTE["muted"],
ls="--",
lw=1.2,
label=r"constraint $x \leq 1$",
)
ax.set_xlim(0.3, 1.9)
ax.set_ylim(0, 2.0)
ax.set_xlabel(r"decision $x$")
ax.set_ylabel("objective value")
ax.set_title("Hard constraint versus quadratic penalty")
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.20), ncol=2)
polish_axes(ax)VisualizationOut [8]Expand

A non-negotiable rule belongs in the feasible set. A preference that permits violations can instead use an explicit penalty with stated units and weight. In either case, check the returned decision. The one-dimensional example shows why a large finite quadratic penalty cannot simply be assumed to satisfy a hard rule.
Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!