Repository: scipy/scipy

Issue:
ENH: remaining minor linprog method = 'simplex' work
We just closed #5400, #7237, and #8174 but I think the tests for these - and other tests originally based on issues - need to be reviewed carefully and strengthened where possible, as some of the tests do not check for everything that they should as written. For instance, the test for issue #5400 only checks that a certain warning is issued. That is not enough to demonstrate that the issue reported in #5400 - that `linprog` returns an infeasible solution with `success=True` - is fixed. I believe these issues are indeed fixed, but we need the tests to demonstrate that more convincingly and help us ensure that they never pop up again.

Also, `linprog` should not raise `ValueError` when `status == 2 and is_feasible`, but should instead return the appropriate status code and message in the `OptimizeResult`.



Discussion:

Resolving patch:
diff --git a/scipy/optimize/_linprog.py b/scipy/optimize/_linprog.py
--- a/scipy/optimize/_linprog.py
+++ b/scipy/optimize/_linprog.py
@@ -491,7 +491,6 @@ def linprog(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None,
 
     """
     meth = method.lower()
-    default_tol = 1e-12 if meth == 'simplex' else 1e-9
 
     if x0 is not None and meth != "revised simplex":
         warning_message = "x0 is used only when method is 'revised simplex'. "
@@ -499,7 +498,7 @@ def linprog(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None,
 
     c, A_ub, b_ub, A_eq, b_eq, bounds, solver_options, x0 = _parse_linprog(
         c, A_ub, b_ub, A_eq, b_eq, bounds, options, x0)
-    tol = solver_options.get('tol', default_tol)
+    tol = solver_options.get('tol', 1e-9)
 
     iteration = 0
     complete = False    # will become True if solved in presolve
diff --git a/scipy/optimize/_linprog_simplex.py b/scipy/optimize/_linprog_simplex.py
--- a/scipy/optimize/_linprog_simplex.py
+++ b/scipy/optimize/_linprog_simplex.py
@@ -34,7 +34,7 @@
 from ._linprog_util import _postsolve
 
 
-def _pivot_col(T, tol=1.0E-12, bland=False):
+def _pivot_col(T, tol=1e-9, bland=False):
     """
     Given a linear programming simplex tableau, determine the column
     of the variable to enter the basis.
@@ -94,7 +94,7 @@ def _pivot_col(T, tol=1.0E-12, bland=False):
     return True, np.ma.nonzero(ma == ma.min())[0][0]
 
 
-def _pivot_row(T, basis, pivcol, phase, tol=1.0E-12, bland=False):
+def _pivot_row(T, basis, pivcol, phase, tol=1e-9, bland=False):
     """
     Given a linear programming simplex tableau, determine the row for the
     pivot operation.
@@ -165,7 +165,7 @@ def _pivot_row(T, basis, pivcol, phase, tol=1.0E-12, bland=False):
     return True, min_rows[0]
 
 
-def _apply_pivot(T, basis, pivrow, pivcol, tol=1e-12):
+def _apply_pivot(T, basis, pivrow, pivcol, tol=1e-9):
     """
     Pivot the simplex tableau inplace on the element given by (pivrow, pivol).
     The entering variable corresponds to the column given by pivcol forcing
@@ -229,7 +229,7 @@ def _apply_pivot(T, basis, pivrow, pivcol, tol=1e-12):
 
 
 def _solve_simplex(T, n, basis, maxiter=1000, phase=2, status=0, message='',
-                   callback=None, tol=1.0E-12, nit0=0, bland=False, _T_o=None):
+                   callback=None, tol=1e-9, nit0=0, bland=False, _T_o=None):
     """
     Solve a linear programming problem in "standard form" using the Simplex
     Method. Linear Programming is intended to solve the following problem form:
@@ -372,7 +372,7 @@ def _solve_simplex(T, n, basis, maxiter=1000, phase=2, status=0, message='',
                             if abs(T[pivrow, col]) > tol]
             if len(non_zero_row) > 0:
                 pivcol = non_zero_row[0]
-                _apply_pivot(T, basis, pivrow, pivcol)
+                _apply_pivot(T, basis, pivrow, pivcol, tol)
                 nit += 1
 
     if len(basis[:m]) == 0:
@@ -424,13 +424,13 @@ def _solve_simplex(T, n, basis, maxiter=1000, phase=2, status=0, message='',
                 status = 1
                 complete = True
             else:
-                _apply_pivot(T, basis, pivrow, pivcol)
+                _apply_pivot(T, basis, pivrow, pivcol, tol)
                 nit += 1
     return nit, status
 
 
 def _linprog_simplex(c, c0, A, b, maxiter=1000, disp=False, callback=None,
-                     tol=1.0E-12, bland=False, _T_o=None, **unknown_options):
+                     tol=1e-9, bland=False, _T_o=None, **unknown_options):
     """
     Minimize a linear objective function subject to linear equality and
     non-negativity constraints using the two phase simplex method.


Test patch:
diff --git a/scipy/optimize/tests/test_linprog.py b/scipy/optimize/tests/test_linprog.py
--- a/scipy/optimize/tests/test_linprog.py
+++ b/scipy/optimize/tests/test_linprog.py
@@ -2,6 +2,7 @@
 Unit test for Linear Programming
 """
 from __future__ import division, print_function, absolute_import
+import sys
 
 import numpy as np
 from numpy.testing import (assert_, assert_allclose, assert_equal,
@@ -46,8 +47,12 @@ def _assert_unbounded(res):
 
 def _assert_unable_to_find_basic_feasible_sol(res):
     # res: linprog result object
+
+    # The status may be either 2 or 4 depending on why the feasible solution
+    # could not be found. If the undelying problem is expected to not have a
+    # feasible solution _assert_infeasible should be used.
     assert_(not res.success, "incorrectly reported success")
-    assert_equal(res.status, 2, "failed to report optimization failure")
+    assert_(res.status in (2, 4), "failed to report optimization failure")
 
 
 def _assert_success(res, desired_fun=None, desired_x=None,
@@ -997,27 +1002,14 @@ def test_bug_5400(self):
 
         c = np.array([-1.0, 1, 1, 1, 1, 1, 1, 1, 1,
                       1, 1, 1, 1, 0, 0, 0, 0, 0, 0])
-
-        if self.method == 'simplex':
-            with pytest.warns(OptimizeWarning):
-                res = linprog(c, A_ub, b_ub, A_eq, b_eq, bounds,
-                              method=self.method, options=self.options)
-            # even though the problem is feasible, this is OK because the
-            # message corresponding with status=2 states that simplex
-            # phase 1 _could not find_ a feasible solution - not that problem
-            # _is_ infeasible
-            assert_equal(res.status, 2)
-            assert_equal(res.message[:36],
-                         "Phase 1 of the simplex method failed")
-        else:
-            with suppress_warnings() as sup:
-                sup.filter(OptimizeWarning,
-                           "Solving system with option 'sym_pos'")
-                sup.filter(RuntimeWarning, "invalid value encountered")
-                sup.filter(LinAlgWarning)
-                res = linprog(c, A_ub, b_ub, A_eq, b_eq, bounds,
-                              method=self.method, options=self.options)
-            _assert_success(res, desired_fun=-106.63507541835018)
+        with suppress_warnings() as sup:
+            sup.filter(OptimizeWarning,
+                       "Solving system with option 'sym_pos'")
+            sup.filter(RuntimeWarning, "invalid value encountered")
+            sup.filter(LinAlgWarning)
+            res = linprog(c, A_ub, b_ub, A_eq, b_eq, bounds,
+                          method=self.method, options=self.options)
+        _assert_success(res, desired_fun=-106.63507541835018)
 
     def test_bug_6139(self):
         # linprog(method='simplex') fails to find a basic feasible solution
@@ -1161,9 +1153,9 @@ def test_bug_8174(self):
             sup.filter(LinAlgWarning)
             res = linprog(c, A_ub, b_ub, A_eq, b_eq, bounds,
                           method=self.method, options=self.options)
-        if self.method == "simplex":
-            assert_equal(res.message[:29], "The solution does not satisfy")
-            assert_equal(res.status, 4)
+
+        if self.options.get('tol', 1e-9) < 1e-10 and self.method == 'simplex':
+            _assert_unable_to_find_basic_feasible_sol(res)
         else:
             _assert_success(res, desired_fun=-2.0080717488789235, atol=1e-6)
 
@@ -1367,51 +1359,82 @@ def test_network_flow(self):
 
 
 class TestLinprogSimplexDefault(LinprogSimplexTests):
-    options = {}
 
-    def test_bug_7237(self):
-        # we want to check for the error
+    def setup_method(self):
+        self.options = {}
+
+    def test_bug_5400(self):
+        with pytest.raises(ValueError):
+            super(TestLinprogSimplexDefault, self).test_bug_5400()
+
+    def test_bug_7237_low_tol(self):
+        # Fails if the tolerance is too strict. Here we test that
+        # even if the solutuion is wrong, the appropriate error is raised.
+        self.options.update({'tol': 1e-12})
         with pytest.raises(ValueError):
             super(TestLinprogSimplexDefault, self).test_bug_7237()
 
-    def test_bug_8174(self):
-        # we want to check for the warning about the pivot
+    def test_bug_8174_low_tol(self):
+        # Fails if the tolerance is too strict. Here we test that
+        # even if the solutuion is wrong, the appropriate warning is issued.
+        self.options.update({'tol': 1e-12})
         with pytest.warns(OptimizeWarning):
             super(TestLinprogSimplexDefault, self).test_bug_8174()
 
 
 class TestLinprogSimplexBland(LinprogSimplexTests):
-    options = {'bland': True}
 
-    # we want to check for the error
+    def setup_method(self):
+        self.options = {'bland': True}
+
     def test_bug_5400(self):
         with pytest.raises(ValueError):
             super(TestLinprogSimplexBland, self).test_bug_5400()
 
-    def test_bug_8174(self):
-        with pytest.warns(OptimizeWarning):
-            pytest.skip("This fails, but it shouldn't.")
+    def test_bug_8174_low_tol(self):
+        # Fails if the tolerance is too strict. Here we test that
+        # even if the solutuion is wrong, the appropriate error is raised.
+        self.options.update({'tol': 1e-12})
+        with pytest.raises(AssertionError):
+            with pytest.warns(OptimizeWarning):
+                super(TestLinprogSimplexBland, self).test_bug_8174()
 
 
 class TestLinprogSimplexNoPresolve(LinprogSimplexTests):
-    options = {'presolve': False}
 
-    def test_bug_6139(self):
+    def setup_method(self):
+        self.options = {'presolve': False}
+
+    is_32_bit = np.intp(0).itemsize < 8
+    is_linux = sys.platform.startswith('linux')
+
+    @pytest.mark.xfail(
+        condition=is_32_bit and is_linux,
+        reason='Fails with warning on 32-bit linux')
+    def test_bug_5400(self):
+        super(TestLinprogSimplexNoPresolve, self).test_bug_5400()
+
+    def test_bug_6139_low_tol(self):
         # Linprog(method='simplex') fails to find a basic feasible solution
         # if phase 1 pseudo-objective function is outside the provided tol.
         # https://github.com/scipy/scipy/issues/6139
         # Without ``presolve`` eliminating such rows the result is incorrect.
+        self.options.update({'tol': 1e-12})
         with pytest.raises(ValueError):
             return super(TestLinprogSimplexNoPresolve, self).test_bug_6139()
 
-    def test_bug_7237(self):
-        # we want to check for the error
+    def test_bug_7237_low_tol(self):
+        # Fails if the tolerance is too strict. Here we test that
+        # even if the solutuion is wrong, the appropriate error is raised.
+        self.options.update({'tol': 1e-12})
         with pytest.raises(ValueError):
             super(TestLinprogSimplexNoPresolve, self).test_bug_7237()
 
-    def test_bug_8174(self):
+    def test_bug_8174_low_tol(self):
+        # Fails if the tolerance is too strict. Here we test that
+        # even if the solutuion is wrong, the appropriate warning is issued.
+        self.options.update({'tol': 1e-12})
         with pytest.warns(OptimizeWarning):
-            # wanted to check for the warning about the pivot
             super(TestLinprogSimplexNoPresolve, self).test_bug_8174()
 
     def test_unbounded_no_nontrivial_constraints_1(self):