Skip to content
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

BUG: optimize: In _linprog_simplex, handle the case where the status returned by _solve_simplex is not 0 or 2. #3888

Merged
merged 1 commit into from
Aug 21, 2014

Conversation

perimosocordiae
Copy link
Member

The local variable message wasn't being set in all cases, causing runtime errors.

The local variable `message` wasn't being set in all cases, causing runtime errors.
@argriffing
Copy link
Contributor

I think this change looks good.

@coveralls
Copy link

Coverage Status

Coverage remained the same when pulling 41394a6 on perimosocordiae:patch-6 into 11d9bd2 on scipy:master.

@argriffing
Copy link
Contributor

A new test would be helpful.

@WarrenWeckesser
Copy link
Member

Also, put a bit more context in the commit message, perhaps something like "BUG: optimize: In _linprog_simplex, handle the case where the status returned by _solve_simplex is not 0 and not 2."

@perimosocordiae perimosocordiae changed the title BUG: Handle the case where status != 2 BUG: optimize: In _linprog_simplex, handle the case where the status returned by _solve_simplex is not 0 or 2. Aug 21, 2014
@perimosocordiae
Copy link
Member Author

I'm having trouble coming up with a minimal test case. The bug occurred when status = 3, which indicates that _solve_simplex was able to find a pivot column, but not a pivot row. This happened to coincide with the objective function being zero (abs(T[-1, -1]) < tol), which meant that the status was not reset to 2.

Unfortunately, I don't know the algorithm well enough to work backwards to input data that generate this behavior.

argriffing added a commit that referenced this pull request Aug 21, 2014
BUG: optimize: In _linprog_simplex, handle the case where the status returned by _solve_simplex is not 0 or 2.
@argriffing argriffing merged commit b954e6d into scipy:master Aug 21, 2014
@argriffing
Copy link
Contributor

Merged, thanks! I'll add a github issue in case anyone wants to try adding the test.

@argriffing
Copy link
Contributor

I might have merged this too early. If phase 1 attempts to find a feasible solution, then shouldn't an error in this phase report that message and status, even if the attempt to find a feasible solution happens to use linear programming which failed for some other reason (e.g. unboundedness)? I agree that it's not good to have the message and status out of sync but maybe it should go the other way, in other words both the status and message should perhaps report feasibility failure rather than both reporting unboundedness, if the linear programming error occurred in phase 1?

@perimosocordiae
Copy link
Member Author

That's fair. In that case, a fix might look like:

if status == 0 and abs(T[-1, -1]) < tol:
    # Remove the pseudo-objective row from the tableau
    # ...etc...
else:
    # Failure to find a feasible starting point
    status = 2

@pv
Copy link
Member

pv commented Aug 21, 2014

linprog is not in 0.14.0, so the API can be changed freely.

@argriffing
Copy link
Contributor

@perimosocordiae on the other hand, is it actually detecting that the original problem is unbounded during phase 1 when it is nominally looking for a feasible solution? If so, then I think the PR works. But if it is solving a different linear programming problem in the process of looking for a feasible solution to the original problem, then if this search fails because the different linear program was unbounded, then I think the user would want to see a message reflecting failure to find a feasible solution to the original problem rather than the details of the mode of failure of that search. If I understood the algorithm better then I'd know which it is.

@perimosocordiae
Copy link
Member Author

I agree entirely. Maybe we should consult @robfalck, because I don't know which makes more sense either.

@argriffing
Copy link
Contributor

I checked the test suite, and the case where the status is something other than 0 or 2 at the end of phase 1 is not covered by any test.

@robfalck
Copy link
Contributor

Good find. The code to better handle non-convergence of Phase 1 needs to be cleaned up. I can do that.

On a similar note, I've found a case where an artificial variable remains basic at the end of Phase 1 (which is known to happen sometimes). I need to add the ability to handle the situation, since the failure to do so leads to incorrect results. This may be related to the issue found by @perimosocordiae.

@robfalck
Copy link
Contributor

Also, when I wrote this code, I was under the assumption that phase one could not fail due to unboundedness (only due to infeasibility). Phase 1 is run if the origin is not a basic feasible solution. If the origin is not a basic feasible solution, then I don't see how the problem could be unbounded. If you have a counter-example, I'd be more than happy to look at this more.

@argriffing
Copy link
Contributor

I'm having trouble coming up with a minimal test case.

Maybe someone at euroscipy sprints could try brutely sampling random linear programs to look for an example? Of course this will fail if examples do not occur with positive probability..

@perimosocordiae
Copy link
Member Author

@robfalck The bug did present when I used (-inf, inf) bounds, which I believe adds an artificial variable. That might be a good starting point.

@ev-br ev-br added this to the 0.15.0 milestone Aug 31, 2014
@argriffing
Copy link
Contributor

I haven't been able to construct a counterexample, but it's possible that np.inf is interacting badly with linprog's usage of masked arrays. numpy.ma will unilaterally mask-out infs.

@argriffing
Copy link
Contributor

Here's an example where np.inf causes things to go weirdly, but in a different way.

    c = [-1,8,4,-6]
    A_ub = [[-7,-7,6,9],
        [1,-1,-3,0],
        [10,-10,-7,7],
        [6,-1,3,4]]
    b_ub = [-3,np.inf,-6,6]
    A_eq = [[-10,1,1,-8]]
    b_eq = [-4]
    res = linprog(
            c,
            A_ub=A_ub,
            b_ub=b_ub,
            A_eq=A_eq,
            b_eq=b_eq,
            bounds=(-np.inf, np.inf))
_linprog.py", line 215, in _pivot_row
    return True, np.ma.where(q == q.min())[0][0]
IndexError: index 0 is out of bounds for axis 0 with size 0

@robfalck
Copy link
Contributor

Thanks for the example, I'll work on a patch.

On Sun, Dec 21, 2014 at 11:10 PM, argriffing notifications@github.com
wrote:

Here's an example where np.inf causes things to go weirdly, but in a
different way.

c = [-1,8,4,-6]
A_ub = [[-7,-7,6,9],
    [1,-1,-3,0],
    [10,-10,-7,7],
    [6,-1,3,4]]
b_ub = [-3,np.inf,-6,6]
A_eq = [[-10,1,1,-8]]
b_eq = [-4]
res = linprog(
        c,
        A_ub=A_ub,
        b_ub=b_ub,
        A_eq=A_eq,
        b_eq=b_eq,
        bounds=(-np.inf, np.inf))

_linprog.py", line 215, in _pivot_row
return True, np.ma.where(q == q.min())[0][0]
IndexError: index 0 is out of bounds for axis 0 with size 0


Reply to this email directly or view it on GitHub
#3888 (comment).

  • Rob Falck

@argriffing
Copy link
Contributor

A recent message on the numpy mailing list does not help my confidence in numpy.ma. I try to avoid it in the same way that I avoid numpy.matrix when possible.

@rgommers
Copy link
Member

rgommers commented Jan 1, 2015

@argriffing it's not that bad and, unlike np.matrix, has interesting use cases. Just don't expect to be able to do with it all the fancy stuff you can do with R's NA.

@argriffing
Copy link
Contributor

On a similar note, I've found a case where an artificial variable remains basic at the end of Phase 1 (which is known to happen sometimes). I need to add the ability to handle the situation, since the failure to do so leads to incorrect results.

Yes, I think this is causing problems like #4746 and #4594. Here's the relevant bit:

    # if pseudo objective is zero, remove the last row from the tableau and
    # proceed to phase 2
    if abs(T[-1, -1]) < tol:
        # Remove the pseudo-objective row from the tableau
        T = T[:-1, :]
        # Remove the artificial variable columns from the tableau
        T = np.delete(T, np.s_[n+n_slack:n+n_slack+n_artificial], 1)
    else:
        # Failure to find a feasible starting point
        status = 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants