-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
BUG: linprog returns 'infeasible' for unbounded case (fix for #11617) #11618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Thanks for trying this @andrewdelong. |
@mdhaber I'm a little puzzled too. Regarding the original bug, that's a case where the problem is unambiguously unbounded, so I think it should definitely return unbounded in that case. The bug only shows up with For the failing test, that's the case where the system is unbounded wrt one variable and infeasible wrt another. In this case there doesn't seem to be a consistent relation between For example, the system below returns "unbounded", both for the original code and proposed changes: c = [-1, -1]
A_eq = [[0, 1], [0, 2]]
b_eq = [1, 1]
linprog(c, None, None, A_eq, b_eq, options={"presolve": False}) because at termination it has homogeneous quantities
but merely changing to
Negative coefficients like
In any of these cases, the Still, if you run the above examples with |
I'm tempted to leave this alone unless there is a really sound way to resolve it. I think this part of the code faithfully corresponds with the algorithm, and we can't guarantee that the code will work on every problem with the default tolerances. Does that make sense? I appreciate your thoughts on this. I don't really have any other ideas, though. |
I think it's totally reasonable not to make the change, so no worries closing it out. I'll just add one last comment on why I lean slightly toward the change. There are two cases under consideration:
The PR fixes (1) but alters the already-inconsistent behaviour of (2) by removing the "bias" and thereby the pretence of consistency in scenario (2). There exists a unit test concerning (2), so someone out there considers "infeasible" to be [always?] correct, matching simplex, but the current code doesn't achieve that consistently for interior-point. As for fidelity to the MOSEK paper, I didn't encounter any specifics in [4] about how to test |
@Kai-Striega I'd appreciate a third opinion on this. I wouldn't ask you to delve into the specifics, just the broad strokes - |
@andrewdelong One thing that would help convince me would be benchmarking on infeasible and unbounded problems. I've wanted to add the infeasible problems from the Netlib LP test suite to our benchmarks. Would you be interested in taking a shot at that? (If you need help getting started, I think I can point you in the right direction.) |
@mdhaber I could but not in the coming weeks. Is your goal to evaluate speed (scipy/benchmarks) or correctness (scipy/optimize/tests)? And in a separate PR I assume. I see you've converted NETLIB instances to Of course, if scipy provided MPS file reading, like cvxopt's fromfile, then users (and tests) could load NETLIB and other files directly to scipy, but that's a whole other feature and not much advantage. |
The goal is to evaluate correctness in classifying them as infeasible or unbounded, not to measure speed. Yes, I did do those conversions using my own MPS reader a few years ago. (I'm not sure if cvxopt's was available, or maybe I just didn't find it.) I just put it up on a repo for you after converting to Python 3 : ) I tested using the script at the bottom that it loads uncompressed MPS files correctly (unless they have ranges - that's not implemented) and saves and loads .npz files. I think you can pretty easily extend the script to run a batch job. Note that the files you download from Netlib are compressed MPS files. They can be uncompressed using Whenever you get around to it, thanks! |
First, welcome @andrewdelong and thank you taking the time and effort, it's made understanding the issue and PR much easier for me. Second my apologies for the delayed response - I've been quite ill over the last month and haven't focused too much on FOSS. I am slightly for the change, but I would prefer to see the results of the benchmarks before we commit to a change. Such a simple problem shouldn't be failing. Especially if the alternative test catches it reliably and the existing code is inconsistent to begin with.
If I understand it correctly, @mdhaber's main concern with changing it isn't the inconsistent behaviour of the existing code, but that this issue essentially stems from numerical errors which could always occur if the problem is picked correctly. But the proposed changes look solid and follow the paper (or at least I couldn't find a reference on which method to use). One more thing to consider, what if someone currently relies on having a specific tolerance and being able to change it via the |
Maybe my concern can be addressed by the answer to this question: If we get to this comparison in the code: |
@Kai-Striega Thanks! I don't know when I could contribute to the benchmarks. This PR can go on the backlog if everyone's comfortable. @mdhaber Good question but I don't know the answer. It's empirically true for the handful of instances I inspected. I suppose it's true to the degree that the gap constraint Alternative fix that is probably a bad idea. I noticed that reference [4] uses different internal tolerances than
I only mention the Table 8.1 tolerances to emphasize a meaningful difference between |
Yes, I remember reviewing the table when I wrote the method, and I believe we agreed to leave separating and exposing all the tolerances to the user in a future PR. It's OK with me for this PR to go on the backburner. Would you like me to check in with you in a few months? |
👍 for this going on the backlog (for now). Maybe, once the lapack wrappers are done, I can take a look at setting up the infeasible benchmarks if no one else has gotten around to them by that point. |
Update. The previous version of this PR (test Old fix fails. The original code ( s = 1e9 # or 1e4, ... 1e9
c = [s, 0., -s]
A_eq = [[s, s, -s]]
b_eq = [-s]
linprog(c, None, None, A_eq, b_eq) terminates due to
The above also indicates that New fix. When Thoughts on alternatives. I'm not sure the new conditions are perfect, but they're somewhat justified and work so far. I can't think of a legitimate role for |
@andrewdelong I'll try to take a look at your most recent work this week Update: currently
With sparse arithmetic, |
@andrewdelong Yes, I see that the new approach has justification. I'm curious how it does on the benchmarks I just added in gh-11729. If we can also find a few unbounded benchmarks and test on those, too, I'd be happy to merge this. Do you have access to Matlab? I thought we could check the duals of these infeasible benchmarks for unboundedness, and use the ones that are unbounded. |
- Add test_bug_11617 - test_enzo_example_c_with_infeasibility now fails with this change (only for interior-point), but I am unsure how to satisfy both tests.
tests are more rigorous (caught extra bugs) and all tests passing now
scipy#11729 - `test_bug_11617` is not parametrized - The logic for determining status {2,3} now passes the tests for this bug and also pass the NETLIB infeasible benchmark introduced in PR scipy#11729 - The logic still prioritizes reporting "infeasible" if it can be reliably detected. All three cases (if, elif, elif) are required to pass all tests and benchmarks.
@mdhaber Thanks for #11729, as this PR was indeed breaking a couple of the tests you added. The only way I could get it to pass all tests (unit tests + NETLIB tests) was a two-tiered logic: first check for for well-behaved infeasible/unbounded cases (the first Separate infeasibility detection issue. Trivially infeasible instances with small coefficients cause
This happens for simplex and interior-point, caused by def test_bug_postprocess_raises_trivial_infeas(self):
# postprocess raised an uncaught ValueError for a trivially infeasible
# instance with small coefficients
c = [1e-5]
A_eq = [[1e-5]]
b_eq = [0]
bounds = [(1e-5, None)]
res = linprog(c, None, None, A_eq, b_eq, bounds, method=self.method)
_assert_infeasible(res) |
I can't remember how we ended up with this @andrewdelong just checking - are you suggesting that this is feasible within certain tolerances, or are you mostly concerned about the |
@mdhaber My concern was mainly that raising a scipy/scipy/optimize/_linprog_util.py Lines 1500 to 1504 in 0538284
|
@andrewdelong Yeah, and your example problem was assigned |
I think that I set it as an error before we had merged the two solvers, then didn't want to break backwards compatibility. Looking at it now, I think it will be OK to return the same status and set the message to something similar to comment above. |
@Kai-Striega You prefer I try to tack that fix/test into this branch, or open a separate issue? (I don't have a test that triggers the scenario that originally motivated the comment.) |
I would prefer this as its own PR. I don't think there's a need for an issue, just open a PR and reference this conversation.
These occur for the Simplex method on problems 5140, 6139, 7237. There are quite a few combinations of the tests so you will need to find the exact combination yourself (maybe just change the code and see which ones fail) |
Reference issue
Closes #11617.
What does this implement/fix?
Tries to fix issue #11617 by evaluating both
b.T @ y
and-c.T @ x
to determine whether to return 'infeasible' or 'unbounded'. The idea is that the one with largest magnitude is "most responsible" for primal-dual infeasibility (kappa > tol
).However,
test_enzo_example_c_with_infeasibility
now fails with this change (only for interior-point) because it is unbounded in some variables and infeasible in others, so the PR needs input from a maintainer on how to best satisfy both tests.Additional information
I'll comment on the behaviour of the new change with respect to the failing test below:
scipy/scipy/optimize/tests/test_linprog.py
Lines 922 to 935 in 0538284
Running the above test gives with
m = 2
giveswhich is unbounded in
x1
and infeasible inx2
. Linprog returns 'unbounded' both before and after the PR becauseb.T @ y <= tol
at termination, specifically:Running the test with the original
m = 50
used to return 'infeasible' but now returns 'unbounded', because at terminationb.T @ y > tol
even thoughb.T @ y < -c.T @ x
:The problem instance is unbounded in one variable and infeasible in the others, so I assume the desired behaviour is to return 'infeasible', but (a) I am unsure how to implement that and (b) it would still be inconsistent with old code's behaviour for case
m = 2
. (Before this PR, the interior-point method seemed to report 'infeasible' form > 3
and report 'unbounded' form <= 3
.)