Skip to content

Commit bca1d9c

Browse files
committed
Merge pull request scipy#4762 from Andreea-G/linprog_artificial
BUGS: Fixes scipy#4746 and scipy#4594: linprog returns solution violating equality constraint and IndexError when a callback is provided
2 parents f9810b9 + 260f6a8 commit bca1d9c

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

scipy/optimize/_linprog.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,6 @@ def _solve_simplex(T, n, basis, maxiter=1000, phase=2, callback=None,
305305
"""
306306
nit = nit0
307307
complete = False
308-
solution = np.zeros(T.shape[1]-1, dtype=np.float64)
309308

310309
if phase == 1:
311310
m = T.shape[0]-2
@@ -314,6 +313,37 @@ def _solve_simplex(T, n, basis, maxiter=1000, phase=2, callback=None,
314313
else:
315314
raise ValueError("Argument 'phase' to _solve_simplex must be 1 or 2")
316315

316+
if phase == 2:
317+
# Check if any artificial variables are still in the basis.
318+
# If yes, check if any coefficients from this row and a column
319+
# corresponding to one of the non-artificial variable is non-zero.
320+
# If found, pivot at this term. If not, start phase 2.
321+
# Do this for all artificial variables in the basis.
322+
# Ref: "An Introduction to Linear Programming and Game Theory"
323+
# by Paul R. Thie, Gerard E. Keough, 3rd Ed,
324+
# Chapter 3.7 Redundant Systems (pag 102)
325+
for pivrow in [row for row in range(basis.size)
326+
if basis[row] > T.shape[1] - 2]:
327+
non_zero_row = [col for col in range(T.shape[1] - 1)
328+
if T[pivrow, col] != 0]
329+
if len(non_zero_row) > 0:
330+
pivcol = non_zero_row[0]
331+
# variable represented by pivcol enters
332+
# variable in basis[pivrow] leaves
333+
basis[pivrow] = pivcol
334+
pivval = T[pivrow][pivcol]
335+
T[pivrow, :] = T[pivrow, :] / pivval
336+
for irow in range(T.shape[0]):
337+
if irow != pivrow:
338+
T[irow, :] = T[irow, :] - T[pivrow, :]*T[irow, pivcol]
339+
nit += 1
340+
341+
if len(basis[:m]) == 0:
342+
solution = np.zeros(T.shape[1] - 1, dtype=np.float64)
343+
else:
344+
solution = np.zeros(max(T.shape[1] - 1, max(basis[:m]) + 1),
345+
dtype=np.float64)
346+
317347
while not complete:
318348
# Find the pivot column
319349
pivcol_found, pivcol = _pivot_col(T, tol, bland)

scipy/optimize/tests/test_linprog.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,13 @@ def test_network_flow_limited_capacity():
264264
[0, p, p, 0, n],
265265
[0, 0, 0, p, p]]
266266
b_eq = [-4, 0, 0, 4]
267-
res = linprog(c=cost, A_eq=A_eq, b_eq=b_eq, bounds=bounds)
267+
# Including the callback here ensures the solution can be
268+
# calculated correctly, even when phase 1 terminated
269+
# with some of the artificial variables as pivots
270+
# (i.e. basis[:m] contains elements corresponding to
271+
# the artificial variables)
272+
res = linprog(c=cost, A_eq=A_eq, b_eq=b_eq, bounds=bounds,
273+
callback=lambda x, **kwargs: None)
268274
_assert_success(res, desired_fun=14)
269275

270276

@@ -301,14 +307,16 @@ def test_enzo_example():
301307
def test_enzo_example_b():
302308
# rescued from https://github.com/scipy/scipy/pull/218
303309
c = [2.8, 6.3, 10.8, -2.8, -6.3, -10.8]
304-
A_eq = [
305-
[-1, -1, -1, 0, 0, 0],
310+
A_eq = [[-1, -1, -1, 0, 0, 0],
306311
[0, 0, 0, 1, 1, 1],
307312
[1, 0, 0, 1, 0, 0],
308313
[0, 1, 0, 0, 1, 0],
309314
[0, 0, 1, 0, 0, 1]]
310315
b_eq = [-0.5, 0.4, 0.3, 0.3, 0.3]
311-
res = linprog(c=c, A_eq=A_eq, b_eq=b_eq)
316+
# Including the callback here ensures the solution can be
317+
# calculated correctly.
318+
res = linprog(c=c, A_eq=A_eq, b_eq=b_eq,
319+
callback=lambda x, **kwargs: None)
312320
_assert_success(res, desired_fun=-1.77,
313321
desired_x=[0.3, 0.2, 0.0, 0.0, 0.1, 0.3])
314322

@@ -426,5 +434,21 @@ def test_invalid_inputs():
426434
assert_raises(ValueError, linprog, [1,2], A_ub=np.zeros((1,1,3)), b_eq=1)
427435

428436

437+
def test_basic_artificial_vars():
438+
# Test if linprog succeeds when at the end of Phase 1 some artificial
439+
# variables remain basic, and the row in T corresponding to the
440+
# artificial variables is not all zero.
441+
c = np.array([-0.1, -0.07, 0.004, 0.004, 0.004, 0.004])
442+
A_ub = np.array([[1.0, 0, 0, 0, 0, 0], [-1.0, 0, 0, 0, 0, 0],
443+
[0, -1.0, 0, 0, 0, 0], [0, 1.0, 0, 0, 0, 0],
444+
[1.0, 1.0, 0, 0, 0, 0]])
445+
b_ub = np.array([3.0, 3.0, 3.0, 3.0, 20.0])
446+
A_eq = np.array([[1.0, 0, -1, 1, -1, 1], [0, -1.0, -1, 1, -1, 1]])
447+
b_eq = np.array([0, 0])
448+
res = linprog(c, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq,
449+
callback=lambda x, **kwargs: None)
450+
_assert_success(res, desired_fun=0, desired_x=np.zeros_like(c))
451+
452+
429453
if __name__ == '__main__':
430454
run_module_suite()

0 commit comments

Comments
 (0)