Goal
Port the Thierry & Biegler (2020) ℓ₁-exact penalty-barrier wrapper from ripopt#23 / commit 7847bba9 into pounce as pounce-l1penalty, and ship the MPCC paper reproduction that ripopt explicitly deferred (their probe was on the adjacent CUTEst NE-suffix slice). End state: a default-off opt-in wrapper plus an opt-in auto-fallback, validated on (a) pounce's own CUTEst sweep, (b) the same 42-problem NE-suffix probe ripopt ran, and (c) the MPCC benchmarks Thierry & Biegler 2020 report.
Background
See pounce#5 for the algorithmic background. Algorithmic reference: L1PenaltyBarrierNlp in ripopt#23 commit 7847bba9, 550 LoC wrapper. The ripopt closing comment on issue #23 documents 2 hard rescues (DENSCHNENE, HATFLDFLNE), 26 honest-infeasibility upgrades (RestorationFailed / MaxIterations → LocalInfeasibility, often turning 3000-iter budget exhausts into 50–200-iter certificates), and 0 regressions on a 42-problem CUTEst NE-suffix probe; flag-off byte-identical on a 100-problem comparison (99/100; sole diff is wall-time non-determinism on CRESC132).
Architecture decisions
Crate placement. New crate crates/pounce-l1penalty/. Depends on pounce-common, pounce-nlp. Used by pounce-algorithm (for the IPM driver hook) and pounce-cli (for option wiring). Mirrors how pounce-restoration is structured.
Wrapper level. Wrap at the TNLP level — crates/pounce-l1penalty/src/wrapper.rs exposes L1PenaltyBarrierTnlp<'a>(inner: Rc<RefCell<dyn TNLP>>, opts) implementing TNLP. Rationale: the augmented NLP feeds straight into the existing TNLPAdapter → OrigIpoptNlp chain unchanged, which is the cleanest separation. ripopt wraps at NlpProblem (its analog), not at the lower algorithm-side Nlp trait — same call here.
Variable layout. [x(n_orig), p(m_eq), n(m_eq)], total n_orig + 2·m_eq variables. Constraints unchanged in row order.
Detection of equality rows. g_l[i] == g_u[i]. Cached in eq_rows: Vec<usize> at construction time.
Phased plan
Phase 0 — Audit & baseline (1 day, no code shipped)
Goal. Quantify how much of pounce's current failure tail this targets, before writing any wrapper code.
Tasks.
- Walk
benchmarks/cutest/results_feral_v030fma_2026-05-13.json, classify pounce's non-Solve_Succeeded outcomes by status. Extract the RestorationFailed, LocalInfeasibility, Acceptable, MaxIterations problems — these are the auto-fallback candidates.
- Walk the same set, find the subset where
m_eq > 0 (equality rows present). Augment with rank deficiency hints: rerun those problems with print_level=8 and grep for "perturbation" / "Wrong Inertia" patterns. Those are the wrapper's primary targets.
- Pull the 42 problems on ripopt's
benchmarks/cutest/l1_rescue/problems.txt — record pounce's current outcome for each so we can compare like-for-like.
- Pull MPCC benchmark sources (see Phase 4 below) and confirm we have the test problems offline.
Deliverable. benchmarks/cutest/l1_audit/REPORT.md with: pounce-side rescue-candidate count, ripopt 42-problem comparison table (current pounce status), Phase 4 MPCC corpus inventory.
Stop-gate. If fewer than ~5 pounce problems are rescue-candidates, downgrade the priority and revisit. (ripopt found 28 of 42 actionable; we expect a similar ratio on our corpus given shared algorithmic lineage.)
Phase 1 — TNLP wrapper, fixed ρ, default-off byte-identical (3–5 days)
Goal. Land the wrapper and the option wiring; prove it doesn't change any solve trajectory when the flag is off.
Tasks.
- New crate
crates/pounce-l1penalty/. Cargo.toml + README + workspace registration.
src/wrapper.rs: L1PenaltyBarrierTnlp<'a> implementing the TNLP trait. Closely follows ripopt's L1PenaltyBarrierNlp — algorithmic body is essentially a translation. Key changes from ripopt: pounce uses Rc<RefCell<dyn TNLP>> for the inner pointer; sparsity is reported via SparsityRequest<'_> (pounce's enum) rather than separate row/col returns.
src/options.rs: register l1_exact_penalty_barrier (bool, default false), l1_penalty_init (f64, default 1.0). Plumbed into pounce-cli's option-table loader.
- Wire the wrapper into
pounce-algorithm/src/application.rs::optimize_tnlp: when the option is set, replace the user TNLP with Rc::new(RefCell::new(L1PenaltyBarrierTnlp::new(user_tnlp, opts))) before the existing chain. No back-projection yet — Phase 2.
- Tests in
crates/pounce-l1penalty/tests/wrapper.rs (~10 unit tests, mirroring ripopt's tests/l1_penalty_barrier.rs::wrapper_*):
- Variable-layout invariants (n_orig + 2·m_eq, constraint count unchanged).
- Equality-row detection on
g_l[i] == g_u[i].
eval_f augments by ρ·Σ(p+n) correctly.
eval_g rewrites equality rows to c_i(x) − p_i + n_i = g_i.
eval_jac_g includes the +/-I blocks for slacks.
eval_h does not add Hessian contribution from the linear penalty term (slacks have zero Hessian).
- Bounds-info lifts
(x_L, x_U) and adds (0, +∞) for (p, n).
- Starting point lifts
x_0 and seeds (p, n) from max(0, ±c(x_0)).
- Flag-off regression:
benchmarks/cutest/l1_audit/regression_pre_l1.jsonl (current run) vs regression_post_l1_flag_off.jsonl (post-Phase-1, flag off). Acceptance: ≥99/100 byte-identical (matches ripopt's threshold).
Deliverable. Crate compiles, tests pass, flag-off byte-identical on 100-problem CUTEst probe. Flag-on does not yet back-project the solution — that's Phase 2.
Phase 2 — Augmented IPM driver + back-projection (2–3 days)
Goal. Reported (x*, λ*) and reported objective are the original problem's, not the augmented problem's.
Tasks.
- In
pounce-algorithm/src/application.rs::optimize_tnlp, after the augmented IPM solve completes:
- Truncate
x from length n_orig + 2·m_eq back to n_orig.
- Re-evaluate
f(x*) on the user TNLP (strip the ρ·Σ(p+n) term from the reported objective).
- Re-evaluate
c(x*) on the user TNLP (strip the slack contribution from constraint values).
- Map the equality-row multipliers back:
λ_eq[i] is the same scalar in both spaces (the slack rows don't introduce new dual variables, only new primals).
Solution<'_> callback: ensure the user receives original-space (x*, λ*, z_L*, z_U*), not augmented-space.
- Tests:
flag_on_objective_excludes_penalty_term — solve a small problem, assert reported obj == f(x*) not f(x*) + ρ·Σ(p+n).
flag_on_solution_x_truncated — assert x.len() == n_orig.
flag_on_lambda_eq_passes_through — solve a problem with one equality, assert λ_eq matches a known reference within 1e-8.
- Probe one of the audit's rescue candidates: solve with flag on, fixed ρ = 1e3, confirm convergence on at least one problem that fails with flag off.
Deliverable. End-to-end solve in augmented space with original-space reporting. At least one audit candidate transitions failure → success at fixed ρ.
Phase 3 — Byrd–Nocedal–Waltz dynamic ρ + honest-infeasibility upgrade (3–4 days)
Goal. Replace the fixed ρ with the BNW steering rule. Detect non-collapsing slacks and upgrade Optimal / Acceptable → LocalInfeasibility when the original problem is genuinely infeasible at the ℓ₁ stationary point.
Tasks.
- Add an outer ρ-loop in
application.rs around the inner IPM solve. After each inner solve:
- Read the equality-row multipliers
y_eq from the Solution<'_> callback.
- Compute
ρ_new = max(ρ · l1_penalty_increase_factor, l1_steering_factor · ‖y_eq‖∞ + ε), capped at l1_penalty_max.
- Stop when ρ stops increasing (saturated) or
Σ(p+n) < l1_slack_tol (slacks collapsed).
- Honest-infeasibility upgrade: if the inner solve returned
Solve_Succeeded or Solved_To_Acceptable_Level but Σ(p+n) ≥ l1_slack_tol, override ApplicationReturnStatus to Infeasible_Problem_Detected. The point is BNW ℓ₁-stationary but the original problem has no feasible solution at this iterate.
- New options:
l1_penalty_max, l1_penalty_increase_factor, l1_penalty_max_outer_iter, l1_slack_tol, l1_steering_factor — defaults match ripopt's choices.
- Tests:
bnw_rho_escalates_with_lambda — solve a problem where the equality multipliers grow, assert ρ tracks τ·‖y_eq‖∞.
infeasible_certificate_burke_han — minimize subject to x₁²+x₂²=0 with x₁ ≥ 1 (Burke-Han classic infeasible). Assert status → Infeasible_Problem_Detected with collapsed slack mass corresponding to the ℓ₁-best infeasibility distance.
wachter_biegler_anti_case — x₁ = 1, x₁² − x₂² = 0. Assert flag-on hurts (this is the documented anti-case from ripopt). Pin the behavior so we know if upstream feral / IPM changes affect it.
- Re-run audit's 42-problem set: count rescues + honest-infeasibility upgrades. Acceptance: ≥1 hard rescue, ≥10 honest-infeasibility upgrades on our corpus (ripopt got 2 + 26).
Deliverable. benchmarks/cutest/l1_audit/REPORT.md updated with Phase-3 numbers; tests green; comparison table vs ripopt.
Phase 3.5 — Auto-fallback option (1–2 days)
Goal. Make the wrapper invisible-but-effective for users who don't know to opt in.
Tasks.
- New option
l1_fallback_on_restoration_failure: bool (default false). When set, an optimize_tnlp call ending in Restoration_Failed / Infeasible_Problem_Detected / Solved_To_Acceptable_Level triggers an automatic retry with l1_exact_penalty_barrier = true.
- Promotion rule: only replace the original result if the retry returns
Solve_Succeeded. Otherwise return the original result with the retry's iteration count folded into SolveStatistics.
- Tests:
auto_fallback_promotes_resto_failure_to_optimal — pick one of the Phase-3 hard rescues; with vanilla flag set, assert promotion fires and we get Solve_Succeeded.
auto_fallback_keeps_original_when_retry_also_fails — Wächter-Biegler counterexample; vanilla path returns Solve_Succeeded so fallback should not fire. Pick a problem where vanilla returns Restoration_Failed and the wrapper also fails; assert original status is preserved.
- Document the option in
crates/pounce-cli/README.md and the workspace README.
Deliverable. cargo test --workspace green; auto-fallback option documented.
Phase 4 — MPCC paper reproduction (1–2 weeks, the deliverable ripopt deferred)
Goal. Reproduce the specific MPCC benchmarks Thierry & Biegler 2020 report, comparing pounce-flag-on against pounce-flag-off and against the paper's reported numbers. This is the empirical case for the paper's central claim that the wrapper handles MPCC-class degeneracy where stock IPM thrashes.
Tasks.
- Acquire MPCC benchmark sources. Two candidate corpora:
- MacMPEC (Sven Leyffer's collection) — 145 AMPL
.mod files of MPCC test problems. The community standard.
- MPECLib (Ferris et al.). Older but cited by the paper.
The Thierry-Biegler 2020 paper reports on a curated subset of MacMPEC. If we can find the exact subset they used (their Table 1 names), reproduce on those; otherwise reproduce on the full MacMPEC corpus and report both shapes.
- Translate to
.nl via the AMPL CE binary already used in benchmarks/mittelmann/Makefile. New harness benchmarks/mpcc/:
Makefile mirroring benchmarks/mittelmann/Makefile (fetch / translate / run-pounce / run-ipopt / report).
problems.txt listing the corpus.
run_solver.sh parametrized like the Mittelmann version.
- Run four configurations on the corpus:
- pounce, flag off (baseline)
- pounce,
l1_exact_penalty_barrier = true
- pounce,
l1_fallback_on_restoration_failure = true
- ipopt 3.14 + MA57 (reference; the paper compares to this)
- Comparison report
benchmarks/mpcc/REPORT.md:
- Per-problem table: status, iters, wall, objective for all four configurations.
- Aggregate: solve-rate, iter geomean, wall geomean.
- Per-problem comparison vs the paper's Table 1 numbers where available.
- Categorical breakdown: NLP-degenerate vs MPCC-strict-complementarity-violating vs other.
- Acceptance criterion. The wrapper improves solve-rate on the MacMPEC corpus by ≥X% over flag-off pounce, where X is calibrated against the paper's reported delta (read the paper, set the threshold accordingly — placeholder ≥10% pending paper read).
- Stretch: write up findings. Section in
benchmarks/mpcc/REPORT.md summarizing where the wrapper helps, where it doesn't, and how our numbers compare to the paper. This is the MPCC reproduction the field is missing publicly.
Deliverable. benchmarks/mpcc/ harness + report + acceptance criterion met.
Open questions for Phase 4 (resolve in Phase 0 audit).
- Does Thierry-Biegler 2020 list the exact MacMPEC subset in their paper? (PDF links in pounce#5.) If not, default to the full MacMPEC corpus.
- AMPL CE may not translate every MacMPEC
.mod file — confirm coverage before locking the harness in.
- The paper's per-step BNW variant vs our per-outer BNW: if the gap is large we may need to revisit Phase 3 implementation choice.
Phase 5 — Documentation, options reference, release (1–2 days)
- Per-crate README for
pounce-l1penalty.
- New section in workspace README: "Degenerate / MPCC NLPs" with worked example.
- Option reference: extend
crates/pounce-algorithm/src/upstream_options.rs registration list.
- Note in
crates/pounce-cli/README.md and pounce --help covering l1_exact_penalty_barrier and l1_fallback_on_restoration_failure.
- Reference the MPCC reproduction report from the new section.
- Cross-link this issue's eventual closure comment from pounce#5 and from
benchmarks/mpcc/REPORT.md.
Cumulative timeline estimate
| Phase |
Effort |
Cumulative |
| 0 audit |
1 d |
1 d |
| 1 wrapper + flag-off |
3–5 d |
6 d |
| 2 driver + back-proj |
2–3 d |
9 d |
| 3 BNW + infeasibility |
3–4 d |
13 d |
| 3.5 auto-fallback |
1–2 d |
15 d |
| 4 MPCC reproduction |
7–14 d |
29 d |
| 5 docs + release |
1–2 d |
31 d |
≈4 weeks engineering for the full deliverable. Phase 4 dominates and is the part that can be deferred or split off if calendar pressure demands.
Acceptance criteria (whole-issue)
Out of scope (file separately if pursued)
- Per-step BNW (the paper's variant). v1 ships per-outer-iter BNW per ripopt's design choice.
- Replacing pounce's restoration phase with the wrapper. Restoration stays as today; the wrapper is an opt-in alternative for the user.
- Sparse-aware optimization of the slack-block Jacobian assembly — the slack blocks are
±I, so a dense expansion is fine for the volume of problems where the wrapper helps; revisit if 2·m_eq grows past 100k for a real user.
Cross-link
- pounce#5 — research / decision (lead-in to this implementation issue).
- ripopt#23 — algorithmic reference; commit
7847bba9 is the canonical port source.
- Thierry & Biegler 2020 IFAC paper — primary algorithmic reference for the MPCC reproduction in Phase 4.
- pounce#9 — restoration-phase TRON (independent; does not block this).
Goal
Port the Thierry & Biegler (2020) ℓ₁-exact penalty-barrier wrapper from ripopt#23 / commit
7847bba9into pounce aspounce-l1penalty, and ship the MPCC paper reproduction that ripopt explicitly deferred (their probe was on the adjacent CUTEst NE-suffix slice). End state: a default-off opt-in wrapper plus an opt-in auto-fallback, validated on (a) pounce's own CUTEst sweep, (b) the same 42-problem NE-suffix probe ripopt ran, and (c) the MPCC benchmarks Thierry & Biegler 2020 report.Background
See pounce#5 for the algorithmic background. Algorithmic reference:
L1PenaltyBarrierNlpin ripopt#23 commit7847bba9, 550 LoC wrapper. The ripopt closing comment on issue #23 documents 2 hard rescues (DENSCHNENE,HATFLDFLNE), 26 honest-infeasibility upgrades (RestorationFailed/MaxIterations→LocalInfeasibility, often turning 3000-iter budget exhausts into 50–200-iter certificates), and 0 regressions on a 42-problem CUTEst NE-suffix probe; flag-off byte-identical on a 100-problem comparison (99/100; sole diff is wall-time non-determinism onCRESC132).Architecture decisions
Crate placement. New crate
crates/pounce-l1penalty/. Depends onpounce-common,pounce-nlp. Used bypounce-algorithm(for the IPM driver hook) andpounce-cli(for option wiring). Mirrors howpounce-restorationis structured.Wrapper level. Wrap at the
TNLPlevel —crates/pounce-l1penalty/src/wrapper.rsexposesL1PenaltyBarrierTnlp<'a>(inner: Rc<RefCell<dyn TNLP>>, opts)implementingTNLP. Rationale: the augmented NLP feeds straight into the existingTNLPAdapter → OrigIpoptNlpchain unchanged, which is the cleanest separation. ripopt wraps atNlpProblem(its analog), not at the lower algorithm-sideNlptrait — same call here.Variable layout.
[x(n_orig), p(m_eq), n(m_eq)], totaln_orig + 2·m_eqvariables. Constraints unchanged in row order.Detection of equality rows.
g_l[i] == g_u[i]. Cached ineq_rows: Vec<usize>at construction time.Phased plan
Phase 0 — Audit & baseline (1 day, no code shipped)
Goal. Quantify how much of pounce's current failure tail this targets, before writing any wrapper code.
Tasks.
benchmarks/cutest/results_feral_v030fma_2026-05-13.json, classify pounce's non-Solve_Succeededoutcomes by status. Extract theRestorationFailed,LocalInfeasibility,Acceptable,MaxIterationsproblems — these are the auto-fallback candidates.m_eq > 0(equality rows present). Augment with rank deficiency hints: rerun those problems withprint_level=8and grep for "perturbation" / "Wrong Inertia" patterns. Those are the wrapper's primary targets.benchmarks/cutest/l1_rescue/problems.txt— record pounce's current outcome for each so we can compare like-for-like.Deliverable.
benchmarks/cutest/l1_audit/REPORT.mdwith: pounce-side rescue-candidate count, ripopt 42-problem comparison table (current pounce status), Phase 4 MPCC corpus inventory.Stop-gate. If fewer than ~5 pounce problems are rescue-candidates, downgrade the priority and revisit. (ripopt found 28 of 42 actionable; we expect a similar ratio on our corpus given shared algorithmic lineage.)
Phase 1 — TNLP wrapper, fixed ρ, default-off byte-identical (3–5 days)
Goal. Land the wrapper and the option wiring; prove it doesn't change any solve trajectory when the flag is off.
Tasks.
crates/pounce-l1penalty/. Cargo.toml + README + workspace registration.src/wrapper.rs:L1PenaltyBarrierTnlp<'a>implementing theTNLPtrait. Closely follows ripopt'sL1PenaltyBarrierNlp— algorithmic body is essentially a translation. Key changes from ripopt: pounce usesRc<RefCell<dyn TNLP>>for the inner pointer; sparsity is reported viaSparsityRequest<'_>(pounce's enum) rather than separate row/col returns.src/options.rs: registerl1_exact_penalty_barrier(bool, default false),l1_penalty_init(f64, default 1.0). Plumbed intopounce-cli's option-table loader.pounce-algorithm/src/application.rs::optimize_tnlp: when the option is set, replace the user TNLP withRc::new(RefCell::new(L1PenaltyBarrierTnlp::new(user_tnlp, opts)))before the existing chain. No back-projection yet — Phase 2.crates/pounce-l1penalty/tests/wrapper.rs(~10 unit tests, mirroring ripopt'stests/l1_penalty_barrier.rs::wrapper_*):g_l[i] == g_u[i].eval_faugments byρ·Σ(p+n)correctly.eval_grewrites equality rows toc_i(x) − p_i + n_i = g_i.eval_jac_gincludes the+/-Iblocks for slacks.eval_hdoes not add Hessian contribution from the linear penalty term (slacks have zero Hessian).(x_L, x_U)and adds(0, +∞)for(p, n).x_0and seeds(p, n)frommax(0, ±c(x_0)).benchmarks/cutest/l1_audit/regression_pre_l1.jsonl(current run) vsregression_post_l1_flag_off.jsonl(post-Phase-1, flag off). Acceptance: ≥99/100 byte-identical (matches ripopt's threshold).Deliverable. Crate compiles, tests pass, flag-off byte-identical on 100-problem CUTEst probe. Flag-on does not yet back-project the solution — that's Phase 2.
Phase 2 — Augmented IPM driver + back-projection (2–3 days)
Goal. Reported
(x*, λ*)and reported objective are the original problem's, not the augmented problem's.Tasks.
pounce-algorithm/src/application.rs::optimize_tnlp, after the augmented IPM solve completes:xfrom lengthn_orig + 2·m_eqback ton_orig.f(x*)on the user TNLP (strip theρ·Σ(p+n)term from the reported objective).c(x*)on the user TNLP (strip the slack contribution from constraint values).λ_eq[i]is the same scalar in both spaces (the slack rows don't introduce new dual variables, only new primals).Solution<'_>callback: ensure the user receives original-space(x*, λ*, z_L*, z_U*), not augmented-space.flag_on_objective_excludes_penalty_term— solve a small problem, assert reportedobj == f(x*)notf(x*) + ρ·Σ(p+n).flag_on_solution_x_truncated— assertx.len() == n_orig.flag_on_lambda_eq_passes_through— solve a problem with one equality, assertλ_eqmatches a known reference within 1e-8.Deliverable. End-to-end solve in augmented space with original-space reporting. At least one audit candidate transitions failure → success at fixed ρ.
Phase 3 — Byrd–Nocedal–Waltz dynamic ρ + honest-infeasibility upgrade (3–4 days)
Goal. Replace the fixed ρ with the BNW steering rule. Detect non-collapsing slacks and upgrade
Optimal/Acceptable→LocalInfeasibilitywhen the original problem is genuinely infeasible at the ℓ₁ stationary point.Tasks.
application.rsaround the inner IPM solve. After each inner solve:y_eqfrom theSolution<'_>callback.ρ_new = max(ρ · l1_penalty_increase_factor, l1_steering_factor · ‖y_eq‖∞ + ε), capped atl1_penalty_max.Σ(p+n) < l1_slack_tol(slacks collapsed).Solve_SucceededorSolved_To_Acceptable_LevelbutΣ(p+n) ≥ l1_slack_tol, overrideApplicationReturnStatustoInfeasible_Problem_Detected. The point is BNW ℓ₁-stationary but the original problem has no feasible solution at this iterate.l1_penalty_max,l1_penalty_increase_factor,l1_penalty_max_outer_iter,l1_slack_tol,l1_steering_factor— defaults match ripopt's choices.bnw_rho_escalates_with_lambda— solve a problem where the equality multipliers grow, assert ρ tracksτ·‖y_eq‖∞.infeasible_certificate_burke_han— minimize subject tox₁²+x₂²=0withx₁ ≥ 1(Burke-Han classic infeasible). Assert status →Infeasible_Problem_Detectedwith collapsed slack mass corresponding to the ℓ₁-best infeasibility distance.wachter_biegler_anti_case—x₁ = 1, x₁² − x₂² = 0. Assert flag-on hurts (this is the documented anti-case from ripopt). Pin the behavior so we know if upstream feral / IPM changes affect it.Deliverable.
benchmarks/cutest/l1_audit/REPORT.mdupdated with Phase-3 numbers; tests green; comparison table vs ripopt.Phase 3.5 — Auto-fallback option (1–2 days)
Goal. Make the wrapper invisible-but-effective for users who don't know to opt in.
Tasks.
l1_fallback_on_restoration_failure: bool(defaultfalse). When set, anoptimize_tnlpcall ending inRestoration_Failed/Infeasible_Problem_Detected/Solved_To_Acceptable_Leveltriggers an automatic retry withl1_exact_penalty_barrier = true.Solve_Succeeded. Otherwise return the original result with the retry's iteration count folded intoSolveStatistics.auto_fallback_promotes_resto_failure_to_optimal— pick one of the Phase-3 hard rescues; with vanilla flag set, assert promotion fires and we getSolve_Succeeded.auto_fallback_keeps_original_when_retry_also_fails— Wächter-Biegler counterexample; vanilla path returnsSolve_Succeededso fallback should not fire. Pick a problem where vanilla returnsRestoration_Failedand the wrapper also fails; assert original status is preserved.crates/pounce-cli/README.mdand the workspace README.Deliverable.
cargo test --workspacegreen; auto-fallback option documented.Phase 4 — MPCC paper reproduction (1–2 weeks, the deliverable ripopt deferred)
Goal. Reproduce the specific MPCC benchmarks Thierry & Biegler 2020 report, comparing pounce-flag-on against pounce-flag-off and against the paper's reported numbers. This is the empirical case for the paper's central claim that the wrapper handles MPCC-class degeneracy where stock IPM thrashes.
Tasks.
.modfiles of MPCC test problems. The community standard.The Thierry-Biegler 2020 paper reports on a curated subset of MacMPEC. If we can find the exact subset they used (their Table 1 names), reproduce on those; otherwise reproduce on the full MacMPEC corpus and report both shapes.
.nlvia the AMPL CE binary already used inbenchmarks/mittelmann/Makefile. New harnessbenchmarks/mpcc/:Makefilemirroringbenchmarks/mittelmann/Makefile(fetch / translate / run-pounce / run-ipopt / report).problems.txtlisting the corpus.run_solver.shparametrized like the Mittelmann version.l1_exact_penalty_barrier = truel1_fallback_on_restoration_failure = truebenchmarks/mpcc/REPORT.md:benchmarks/mpcc/REPORT.mdsummarizing where the wrapper helps, where it doesn't, and how our numbers compare to the paper. This is the MPCC reproduction the field is missing publicly.Deliverable.
benchmarks/mpcc/harness + report + acceptance criterion met.Open questions for Phase 4 (resolve in Phase 0 audit).
.modfile — confirm coverage before locking the harness in.Phase 5 — Documentation, options reference, release (1–2 days)
pounce-l1penalty.crates/pounce-algorithm/src/upstream_options.rsregistration list.crates/pounce-cli/README.mdandpounce --helpcoveringl1_exact_penalty_barrierandl1_fallback_on_restoration_failure.benchmarks/mpcc/REPORT.md.Cumulative timeline estimate
≈4 weeks engineering for the full deliverable. Phase 4 dominates and is the part that can be deferred or split off if calendar pressure demands.
Acceptance criteria (whole-issue)
cargo test --workspacegreen withpounce-l1penaltyincluded.Solve_Succeededwithout regressing anySolve_Succeededproblem.pounce-l1penaltyREADME, workspace README section, options reference all published.Out of scope (file separately if pursued)
±I, so a dense expansion is fine for the volume of problems where the wrapper helps; revisit if2·m_eqgrows past 100k for a real user.Cross-link
7847bba9is the canonical port source.