A first MPCC, walked through¶
The quickstart problem unpacked. We solve
The complementarity says “at most one of \(x_0\), \(x_1\) is positive at the optimum.” Since the unconstrained minimum is at \((2, 1)\) — both positive — the complementarity will force one of the two to zero. Pulling \(x_1 \to 0\) costs \((1-0)^2 = 1\); pulling \(x_0 \to 0\) costs \((0-2)^2 = 4\). The optimum is therefore \(x^\* = (2, 0)\) with \(f^\* = 1\).
import warnings
import numpy as np
import pympcc
problem = pympcc.MPCCProblem(
n=2, n_comp=1,
x0=np.array([0.5, 0.5]),
xl=np.zeros(2),
objective=lambda x: (x[0] - 2.0) ** 2 + (x[1] - 1.0) ** 2,
gradient=lambda x: np.array([2.0 * (x[0] - 2.0), 2.0 * (x[1] - 1.0)]),
comp_G=lambda x: np.array([x[0]]),
comp_G_jacobian=lambda x: np.array([[1.0, 0.0]]),
comp_H=lambda x: np.array([x[1]]),
comp_H_jacobian=lambda x: np.array([[0.0, 1.0]]),
)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
result = pympcc.solve(problem, strategy="scholtes")
print(f"x* = {result.x}")
print(f"f* = {result.obj:.6f} (reference 1)")
print(f"success = {result.success}")
x* = [2.0000000e+00 9.9950254e-09]
f* = 1.000000 (reference 1)
success = True
Reading the result¶
Every solve returns an MPCCResult dataclass. The fields you’ll touch most often:
print(f"x = {result.x}")
print(f"obj = {result.obj:.6e}")
print(f"comp_residual = {result.comp_residual:.2e} # max(G_i · H_i)")
print(f"kkt_residual = {result.kkt_residual:.2e} # KKT optimality residual")
print(f"stationarity = {result.stationarity}")
print(f"per_pair_status = {result.per_pair_status}")
print(f"n_outer = {len(result.history)}")
x = [2.0000000e+00 9.9950254e-09]
obj = 1.000000e+00
comp_residual = 2.00e-08 # max(G_i · H_i)
kkt_residual = 9.45e-12 # KKT optimality residual
stationarity = S-stationary
per_pair_status = ['H_active']
n_outer = 9
comp_residualconfirms the complementarity is satisfied: \(G_i \cdot H_i \approx 0\).kkt_residualis the MPCC-KKT optimality residual; small means we landed at a stationary point.per_pair_statusclassifies each pair:"G_active"(here \(x_1 = 0\) so \(H = 0\)),"H_active","biactive"(both zero), or"inactive".stationarityis the strongest CQ-respecting label the solver can certify; for this problem we getS-stationary.
Per-pair table¶
For larger problems with many comp pairs, result.to_dataframe() returns a per-pair view (requires pandas):
try:
df = result.to_dataframe()
print(df.to_string(index=False))
except ImportError:
print("pandas not installed — install pympcc[all] for to_dataframe support")
pandas not installed — install pympcc[all] for to_dataframe support
Where to next¶
Tour of strategies — the same problem solved by all six canonical reformulations.
KKT residual — interpreting the optimality residual.
Stationarity hierarchy — S / M / C / W in one notebook.