Objectives, Decision Variables, Constraints, and Feasibility
A production example links objectives, domains, and constraints to feasible plans, LP and integer optima, optimality gaps, and independent audits.
Machine Learning from ScratchAll chapters Objectives, Decision Variables, Constraints, and FeasibilityFull chapter · Sections in this chapter1 / 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
Objectives, Decision Variables, Constraints, and Feasibility
A packaging plant runs two products on shared equipment. Every pallet of product 1 consumes 2 machine-hours and 1 labour-hour; every pallet of product 2 consumes 1 machine-hour and 2 labour-hours. Today the plant has 8 machine-hours and 8 labour-hours available, product 1 earns $40 per pallet, and product 2 earns $30 per pallet. The plant manager does not want a prediction of tomorrow's demand. She wants an answer to a decision question: how many pallets of each product should we build with the hours available?
Answering that question requires three separate pieces of work. First we must translate the situation into mathematics: name the quantities we control, write an expression that scores each alternative, and state the rules alternatives must obey. This is formulation. Second we search for a good or provably best alternative, by hand on a small problem or with a solver on a larger one. This is optimization. Third we independently check the returned decision against the model and operational requirements. This is solution validation. A solver status describes its numerical search; it cannot establish that the model represents the plant correctly.
This chapter introduces the formulation vocabulary used throughout constrained optimization: decision variables, domains, objectives, hard constraints, soft penalties, and feasible sets. Factory planning, minimum-cost flow, portfolio optimization, rostering, and routing all require these choices. Their variable domains and constraint functions differ; they are not interchangeable coefficient matrices.
Consider how this differs from unconstrained least-squares fitting in Linear and Generalized Linear Models and Gradient Descent and Numerical Optimization. In that fitting problem the decisions are coefficients , the objective measures residual error, and the domain permits any real coefficient vector. Operational optimization instead chooses actions such as production quantities, with explicit resource limits and a value assigned to each action. Fitting can also include constraints, so the distinction is the meaning of the decisions and objective rather than whether constraints exist. A good prediction alone does not specify a feasible production plan.
Advantages
An explicit formulation makes assumptions inspectable. Record each coefficient with its name, unit, source, and owner so that a planner, analyst, or engineer can check it. When a plan changes, comparing model versions can reveal whether the change came from the objective, a constraint, or the input data.
Bounds can quantify search quality under a fixed model. A feasible incumbent and a valid bound bracket the optimum, allowing us to report the largest remaining improvement consistent with those numbers. Prediction intervals address uncertainty in outcomes; optimization bounds address the objective of the stated decision model. Neither compensates for incorrect coefficients or missing requirements.
A formulation can be re-solved with fresh capacity and demand or extended when requirements change. In suitable continuous models, dual multipliers also describe local sensitivity to a resource limit. A nonzero multiplier identifies a priced resource at that solution, but a binding constraint can have a zero multiplier, and discrete changes need separate analysis.
Finally, a formulation forces us to separate feasibility from profitability. Requirements can conflict before we compare profits. Asking "is the feasible set empty?" separates that question from ranking feasible plans.
Disadvantages
The model omits details of the world. An optimum can therefore prescribe a poor operational decision when a relevant requirement is missing or a coefficient is wrong. Conversely, an infeasible model may reveal incompatible requirements, rather than a formulation error. Both possibilities require investigation.
Computational difficulty depends on structure as well as size. General mixed-integer linear optimization is NP-hard, although particular instances can be easy. Continuous nonlinear problems may be convex or nonconvex. A local numerical solver can terminate at a stationary point or on a step or objective tolerance; successful termination alone does not establish a global optimum.
Solver statuses must be read with their tolerances. A mixed-integer optimal status generally means the incumbent and search bound meet the requested stopping criteria, using floating-point feasibility and integrality tolerances. It is stronger than merely finding no better plan, but it is not an exact-arithmetic proof. Tolerance settings depend on the solver and options. Scaling and units affect their physical interpretation and how reliably numerical tests can be met; there is no universal rule.
Formulations can go stale. Coefficients that were correct last quarter can become wrong without an obvious warning when costs, capacities, or operating conditions change. If relevant costs change and the inputs are not updated or revalidated, the model can continue recommending plans based on an obsolete cost structure. Periodic reviews and updates require ongoing work for an actively maintained model.
Formula
We will formulate the plant decision in four steps: collect the quantities we control, choose their domains, define the objective, and impose the constraints. Writing those components separately makes it possible to check the meaning of each formula against the production example.
The first layer is the set of quantities under our control. We do not describe them with a paragraph; we collect them into a single vector so that every later operation (scoring, checking constraints, reporting a plan) addresses them positionally.
where:
- : the full decision vector collecting all quantities we control
- : the -th component, a single decision variable
- : the number of decision variables
- : the ambient space allowing each coordinate to be any real number
Each component is a decision variable: a quantity we are free to set, as opposed to a quantity we observe or predict. In the packaging example , is the number of pallets of product 1, and is the number of pallets of product 2. The choice to treat them as real numbers is already a modelling assumption; it says a fractional pallet makes sense, which we will revisit when integrality enters.
The vector alone permits values the application may rule out, such as negative production or fractional vehicle counts. We choose a domain . Here we use a product of individual variable domains and express coupling requirements as constraints. A domain can also be defined more generally. Nonnegative real variables model divisible quantities, integer variables model counts, and binary variables model yes/no choices.
The third layer is the objective function, a scalar-valued map that scores a decision. This chapter uses scalar objectives; multiobjective methods require a stated comparison rule. For the general theory we use minimization. The production example is naturally written as profit maximization and is negated before calling a minimization solver. When the extrema are attained,
where:
- : the domain, the set of individually admissible decisions
- : the objective function being maximized or negated for minimization
- : the decision vector over which the objective is optimized
- : the negated objective, so the minimum of the negation equals the negative of the maximum of
A maximization of profit therefore becomes a minimization of negative profit. The best decision vectors are the same, while the optimal objective value changes sign. This is why the implementation negates both the coefficients passed to the solver and the returned objective.
The fourth layer is constraints, the rules a decision must satisfy. We write inequality constraints as for and equality constraints as for , where:
- : the -th inequality constraint function, moved to the left-hand side so that measures violation
- : the -th equality constraint function, moved to the left-hand side so that at every feasible decision
- : the number of inequality constraints
- : the number of equality constraints
The convention that everything is moved to the left-hand side is deliberate: it makes the right-hand side a constant, and it makes the sign of meaningful, since now literally measures violation. Without that convention, a positive value of could mean violation or slack depending on how the modeller wrote the original inequality, and every diagnostic downstream would inherit the ambiguity.
The feasible set collects every decision that breaks no rule:
where:
- : the feasible set, the collection of all decisions satisfying every constraint
- : a candidate decision vector drawn from the domain
- : the domain of individually admissible decisions
- : the -th inequality constraint function, satisfying
- : the -th equality constraint function, satisfying
Read the braces as "the set of all in such that...". This set is the central object of the chapter. It is not a score, and it does not rank anything; it only says yes or no. When we later ask a solver to minimize, we are asking two questions at once: is non-empty, and if so, which of its members minimizes ?
A general mathematical program is
where:
- : the decision vector chosen from the feasible set
- : the feasible set of admissible decisions defined above
- : the objective function that scores a decision
- : the optimal value, defined as the greatest lower bound of feasible objective values
Read this as the greatest lower bound of over feasible decisions. If some feasible attains that value, it is an optimal solution. If , the problem is infeasible and we conventionally set . If feasible objective values decrease without bound, then . A fourth possibility is a finite value that is not attained: minimizing over has infimum zero but no minimizer. Infeasibility can reflect incompatible requirements, and unboundedness can be a property of the model; neither automatically proves a modelling error.
For the packaging example, with profit coefficients and , the problem is
where:
- : number of pallets of product 1 to build
- : number of pallets of product 2 to build
- : maximize the objective over the two decision variables
- : machine-hours consumed, with 2 hours per pallet of product 1 and 1 per pallet of product 2; must not exceed 8
- : labour-hours consumed, with 1 hour per pallet of product 1 and 2 per pallet of product 2; must not exceed 8
- : profit per pallet of product 1
- : profit per pallet of product 2
Compressing this into matrix notation is mostly bookkeeping, but the bookkeeping is what lets us talk to software. Let
where:
- : the resource-consumption matrix, with the amount of resource that one unit of activity consumes
- : the vector of available resource capacities
- : the vector of profit coefficients per pallet
- : the decision vector of activity levels
The product is a row vector times a column vector, which by definition equals : the transpose turns the column on its side so the shapes line up, and the single resulting number is the objective value. Likewise is a vector whose first entry is and whose second entry is , so the pair of inequalities is exactly , where the comparison is applied entry by entry. The compact form of our problem is
where:
- : the resource-consumption matrix defined above
- : the capacity vector, so each entry of must not exceed its paired entry in
- : the profit-coefficient vector, so is total profit
- : the non-negative decision vector
Each row of is one resource constraint and each column is one variable. In this model, records the amount of resource consumed by one unit of activity . A transposed or mislabelled entry changes that meaning. Check a small known decision by multiplying it by and independently calculating each resource total.
Decision variables, domains, and objective functions
Choosing the variables shapes how we write the objective, domains, and constraints. A good variable is something a decision maker can set, measured in a unit that appears in the objective and the constraints. Two habits make a formulation easier to check:
- Keep units consistent within a column. If is pallets per day, the objective coefficient must be profit per pallet and the constraint coefficient must be hours per pallet, not hours per week.
- Prefer marginal coefficients to average quantities when the model aggregates by addition. If the first 100 pallets cost 5 per unit to make and the next 100 cost 7, a single linear coefficient cannot express that; we need two variables with a capacity link, or a piecewise-linear construction.
A physically divisible quantity, such as litres of solvent, can use a continuous domain. A head count or indivisible pallet count uses an integer domain; an open-or-closed facility decision uses a binary domain . An aggregate quantity such as full-time-equivalent staffing may still be divisible, so choose the domain from the actual decision. Integrality creates combinatorial choices and can substantially change computational difficulty, even when the coefficients stay linear.
Suppose a planning team wants to balance profit, service level, emissions, and overtime. Common ways to specify priorities include a weighted scalar objective, optimizing one measure subject to targets on the others, and lexicographic optimization. The weighted objective is , where:
- : indexes the objective components being combined
- : the weight assigned to objective component , a policy choice that must be stated openly
- : the value of the -th objective component for decision
For a lexicographic solve, first obtain the primary optimum, then constrain that objective to its attained optimal value while optimizing the secondary measure. With numerical solvers the allowed primary tolerance must be stated. Weighted sums also require explicit policy weights and compatible scales. Changing weights can change the objective and selected decision, but different weights need not produce different optimal decisions.
Hard constraints, soft penalties, and feasibility
A hard constraint excludes violations from the mathematical feasible set. It is appropriate when violating the rule is unacceptable for the modeled decision. That mathematical exclusion does not guarantee a numerically returned or operationally executed plan is valid: the model, data, numerical tolerances, and final decision still need checking.
A soft constraint is a rule we would like to respect but are willing to trade off. The standard encoding is a penalty added to the objective. Using the positive-part function , which equals when is positive and zero otherwise, the penalty formulation is (note here is the fixed penalty weight, distinct from the Lagrange multipliers discussed in the duality subsection):
where:
- : the decision vector, chosen from the domain
- : the domain of individually admissible decisions
- : the original objective value of decision
- : the number of soft inequalities being penalized; unpenalized equalities remain hard constraints
- : the -th constraint function, positive where the rule is violated
- : the positive part , the amount by which constraint is violated
- : a weight with units of objective per unit of squared violation
If every , the penalty vanishes and the original objective is unchanged. The squared positive part is continuously differentiable as a function of its scalar residual; composition is continuously differentiable when the constraint functions are. This does not generally enforce exact feasibility at a finite weight. In the later one-dimensional example, every finite quadratic weight leaves a violation, although other problems can already have feasible penalized minimizers. A linear positive-part penalty can be exact under additional conditions. For example, with the hard equalities retained in the minimization domain, an attained primal optimum, and attained strong-dual multipliers , a weight makes every globally optimal penalized decision feasible, provided it is minimized globally over the same domain. This is a conditional result, not a guarantee for arbitrary local solvers. Residuals must have compatible scales or use separate weights per constraint.
To investigate feasibility separately from the original objective, minimize a scaled measure of total constraint violation:
where:
- : the decision vector, chosen from the domain
- : the number of constraints being checked for violation
- : the -th constraint function, positive where the rule is violated
- : the positive part , the amount by which constraint is violated
- : the magnitude of equality violation for equality , with equalities
- : the infimum of the summed violation measures; normalize residual units before adding them
An actual decision with zero total violation is a feasible witness. If the globally optimal violation value is strictly positive, no feasible decision exists in . An infimum of zero alone does not establish feasibility: together with is infeasible even though violations can approach zero. A local solver finding a positive violation is also not an infeasibility proof. This phase-one diagnostic shows which requirements the chosen minimum-violation compromise relaxes; it need not identify a unique or irreducible conflict. Residual scales determine which compromises it prefers.
A related construction is the Lagrangian, whose minimization over the retained domain gives a dual bound. Define
where:
- : the objective value of decision
- : the -th constraint function
- : the Lagrange multiplier for constraint ; with the convention non-negative multipliers make the infimum of the Lagrangian over the domain a lower bound on
- : the vector of all multipliers
For each non-negative multiplier , the term is non-positive when constraint is satisfied and non-negative when that individual constraint is violated. For a fixed positive multiplier, larger slack makes the term more negative, while a larger positive violation makes it more positive. If the multiplier is zero, the term remains zero. The sum can contain both signs at an infeasible decision, so one violation does not imply that the entire Lagrangian lies above the objective. Unlike a positive-part penalty, the signed Lagrangian term need not be zero throughout the feasible set. Its purpose here is to construct lower bounds by minimizing over the domain; it does not directly impose feasibility. Equality constraints may remain in that domain, or be dualized separately with unrestricted multipliers. The next subsection derives the bound.
Linear, integer, quadratic, and nonlinear formulations
The formulation classes distinguish permitted expressions and variable domains. Their computational properties depend on structure and assumptions. Remember the four names LP, MILP, QP, and NLP: they identify the model classes discussed in solver documentation.
Linear programs (LPs) have affine objectives and affine constraints over continuous variables. Their feasible regions are polyhedra. For a nonempty bounded feasible polyhedron, a linear objective attains an optimum at a vertex, although other optimal points may exist along an edge. Without suitable conditions a polyhedron may have no vertices. LP relaxations provide bounds for mixed-integer linear models and are used in branch-and-bound.
Mixed-integer linear programs (MILPs) retain affine objectives and affine constraints while restricting some variables to integers or binaries. Binary variables can encode logical decisions; a big-M construction needs valid finite bounds and a justified M. General MILP is NP-hard, while special structures may permit efficient methods. Its continuous LP relaxation enlarges the feasible set, which gives the bound derived below.
Quadratic programs (QPs) use an objective and affine constraints. For symmetric positive-semidefinite over a convex continuous domain, the objective is convex. Mean-variance portfolio models use this form. Ridge regression gives another example. Within that regression example, denotes the design matrix, the response vector, the coefficient vector, and the identity matrix of the coefficient dimension; this use of is separate from the optimization-domain notation above. For penalty weight , expanding gives . Thus and reproduce the objective apart from the constant , which does not change its minimizers. The unconstrained case has no additional constraints.
Nonlinear programs (NLPs) allow nonlinear objectives or constraints. They can model blending or physical relationships, but convexity must be checked. Local methods need not find a global optimum and may stop without reaching a local minimum. Finite-domain constraint programming is another modelling style, combining discrete domains with logical and arithmetic constraints, as in rostering.
Removing integrality from a MILP gives its continuous LP relaxation. Removing integrality from a nonlinear mixed-integer problem gives a continuous relaxation that may still be nonlinear. If a globally solved relaxation returns a decision satisfying the original domains and constraints, that decision also solves the original problem, because it attains the relaxation bound. A fractional relaxation solution remains a bound, rather than a valid integer plan.
Bounds, optimality gaps, and independent solution checks
Suppose we cannot solve a problem exactly, or we want to know how much we would gain from trying harder. We need a number that no feasible decision can beat. Any such number is a valid bound, and a relaxation supplies one.
Take a mixed-integer problem and delete the integrality restrictions, obtaining a relaxation with feasible set . Because we minimized over a larger set, the relaxed optimum cannot be larger:
where:
- : the optimal value of the continuous relaxation (integrality removed); for a MILP, this is an LP
- : the optimal value of the original mixed-integer problem
- : the feasible set of the original problem
- : the feasible set of the relaxation, with
For a maximization this reverses to , so the relaxation is optimistically better than reality: it says "if fractions were allowed, you could earn 186.67", whereas the true integer profit is only 180. Note that the LP value 186.67 is the maximization value; in the minimization form used earlier for the gap the corresponding numbers are and . Every real plan is bounded by that promise, which is exactly what makes the number useful. The bound does not depend on the quality of our incumbent; it depends only on the model, so it can certify even a plan we have not found yet.
The Lagrangian also gives a lower bound. Let for every inequality constraint and let be any feasible decision. Then each , so
Here is the Lagrangian, is the minimization objective, and the multipliers weight the inequality residuals . Each added term is non-positive at a feasible point. Now minimize over the entire domain , which includes :
The function is the dual function: its value is the infimum of the Lagrangian over all domain-allowed decisions, without requiring the relaxed inequalities. Because this chain holds for every feasible , it also gives
The infimum is the greatest lower bound, which need not be attained by a decision. This is weak duality. Maximizing over non-negative multipliers seeks the strongest bound in this family; it does not guarantee equality with the primal optimum. A feasible incumbent supplies an upper bound for minimization, while this dual value supplies a lower bound. Together they bracket the unknown optimum.
With a candidate value from some plan or incumbent and a lower bound , the relative optimality gap is
where:
- : the objective value of the best incumbent plan found so far
- : a valid lower bound on the optimal value
- : the absolute optimality gap, the amount the incumbent might still be improvable
- : the absolute value of the incumbent, normalizing the gap to a relative fraction
The numerator bounds the possible improvement of a feasible incumbent over the unknown minimum. Dividing by expresses that bound relative to the magnitude of the incumbent, not relative to the unknown optimum. This formula is undefined when ; report the absolute bound gap or state the solver's explicit convention for that case.
Bounds and gaps quantify search quality, but a small gap does not replace checking candidate feasibility, objective accounting, or model inputs. Use three independent cross-checks:
- Recompute the objective from the reported decision and compare it with the solver's reported value, checking for sign errors and truncated output.
- Check variable domains, including bounds and integrality, and recompute every constraint residual with stated tolerances. For a general inequality , the violation is . For an affine inequality , the signed slack is , where is the -th capacity and is the -th coefficient row. A negative slack means the inequality is violated; a non-negative slack is unused capacity. Report the largest violation in each relevant residual scale. For an equality , check the magnitude against its tolerance.
- On small problems, compare with a second method such as exhaustive enumeration. When enumeration is impossible, compare with a different solver or a differently formulated model. Different implementations can still share input or formulation errors.
These checks connect a solver status to a plan whose objective and feasibility have been independently examined.
Mathematical Properties
- A convex domain, convex inequality functions, and affine equality functions give a convex feasible set. With a convex objective, any local minimum over that set is global.
- A linear objective over a nonempty bounded polyhedron has at least one optimal vertex. A constant objective on the entire real line has a finite optimum but no vertex, illustrating why the geometric conditions matter.
- Adding constraints to a fixed model can only shrink its feasible set, so its optimal minimization value cannot decrease. If every old feasible decision embeds in an expanded model with the same objective value, the expanded minimization value cannot increase. Without that embedding, adding variables alone gives no such monotonicity guarantee.
- Weak duality does not require convexity. Convexity with a suitable constraint qualification, such as Slater's condition, is a sufficient route to strong duality, not a necessary one. Dual attainment is an additional issue; equality of optimal values alone does not guarantee an optimal multiplier exists.
- Integrality is not preserved by relaxation, and the difference between the integer optimum and the relaxed optimum is the integrality gap. It can be zero, small, or arbitrarily large, so LP bounds are informative in some models and nearly useless in others.
- Floating-point feasibility is checked with stated tolerances. Choose checks appropriate to each constraint's units and scales, inspect signed residuals, and report violations rather than assuming one numerical threshold suits every model.
Visualizing the Feasible Set and the Optimality Gap
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.
Example: Allocating Two Products Across Two Resources
We now solve the packaging problem completely by hand, then repeat the exercise with whole pallets, and finally measure the gap between the two. Hand-solving is not busywork: it gives us a reference value against which every solver call in the next section can be audited.
The first step is to find the feasible set, because without it we have no candidates to score. Each resource inequality becomes an equality on its boundary line. The machine constraint has boundary , which meets the -axis at and the -axis at . The labour constraint has boundary , meeting the axes at and . The non-negativity domains cut everything to the first quadrant, so the feasible region is the quadrilateral with corners
The corner comes from the first line at ; the corner comes from the second line at ; the corner implied by the second line is cut off by the first line and is therefore not feasible. To find the intersection we solve the two equalities. Doubling the first gives . Subtracting the second, , yields , so , and substituting back gives . Both are positive, so this point is in the feasible set.
Evaluating the objective at each corner:
| Corner | Machine slack | Labour slack | Profit | ||
|---|---|---|---|---|---|
| A | 0 | 0 | 8 | 8 | 0 |
| B | 4 | 0 | 0 | 4 | 160 |
| C | 8/3 | 8/3 | 0 | 0 | 560/3 ≈ 186.67 |
| D | 0 | 4 | 4 | 0 | 120 |
The largest value is at corner C, where both resources are exactly exhausted. Checking the algebra: and , and the sum is . Because the objective is linear and the feasible set is a polygon, the maximum must occur at a corner, so we have solved the continuous problem exactly without any software. The slack columns are worth reading alongside the profit: at C both are zero, which is the geometric signature of a vertex formed by two binding constraints.
Now suppose pallets cannot be split. We enumerate every lattice point with and and keep those satisfying both inequalities and . The feasible lattice points are exactly those whose coordinates satisfy both resource limits, because nonnegativity and the resource limits rule out any coordinate above 4, independently of the objective. The top candidates, ranked by profit , are:
| Machine slack | Labour slack | Profit | ||
|---|---|---|---|---|
| 3 | 2 | 0 | 1 | 180 |
| 2 | 3 | 1 | 0 | 170 |
| 4 | 0 | 0 | 4 | 160 |
| 3 | 1 | 1 | 3 | 150 |
| 2 | 2 | 2 | 2 | 140 |
The best integer plan is , with profit , leaving one labour-hour idle () and no machine-hours idle (). Note how close the top candidates are: earns 170 and earns 180, a ten-unit difference produced by swapping which resource is left slack. Rounding the continuous optimum to would have been a mistake, since violates the machine constraint; rounding down to loses 40 units of profit. Rounding is a heuristic with no general feasibility or optimality guarantee; this example shows infeasibility when rounding up and lost profit when rounding down.
Finally we quantify how much the integer restriction costs. In minimization form the integer optimum is and the LP relaxation bound is ; using profit values directly gives the same relative gap,
The gap relative to the integer profit is . This quantifies the initial LP bound's looseness. Enumeration has already established that 180 is the exact integer optimum for this tiny model. A MILP solver that certifies that optimum can tighten its search bound to 180 and report a zero final gap.
Implementation in SciPy and NumPy
We use SciPy because it exposes all three layers we need in one place: linprog for the relaxation, milp for the integer problem, and minimize for the penalty experiment. The organizing principle is to build the audit trail first, because a solver's own report should never be the only evidence we keep. Each code block below corresponds to a step in the hand solution, so the numeric outputs can be checked against the algebra rather than accepted on faith.
PythonIn [9]Expand
# Decision variables: pallets of product 1 and product 2.
A = np.array(
[
[2.0, 1.0], # machine-hours per pallet
[1.0, 2.0],
]
) # labour-hours per pallet
b = np.array([8.0, 8.0]) # available machine-hours, labour-hours
c = np.array([40.0, 30.0]) # profit per pallet (currency units)We start with brute force, which is both the ground truth for this tiny instance and the pattern we would use for any verification by enumeration.
PythonIn [10]Expand
enumerated = []
for point in itertools.product(range(5), repeat=2):
x = np.array(point, dtype=float)
slack = b - A @ x
if np.all(slack >= -1e-12):
enumerated.append(
(int(x[0]), int(x[1]), float(c @ x), float(slack.min()))
)
best_enum = max(enumerated, key=lambda row: row[2])
n_feasible_enum = len(enumerated)ConsoleOut [11]Expand
feasible integer plans with 0 <= x_j <= 4 : 17 best by enumeration: x1=3, x2=2, profit=180.00, tightest slack=0.0
Next we solve the LP relaxation. linprog minimizes, so we pass and negate the returned objective to recover profit.
PythonIn [12]Expand
lp = linprog(c=-c, A_ub=A, b_ub=b, bounds=[(0.0, None)] * 2, method="highs")
if not lp.success or lp.x is None or lp.fun is None:
raise RuntimeError(f"LP failed: {lp.message}")
lp_profit = float(-lp.fun)Then the integer problem. milp accepts a LinearConstraint matrix with lower and upper vectors, which is the general form ; passing as the lower bound makes it a pure constraint. integrality marks which variables must be whole numbers, and setting the relative gap option to zero asks the solver to prove optimality rather than stop at its default tolerance.
PythonIn [13]Expand
milp_fit = milp(
c=-c,
constraints=LinearConstraint(A, -np.inf, b),
integrality=np.ones(2),
bounds=Bounds(np.zeros(2), np.full(2, np.inf)),
options={"mip_rel_gap": 0.0},
)ConsoleOut [14]Expand
LP relaxation : profit=186.6667 at x=[2.6667 2.6667] MILP solution : profit=180.0000 at x=[3. 2.] LP-relaxation gap : 3.70% MILP solver gap : 0.00%
The LP relaxation returns approximately with profit 186.67. The integer solver returns with profit 180. The computed LP-relaxation gap is 3.70 percent, while the solver's final MILP search gap is zero, matching the independently enumerated integer optimum.
The bound and the incumbent are easier to trust when they appear on the same scale. The relaxation is optimistic, the integer plan is attainable, and the gap is the distance between them.
PythonIn [16]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)
positions = [0, 1]
bars = ax.barh(
positions,
gap_values,
color=[PALETTE["primary"], PALETTE["amber"]],
height=0.5,
)
ax.set_yticks(positions, gap_labels)
ax.set_xlim(0.0, max(gap_values) * 1.18)
ax.set_ylim(-0.6, 1.8)
ax.set_xlabel("Profit (currency units)")
ax.set_title("Relaxation bound and integer incumbent")
label_bars(ax, bars, labels=[f"{value:.2f}" for value in gap_values])
ax.text(
0.02,
0.96,
f"LP-relaxation gap: {gap_relative:.2%}\nFinal MILP gap: {milp_fit.mip_gap:.2%}",
transform=ax.transAxes,
va="top",
color=PALETTE["ink"],
bbox=dict(facecolor=PALETTE["paper"], alpha=0.92, edgecolor="none"),
)
polish_axes(ax)
plt.show()VisualizationOut [16]Expand

The hand calculation, enumeration, and MILP solver agree on this instance. If they disagreed, we would investigate the formulation, implementation, and numerical behavior. Agreement is a useful cross-check, but shared input errors could still affect every method.
The audit function below is the part we would keep even in production. It ignores the solver's claim and recomputes everything from the decision vector: non-negativity, the signed slack of each constraint, the total overspend, the largest resource violation, the objective, and integrality.
PythonIn [17]Expand
def audit(x, tol=1e-9):
"""Independent check of a claimed solution: feasibility first, then value."""
x = np.asarray(x, dtype=float)
if x.shape != c.shape or not np.all(np.isfinite(x)):
raise ValueError(
"Expected a finite decision vector with one value per product"
)
slack = b - A @ x
return {
"x": x,
"slack": slack,
"overspend": float(np.sum(np.maximum(0.0, -slack))),
"max_resource_violation": float(np.max(np.maximum(0.0, -slack))),
"profit": float(c @ x),
"integer": bool(np.all(np.abs(x - np.round(x)) <= tol)),
"feasible": bool(np.all(x >= -tol) and np.all(slack >= -tol)),
}ConsoleOut [18]Expand
LP relaxation feasible=True integer=False overspend=0.0e+00 max_resource_violation=0.0e+00 profit=186.6667 MILP solver feasible=True integer=True overspend=0.0e+00 max_resource_violation=0.0e+00 profit=180.0000 enumeration feasible=True integer=True overspend=0.0e+00 max_resource_violation=0.0e+00 profit=180.0000
The LP answer satisfies the resource limits and nonnegativity but is fractional. The MILP and enumerated answers also satisfy the integer domain and have the same profit. This separates a feasible relaxation point from a feasible integer plan without relying solely on solver status.
The audit is most informative when the residuals are plotted. The three methods are shown in a fixed order, and no random sampling or tie-breaking is used in this comparison.
PythonIn [20]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)
machine_bars = ax.bar(
slack_positions - slack_width / 2.0,
slack_matrix[:, 0],
width=slack_width,
color=PALETTE["primary"],
label="Machine slack",
)
labour_bars = ax.bar(
slack_positions + slack_width / 2.0,
slack_matrix[:, 1],
width=slack_width,
color=PALETTE["amber"],
label="Labour slack",
)
label_bars(
ax, machine_bars, labels=[f"{value:.0f}" for value in slack_matrix[:, 0]]
)
label_bars(
ax, labour_bars, labels=[f"{value:.0f}" for value in slack_matrix[:, 1]]
)
ax.set_ylim(-0.05, 1.18)
ax.set_xticks(slack_positions, audit_names)
ax.set_ylabel("Slack (hours)")
ax.set_title("Independent slack audit across solution methods")
ax.axhline(0.0, color=PALETTE["muted"], lw=1.0)
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.20), ncol=2)
polish_axes(ax)
plt.show()VisualizationOut [20]Expand

The last experiment replaces the two resource limits by a quadratic penalty while retaining continuous nonnegative variables. On this particular convex model, increasing the weight moves the penalized solution toward the LP optimum and reduces resource overuse. This is a property to verify for the model, rather than a consequence of continuity alone.
PythonIn [21]Expand
def penalised(lam):
"""Negative profit plus a quadratic penalty on resource overuse."""
def objective(x):
over = np.maximum(0.0, A @ x - b)
return float(-(c @ x) + lam * np.sum(over**2))
return objective
penalty_rows = []
for lam in (0.5, 5.0, 50.0, 500.0):
fit = minimize(
penalised(lam),
x0=np.array([1.0, 1.0]),
method="L-BFGS-B",
bounds=[(0.0, None)] * 2,
options={"gtol": 1e-10, "maxiter": 10000},
)
if not fit.success or fit.x is None:
raise RuntimeError(f"Penalty solve failed: {fit.message}")
report = audit(fit.x)
penalty_rows.append(
(lam, fit.x[0], fit.x[1], report["overspend"], report["profit"])
)ConsoleOut [22]Expand
lambda x1 x2 overspend profit
0.5 11.556 1.556 2.33e+01 508.89
5 3.556 2.556 2.33e+00 218.89
50 2.756 2.656 2.33e-01 189.89
500 2.676 2.666 2.33e-02 186.99
LP relaxation target for comparison: 186.6667For these weights the penalized solution exceeds the hard limits and reports profit unavailable to any feasible plan. As the weight increases, overuse declines and the profit approaches the continuous relaxation value. No integrality requirement appears in this penalty objective, so it does not enforce whole pallets. A penalized solution might happen to be integral in another problem; that coincidence is not a domain guarantee.
Key Parameters
For linprog, c is minimized, so profit maximization requires negating it. A_ub and b_ub specify row-wise inequalities ; A_eq and b_eq specify equalities. bounds supplies lower and upper values per variable, with None meaning no bound. The default method="highs" chooses between HiGHS dual simplex and interior point; it is not a combined-method guarantee.
For milp, constraints accepts a LinearConstraint, a tuple convertible to one, or a sequence of those inputs. Variable bounds use the separate bounds=Bounds(...) argument. integrality uses 1 for ordinary integer variables and 0 for continuous variables. mip_rel_gap controls the requested relative search gap and time_limit caps allotted time; a time-limited result may have no feasible incumbent.
For minimize, x0 supplies the starting decision. method="L-BFGS-B" is a limited-memory quasi-Newton method for box-constrained continuous optimization. It does not handle general constraints through its constraints argument; choose a method that supports them if required. Its gtol is a threshold on the maximum absolute projected-gradient component. Termination can also depend on objective reduction or evaluation limits, so inspect the status and message before relying on a result.
Key Methods
The linprog and milp result interfaces used here provide status, success, and message; inspect these fields before using the candidate. linprog returns status == 0 for an optimal solve, 2 for infeasibility, and 3 for unboundedness; the message string names the cause. milp returns status == 0 when an optimal solution was found to the requested gap, 1 when a time or iteration limit was hit (which may leave x as None if no feasible incumbent was found), and 2 when the problem was proven infeasible. These are floating-point termination reports interpreted with the requested gap and numerical tolerances; the status alone is not an exact-arithmetic proof.
The .x field supplies the candidate decision and .fun supplies the solver's objective value in minimization form. The audit accepts that candidate but recomputes its value and constraints from A, b, and c; it does not trust the reported .fun. Check result status and the presence of these fields before using them.
Practical Implications
The first practical question is whether the task is a decision problem at all. If the deliverable is a number about the future, such as a demand forecast, a default probability, or a survival curve, the models of the earlier parts are the right tools, and they should be evaluated with held-out data. If the deliverable is an action that must satisfy stated rules, we need a formulation, and its operational outcome needs a separate evaluation, for example through simulation or sensitivity analysis. A bound assesses the action's objective within the stated model; it does not validate the model's coefficients.
Accurately solving a model does not verify its coefficients. A coefficient entered one hundred times too large can change the selected plan while the solver correctly optimizes the supplied model. Record the owner, units, source date, and transformation of each input, then recompute the recommended plan through an independent accounting path. Where coefficients are uncertain, compare explicit scenarios and document how the decisions change.
Benchmark the actual formulation under an explicit compute budget. Size alone does not predict runtime: coefficient structure, bounds, integrality, and nonconvexity all matter. For mixed-integer work, stronger formulations, valid inequalities, symmetry reduction, and tight valid bounds can improve search. Decide whether a nonzero optimality gap is acceptable from the decision's requirements, rather than applying a universal one-percent rule. For a nonconvex continuous model, multistart or global methods may help, but any claimed global guarantee still needs a valid certificate or bound.
When a model is reported infeasible, inspect data and domain restrictions and consider a scaled violation-minimization diagnostic, including equalities. A positive local diagnostic value alone is not a proof of infeasibility. When an objective is unbounded, look for a sequence of feasible decisions whose minimized objective values decrease without limit, then determine whether it reflects a missing operational limit or the intended model. If a plan looks implausible despite an optimal status, check missing constraints, coefficient labels, units, and objective signs. Recompute residuals with stated tolerances and record the status, bound, and gap.
Choose hard constraints for non-negotiable requirements and penalties for preferences that permit tradeoffs. State the penalty units, weight, and tolerated violations. Monitor the resulting decisions; if violations conflict with the intended policy, revisit the model or the requirement instead of assuming the chosen weight enforces it.
Finally, treat formulations as versioned artifacts. When input data, regulations, or business definitions change, review coefficients, constraints, and variable meanings. A model that is re-solved nightly should carry a validation harness that re-checks a handful of known instances, re-computes the audit for every returned plan, and records the solver status, the bound, and the gap alongside the decision and a link to retained, immutable versions of the model and its inputs. That record preserves the input binding and search information for a later audit. A periodic coefficient review, a change log, and a named model owner turn the formulation from a one-off calculation into a maintained decision asset.
Summary
A constrained optimization problem has four components: a decision vector whose entries are the quantities we control, a domain that says which values are admissible, an objective that scores each decision, and constraints that partition decisions into feasible and infeasible. Written compactly, we minimize over the set , and in the linear case this becomes the familiar subject to and . The formulation is a model of the decision, not the decision itself, and its quality depends on consistent units, coefficient provenance, faithful requirements, and a suitable solver.
Formulation translates a decision into variables, domains, an objective, and constraints. Optimization searches that model: LPs with a nonempty bounded feasible polyhedron admit an optimal vertex, mixed-integer solvers use bounds to limit the possible improvement of the incumbent objective, and nonconvex local methods provide no automatic global certificate. Validation independently recomputes objectives, residuals, and domain requirements. Relaxations and Lagrangian dual values supply bounds, while feasible decisions supply achievable values. Penalties price violations; they do not automatically enforce hard rules or integrality. The worked allocation example distinguishes a fractional LP bound, the exact enumerated integer optimum, and the solver's final gap, giving us a way to read the more specialized models that follow.
Quiz
The questions use the chapter's production model, sign convention, penalty example, and optimality bounds.
Join the discussionAsk a question or share what you learned.
Keep exploring
Reference
Citation details
Cite or share this article.


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!