Reference Section 4.2 in Biegler (2010)
Unconstrained comparison¶
For twice continuously differentiable objectives, a local unconstrained minimum has zero gradient and positive-semidefinite Hessian. These are necessary conditions; they do not classify every stationary point. The definitions, derivative tests and proofs have their existing home in Unconstrained Optimality Conditions. Here the feasible neighborhood and constraint reactions change the test.
Balance of Forces Interpretation¶
Here is a helpful handout.
Consider subject to . The objective is a landscape; its downhill force is . The fence force is , with , pointing toward the feasible side.

The drawn arrows are gradients, pointing toward increasing values. At a regular constrained minimum with an active pushing fence,
The objective contour is tangent to the fence. Opposite gradient directions do not require equal lengths: the multiplier supplies the scale. Rescaling a constraint changes the numerical multiplier, so raw arrow lengths are not force magnitudes.
Source: Biegler (2010), Figures 4.5–4.6, pp.68–69. The handout and website use this same prepared drawing.
An equality acts as a rail. Its force can point either way, because is unrestricted.

Balance is . For this illustrative drawing the normalized gradients are approximately
They give , . The direction is tangent to the rail, enters the feasible side of the fence, and raises the objective to first order. Its negative crosses the fence. Thus there is feasible motion, but no nonzero critical direction at this intersection. “No allowable directions” in a curvature discussion refers to the critical cone, not all feasible motion.
For the displayed rounded vectors, the equality tangent space is one-dimensional: every tangent vector is , . The exact dot products of these supplied decimals are
The linearized inequality therefore requires , and zero first-order objective change then requires . This establishes the whole critical cone rather than testing only one direction. These conclusions describe the prepared drawing; the derivatives of the later log-model are calculated independently.
Removing the rail permits a downhill direction along the fence toward its tangency minimum. For example, has and . But , so the rail excludes this descent direction. Source: Biegler (2010), Figure 4.7 and Eq. (4.9), pp.70–71. The later numerical example uses its own objective and gradients; these normalized drawing vectors are not substituted into that model.
Karush-Kuhn-Tucker (KKT) necessary conditions¶
Use row-oriented Jacobians and and
Assume data on an open neighborhood of a feasible local minimum. If LICQ holds there, the following necessary multiplier conditions apply:
Evaluate these at the candidate. Equality multipliers have no sign restriction. Dot-product complementarity is equivalent to the component conditions because every summand is nonpositive. The prepared product-vector diagram compares nonpositive terms with a zero vector. If any term is strictly negative, no other nonpositive term can cancel it to make the sum zero.

Inactive constraints have , hence . Active constraints have and may have a positive multiplier (strongly active) or zero multiplier (weakly active).
Regularity is a hypothesis. LICQ means the equality gradients and active inequality gradients are linearly independent. It is one sufficient qualification for the first-order necessary conditions. For subject to , the only feasible point is a strict global minimum, but cannot hold at zero for any multiplier. The constraint gradient is zero. See Constraint Qualifications for the full theory.
Feasible and critical directions differ. The linearized feasible cone imposes and on active inequalities. At a KKT point,
Critical directions have zero first-order objective change. Since every term on the right is nonnegative, they are exactly the directions satisfying
A positive multiplier does not prevent every feasible inward move; it makes that move raise the objective to first order. Such a direction is not critical. These are first-order tangent tests; nonlinear constraints need not remain feasible along an entire straight step.

For at the origin, has and critical directions . Its feasible directions with already raise the objective. For , and every linearized feasible direction is critical.
With data on an open neighborhood and LICQ at a local minimum, second-order necessity gives
Together with KKT, strict positivity on every nonzero critical direction is sufficient for a strict local minimum. The two examples satisfy it on their respective cones. If the critical cone is , the strict condition is vacuous; feasible noncritical directions are controlled by their positive first-order change. Full theorem/proof material has its existing home in Second-Order Conditions.
Sources: Biegler (2010), Eqs. (4.9)–(4.12), pp.70–71; Theorems 4.14–4.15, pp.77–78; critical cone in §4.3.3, p.79; Theorems 4.17–4.18, pp.80–81. The constraint-qualification counterexample follows directly from its singleton feasible set and zero constraint derivative.
Kinematic Interpretation via Example¶
Consider the following two dimensional optimization problem:
import sys
if "google.colab" in sys.modules:
import urllib.request
urllib.request.urlretrieve(
"https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py",
"helper.py",
)
import helper
helper.easy_install()
else:
sys.path.insert(0, "..")
import helper
helper.set_plotting_style()
import numpy as np
import matplotlib.pyplot as plt
import pyomo.environ as pyo
Define Function for Visualization¶
## Objective function
def f(x):
"""Evaluate the two-variable objective function."""
return (
x[0] ** 2
- 4 * x[0]
+ 1.5 * x[1] ** 2
- 7 * x[1]
+ x[0] * x[1]
+ 9
- np.log(x[0])
- np.log(x[1])
)
## Gradient of objective f(x)
def df(x):
"""Evaluate the objective gradient."""
return np.array((2 * x[0] - 4 + x[1] - 1 / x[0], 3 * x[1] - 7 + x[0] - 1 / x[1]))
## Gradient of inequality constraint g(x)
def dg(x):
"""Evaluate the inequality-constraint gradient."""
return np.array((-x[1], -x[0]))
## Gradient of equality constraint h(x)
def dh(x):
"""Evaluate the equality-constraint gradient."""
return np.array([2, -1])
Define function to solve optimization problem with Pyomo¶
def solve_opt(consider_g, consider_h):
"""Solve the example with the selected constraints enabled."""
## Create concrete Pyomo model
m = pyo.ConcreteModel()
## Declare variables with initial values
m.x1 = pyo.Var(bounds=(0, 100), initialize=10)
m.x2 = pyo.Var(bounds=(0, 100), initialize=10)
## Declare objective
m.OBJ = pyo.Objective(
expr=m.x1**2
- 4 * m.x1
+ 1.5 * m.x2**2
- 7 * m.x2
+ m.x1 * m.x2
+ 9
- pyo.log(m.x1)
- pyo.log(m.x2),
sense=pyo.minimize,
)
if consider_g:
## Add inequality constraint
m.con1 = pyo.Constraint(expr=4 - m.x1 * m.x2 <= 0)
if consider_h:
## Add equality constraint
m.con2 = pyo.Constraint(expr=2 * m.x1 - m.x2 == 0)
## Specify IPOPT as solver
solver = pyo.SolverFactory("ipopt")
## Solve the model
results = solver.solve(m, tee=True)
assert pyo.check_optimal_termination(results), (
f"Solve failed: status={results.solver.status}, "
f"termination={results.solver.termination_condition}"
)
## Return the solution
return [pyo.value(m.x1), pyo.value(m.x2)]Three constraint cases¶
The three solves below feed the shared prepared figure after Take 3. The complete visualization, plotting implementation and archived results have one source in this notebook.
Take 1: Unconstrained¶
kkt_solutions = {}
xsln = solve_opt(False, False)
kkt_solutions["unconstrained"] = list(xsln)
print("Primal point:", np.asarray(xsln))
print("Objective:", f(xsln))
Ipopt 3.13.2:
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit http://projects.coin-or.org/Ipopt
This version of Ipopt was compiled from source code available at
https://github.com/IDAES/Ipopt as part of the Institute for the Design of
Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.
This version of Ipopt was compiled using HSL, a collection of Fortran codes
for large-scale scientific computation. All technical papers, sales and
publicity material resulting from use of the HSL codes within IPOPT must
contain the following acknowledgement:
HSL, a collection of Fortran codes for large-scale scientific
computation. See http://www.hsl.rl.ac.uk.
******************************************************************************
This is Ipopt version 3.13.2, running with linear solver ma27.
Number of nonzeros in equality constraint Jacobian...: 0
Number of nonzeros in inequality constraint Jacobian.: 0
Number of nonzeros in Lagrangian Hessian.............: 3
Total number of variables............................: 2
variables with only lower bounds: 0
variables with lower and upper bounds: 2
variables with only upper bounds: 0
Total number of equality constraints.................: 0
Total number of inequality constraints...............: 0
inequality constraints with only lower bounds: 0
inequality constraints with lower and upper bounds: 0
inequality constraints with only upper bounds: 0
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
0 2.4439483e+02 0.00e+00 3.29e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
1 -8.0094510e-01 0.00e+00 4.03e-01 -1.0 8.53e+00 - 9.05e-01 1.00e+00f 1
2 -8.7120418e-01 0.00e+00 2.51e-03 -1.0 1.31e-01 - 1.00e+00 1.00e+00f 1
3 -8.7421973e-01 0.00e+00 5.46e-04 -2.5 3.76e-02 - 1.00e+00 1.00e+00f 1
4 -8.7422408e-01 0.00e+00 1.29e-06 -3.8 1.78e-03 - 1.00e+00 1.00e+00f 1
5 -8.7422408e-01 0.00e+00 6.93e-10 -5.7 4.12e-05 - 1.00e+00 1.00e+00f 1
6 -8.7422408e-01 0.00e+00 9.45e-14 -8.6 4.81e-07 - 1.00e+00 1.00e+00f 1
Number of Iterations....: 6
(scaled) (unscaled)
Objective...............: -8.7422408318635414e-01 -8.7422408318635414e-01
Dual infeasibility......: 9.4500693109869893e-14 9.4500693109869893e-14
Constraint violation....: 0.0000000000000000e+00 0.0000000000000000e+00
Complementarity.........: 2.5065634500861016e-09 2.5065634500861016e-09
Overall NLP error.......: 2.5065634500861016e-09 2.5065634500861016e-09
Number of objective function evaluations = 7
Number of objective gradient evaluations = 7
Number of equality constraint evaluations = 0
Number of inequality constraint evaluations = 0
Number of equality constraint Jacobian evaluations = 0
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations = 6
Total CPU secs in IPOPT (w/o function evaluations) = 0.000
Total CPU secs in NLP function evaluations = 0.000
EXIT: Optimal Solution Found.
Primal point: [1.34754858 2.04699111]
Objective: -0.8742240831863559
Take 2. With ¶
xsln = solve_opt(True, False)
kkt_solutions["inequality"] = list(xsln)
print("Primal point:", np.asarray(xsln))
print("Objective:", f(xsln))
Ipopt 3.13.2:
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit http://projects.coin-or.org/Ipopt
This version of Ipopt was compiled from source code available at
https://github.com/IDAES/Ipopt as part of the Institute for the Design of
Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.
This version of Ipopt was compiled using HSL, a collection of Fortran codes
for large-scale scientific computation. All technical papers, sales and
publicity material resulting from use of the HSL codes within IPOPT must
contain the following acknowledgement:
HSL, a collection of Fortran codes for large-scale scientific
computation. See http://www.hsl.rl.ac.uk.
******************************************************************************
This is Ipopt version 3.13.2, running with linear solver ma27.
Number of nonzeros in equality constraint Jacobian...: 0
Number of nonzeros in inequality constraint Jacobian.: 2
Number of nonzeros in Lagrangian Hessian.............: 3
Total number of variables............................: 2
variables with only lower bounds: 0
variables with lower and upper bounds: 2
variables with only upper bounds: 0
Total number of equality constraints.................: 0
Total number of inequality constraints...............: 1
inequality constraints with only lower bounds: 0
inequality constraints with lower and upper bounds: 0
inequality constraints with only upper bounds: 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
0 2.4439483e+02 0.00e+00 3.60e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
1 4.4804168e+01 0.00e+00 3.92e+00 -1.0 2.13e+02 - 8.82e-01 4.46e-01f 1
2 5.4337665e+00 0.00e+00 1.68e+00 -1.0 2.39e+00 - 1.00e+00 1.00e+00f 1
3 5.2037708e-02 0.00e+00 8.16e-01 -1.0 9.15e-01 - 1.00e+00 1.00e+00f 1
4 -4.4584904e-01 0.00e+00 1.06e-01 -1.7 2.32e-01 - 1.00e+00 1.00e+00h 1
5 -4.8930521e-01 0.00e+00 1.81e-03 -2.5 5.28e-02 - 1.00e+00 1.00e+00h 1
6 -4.9427819e-01 0.00e+00 8.51e-06 -3.8 8.98e-03 - 1.00e+00 1.00e+00h 1
7 -4.9445150e-01 0.00e+00 7.69e-09 -5.7 3.02e-04 - 1.00e+00 1.00e+00h 1
8 -4.9445337e-01 0.00e+00 8.07e-13 -8.6 3.28e-06 - 1.00e+00 1.00e+00h 1
Number of Iterations....: 8
(scaled) (unscaled)
Objective...............: -4.9445336916190952e-01 -4.9445336916190952e-01
Dual infeasibility......: 8.0657449567487133e-13 8.0657449567487133e-13
Constraint violation....: 0.0000000000000000e+00 0.0000000000000000e+00
Complementarity.........: 2.5083384199831909e-09 2.5083384199831909e-09
Overall NLP error.......: 2.5083384199831909e-09 2.5083384199831909e-09
Number of objective function evaluations = 9
Number of objective gradient evaluations = 9
Number of equality constraint evaluations = 0
Number of inequality constraint evaluations = 9
Number of equality constraint Jacobian evaluations = 0
Number of inequality constraint Jacobian evaluations = 9
Number of Lagrangian Hessian evaluations = 8
Total CPU secs in IPOPT (w/o function evaluations) = 0.001
Total CPU secs in NLP function evaluations = 0.000
EXIT: Optimal Solution Found.
Primal point: [1.79811993 2.2245457 ]
Objective: -0.49445336916190874
Take 3. With and ¶
xsln = solve_opt(True, True)
kkt_solutions["both"] = list(xsln)
print("Primal point:", np.asarray(xsln))
print("Objective:", f(xsln))
Ipopt 3.13.2:
******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
Ipopt is released as open source code under the Eclipse Public License (EPL).
For more information visit http://projects.coin-or.org/Ipopt
This version of Ipopt was compiled from source code available at
https://github.com/IDAES/Ipopt as part of the Institute for the Design of
Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.
This version of Ipopt was compiled using HSL, a collection of Fortran codes
for large-scale scientific computation. All technical papers, sales and
publicity material resulting from use of the HSL codes within IPOPT must
contain the following acknowledgement:
HSL, a collection of Fortran codes for large-scale scientific
computation. See http://www.hsl.rl.ac.uk.
******************************************************************************
This is Ipopt version 3.13.2, running with linear solver ma27.
Number of nonzeros in equality constraint Jacobian...: 2
Number of nonzeros in inequality constraint Jacobian.: 2
Number of nonzeros in Lagrangian Hessian.............: 3
Total number of variables............................: 2
variables with only lower bounds: 0
variables with lower and upper bounds: 2
variables with only upper bounds: 0
Total number of equality constraints.................: 1
Total number of inequality constraints...............: 1
inequality constraints with only lower bounds: 0
inequality constraints with lower and upper bounds: 0
inequality constraints with only upper bounds: 1
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
0 2.4439483e+02 1.00e+01 2.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0
1 4.4148045e+01 5.54e+00 2.30e+00 -1.0 2.13e+02 - 8.84e-01 4.46e-01f 1
2 8.2513369e+00 4.16e-01 1.12e+00 -1.0 3.36e+00 - 1.00e+00 9.25e-01f 1
3 1.0558961e+00 0.00e+00 8.05e-01 -1.0 9.84e-01 - 1.00e+00 1.00e+00f 1
4 2.1482166e-01 0.00e+00 1.14e-01 -1.7 2.30e-01 - 1.00e+00 1.00e+00f 1
5 1.6203098e-01 0.00e+00 9.32e-04 -2.5 2.25e-02 - 1.00e+00 1.00e+00h 1
6 1.5801768e-01 0.00e+00 1.94e-06 -3.8 3.61e-03 - 1.00e+00 1.00e+00h 1
7 1.5786332e-01 0.00e+00 1.79e-09 -5.7 1.44e-04 - 1.00e+00 1.00e+00h 1
8 1.5786148e-01 0.00e+00 2.44e-13 -8.6 1.73e-06 - 1.00e+00 1.00e+00h 1
Number of Iterations....: 8
(scaled) (unscaled)
Objective...............: 1.5786147595031919e-01 1.5786147595031919e-01
Dual infeasibility......: 2.4404695293715257e-13 2.4404695293715257e-13
Constraint violation....: 0.0000000000000000e+00 0.0000000000000000e+00
Complementarity.........: 2.5065016939133549e-09 2.5065016939133549e-09
Overall NLP error.......: 2.5065016939133549e-09 2.5065016939133549e-09
Number of objective function evaluations = 9
Number of objective gradient evaluations = 9
Number of equality constraint evaluations = 9
Number of inequality constraint evaluations = 9
Number of equality constraint Jacobian evaluations = 9
Number of inequality constraint Jacobian evaluations = 9
Number of Lagrangian Hessian evaluations = 8
Total CPU secs in IPOPT (w/o function evaluations) = 0.001
Total CPU secs in NLP function evaluations = 0.000
EXIT: Optimal Solution Found.
Primal point: [1.41421356 2.82842711]
Objective: 0.1578614759503214
# Extract the already-computed notebook results; no additional solves.
x1_grid = np.linspace(0.4, 4.0, 200)
x2_grid = np.linspace(0.4, 5.0, 200)
X_grid, Y_grid = np.meshgrid(x1_grid, x2_grid)
results = {
"grid": {"x1": x1_grid.tolist(), "x2": x2_grid.tolist(),
"objective": f((X_grid, Y_grid)).tolist()},
"panels": [
{"case": name, "solution": kkt_solutions[name],
"objective": float(f(kkt_solutions[name])),
"gradient_f": df(kkt_solutions[name]).tolist(),
"gradient_g": dg(kkt_solutions[name]).tolist(),
"gradient_h": dh(kkt_solutions[name]).tolist()}
for name in ["unconstrained", "inequality", "both"]
],
}
helper.save_results(
"kkt-geometry", results,
notebook="notebooks/7-dev/Local-Optimality.ipynb",
source_tag=None,
source_cells=[("notebooks/7-dev/Local-Optimality.ipynb", tag) for tag in
["results:kkt-log-model", "results:kkt-log-functions", "results:kkt-unconstrained",
"results:kkt-inequality", "results:kkt-both"]],
description="Three canonical Ipopt solutions, objective grid and gradients for the shared KKT geometry.",
solver="ipopt", quiet=True,
)
# The sole shared plotting implementation; archived data are supplied as results.
# Hatch constants follow figures/plots/_house.py; literal values work on Colab.
HATCH_CYCLE = ("//",)
SHADE_ALPHA = 0.18
X1LO, X1HI = 0.4, 4.0
X2LO, X2HI = 0.4, 5.0
# One scale for every gradient arrow. Uniform on purpose: the notebook's own
# discussion cell asks why the arrows are not the same length, and the answer
# (the multipliers, not the drawing) only works if the drawing is honest.
SCALE = 0.35
def _contours(ax):
X, Y = np.meshgrid(results["grid"]["x1"], results["grid"]["x2"])
Z = np.asarray(results["grid"]["objective"])
ax.contour(X, Y, Z, levels=np.arange(-1.0, 12.0, 1.0), colors="0.65", linewidths=0.8)
ax.set_xlim(X1LO, X1HI)
ax.set_ylim(X2LO, X2HI)
ax.set_xlabel("$x_1$")
ax.set_aspect("equal", adjustable="box")
def _arrow(ax, base, vec, label, scale=1.0, offset=(0.1, 0.1), **kwargs):
"""One gradient, drawn AND named. The name is the greyscale-safe identity."""
kwargs.setdefault("width", 0.045)
kwargs.setdefault("length_includes_head", True)
tip = base + scale * vec
ax.arrow(base[0], base[1], scale * vec[0], scale * vec[1], zorder=5, **kwargs)
ax.annotate(
label,
xy=(tip[0] + offset[0], tip[1] + offset[1]),
fontsize=14,
zorder=6,
bbox=dict(facecolor="white", edgecolor="none", pad=1.0),
)
def make_figure():
x_a, x_b, x_c = [np.asarray(p["solution"]) for p in results["panels"]]
fig, axes = plt.subplots(1, 3, figsize=(10.5, 3.9))
x1 = np.linspace(X1LO, X1HI, 200)
for ax in axes:
_contours(ax)
axes[0].set_ylabel("$x_2$")
# --- Take 1: unconstrained -------------------------------------------
axes[0].plot(*x_a, marker="*", markersize=18, color="black", linestyle="none")
axes[0].set_title("1. unconstrained", fontsize=14)
# --- Takes 2 and 3 share the inequality -------------------------------
for ax in axes[1:]:
g_boundary = 4.0 / x1
ax.plot(x1, g_boundary, color="#0072B2", linestyle="--", linewidth=2.5)
# Infeasible side: x1 x2 < 4, i.e. below the hyperbola.
ax.fill_between(
x1,
X2LO,
np.minimum(g_boundary, X2HI),
facecolor="0.55",
alpha=SHADE_ALPHA,
hatch=HATCH_CYCLE[0],
edgecolor=plt.rcParams["hatch.color"],
linewidth=0.0,
zorder=0,
)
ax.annotate(
"$g(\\mathbf{x}) \\leq 0$",
xy=(2.55, 0.72),
fontsize=13,
bbox=dict(facecolor="white", edgecolor="none", pad=1.0),
)
axes[1].plot(*x_b, marker="*", markersize=18, color="black", linestyle="none")
axes[1].set_title(r"2. add $g(\mathbf{x}) \leq 0$", fontsize=14)
_arrow(axes[1], x_b, np.asarray(results["panels"][1]["gradient_f"]), r"$\nabla f$", scale=SCALE, color="black")
_arrow(
axes[1],
x_b,
np.asarray(results["panels"][1]["gradient_g"]),
r"$\nabla g$",
scale=SCALE,
color="#0072B2",
offset=(-0.15, -0.42),
)
# --- Take 3: the full KKT picture -------------------------------------
axes[2].plot(x1, 2 * x1, color="#E69F00", linestyle="-.", linewidth=2.5)
axes[2].annotate(
"$h(\\mathbf{x}) = 0$",
xy=(2.52, 4.52),
fontsize=13,
bbox=dict(facecolor="white", edgecolor="none", pad=1.0),
)
axes[2].plot(*x_c, marker="*", markersize=18, color="black", linestyle="none")
axes[2].set_title(r"3. add $h(\mathbf{x}) = 0$", fontsize=14)
_arrow(axes[2], x_c, np.asarray(results["panels"][2]["gradient_f"]), r"$\nabla f$", scale=SCALE, color="black")
_arrow(
axes[2],
x_c,
np.asarray(results["panels"][2]["gradient_g"]),
r"$\nabla g$",
scale=SCALE,
color="#0072B2",
offset=(0.16, -0.40),
)
_arrow(
axes[2],
x_c,
np.asarray(results["panels"][2]["gradient_h"]),
r"$\nabla h$",
scale=SCALE,
color="#E69F00",
offset=(0.08, -0.32),
)
fig.tight_layout()
return fig
fig = make_figure()
helper.save_figure(fig, "kkt-geometry", quiet=True)

Discussion¶
Why are the gradient vectors not the same length? I thought the forces were balanced...
Analysis without Constraints¶
For the unconstrained problem, stationarity requires
Numerically, and . Moreover,
so is a strict local minimizer.
Analysis with Constraints¶
KKT system and exact certificate¶
For the constrained problem, define
The concrete KKT system is
with and . Products and reciprocals make the stationarity equations nonlinear. Complementarity supplies separate active-state branches, while domain, primal feasibility and multiplier signs remain mandatory checks on every root.
The exact point is . Solving the KKT conditions gives the rounded values
with , , and . The Lagrangian Hessian is
The exact multipliers are and . The active inequality and equality gradients are independent. Since the inequality multiplier is positive, a critical direction must be tangent to both constraints; the critical cone is , so the second-order sufficient condition is satisfied vacuously. Feasible motion still exists: is tangent to the equality and enters the inequality interior, but raises the objective to first order.
This particular example also admits a global certificate. On its positive log domain, is equivalent to , a convex inequality; its second derivative in is . The equality is affine, and the objective Hessian is positive definite throughout the domain: its diagonal entries exceed 2 and 3, with off-diagonal entry 1, so its determinant exceeds 5. The problem therefore has a convex feasible set and a strictly convex objective even though the original product constraint function is not convex. The local minimum is the unique global minimum. This illustrates the representation distinction in Convexity Revisited, rather than applying a failed structural test as a converse.
Sources: Biegler (2010), Eqs. (4.16)–(4.24), p.72; Theorem 4.3, p.65, and Theorem 4.18, p.81. The equivalent-constraint/global certificate and exact multipliers are independently derived from the displayed model. Objective-value source correction: the displayed model has constant +9, yielding approximately -0.874224 without constraints and 0.157862 with both constraints. The book’s -2.8742 and -1.8421 are two units lower and correspond to replacing that constant by +7. The additive constant changes values but not gradients, minimizers, multipliers or curvature. We keep the displayed +9 model and its independently evaluated values.

Active-branch and Hessian details for this same model¶
If the equality is retained but the inequality is assumed slack, set and . The restricted objective is
Stationarity gives , or . Its positive root is , with . But , so this branch is infeasible. The active branch instead enforces , and the positive domain selects the exact point derived above. A root is accepted only after domain, primal feasibility, multiplier sign and complementarity checks.
The Lagrangian Hessian is a sum of scalar-component Hessians,
At the exact constrained point, the diagonal entries are 2.5 and 3.125, and the off-diagonal is . Biegler Eq.(4.24) p.72 prints a positive off-diagonal. The errata fixes the diagonal signs but leaves that further sign error. The critical-direction matrix
has determinant , so its null space is zero. It describes directions tangent to the equality and the positively weighted active inequality. It does not describe every feasible direction: is an inward feasible rail direction and increases the objective to first order.

Printed orientation discrepancy. Biegler Eq.(4.9) p.70 uses transposed vector-constraint gradients, while Eqs.(4.17)–(4.18) p.72 use columns for the same stationarity components. The course consistently defines with constraint rows (), so is a constraint-change vector and is an -component gradient contribution. A single scalar-constraint gradient is a column. This resolves the dimensions without adopting two definitions of the same symbol.

Sources: Biegler Eqs.(4.13), (4.16)–(4.25), pp.71–72, with the displayed positive-domain substitutions and exact derivatives independently checked. The complete multiplier-sensitivity and strong/weak reference is Analysis of KKT Conditions.
