Skip to content

Postsolve for infeasibility certificates - #48

Merged
dance858 merged 11 commits into
dance858:mainfrom
LucasBoTang:main
Jul 30, 2026
Merged

Postsolve for infeasibility certificates#48
dance858 merged 11 commits into
dance858:mainfrom
LucasBoTang:main

Conversation

@LucasBoTang

@LucasBoTang LucasBoTang commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What it does

PSLP's postsolve maps a solution of the reduced problem back to the
original problem, but not the certificate returned when the problem is
infeasible or unbounded. This PR adds that mapping for both cases:

  • primal infeasible: the Farkas dual ray is lifted back to the original rows;
  • dual infeasible: the Farkas primal ray is lifted back to the original columns.

Interface

// primal infeasibility
void postsolve_primal_infeas_ray(Presolver *presolver, const double *y, double *y_orig);
// dual infeasibility
void postsolve_dual_infeas_ray(Presolver *presolver, const double *x, double *x_orig);

Tests

tests/test_ray_postsolve.h:

  • One unit test per reduction.
  • End-to-end tests on small infeasible / unbounded problems to verify
    certificate validity.
  • Checks the Farkas conditions directly.

@LucasBoTang

Copy link
Copy Markdown
Contributor Author

For each of the 29 netlib infeasible LPs I ran the full pipeline end to end:

  1. Presolve the LP with PSLP;
  2. Solve the reduced LP with Gurobi (Presolve=0, InfUnbdInfo=1) to get its Farkas ray;
  3. Lift the ray with postsolve_primal_infeas_ray;
  4. Verify the lifted ray on the original LP with an independent check (z = -Aᵀy, Farkas support < 0).

Only 11 of the 29 are reduced without being detected infeasible outright, so only those reach the postsolve. For all 11 the lifted ray is a valid Farkas certificate of the original problem, and the Farkas support matches exactly between the reduced and original problems, so the lift is faithful.

problem m×n → reduced red_support orig_support
box1 231×261 → 21×66 −1 −1 VALID
cplex2 224×221 → 222×219 −2.65e−6 −2.65e−6 VALID
ex72a 197×215 → 30×47 −1 −1 VALID
ex73a 193×211 → 31×49 −1 −1 VALID
forest6 66×95 → 66×90 −2.60e5 −2.60e5 VALID
klein2 477×54 → 476×54 −1.38e5 −1.38e5 VALID
klein3 994×88 → 993×88 −3.68e5 −3.68e5 VALID
pang 361×460 → 290×329 −118.4 −118.4 VALID
qual 323×464 → 188×304 −236.1 −236.1 VALID
refinery 323×464 → 187×302 −2071 −2071 VALID
vol1 323×464 → 188×304 −1.81e6 −1.81e6 VALID

@dance858

dance858 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

This is awesome, thank you so much for submitting this contribution. Sorry for the delay with getting back to you.

Two comments from me below and one regression test from Uncle Claude (I'm very happy to discuss and reconsider in case you don't agree on something. And in case Uncle Claude is wrong, please let me know.):

  1. Can you move the new ray postsolve code into its own file, maybe RayPostsolver.c (keeping the declarations in Postsolver.h)? I think it's a nice piece of modular code that deserves its own file.
  2. I don't fully understand the sign convention on the multipliers. The regular postsolve() expects y_i \geq 0 when row i is active at its lhs (that's how I read retrieve_lhs_change), but the ray postsolve seems to assume y_i \geq 0 when row i is active at its rhs (as in is_primal_ray_valid_certificate). Is my observation correct? Ideally we would have the same sign convention. Is it difficult to fix that?

I also asked Uncle Claude for a correctness review. Please see the attached file.

Once again, thank you for this contribution. I'll make sure to be much faster with feedback in the next round.

@LucasBoTang

Copy link
Copy Markdown
Contributor Author

Thanks for digging into this. For the sign convention, you're right, but it's deliberate: I followed Gurobi. It draws the same distinction between its own two attributes, for a >= row Pi is >= 0 but FarkasDual is <= 0. So postsolve() follows Pi and the ray postsolve follows FarkasDual, and you can hand a Gurobi Farkas ray straight to postsolve_primal_infeas_ray without touching the signs. That's how I ran the netlib validation.

If we go with one convention, one of the two ends up reversed against the solver and users have to negate the ray themselves. I'd rather keep it and just document both conventions in Postsolver.h. Happy to change it if you want.

I'll get on the rest in the meantime.

@dance858

Copy link
Copy Markdown
Owner

Thanks for digging into this. For the sign convention, you're right, but it's deliberate: I followed Gurobi. It draws the same distinction between its own two attributes, for a >= row Pi is >= 0 but FarkasDual is <= 0. So postsolve() follows Pi and the ray postsolve follows FarkasDual, and you can hand a Gurobi Farkas ray straight to postsolve_primal_infeas_ray without touching the signs. That's how I ran the netlib validation.

Interesting! Didn't know that.

If we go with one convention, one of the two ends up reversed against the solver and users have to negate the ray themselves. I'd rather keep it and just document both conventions in Postsolver.h. Happy to change it if you want.

Totally fine with me to go with the simpler approach of just documenting it. Perhaps just add a comment above postsolve_primal_infeas_ray and postsolve_dual_infeas_ray in PSLP_API.h?

I'll get on the rest in the meantime.

Awesome, let me know if there's anything I can do to help.

@LucasBoTang

Copy link
Copy Markdown
Contributor Author

On the file split: I left retrieve_deleted_row, retrieve_added_row and retrieve_added_rows in Postsolver.c since both postsolve paths use them, and just declared them in Postsolver.h.

It could also be in an independent shared file. Let me know what you think.

@LucasBoTang

Copy link
Copy Markdown
Contributor Author

Also added the sign convention comment above both functions in PSLP_API.h, and fixed the multiplier bug regression test.

@dance858 dance858 self-assigned this Jul 30, 2026
@dance858

Copy link
Copy Markdown
Owner

Awesome, great contribution! Merging.

@dance858
dance858 merged commit 320f48b into dance858:main Jul 30, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants