Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/scipy/optimize/_linprog_rs.py: 9%

194 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-12 06:31 +0000

1"""Revised simplex method for linear programming 

2 

3The *revised simplex* method uses the method described in [1]_, except 

4that a factorization [2]_ of the basis matrix, rather than its inverse, 

5is efficiently maintained and used to solve the linear systems at each 

6iteration of the algorithm. 

7 

8.. versionadded:: 1.3.0 

9 

10References 

11---------- 

12.. [1] Bertsimas, Dimitris, and J. Tsitsiklis. "Introduction to linear 

13 programming." Athena Scientific 1 (1997): 997. 

14.. [2] Bartels, Richard H. "A stabilization of the simplex method." 

15 Journal in Numerische Mathematik 16.5 (1971): 414-434. 

16 

17""" 

18# Author: Matt Haberland 

19 

20import numpy as np 

21from numpy.linalg import LinAlgError 

22 

23from scipy.linalg import solve 

24from ._optimize import _check_unknown_options 

25from ._bglu_dense import LU 

26from ._bglu_dense import BGLU as BGLU 

27from ._linprog_util import _postsolve 

28from ._optimize import OptimizeResult 

29 

30 

31def _phase_one(A, b, x0, callback, postsolve_args, maxiter, tol, disp, 

32 maxupdate, mast, pivot): 

33 """ 

34 The purpose of phase one is to find an initial basic feasible solution 

35 (BFS) to the original problem. 

36 

37 Generates an auxiliary problem with a trivial BFS and an objective that 

38 minimizes infeasibility of the original problem. Solves the auxiliary 

39 problem using the main simplex routine (phase two). This either yields 

40 a BFS to the original problem or determines that the original problem is 

41 infeasible. If feasible, phase one detects redundant rows in the original 

42 constraint matrix and removes them, then chooses additional indices as 

43 necessary to complete a basis/BFS for the original problem. 

44 """ 

45 

46 m, n = A.shape 

47 status = 0 

48 

49 # generate auxiliary problem to get initial BFS 

50 A, b, c, basis, x, status = _generate_auxiliary_problem(A, b, x0, tol) 

51 

52 if status == 6: 

53 residual = c.dot(x) 

54 iter_k = 0 

55 return x, basis, A, b, residual, status, iter_k 

56 

57 # solve auxiliary problem 

58 phase_one_n = n 

59 iter_k = 0 

60 x, basis, status, iter_k = _phase_two(c, A, x, basis, callback, 

61 postsolve_args, 

62 maxiter, tol, disp, 

63 maxupdate, mast, pivot, 

64 iter_k, phase_one_n) 

65 

66 # check for infeasibility 

67 residual = c.dot(x) 

68 if status == 0 and residual > tol: 

69 status = 2 

70 

71 # drive artificial variables out of basis 

72 # TODO: test redundant row removal better 

73 # TODO: make solve more efficient with BGLU? This could take a while. 

74 keep_rows = np.ones(m, dtype=bool) 

75 for basis_column in basis[basis >= n]: 

76 B = A[:, basis] 

77 try: 

78 basis_finder = np.abs(solve(B, A)) # inefficient 

79 pertinent_row = np.argmax(basis_finder[:, basis_column]) 

80 eligible_columns = np.ones(n, dtype=bool) 

81 eligible_columns[basis[basis < n]] = 0 

82 eligible_column_indices = np.where(eligible_columns)[0] 

83 index = np.argmax(basis_finder[:, :n] 

84 [pertinent_row, eligible_columns]) 

85 new_basis_column = eligible_column_indices[index] 

86 if basis_finder[pertinent_row, new_basis_column] < tol: 

87 keep_rows[pertinent_row] = False 

88 else: 

89 basis[basis == basis_column] = new_basis_column 

90 except LinAlgError: 

91 status = 4 

92 

93 # form solution to original problem 

94 A = A[keep_rows, :n] 

95 basis = basis[keep_rows] 

96 x = x[:n] 

97 m = A.shape[0] 

98 return x, basis, A, b, residual, status, iter_k 

99 

100 

101def _get_more_basis_columns(A, basis): 

102 """ 

103 Called when the auxiliary problem terminates with artificial columns in 

104 the basis, which must be removed and replaced with non-artificial 

105 columns. Finds additional columns that do not make the matrix singular. 

106 """ 

107 m, n = A.shape 

108 

109 # options for inclusion are those that aren't already in the basis 

110 a = np.arange(m+n) 

111 bl = np.zeros(len(a), dtype=bool) 

112 bl[basis] = 1 

113 options = a[~bl] 

114 options = options[options < n] # and they have to be non-artificial 

115 

116 # form basis matrix 

117 B = np.zeros((m, m)) 

118 B[:, 0:len(basis)] = A[:, basis] 

119 

120 if (basis.size > 0 and 

121 np.linalg.matrix_rank(B[:, :len(basis)]) < len(basis)): 

122 raise Exception("Basis has dependent columns") 

123 

124 rank = 0 # just enter the loop 

125 for i in range(n): # somewhat arbitrary, but we need another way out 

126 # permute the options, and take as many as needed 

127 new_basis = np.random.permutation(options)[:m-len(basis)] 

128 B[:, len(basis):] = A[:, new_basis] # update the basis matrix 

129 rank = np.linalg.matrix_rank(B) # check the rank 

130 if rank == m: 

131 break 

132 

133 return np.concatenate((basis, new_basis)) 

134 

135 

136def _generate_auxiliary_problem(A, b, x0, tol): 

137 """ 

138 Modifies original problem to create an auxiliary problem with a trivial 

139 initial basic feasible solution and an objective that minimizes 

140 infeasibility in the original problem. 

141 

142 Conceptually, this is done by stacking an identity matrix on the right of 

143 the original constraint matrix, adding artificial variables to correspond 

144 with each of these new columns, and generating a cost vector that is all 

145 zeros except for ones corresponding with each of the new variables. 

146 

147 A initial basic feasible solution is trivial: all variables are zero 

148 except for the artificial variables, which are set equal to the 

149 corresponding element of the right hand side `b`. 

150 

151 Runnning the simplex method on this auxiliary problem drives all of the 

152 artificial variables - and thus the cost - to zero if the original problem 

153 is feasible. The original problem is declared infeasible otherwise. 

154 

155 Much of the complexity below is to improve efficiency by using singleton 

156 columns in the original problem where possible, thus generating artificial 

157 variables only as necessary, and using an initial 'guess' basic feasible 

158 solution. 

159 """ 

160 status = 0 

161 m, n = A.shape 

162 

163 if x0 is not None: 

164 x = x0 

165 else: 

166 x = np.zeros(n) 

167 

168 r = b - A@x # residual; this must be all zeros for feasibility 

169 

170 A[r < 0] = -A[r < 0] # express problem with RHS positive for trivial BFS 

171 b[r < 0] = -b[r < 0] # to the auxiliary problem 

172 r[r < 0] *= -1 

173 

174 # Rows which we will need to find a trivial way to zero. 

175 # This should just be the rows where there is a nonzero residual. 

176 # But then we would not necessarily have a column singleton in every row. 

177 # This makes it difficult to find an initial basis. 

178 if x0 is None: 

179 nonzero_constraints = np.arange(m) 

180 else: 

181 nonzero_constraints = np.where(r > tol)[0] 

182 

183 # these are (at least some of) the initial basis columns 

184 basis = np.where(np.abs(x) > tol)[0] 

185 

186 if len(nonzero_constraints) == 0 and len(basis) <= m: # already a BFS 

187 c = np.zeros(n) 

188 basis = _get_more_basis_columns(A, basis) 

189 return A, b, c, basis, x, status 

190 elif (len(nonzero_constraints) > m - len(basis) or 

191 np.any(x < 0)): # can't get trivial BFS 

192 c = np.zeros(n) 

193 status = 6 

194 return A, b, c, basis, x, status 

195 

196 # chooses existing columns appropriate for inclusion in initial basis 

197 cols, rows = _select_singleton_columns(A, r) 

198 

199 # find the rows we need to zero that we _can_ zero with column singletons 

200 i_tofix = np.isin(rows, nonzero_constraints) 

201 # these columns can't already be in the basis, though 

202 # we are going to add them to the basis and change the corresponding x val 

203 i_notinbasis = np.logical_not(np.isin(cols, basis)) 

204 i_fix_without_aux = np.logical_and(i_tofix, i_notinbasis) 

205 rows = rows[i_fix_without_aux] 

206 cols = cols[i_fix_without_aux] 

207 

208 # indices of the rows we can only zero with auxiliary variable 

209 # these rows will get a one in each auxiliary column 

210 arows = nonzero_constraints[np.logical_not( 

211 np.isin(nonzero_constraints, rows))] 

212 n_aux = len(arows) 

213 acols = n + np.arange(n_aux) # indices of auxiliary columns 

214 

215 basis_ng = np.concatenate((cols, acols)) # basis columns not from guess 

216 basis_ng_rows = np.concatenate((rows, arows)) # rows we need to zero 

217 

218 # add auxiliary singleton columns 

219 A = np.hstack((A, np.zeros((m, n_aux)))) 

220 A[arows, acols] = 1 

221 

222 # generate initial BFS 

223 x = np.concatenate((x, np.zeros(n_aux))) 

224 x[basis_ng] = r[basis_ng_rows]/A[basis_ng_rows, basis_ng] 

225 

226 # generate costs to minimize infeasibility 

227 c = np.zeros(n_aux + n) 

228 c[acols] = 1 

229 

230 # basis columns correspond with nonzeros in guess, those with column 

231 # singletons we used to zero remaining constraints, and any additional 

232 # columns to get a full set (m columns) 

233 basis = np.concatenate((basis, basis_ng)) 

234 basis = _get_more_basis_columns(A, basis) # add columns as needed 

235 

236 return A, b, c, basis, x, status 

237 

238 

239def _select_singleton_columns(A, b): 

240 """ 

241 Finds singleton columns for which the singleton entry is of the same sign 

242 as the right-hand side; these columns are eligible for inclusion in an 

243 initial basis. Determines the rows in which the singleton entries are 

244 located. For each of these rows, returns the indices of the one singleton 

245 column and its corresponding row. 

246 """ 

247 # find indices of all singleton columns and corresponding row indicies 

248 column_indices = np.nonzero(np.sum(np.abs(A) != 0, axis=0) == 1)[0] 

249 columns = A[:, column_indices] # array of singleton columns 

250 row_indices = np.zeros(len(column_indices), dtype=int) 

251 nonzero_rows, nonzero_columns = np.nonzero(columns) 

252 row_indices[nonzero_columns] = nonzero_rows # corresponding row indicies 

253 

254 # keep only singletons with entries that have same sign as RHS 

255 # this is necessary because all elements of BFS must be non-negative 

256 same_sign = A[row_indices, column_indices]*b[row_indices] >= 0 

257 column_indices = column_indices[same_sign][::-1] 

258 row_indices = row_indices[same_sign][::-1] 

259 # Reversing the order so that steps below select rightmost columns 

260 # for initial basis, which will tend to be slack variables. (If the 

261 # guess corresponds with a basic feasible solution but a constraint 

262 # is not satisfied with the corresponding slack variable zero, the slack 

263 # variable must be basic.) 

264 

265 # for each row, keep rightmost singleton column with an entry in that row 

266 unique_row_indices, first_columns = np.unique(row_indices, 

267 return_index=True) 

268 return column_indices[first_columns], unique_row_indices 

269 

270 

271def _find_nonzero_rows(A, tol): 

272 """ 

273 Returns logical array indicating the locations of rows with at least 

274 one nonzero element. 

275 """ 

276 return np.any(np.abs(A) > tol, axis=1) 

277 

278 

279def _select_enter_pivot(c_hat, bl, a, rule="bland", tol=1e-12): 

280 """ 

281 Selects a pivot to enter the basis. Currently Bland's rule - the smallest 

282 index that has a negative reduced cost - is the default. 

283 """ 

284 if rule.lower() == "mrc": # index with minimum reduced cost 

285 return a[~bl][np.argmin(c_hat)] 

286 else: # smallest index w/ negative reduced cost 

287 return a[~bl][c_hat < -tol][0] 

288 

289 

290def _display_iter(phase, iteration, slack, con, fun): 

291 """ 

292 Print indicators of optimization status to the console. 

293 """ 

294 header = True if not iteration % 20 else False 

295 

296 if header: 

297 print("Phase", 

298 "Iteration", 

299 "Minimum Slack ", 

300 "Constraint Residual", 

301 "Objective ") 

302 

303 # :<X.Y left aligns Y digits in X digit spaces 

304 fmt = '{0:<6}{1:<10}{2:<20.13}{3:<20.13}{4:<20.13}' 

305 try: 

306 slack = np.min(slack) 

307 except ValueError: 

308 slack = "NA" 

309 print(fmt.format(phase, iteration, slack, np.linalg.norm(con), fun)) 

310 

311 

312def _display_and_callback(phase_one_n, x, postsolve_args, status, 

313 iteration, disp, callback): 

314 if phase_one_n is not None: 

315 phase = 1 

316 x_postsolve = x[:phase_one_n] 

317 else: 

318 phase = 2 

319 x_postsolve = x 

320 x_o, fun, slack, con = _postsolve(x_postsolve, 

321 postsolve_args) 

322 

323 if callback is not None: 

324 res = OptimizeResult({'x': x_o, 'fun': fun, 'slack': slack, 

325 'con': con, 'nit': iteration, 

326 'phase': phase, 'complete': False, 

327 'status': status, 'message': "", 

328 'success': False}) 

329 callback(res) 

330 if disp: 

331 _display_iter(phase, iteration, slack, con, fun) 

332 

333 

334def _phase_two(c, A, x, b, callback, postsolve_args, maxiter, tol, disp, 

335 maxupdate, mast, pivot, iteration=0, phase_one_n=None): 

336 """ 

337 The heart of the simplex method. Beginning with a basic feasible solution, 

338 moves to adjacent basic feasible solutions successively lower reduced cost. 

339 Terminates when there are no basic feasible solutions with lower reduced 

340 cost or if the problem is determined to be unbounded. 

341 

342 This implementation follows the revised simplex method based on LU 

343 decomposition. Rather than maintaining a tableau or an inverse of the 

344 basis matrix, we keep a factorization of the basis matrix that allows 

345 efficient solution of linear systems while avoiding stability issues 

346 associated with inverted matrices. 

347 """ 

348 m, n = A.shape 

349 status = 0 

350 a = np.arange(n) # indices of columns of A 

351 ab = np.arange(m) # indices of columns of B 

352 if maxupdate: 

353 # basis matrix factorization object; similar to B = A[:, b] 

354 B = BGLU(A, b, maxupdate, mast) 

355 else: 

356 B = LU(A, b) 

357 

358 for iteration in range(iteration, maxiter): 

359 

360 if disp or callback is not None: 

361 _display_and_callback(phase_one_n, x, postsolve_args, status, 

362 iteration, disp, callback) 

363 

364 bl = np.zeros(len(a), dtype=bool) 

365 bl[b] = 1 

366 

367 xb = x[b] # basic variables 

368 cb = c[b] # basic costs 

369 

370 try: 

371 v = B.solve(cb, transposed=True) # similar to v = solve(B.T, cb) 

372 except LinAlgError: 

373 status = 4 

374 break 

375 

376 # TODO: cythonize? 

377 c_hat = c - v.dot(A) # reduced cost 

378 c_hat = c_hat[~bl] 

379 # Above is much faster than: 

380 # N = A[:, ~bl] # slow! 

381 # c_hat = c[~bl] - v.T.dot(N) 

382 # Can we perform the multiplication only on the nonbasic columns? 

383 

384 if np.all(c_hat >= -tol): # all reduced costs positive -> terminate 

385 break 

386 

387 j = _select_enter_pivot(c_hat, bl, a, rule=pivot, tol=tol) 

388 u = B.solve(A[:, j]) # similar to u = solve(B, A[:, j]) 

389 

390 i = u > tol # if none of the u are positive, unbounded 

391 if not np.any(i): 

392 status = 3 

393 break 

394 

395 th = xb[i]/u[i] 

396 l = np.argmin(th) # implicitly selects smallest subscript 

397 th_star = th[l] # step size 

398 

399 x[b] = x[b] - th_star*u # take step 

400 x[j] = th_star 

401 B.update(ab[i][l], j) # modify basis 

402 b = B.b # similar to b[ab[i][l]] = 

403 

404 else: 

405 # If the end of the for loop is reached (without a break statement), 

406 # then another step has been taken, so the iteration counter should 

407 # increment, info should be displayed, and callback should be called. 

408 iteration += 1 

409 status = 1 

410 if disp or callback is not None: 

411 _display_and_callback(phase_one_n, x, postsolve_args, status, 

412 iteration, disp, callback) 

413 

414 return x, b, status, iteration 

415 

416 

417def _linprog_rs(c, c0, A, b, x0, callback, postsolve_args, 

418 maxiter=5000, tol=1e-12, disp=False, 

419 maxupdate=10, mast=False, pivot="mrc", 

420 **unknown_options): 

421 """ 

422 Solve the following linear programming problem via a two-phase 

423 revised simplex algorithm.:: 

424 

425 minimize: c @ x 

426 

427 subject to: A @ x == b 

428 0 <= x < oo 

429 

430 User-facing documentation is in _linprog_doc.py. 

431 

432 Parameters 

433 ---------- 

434 c : 1-D array 

435 Coefficients of the linear objective function to be minimized. 

436 c0 : float 

437 Constant term in objective function due to fixed (and eliminated) 

438 variables. (Currently unused.) 

439 A : 2-D array 

440 2-D array which, when matrix-multiplied by ``x``, gives the values of 

441 the equality constraints at ``x``. 

442 b : 1-D array 

443 1-D array of values representing the RHS of each equality constraint 

444 (row) in ``A_eq``. 

445 x0 : 1-D array, optional 

446 Starting values of the independent variables, which will be refined by 

447 the optimization algorithm. For the revised simplex method, these must 

448 correspond with a basic feasible solution. 

449 callback : callable, optional 

450 If a callback function is provided, it will be called within each 

451 iteration of the algorithm. The callback function must accept a single 

452 `scipy.optimize.OptimizeResult` consisting of the following fields: 

453 

454 x : 1-D array 

455 Current solution vector. 

456 fun : float 

457 Current value of the objective function ``c @ x``. 

458 success : bool 

459 True only when an algorithm has completed successfully, 

460 so this is always False as the callback function is called 

461 only while the algorithm is still iterating. 

462 slack : 1-D array 

463 The values of the slack variables. Each slack variable 

464 corresponds to an inequality constraint. If the slack is zero, 

465 the corresponding constraint is active. 

466 con : 1-D array 

467 The (nominally zero) residuals of the equality constraints, 

468 that is, ``b - A_eq @ x``. 

469 phase : int 

470 The phase of the algorithm being executed. 

471 status : int 

472 For revised simplex, this is always 0 because if a different 

473 status is detected, the algorithm terminates. 

474 nit : int 

475 The number of iterations performed. 

476 message : str 

477 A string descriptor of the exit status of the optimization. 

478 postsolve_args : tuple 

479 Data needed by _postsolve to convert the solution to the standard-form 

480 problem into the solution to the original problem. 

481 

482 Options 

483 ------- 

484 maxiter : int 

485 The maximum number of iterations to perform in either phase. 

486 tol : float 

487 The tolerance which determines when a solution is "close enough" to 

488 zero in Phase 1 to be considered a basic feasible solution or close 

489 enough to positive to serve as an optimal solution. 

490 disp : bool 

491 Set to ``True`` if indicators of optimization status are to be printed 

492 to the console each iteration. 

493 maxupdate : int 

494 The maximum number of updates performed on the LU factorization. 

495 After this many updates is reached, the basis matrix is factorized 

496 from scratch. 

497 mast : bool 

498 Minimize Amortized Solve Time. If enabled, the average time to solve 

499 a linear system using the basis factorization is measured. Typically, 

500 the average solve time will decrease with each successive solve after 

501 initial factorization, as factorization takes much more time than the 

502 solve operation (and updates). Eventually, however, the updated 

503 factorization becomes sufficiently complex that the average solve time 

504 begins to increase. When this is detected, the basis is refactorized 

505 from scratch. Enable this option to maximize speed at the risk of 

506 nondeterministic behavior. Ignored if ``maxupdate`` is 0. 

507 pivot : "mrc" or "bland" 

508 Pivot rule: Minimum Reduced Cost (default) or Bland's rule. Choose 

509 Bland's rule if iteration limit is reached and cycling is suspected. 

510 unknown_options : dict 

511 Optional arguments not used by this particular solver. If 

512 `unknown_options` is non-empty a warning is issued listing all 

513 unused options. 

514 

515 Returns 

516 ------- 

517 x : 1-D array 

518 Solution vector. 

519 status : int 

520 An integer representing the exit status of the optimization:: 

521 

522 0 : Optimization terminated successfully 

523 1 : Iteration limit reached 

524 2 : Problem appears to be infeasible 

525 3 : Problem appears to be unbounded 

526 4 : Numerical difficulties encountered 

527 5 : No constraints; turn presolve on 

528 6 : Guess x0 cannot be converted to a basic feasible solution 

529 

530 message : str 

531 A string descriptor of the exit status of the optimization. 

532 iteration : int 

533 The number of iterations taken to solve the problem. 

534 """ 

535 

536 _check_unknown_options(unknown_options) 

537 

538 messages = ["Optimization terminated successfully.", 

539 "Iteration limit reached.", 

540 "The problem appears infeasible, as the phase one auxiliary " 

541 "problem terminated successfully with a residual of {0:.1e}, " 

542 "greater than the tolerance {1} required for the solution to " 

543 "be considered feasible. Consider increasing the tolerance to " 

544 "be greater than {0:.1e}. If this tolerance is unnaceptably " 

545 "large, the problem is likely infeasible.", 

546 "The problem is unbounded, as the simplex algorithm found " 

547 "a basic feasible solution from which there is a direction " 

548 "with negative reduced cost in which all decision variables " 

549 "increase.", 

550 "Numerical difficulties encountered; consider trying " 

551 "method='interior-point'.", 

552 "Problems with no constraints are trivially solved; please " 

553 "turn presolve on.", 

554 "The guess x0 cannot be converted to a basic feasible " 

555 "solution. " 

556 ] 

557 

558 if A.size == 0: # address test_unbounded_below_no_presolve_corrected 

559 return np.zeros(c.shape), 5, messages[5], 0 

560 

561 x, basis, A, b, residual, status, iteration = ( 

562 _phase_one(A, b, x0, callback, postsolve_args, 

563 maxiter, tol, disp, maxupdate, mast, pivot)) 

564 

565 if status == 0: 

566 x, basis, status, iteration = _phase_two(c, A, x, basis, callback, 

567 postsolve_args, 

568 maxiter, tol, disp, 

569 maxupdate, mast, pivot, 

570 iteration) 

571 

572 return x, status, messages[status].format(residual, tol), iteration