Changelog¶
All notable changes to this project will be documented in this file.
The format follows Keep a Changelog. This project adheres to Semantic Versioning.
[Unreleased]¶
[1.0.0] - 2026-05-05¶
The 1.0 release — feature freeze for the public API. Every Phase-1
through Phase-3 roadmap item is shipped; remaining work is opt-in
research (B&B, EPEC v2, MPECopt piecewise-SQP) tracked in
ROADMAP.md.
Added¶
Executable example notebooks (ReadTheDocs)
13 new myst-nb notebooks under
docs/examples/— every cell is executed by Sphinx on each docs build withnb_execution_raise_on_error=True, so example output cannot drift from the code.Getting started:
simple_qpcc,strategies_tour,mcp_form,sparse_jacobians,stationarity_hierarchy,kkt_residual.Strategies & solvers:
slack_strategy,ncp_variants_tour,multistart,warm_start.Diagnostics & certification:
presolve,diagnostics_tour,tnlp_refinement.docs/examples/index.mdreorganised into four thematic sections; cross-links between notebooks where features compose (e.g.kkt_residual→tnlp_refinement→diagnostics_tour).examples/README.mdrewritten as an index pointing at the rendered notebooks instead of duplicating their content.
Changed¶
pyproject.tomlclassifier flipped fromDevelopment Status :: 4 - BetatoDevelopment Status :: 5 - Production/Stable.
Stability¶
Public API surface (everything re-exported from
pympcc/__init__.py) is now considered stable. Subsequent 1.x releases will follow semantic versioning: additive changes in minor releases, breaking changes deferred to 2.0.
[0.6.0] - 2026-05-05¶
The Phase-3 hardening release. Closes every audit-flagged debt item
in ROADMAP.md §7 ahead of a public 1.0; no API breakage.
Added¶
filterSQP backend (§3.2)
New
pympcc/_filtersqp_adapter.pyplusBackendNameliteral expansion toLiteral["ipopt", "filterSQP", "scipy"]. Strategies with dense Jacobians dispatch through the optionalpyfiltersqpextra.
Documentation (§7.4.4)
New
docs/user_guide/strategy_selection.md— TL;DR rule of thumb, decision tree keyed on observable problem properties, FB-stalling fallbacks, and warm-start guidance.New
docs/user_guide/troubleshooting.md— symptom-to-action map for IPOPT divergence, restoration loops, biactive pairs, when to usetnlp_refine, and a glossary of every diagnostic field onMPCCResult.New
docs/user_guide/epec.md— the §5.5 EPEC v1 docs gap; Cournot-game quickstart,result.xslicing recipe, derivative requirements (JAX-only in v1), v1 limitations.New
examples/README.md— index of everyexamples/*.pyscript with cross-links to user-guide pages for features documented there.docs/user_guide/strategies.md— added a full NCP-variants section (smooth_min,chen_chen_kanzow,kanzow_schwartz,chen_mangasarian,billups,veelken_ulbrich_pow,veelken_ulbrich_sin) with formulas, parameter ranges, and when-to-reach-for-it blurbs.
Tooling (§7.5)
New
.pre-commit-config.yamlmirroring the CI lint + type-check job:ruff check,ruff format,mypy --ignore-missing-imports, trailing-whitespace, EOF-fixer,check-yaml,check-toml.New
pympcc[all]optional-dependencies group aggregating numba + jax + scipy + pyomo + pyfiltersqp.New
test-all-extrasCI job exercising every optional extra in one environment to catch interaction bugs.Python 3.13 added to the
pyproject.tomlclassifier list (the CI matrix already had it).
Public API tidying (§7.4.2)
pympcc.frontend.from_nl,pympcc.frontend.from_pyomo,pympcc.frontend.apply_solution,pympcc.frontend.PyomoMPCCre-exported at thepympcc.frontendnamespace.pympcc.from_lower_level,pympcc.from_epec,pympcc.Leader,pympcc.LowerLevelre-exported at the top level.pympcc.jac_condition_numberlifted to the public surface for consistency with the six other diagnostic exports.
Typing (§7.4.3)
New
pympcc/_typing.pycentralising theLiteralaliases:StrategyName,BackendName,FDMode,Derivatives,InnerTolMode. Strategy / backend kwargs across the public API now narrow to these literals instead of barestr.Return-type annotations tightened on
_diagnostics.active_sets/classify_cq/jac_norms/merit_cross_check/ initial_point_statistics/degeneracy_reportandsensitivity.active_row_labels.
Changed¶
Multistart determinism (§7.3.2)
Every parallel-path test was added:
problem.x0immutability across spawn-mode workers, bit-identical iterates across runs at the same seed, identical iterates betweenn_jobs=1andn_jobs=2. The seed propagation viaSeedSequence.spawn(n_starts)was already shipped in 0.5.0; this release locks it in with regression tests.multistart.pydocuments the broad-by-designexcept Exceptioncatches;MultiStartResult.failuresrecords type + message per failed start.
Hot-path callbacks (§7.1)
ncp.pydense-Jacobian path: pre-allocated(m, n)output buffer at strategy construction; per-call writes block-rows in-place. Replaces anp.vstackallocation per IPOPT iteration.scholtes.pyandlin_fukushima.pycleanup-Hessian wrappers: pre-allocate the zero-padded Lagrangian buffer; per-call slice-writelam_cuinto it. Replaces anp.concatenateper cleanup IPOPT iteration._tnlp.pymultiplier rescaling: dropped redundantnp.asarray(p.comp_G_scale, dtype=float)rewrap — scale arrays are canonicalised atMPCCProblem.__post_init__.
Internal exception handling (§7.2.7)
sensitivity.py:_build_hessianfailure path: tightened from bareexcept Exception:to a narrow tuple(ArithmeticError, AttributeError, TypeError, ValueError, RuntimeError, np.linalg.LinAlgError)plus alogger.debugemit so previously-silent fallbacks now surface in debug logs.Same tightening in
_base.py:_maybe_run_cleanup.
README + docs/index.md
“Six reformulation strategies” → “Thirteen reformulation strategies” (six canonical + seven NCP variants), with a link to the new selection guide.
mypy configuration (§7.4.3)
Replaced the global
ignore_missing_imports = truewith module-scoped overrides for cyipopt / scipy / pyomo / jax / jaxlib / numba / pyfiltersqp / pandas /pympcc.cython.*. User code importing pympcc now benefits from full strict-typing visibility.
Internal¶
Centralised constants (§7.2.5)
pympcc/_constants.pyalready shipped in 0.5.0; this release addsSPARSITY_TOLand migrates ~20 remaining call sites (_diagnostics,_stationarity,_sosc,sensitivity,_tnlp,_autodiff,slack,augmented_lagrangian,benchmarks/macmpec,models,problem,_jax).Empirical-default values (
cleanup_obj_worsen_tol=1e-3,comp_eps_ratio_theta=10.0,eps_hold_factor=3.0) now have one-line justifications in_base.py’sSAFEGUARD_DEFAULTS/CLEANUP_DEFAULTS.
Strategy __init__ deduplication (§7.2.1)
BaseStrategy._init_continuation_options(problem, ipopt_options, defaults, kwargs)is the single source of truth for the merge / validate / set-attrs / safeguard / cleanup boilerplate. Five strategy classes (scholtes,smoothing,lin_fukushima,slack,_SmoothNCPBase) collapsed from a 23-line__init__to a single line.
NCP variant deduplication (§7.2.2)
New
_RegistryShim(_SmoothNCPBase)base binds a(phi, grad)pair frompympcc._reformulation.NCP_REGISTRYto the_SmoothNCPBaseε-continuation harness viapartial(...).The seven dedicated NCP variant classes (
SmoothMinStrategy,ChenChenKanzowStrategy, …,VeelkenUlbrichSinStrategy) demoted to ~15-line shims (down from ~40-50 each). Public string names in_STRATEGIESare unchanged; subclass overrides +tests/test_ncp_strategies.pyunaffected.Net: −139 lines in
pympcc/strategies/.
God-module splits (§7.2.3)
pympcc/_presolve.py(1247 → 379 LOC) split into_presolve_detect.py(402 LOC),_presolve_fbbt.py(158 LOC),_presolve_reduce.py(388 LOC).PresolveMapand the publicpresolve()entry point stay in_presolve.py; private helpers re-exported for backward compat with test imports.pympcc/problem.py(1543 → 449 LOC) split into_problem_derivatives.py(206 LOC) and_problem_reduction.py(972 LOC).MPCCProblemdataclass +__post_init__orchestrator +_validate+_check_shapestay.pympcc/strategies/_base.py(1636 → 778 LOC) refactored with a three-mixin composition:_safeguards_mixin.py/_cleanup_mixin.py/_continuation_mixin.py.class BaseStrategy(SafeguardsMixin, CleanupMixin, ContinuationMixin, ABC)— every existingself._method(...)call resolves through normal MRO; subclass overrides keep working unchanged.
Time-limit enforcement (§7.3.1)
Already shipped in 0.5.0; verified in this release that every inner IPOPT solve receives
max_cpu_timefrom the remaining wall-clock budget (covers ε-continuation main loop, cleanup phase, and the augmented Lagrangian outer loop).
Community files (§7.4.1)¶
LICENSE,CONTRIBUTING.md,CODE_OF_CONDUCT.md,AUTHORS.mdshipped in 0.5.0;CONTRIBUTING.mdupdated this release with the pre-commit install + run flow.
Deferred (P2 polish, post-1.0)¶
Test directory split into
tests/unit/,tests/integration/,tests/bench/(mechanical move; would touch every test path).Dedicated
examples/*.pyscripts forsmoothing,lin_fukushima,augmented_lagrangian, NCP variants, the Pyomo frontend, presolve, and multistart. The newexamples/README.mdcross-links to user-guide pages where these features have runnable snippets; standalone scripts are an incremental win.
[0.5.0] - 2026-05-01¶
Added¶
Sphinx documentation site
New
docs/tree with Furo theme, autodoc + autosummary API reference, numpy-style docstring rendering, and intersphinx links to NumPy / JAX / SciPy. Built on every push by ReadTheDocs (.readthedocs.yaml).Three executable notebooks under
docs/examples/powered bymyst-nb(bilevel_demo,solve_jax_demo,parametric_sweep) — the output blocks are produced from real solves on every build, so they cannot drift from the code.Theory primer under
docs/theory/covering MPCC fundamentals, why generic LICQ fails, MPCC-LICQ / MPCC-MFCQ, the S/M/C/A/W/B-stationarity hierarchy, and MPCC-SOSC with the biactive critical-cone wedge.User guide pages under
docs/user_guide/covering problem setup, strategies, diagnostics, sparse / slack form, sensitivity, autodiff, bilevel, presolve / multistart, and AMPL I/O.README slimmed to a one-page pitch with deep-links into the docs; per-feature examples moved into the user guide.
pyproject.tomlgains a[docs]extra (sphinx,myst-nb,furo,sphinx-copybutton,sphinx-autodoc-typehints,matplotlib).
JAX-differentiable solve (§5.6)
New :class:
pympcc.ParametricMPCCdataclass — a parametric problem description whoseobjective,eq_constraints,ineq_constraints,comp_G,comp_Hcallables take(x, theta)with JAX-traceable bodies.materialise(theta, x0)closesthetaand returns an :class:MPCCProblemwithderivatives="jax".New :func:
pympcc.solve_jax(parametric, theta, *, x0, strategy=..., **solve_kwargs)registered asjax.custom_vjp: forward calls :func:pympcc.solvewithtnlp_refine=True(§2.6); backward performs an sIPOPT-style adjoint solve of the KKT saddle systemK·[u; w] = [v; 0]once and returnsθ̄ = −(uᵀ ∂(∇_xL)/∂θ + wᵀ ∂c/∂θ)via :func:jax.vjpon the parametric callables, so pytree θ shapes pass through.Skip behaviour mirrors :func:
pympcc.sensitivity: returns a zero θ-cotangent with aUserWarningwhen the forward solve fails to converge or the optimum is biactive (IFT prerequisites invalid).Public exports:
pympcc.ParametricMPCC,pympcc.solve_jax.11 new tests in
tests/test_autodiff.pycovering forward-only parity with :func:pympcc.solve, closed-form gradient on a parametric equality NLP, FD verification ofjax.gradand the fulldx*/dθJacobian (assembled per-row via :func:jax.grad), the biactive skip path, and the documented incompatibility with :func:jax.jit.
Parametric sensitivity analysis (§5.1)
New module :mod:
pympcc.sensitivityexposingpympcc.sensitivity(result, problem, *, dgrad_L_dp, dc_dp, ...)— a low-level KKT-linear-solve primitive that returnsdx*/dpanddλ*/dpat a converged MPCC solution by implicit differentiation.Solves the saddle-point system
[[H, J_cᵀ], [J_c, 0]] · [dx; dλ] = −[dgrad_L_dp; dc_dp]whereHis the Lagrangian Hessian (reusing the §2.3 Hessian builder) andJ_cis the active-constraint Jacobian (reusing the §2.1 active-set machinery).Skips with
skipped_reasonset when MPCC-LICQ prerequisites fail:"not_converged"(failed result),"biactive_pairs"(IFT invalid at biactive points), or"no_hessian_callable_and_fd_failed"(FD fallback raised).Tikhonov-regularised
lstsqfallback when the dense KKT solve fails;rank_deficitandused_pseudoinverseflags surface the fallback path.Companion helper
pympcc.active_row_labels(result, problem)returns the per-row labels (("h", k)/("G", i)/("H", i)/("g", j)/("xL"|"xU", j)) the caller’sdc_dprows must follow, so users can assemble the RHS without first running a sensitivity solve.Picks up TNLP-refined multipliers (§2.6) automatically when
tnlp_refine=Truewas passed to :func:pympcc.solve; falls back to zero multipliers with aUserWarningwhen no analytic Hessian is available (the fallback is exact only when constraints are linear inx).Public exports:
pympcc.sensitivity,pympcc.SensitivityResult,pympcc.active_row_labels.17 new tests in
tests/test_sensitivity.pycovering closed-form IFT on a parametric equality NLP, end-to-end FD verification on a branch-selection MPCC (single- and multi-parameter), shape validation, TNLP-vs-zero-multiplier paths, and skipped-result paths.
PATH-style multi-merit & degeneracy diagnostics (§2.7)
New
result.merit_cross_check(dict) cross-checks three independent MPCC merit functions at the converged point: Fischer-Burmeister|G + H − √(G² + H²)|, min-map|min(G, H)|, and inner-product|G · H|. Reports per-merit*_maxand*_mean, plus adisagreement_ratio = max / min(merit_maxes)— close to 1 when merits agree (healthy convergence), large when they disagree (scaling mismatch or near-degeneracy).New
result.jac_row_norms/result.jac_col_norms(dicts) reportmax,min, and near-zero count over the active-constraint Jacobian (rows: ∇h, ∇G_{I_G}, ∇H_{I_H}, ∇g_{I_g}, active bounds). Near-zero rows/cols are degeneracy signals.New
result.degeneracy_report(dict) aggregatesn_biactive,n_zero_rows,n_zero_cols,min_singular_valueof the active Jacobian, and the merit disagreement ratio for one-glance assessment.New
result.initial_point_stats(dict) — PATH’soutput_initial_point_statisticsparity: reportscomp_residual,min_map_residual,max_bound_violation,ineq_residual, andeq_residualevaluated at the user’sx0before any solve work.All five fields are populated only when the solver is invoked with
diagnostics=True; default solves are unchanged (no overhead).result.summary(verbosity=1)now renders a “Merit cross-check” line and a “Degeneracy” line whenever those diagnostics are populated.result.to_json()includes all five new fields.Public functions exported at the package root:
pympcc.merit_cross_check,pympcc.jac_norms,pympcc.degeneracy_report,pympcc.initial_point_statistics.18 new tests in
tests/test_path_diagnostics.pycovering each merit’s formula, empty-problem handling, biactive detection, end-to-end solve with diagnostics on/off, JSON roundtrip, and summary rendering.
Box-MCP / doubly-bounded canonical form (§4.9 — Phase 1)
New
comp_box_pairsfield onMPCCProblemdeclaresxl[var_idx] <= x[var_idx] <= xu[var_idx] ⊥ F_fn(x)and dispatches by bound finiteness:Lower-only finite → comp pair
(x[var_idx] - xl) >= 0 ⊥ F(x) >= 0.Upper-only finite → comp pair
(xu - x[var_idx]) >= 0 ⊥ -F(x) >= 0.Free (both infinite) → equality
F(x) = 0appended toeq_constraints.Doubly-bounded (both finite) →
NotImplementedErrorpointing to §4.9 Phase 2 / median NCP (§3.5 ext).
n_compandn_eqare auto-bumped to count synthesized rows; users supplyn_comp/n_eqfor the base problem only.2-tuple
(var_idx, F_fn)form falls back to forward fd for the F-row Jacobian; 3-tuple(var_idx, F_fn, F_jac_fn)uses the user callable.Mutually exclusive with
comp_var_pairsandcomp_var_pairs_bulkin this release. Sparse basecomp_G/Hand sparseeq_jacobianare rejected (deferred).21 new tests in
tests/test_box_mcp.pycovering all three categories, mixed combination, fd fallback, and every error path.
AMPL .nl reader (§6.4)
New
pympcc.frontend.amplmodule — a self-contained text-format.nlreader that produces anMPCCProblemdirectly. No external AMPL runtime or solver SDK required.from_nl(path)— read an AMPL text-format.nlfile and return anMPCCProblem. Complementarity constraints (encoded via bound-type-5 in thebsegment, the AMPL/MacMPEC convention) route into the bulk MCP form (comp_var_pairs_bulk). Equality and inequality constraints are split based on thersegment bound types.Header parser (
parse_header) covers the 10 fixed numeric lines plus the optional suffix-counts line; binary.nlis rejected with a clear error.Body-segment parser (
parse_body) handlesC,O,b,r,k,J,G,S,x,dsegments; defined variables (V), logical constraints (L), and imported functions (F) raise a clear error.Op-tree reader (
_read_optree) handles the ~30 AMPL operators MacMPEC uses (binary arithmetico0–o5,o27atan2, transcendentalso37–o52,o53sumlist,o76square); unsupported ops raiseNLParseErrorwith the op code in the message.Op-tree evaluation (
eval_value) and forward-mode AD (eval_grad) walk the parsed tree; gradients verified against central-finite- differences on randomized expressions.55 new tests in
tests/test_ampl_reader.pycovering header parsing, body segments, op-tree recursion, evaluator correctness, gradient matches against finite differences, and an end-to-end round-trip (hand-authored 2-variable MPCC →pympcc.solve→ unique optimum recovered).
MacMPEC .nl fixtures + benchmark CLI
Hand-authored
.nlfixtures forsimple,kth1,ralph1undertests/fixtures/nl/; each is parsed viafrom_nland parity-tested against its analyticalProblemSpeccounterpart inpympcc.benchmarks._problems.New helper
pympcc.benchmarks._nl_loader.load_nl_directory()scans a directory of.nlfiles and producesProblemSpecrecords by joining on stem against the registry (f_opt/tolerances are reused).python -m pympcc.benchmarks.macmpec --from-nl <dir>runs the benchmark suite against an.nldirectory instead of the built-in Python registry; combinable with--problemsto filter by name.--skip <names>flag on the benchmark CLI to exclude specific problems (useful for skipping large MCPs whose per-row Python-callable dispatch makes them currently impractical).
Fixed¶
from_nl()now correctly populatesn_eqandn_ineqon the returnedMPCCProblem. Previously the equality and inequality callables were built but the row counts defaulted to 0, so all general constraints were silently dropped from the NLP — affected ~12 MacMPEC problems (bard3,ex9.1.x,bilevel3, etc.) that appeared to “solve” to unbounded objectives.
Tests¶
1068 passed, 6 skipped, 54 xfailed (was 814 in 0.4.0).
[0.4.3] - 2026-04-29¶
Added¶
Bilevel KKT-emitter frontend (§5.4)
New module
pympcc.bilevelexposingfrom_lower_level(...), which rewrites a bilevel programmin_{x,y} F(x,y) s.t. y ∈ argmin_y{f(x,y): g(x,y)≤0, h(x,y)=0}into anMPCCProblemby emitting the lower-level KKT system (stationarity, feasibility, andλ ≥ 0 ⊥ −g(x,y) ≥ 0).Variable layout
z = [x_upper, y_lower, λ, μ]; the λ block is automatically lower-bounded at zero, and the μ block is left free.derivatives="jax"(default) builds the stationarity rows viajax.gradand fills every Jacobian via JAX autodiff;derivatives="fd"falls back to nested finite differences. No new dependencies.23 new tests in
tests/test_bilevel.pycovering construction, KKT residuals at analytical optima, end-to-end solves under both backends, and input validation.
Bulk MCP form (comp_var_pairs_bulk)
New
MPCCProblem.comp_var_pairs_bulkfield for large-scale MCPs; takes a 4-tuple(var_idxs, h_bulk_fn, h_bulk_jac_fn, h_bulk_jac_sparsity)whereh_bulk_fn(x) → ndarray (k,)andh_bulk_jac_fn(x) → flat nnz valuesare evaluated once per callback instead of per row.comp_G(x) = x[var_idxs](view, no copy);comp_G_jacobianis a constantones(k)callable with sparsity(arange(k), var_idxs); H sparsity is forwarded through unchanged.Mutually exclusive with the per-row
comp_var_pairs; raisesValueErrorwhen both are set.Designed for
n ≳ 10⁵,k ≳ 10⁴. Measured ~2900× speedup forcomp_Hand ~7700× forcomp_H_jacobianatk=1000versus the per-row form.
Changed¶
MCP per-row form: vectorised value/Jacobian closures.
_normalize_var_pairsnow writes into a pre-allocatedoutbuffer instead of building a Python list per row, removing thenp.array([...])allocation on everycomp_Hcall.MCP per-row Jacobian: hoisted size validation to construction. Each
h_jac_fnis evaluated once atx0to verify it matchesh_cols; the per-callbackif vals.size != ...branch is gone.MCP fd-fallback guard. Building a 2-tuple MCP whose finite-difference Jacobian would evaluate
h_fnmore than 10⁷ times per call now raisesValueErrorpointing the user toh_jac_fnorcomp_var_pairs_bulk.
Fixed¶
BaseStrategy._build_safeguard_mode:safeguards="all"no longer silently upgradesinner_tol_modefrom"linear"to"quadratic". The default mode is preserved; users who want quadratic must pass it explicitly.
Tests¶
11 new tests in
tests/test_mcp_var_pairs.pycoveringTestBulkForm(construction, G/H values, constant ones G-Jacobian, lower-bound clamp, parity with the per-row form, mutual exclusion, validation errors) andTestFdGuard(large-fd rejection, small-fd allowed).
[0.4.2] - 2026-04-28¶
Added¶
NCP-function reformulation menu (§3.5)
SmoothMinStrategy(strategy="smooth_min") — smoothed min-NCP:φ_ε(G,H) = ½(G+H−√((G−H)²+4ε²)) = 0ChenChenKanzowStrategy(strategy="chen_chen_kanzow") — convex combination of Fischer-Burmeister and inner-product:φ_{λ,ε}(G,H) = λ·φ_FB,ε(G,H) + (1−λ)·G·H = 0; parameterlam ∈ (0,1](default 0.5)KanzowSchwartzStrategy(strategy="kanzow_schwartz") — one-parameter FB family:φ_{λ,ε}(G,H) = G+H−√(G²+H²+2λGH+ε²) = 0; parameterlam ∈ [0,1)(default 0.5)All three inherit
_SmoothNCPBase(inpympcc/strategies/ncp.py) which reuses the ε-continuation harness fromSmoothingStrategy; dense and sparse Jacobian paths both supported via the genericeval_weighted_unionkernelMPCC multipliers recovered via
μ_G = λ_G + α⊙λ_φ,μ_H = λ_H + β⊙λ_φ43 new tests in
tests/test_ncp_strategies.py
[0.4.1] - 2026-04-28¶
Fixed¶
CI:
scipynot installed in base conda environment —verify_b_stationarityraisedModuleNotFoundErroreven for tests that never reached the LP-solving code, causing 17 test failures. Thefrom scipy.optimize import linprogimport is now deferred past the early-exit guards so it only executes when the LP is actually needed.scipy>=1.10added to the basemicromambatest environment.Coverage threshold not reached (83.73 % < 85 %) —
pympcc/benchmarks/macmpec.py(CLI entry-point) counted against coverage but was never imported by the test suite. Added to[tool.coverage.run] omit; coverage now sits at 91 %.Lint (ruff) errors — fixed I001 (unsorted import blocks in
__init__.py,solver.py,_sosc.py,_tnlp.py,strategies/_base.py), E702 (inline semicolons in_presolve.py), and F841 / F401 (unused names inbenchmarks/macmpec.pyandsolver.py).
[0.4.0] - 2026-04-28¶
Added¶
Variable-paired complementarity — MCP form (comp_var_pairs)
New
MPCCProblem.comp_var_pairsfield: list of(var_idx, h_fn)or(var_idx, h_fn, h_jac_fn)tuples declaringx[var_idx] ≥ 0 ⊥ h_fn(x) ≥ 0without writingcomp_G(x) = x[var_idxs]manuallyTwo modes: all-var-pairs (
comp_G=None, every pair fromcomp_var_pairs); mixed (comp_Gsupplies the firstn_comp − kpairs,comp_var_pairsappendskmore)G-side Jacobian rows are always exact (identity); H-side rows use
h_jac_fnwhen provided, otherwise forward finite differences per rowxl[var_idx]is silently clamped tomax(xl[var_idx], 0.0)Fully composable with
derivatives="fd"/derivatives="jax"and all six strategiesModule:
pympcc/problem.py—_normalize_var_pairs()methodTests:
tests/test_mcp_var_pairs.py(21 cases)
derivatives shorthand on MPCCProblem
derivatives="fd"orderivatives="jax"fills every unset derivative field (gradient,comp_G_jacobian,comp_H_jacobian,ineq_jacobian,eq_jacobian) with the corresponding sentinel at once — replaces setting each individually
MPCC-SOSC: second-order sufficient conditions (sosc_check)
pympcc.sosc_check(result, problem)checks that the reduced Lagrangian Hessian is positive definite on the MPCC critical cone — certifyingx*as a strict local minimiserAlgorithm: build active-constraint gradient matrix A → null-space basis Z via SVD → reduced Hessian W = Z^T H Z → min eigenvalue test
Hessian source (priority):
MPCCProblem.lagrangian_hessian(user-supplied) → central FD of ∇_x L using TNLP-refined multipliers (best) or zero multipliers (conservative)Returns
{"sosc": bool|None, "min_eigenvalue": float|None, "null_space_dim", "n_active", "skipped_reason"}Biactive pairs (
I_00 ≠ ∅) →sosc=None, skipped_reason="biactive_pairs"Non-converged result →
sosc=None, skipped_reason="not_converged"Three new
MPCCResultfields:sosc,sosc_min_eigenvalue,sosc_skipped_reasonWired into
MPCCSolver._attach_diagnostics(runs whendiagnostics=True)Exported at top-level:
pympcc.sosc_checkModule:
pympcc/_sosc.pyTests:
tests/test_sosc.py(16 cases)
TNLP active-set refinement — certified MPCC multipliers (tnlp_refine=True)
solve(problem, tnlp_refine=True)re-solves a tightened NLP with the active set(I_G, I_H)fixed as equality constraints; extracts clean MPCC multipliers μ_G, μ_HStationarity upgrade: result labelled
"S-stationary"when all multipliers ≥ −tol,"W-stationary"when any are negative (previously"not S-stationary")Flip-and-retry guard: if the initial TNLP finds W-stationary and ≤ 20 % of pairs had the wrong side pinned, swaps those pairs and re-solves once
Biactivity pre-screen: skips the TNLP when more than 10 % of pairs are biactive (
G_i ≤ bi_tolandH_i ≤ bi_tol), avoiding restoration failures on degenerate iteratesNew
MPCCResultfields:mult_comp_G_mpcc,mult_comp_H_mpcc,tnlp_refined(TNLPResult)TNLPResultdataclass:x,obj,status,message,success,mult_comp_G,mult_comp_H,mult_ineq,mult_eq,kkt_residual,stationarity,n_iter,solve_time,active_set,n_violationsModule:
pympcc/_tnlp.pyTests:
tests/test_tnlp.py
Per-pair status and structured result export
result.per_pair_status— always populated after every solve; each entry is one of"G_active"(G_i≈0, H_i>0),"H_active","biactive", or"inactive"using adaptive thresholdmax(sqrt(comp_residual), 1e-6)result.to_json()— serialises the full result to a JSON string (arrays as lists,Noneas JSON null, history omitted)result.to_dataframe()— returns a per-pairpandas.DataFramewith columnspair,G,H,GH,status, and (when TNLP refined)mu_G,mu_HTests:
tests/test_per_pair_status.py(20 cases),tests/test_summary.py(19 cases)
MacMPEC benchmark runner (pympcc.benchmarks)
pympcc.benchmarks.run_benchmark(problems, strategies, ...)runs any subset of the 13-problem suite and returns a list ofBenchmarkResultdataclassesprint_table(results)— Leyffer-style fixed-width results tablesave_csv(results, path)— CSV exportCLI:
python -m pympcc.benchmarks.macmpec [--strategy ...] [--problems ...] [--out file.csv] [--quiet]Problem registry moved to
pympcc/benchmarks/_problems.py;tests/macmpec_problems.pyis now a thin re-export shimModule:
pympcc/benchmarks/(__init__.py,_problems.py,macmpec.py)
Fixed¶
CI badge URL in README corrected (
davidvillacis→dvillacis)Repository, Bug Tracker, and Changelog URLs in
pyproject.tomlcorrected (davidvillacis→dvillacis)
Tests¶
814 passed, 6 skipped, 54 xfailed (was 635 in 0.3.0)
New:
test_sosc.py,test_tnlp.py,test_mcp_var_pairs.py,test_per_pair_status.py,test_summary.py,test_multistart.py,test_autoscale.py,test_cleanup_hessian.py,test_derivatives_default.py,test_matched_tol.py,test_safeguard_plateau_blowup.py,test_tnlp.py
[0.3.0] - 2026-04-26¶
Added¶
Presolve layer (pympcc.presolve, pympcc.PresolveMap)
Opt-in via
solve(problem, presolve=True)orMPCCSolver(problem, presolve=True); runs once at solver construction, returns a reduced problem to the strategy, then expands the result back to the original variable spaceA1 — pinned-variable elimination: substitutes out every
jwithxl[j] == xu[j]A2 — feasibility-based bound tightening (FBBT) over linear constraints; iterates to fixpoint and pins variables whose bounds collapse
A4 — empty Jacobian row / column pruning: drops constraints with no nonzero Jacobian entries and variables with no nonzero column
B1 — dead complementarity-pair pruning: drops pairs where one side is structurally zero and trivially satisfied at
x0B2 — forced complementarity-pair detection: pairs where both sides are linear with a forced sign get reduced to a single equality
B3 — prefix-equality pass: identifies linear comp pairs whose sign is determined by domain bounds and rewrites them as equalities; emits an infeasibility warning when both sides are forced strictly positive
PresolveMap.expand_resultre-evaluatescomp_G/comp_Hon the original problem, scattersresult.xand per-iterationhistory[k].xback to the original size, and zero-pads multipliers / unit-pads pair scales for pruned indices
Constraint qualification diagnostics (pympcc.classify_cq, pympcc.active_sets)
Opt-in via
solve(problem, diagnostics=True); the result gainsresult.cq∈ {"MPCC-LICQ","MPCC-MFCQ","none"},result.cq_active_set_sizes, andresult.cq_rank_deficitactive_sets(result, problem, tol)returns the index partition{I_g, I_G, I_H, I_00, I_xL, I_xU}; LICQ tested via SVD rank of the stacked active-gradient matrix; MFCQ tested via an LP direction-finding subproblem (HiGHS) that maximises strict-descent slack subject to the active eq-block and bound-respecting branches
B-stationarity auto-attached
With
diagnostics=True,verify_b_stationarityruns after the inner solve and populatesresult.b_stationary,result.b_stationary_witness, andresult.b_stationary_min_descentMPCCSolver(..., b_stat_max_biactive=10)caps the biactive-branch enumeration to keep the verification cheap on large problems
MPCCResult extensions
Six new fields:
cq,cq_active_set_sizes,cq_rank_deficit,b_stationary,b_stationary_witness,b_stationary_min_descent. All default toNone; populated only whendiagnostics=True
Fixed¶
B-stationarity tangent cone at I_p0 / I_0p (behaviour change)
verify_b_stationaritypreviously linearised the strict-positive sides of the biactive branches as inequalities (∇G_i·d ≥ 0on I_0p,∇H_i·d ≥ 0on I_p0). The MPCC linearised tangent cone forces these to equalities: a strictly positive component locally pins the zero-side gradient direction. The inequality form admitted spurious descent directions and could falsely flag some genuine global optima as not-B-stationary. Fixed by appendingJG[I_0p]andJH[I_p0]to the equality block instead of the inequality block in_stationarity.py. All seven existing B-stat tests continue to pass
Tests¶
635 passed, 1 skipped, 54 xfailed (was 569 in 0.2.0)
New:
test_presolve.py,test_fbbt.py,test_empty_rows_cols.py,test_forced_pair.py,test_prefix_eq.py,test_diagnostics.py,test_b_stationarity.py,test_auto_epsilon_0.py,test_problem_scaling.py,test_restoration_awareness.py,test_rollback_backoff.py
[0.2.0] - 2026-04-23¶
Added¶
KKT stationarity residual
compute_kkt_residual(result, problem, *, mpcc_mult_G, mpcc_mult_H, mult_x_L, mult_x_U)— computes‖∇f + Jgᵀλ_g + Jhᵀλ_h + JGᵀμ_G + JHᵀμ_H − z_L + z_U‖_∞; requires explicit MPCC multipliers to account for strategy-specific reformulation termsMPCCResult.kkt_residual— populated automatically by all six strategies using the correct per-strategy MPCC multiplier corrections: Scholtes/Direct:μ_G = λ_G + H⊙λ_GH,μ_H = λ_H + G⊙λ_GH; Lin-Fukushima: additionally+λ_GPHfor both; Smoothing:μ_G = λ_G + (1−G/r)⊙λ_φ,μ_H = λ_H + (1−H/r)⊙λ_φ,r=√(G²+H²+ε²); Augmented Lagrangian:μ_G = λ_G + μ_AL⊙H,μ_H = λ_H + μ_AL⊙G; Slack: direct z-space multipliers, variable bound multipliers truncated to[:n]compute_kkt_residualexported in the top-levelpympccnamespace
Bound validation
MPCCProblem._validate()now emits aUserWarningwhenx0violates any finite variable bound; IPOPT projectsx0internally, so this is a warning not an error
Stationarity documentation
Interior-point bias caveat added to
pympcc._stationaritymodule docstring: IPOPT’s barrier forces μ_G, μ_H ≥ 0 for all active lower-bound constraints at convergence, makingclassify_stationarityalmost always return"S-stationary";kkt_residualis the primary quality metric
Performance kernels (pympcc._kernels)
eval_phi_eps_weighted_union— fused Fischer-Burmeister weighted union kernel (Numba JIT)coo_to_dense— COO-to-dense conversion kernel (Numba JIT)Pre-allocated Jacobian output buffers:
_union_bufaliased as a view into_jac_flat_buf, eliminating the last per-call allocation on the hot path
MacMPEC benchmark suite
Expanded to 13 problems (was 8); added
kth2,outrata32,simple_ineq,chain2,bilevel1
CI/CD
New
test-numbajob in.github/workflows/tests.ymlverifies JIT kernels compile and produce correct results on Python 3.11 + numba≥0.57; pre-warms the Numba cache before the test run
Analytical Lagrangian Hessian support
Four new optional fields on
MPCCProblem:lagrangian_hessian— callable(x, lagrange, obj_factor) → nnz_valuesfor the strategies that operate in the original x-space (direct,scholtes,lin_fukushima)lagrangian_hessian_sparsity— COO lower-triangle sparsity pattern(row_indices, col_indices)for the abovelagrangian_hessian_slack— same callable signature but evaluated in the lifted z-spacez = [x, s_G, s_H]; used by theslackstrategylagrangian_hessian_slack_sparsity— COO lower-triangle sparsity pattern for the lifted Hessian
Strategies
direct,scholtes, andlin_fukushimaquerylagrangian_hessianand fall back to JAX autodiff (if available) or L-BFGSStrategy
slackquerieslagrangian_hessian_slackand falls back to JAX autodiff or L-BFGS_has_manual_hessian()helper onBaseStrategychecks whetherlagrangian_hessianis populatedConstraint multiplier ordering for
lagrangian_hessian:[g (n_ineq), h (n_eq), G (n_comp), H (n_comp), G·H (n_comp)];lin_fukushimaappends an additionalG+H (n_comp)block (which has zero Hessian, so the same callable works for all three strategies)Constraint multiplier ordering for
lagrangian_hessian_slack:[h (n_eq), G−s_G (n_comp), H−s_H (n_comp), s_G·s_H (n_comp)]wherez = [x, s_G, s_H]augmented_lagrangianandsmoothingare intentionally excluded: the PHR penalty and φ_ε smoothing introduce Hessian terms that cannot be expressed as a static callable independent of the strategy internalsJAX autodiff Hessian fallback remains available via
pip install "pympcc[jax]"for all four supported strategies
[0.1.0] — 2026-04-21¶
Initial release.
Added¶
Core problem interfaces
MPCCProblem— numeric problem definition with dense or sparse Jacobians; all callables validated at construction by evaluating atx0StructuredMPCC— higher-level interface accepting linear constraints as matrices (A_eq,b_eq,A_ineq,b_ineq) alongside nonlinear callables; converts toMPCCProblemvia.to_mpcc_problem()Finite-difference Jacobian fallback: pass
"fd"as any Jacobian argument inStructuredMPCC(gradient,comp_G_jacobian,comp_H_jacobian,jac_eq_nl,jac_ineq_nl); supports forward and central differences; warns at construction
Strategies — six NLP-based reformulations
"direct"— single IPOPT solve withG·H ≤ 0"scholtes"— sequential relaxationG·H ≤ ε,ε → 0(Scholtes 2001)"smoothing"— sequential Fischer-Burmeister smoothingφ_ε(G,H) = 0,ε → 0"lin_fukushima"— sequentialG·H ≤ εandG+H ≥ ε; guarantees MPCC-MFCQ (Lin & Fukushima 2003)"augmented_lagrangian"— PHR penalty in the objective; complementarity never enters the NLP constraints"slack"— liftsG(x),H(x)to slack variabless_G,s_H; complementarity rows have zero x-entries (2·n_compnonzeros total, independent ofn)
All iterative strategies share: dual warm-starting between outer iterations (dual_warmstart=True), configurable epsilon_0, reduction, max_iter, epsilon_min.
Sparse Jacobian support
Four optional
*_jacobian_sparsityfields onMPCCProblem(COO format:(row_indices, col_indices))When set, Jacobian callables return 1-D nnz-value arrays;
_SparseNLPadapter passes the structure to IPOPT’sjacobianstructure()All strategies auto-detect sparsity and dispatch to the sparse NLP path
Derived blocks (
G·H,G+H,φ_ε) computed from union of G and H sparsity patterns — no dense(m, n)matrix allocated on hot-path callbacksUnion index maps (
map1,map2) precomputed once per solve in_make_union_maps
Performance kernels (pympcc._kernels)
eval_weighted_union— fills derived-block values from union sparsity without allocation; optional pre-allocated output bufferweighted_row_sum—out[i,j] = α[i]·A[i,j] + β[i]·B[i,j]in-place, single passscatter_add— equivalent tonp.add.atfor gradient accumulationOptional Numba JIT compilation (
pip install "pympcc[numba]"); pure-NumPy fallbacks when Numba is absent;HAS_NUMBAflag
Result and diagnostics
MPCCResult— solution, objective, complementarity values, success flag, IPOPT status, per-iteration history, constraint multipliers, stationarity classificationIterationInfo— per-outer-iteration snapshot:epsilon,x,obj,status,message,comp_residualclassify_stationarity(result, problem, tol)— classifies converged solutions as"S-stationary","not stationary", or"unknown"based on biactive set and multiplier signs
Benchmark suite
8 MacMPEC problems (
tests/macmpec_problems.py) with exact Jacobians and verified optimal valuesParametrized benchmark tests across all strategies: convergence, objective accuracy (≤ 1% relative), complementarity feasibility (< 1e-4)
Examples (examples/)
01–05: stationarity hierarchy demonstrations06: strategy comparison on the same problem07: sparse Jacobian API walkthrough08: parallel sparse solve09: slack strategy — nnz reduction table, timing, stationarity on an imaging-proxy problem