Stationarity hierarchy: S / M / C / W¶
Because MPCCs generically fail standard LICQ at any solution with a biactive pair, the textbook KKT conditions don’t apply directly. The MPCC literature defines a hierarchy of stationarity certificates specialised to complementarity structure. Strongest to weakest:
Level |
Sign condition on biactive multipliers \((\mu_G^i, \mu_H^i)\) |
|---|---|
S-stationary |
both \(\ge 0\) |
M-stationary |
both \(\ge 0\), or one is zero |
C-stationary |
\(\mu_G^i \cdot \mu_H^i \ge 0\) |
W-stationary |
KKT of the relaxed NLP holds — no sign condition on biactive multipliers |
For pairs that are not biactive (only one side at zero), all four levels collapse: the active side requires \(\mu \ge 0\) as in standard NLP. The hierarchy only differs at biactive points.
pympcc.classify_stationarity(result, problem, mu_G, mu_H) takes the multipliers and returns the strongest level certified.
A non-biactive S-stationary point¶
The quickstart problem ends at \(x^\* = (2, 0)\) with H_active (only \(x_1\) at zero, \(x_0 = 2 > 0\)). The biactive set is empty, so any converged solution is automatically S-stationary.
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)
r = pympcc.solve(problem, strategy="scholtes", tnlp_refine=True)
print(f"x* = {r.x}")
print(f"per_pair_status = {r.per_pair_status}")
print(f"stationarity = {r.stationarity}")
x* = [2.0000000e+00 9.9950254e-09]
per_pair_status = ['H_active']
stationarity = S-stationary
tnlp_refine=True triggers a second NLP solve with the active set fixed as equalities — that produces clean MPCC multipliers, so the stationarity classification is genuine rather than vacuous.
A biactive S-stationary point¶
The MacMPEC kth1 problem:
has its optimum at \(x^\* = (0, 0)\) — biactive. The MPCC multipliers \(\mu_G = \mu_H = 1\) (from \(\nabla f = (1,1)\) being absorbed into the comp gradients) satisfy the S-stationary sign condition.
problem_kth1 = pympcc.MPCCProblem(
n=2, n_comp=1,
x0=np.array([0.5, 0.5]),
xl=np.zeros(2),
objective=lambda x: x[0] + x[1],
gradient=lambda x: np.ones(2),
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)
r = pympcc.solve(problem_kth1, strategy="scholtes", tnlp_refine=True)
print(f"x* = {r.x}")
print(f"per_pair_status = {r.per_pair_status}")
print(f"stationarity = {r.stationarity}")
if r.tnlp_refined is not None:
print(f"μ_G (TNLP) = {r.tnlp_refined.mult_comp_G}")
print(f"μ_H (TNLP) = {r.tnlp_refined.mult_comp_H}")
x* = [0. 0.]
per_pair_status = ['biactive']
stationarity = S-stationary
μ_G (TNLP) = [0.]
μ_H (TNLP) = [0.]
Both multipliers are positive at the biactive point — that’s the S-stationary signature.
How classify_stationarity makes its decision¶
The classifier reads the converged multipliers off the result object — specifically the slice of result.mult_g that corresponds to the \(G\) and \(H\) rows — and applies the sign tests in the hierarchy table at the top of this page. You can call it directly:
from pympcc import classify_stationarity
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
r = pympcc.solve(problem_kth1, strategy="scholtes", tnlp_refine=True)
print(f"classify_stationarity → {classify_stationarity(r, problem_kth1)}")
print(f"result.stationarity → {r.stationarity}")
classify_stationarity → S-stationary
result.stationarity → S-stationary
Both routes return the same label; result.stationarity is just classify_stationarity(result, problem) cached on the result for convenience.
In practice, IPOPT’s interior-point bias forces \(\mu_G, \mu_H \ge 0\) at convergence on all active lower bounds — so without the TNLP refinement step the classifier almost always returns S-stationary, regardless of whether the point is genuinely S- or only W-/C-stationary in MPCC terms. The TNLP refinement notebook explains how to recover the genuine MPCC multipliers.