Tour of strategies¶
The same MPCC, solved by all six canonical reformulations. The package ships six strategies (plus seven NCP-function variants documented in NCP variants):
Strategy |
One-line description |
|---|---|
|
Single IPOPT solve with |
|
Sequential |
|
Fischer-Burmeister smoothing \(\varphi_\varepsilon(G, H) = 0\). |
|
|
|
PHR penalty on |
|
Lifts \(s_G = G(x)\), \(s_H = H(x)\). Best for large |
For when to pick each, see the strategy selection guide.
import warnings
import numpy as np
import pympcc
def make_problem():
return 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]]),
)
strategies = ["direct", "scholtes", "smoothing", "lin_fukushima",
"augmented_lagrangian", "slack"]
print(f" {'strategy':<22} {'f*':>8} {'comp_res':>10} {'kkt_res':>10} status")
print(" " + "-" * 22 + " " + "-" * 8 + " " + "-" * 10 + " " + "-" * 10 + " " + "-" * 14)
for s in strategies:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
r = pympcc.solve(make_problem(), strategy=s)
print(
f" {s:<22} {r.obj:>8.4f} "
f"{r.comp_residual:>10.2e} {r.kkt_residual:>10.2e} "
f"{r.stationarity}"
)
strategy f* comp_res kkt_res status
---------------------- -------- ---------- ---------- --------------
direct 1.0000 7.95e-09 3.55e-10 S-stationary
scholtes 1.0000 2.00e-08 9.45e-12 S-stationary
smoothing 1.0000 0.00e+00 4.45e-16 S-stationary
lin_fukushima 1.0000 2.00e-08 8.28e-12 S-stationary
augmented_lagrangian 1.0000 2.99e-09 3.17e-10 S-stationary
slack 1.0000 2.00e-08 1.85e-12 S-stationary
All six converge to \(f^\* = 1\) at \(x^\* = (2, 0)\). They differ in:
Iteration count —
directis one IPOPT solve;scholtes/smoothing/lin_fukushimarun an outer ε-loop;augmented_lagrangianruns an outer ρ-escalation loop.CQ guarantees —
directtypically violates LICQ at the optimum (still converges in practice but multipliers may be poorly defined);scholtesandlin_fukushimaare MFCQ-respecting in the limit.Per-iteration history —
result.historyrecords every outer iterate for the iterative strategies.
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
r = pympcc.solve(make_problem(), strategy="scholtes")
print(f"Scholtes ran {len(r.history)} outer iterations")
for k, info in enumerate(r.history):
print(f" iter {k}: ε = {info.epsilon:.2e} "
f"obj = {info.obj:.6f} "
f"comp_res = {info.comp_residual:.2e}")
Scholtes ran 9 outer iterations
iter 0: ε = 1.00e+00 obj = 0.233361 comp_res = 1.00e+00
iter 1: ε = 1.00e-01 obj = 0.901922 comp_res = 1.00e-01
iter 2: ε = 1.00e-02 obj = 0.990019 comp_res = 1.00e-02
iter 3: ε = 1.00e-03 obj = 0.999000 comp_res = 1.00e-03
iter 4: ε = 1.00e-04 obj = 0.999900 comp_res = 1.00e-04
iter 5: ε = 1.00e-05 obj = 0.999990 comp_res = 1.00e-05
iter 6: ε = 1.00e-06 obj = 0.999999 comp_res = 1.01e-06
iter 7: ε = 1.00e-07 obj = 1.000000 comp_res = 1.10e-07
iter 8: ε = 1.00e-08 obj = 1.000000 comp_res = 2.00e-08
The ε schedule shrinks geometrically toward zero; each outer solve warm-starts from the previous primal-dual point.