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

73 statements  

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

1"""HiGHS Linear Optimization Methods 

2 

3Interface to HiGHS linear optimization software. 

4https://highs.dev/ 

5 

6.. versionadded:: 1.5.0 

7 

8References 

9---------- 

10.. [1] Q. Huangfu and J.A.J. Hall. "Parallelizing the dual revised simplex 

11 method." Mathematical Programming Computation, 10 (1), 119-142, 

12 2018. DOI: 10.1007/s12532-017-0130-5 

13 

14""" 

15 

16import inspect 

17import numpy as np 

18from ._optimize import _check_unknown_options, OptimizeWarning, OptimizeResult 

19from warnings import warn 

20from ._highs._highs_wrapper import _highs_wrapper 

21from ._highs._highs_constants import ( 

22 CONST_I_INF, 

23 CONST_INF, 

24 MESSAGE_LEVEL_NONE, 

25 HIGHS_OBJECTIVE_SENSE_MINIMIZE, 

26 

27 MODEL_STATUS_NOTSET, 

28 MODEL_STATUS_LOAD_ERROR, 

29 MODEL_STATUS_MODEL_ERROR, 

30 MODEL_STATUS_PRESOLVE_ERROR, 

31 MODEL_STATUS_SOLVE_ERROR, 

32 MODEL_STATUS_POSTSOLVE_ERROR, 

33 MODEL_STATUS_MODEL_EMPTY, 

34 MODEL_STATUS_OPTIMAL, 

35 MODEL_STATUS_INFEASIBLE, 

36 MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE, 

37 MODEL_STATUS_UNBOUNDED, 

38 MODEL_STATUS_REACHED_DUAL_OBJECTIVE_VALUE_UPPER_BOUND 

39 as MODEL_STATUS_RDOVUB, 

40 MODEL_STATUS_REACHED_OBJECTIVE_TARGET, 

41 MODEL_STATUS_REACHED_TIME_LIMIT, 

42 MODEL_STATUS_REACHED_ITERATION_LIMIT, 

43 

44 HIGHS_SIMPLEX_STRATEGY_CHOOSE, 

45 HIGHS_SIMPLEX_STRATEGY_DUAL, 

46 

47 HIGHS_SIMPLEX_CRASH_STRATEGY_OFF, 

48 

49 HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_CHOOSE, 

50 HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_DANTZIG, 

51 HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_DEVEX, 

52 HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_STEEPEST_EDGE, 

53 

54 HIGHS_VAR_TYPE_CONTINUOUS, 

55) 

56from scipy.sparse import csc_matrix, vstack, issparse 

57 

58 

59def _highs_to_scipy_status_message(highs_status, highs_message): 

60 """Converts HiGHS status number/message to SciPy status number/message""" 

61 

62 scipy_statuses_messages = { 

63 None: (4, "HiGHS did not provide a status code. "), 

64 MODEL_STATUS_NOTSET: (4, ""), 

65 MODEL_STATUS_LOAD_ERROR: (4, ""), 

66 MODEL_STATUS_MODEL_ERROR: (2, ""), 

67 MODEL_STATUS_PRESOLVE_ERROR: (4, ""), 

68 MODEL_STATUS_SOLVE_ERROR: (4, ""), 

69 MODEL_STATUS_POSTSOLVE_ERROR: (4, ""), 

70 MODEL_STATUS_MODEL_EMPTY: (4, ""), 

71 MODEL_STATUS_RDOVUB: (4, ""), 

72 MODEL_STATUS_REACHED_OBJECTIVE_TARGET: (4, ""), 

73 MODEL_STATUS_OPTIMAL: (0, "Optimization terminated successfully. "), 

74 MODEL_STATUS_REACHED_TIME_LIMIT: (1, "Time limit reached. "), 

75 MODEL_STATUS_REACHED_ITERATION_LIMIT: (1, "Iteration limit reached. "), 

76 MODEL_STATUS_INFEASIBLE: (2, "The problem is infeasible. "), 

77 MODEL_STATUS_UNBOUNDED: (3, "The problem is unbounded. "), 

78 MODEL_STATUS_UNBOUNDED_OR_INFEASIBLE: (4, "The problem is unbounded " 

79 "or infeasible. ")} 

80 unrecognized = (4, "The HiGHS status code was not recognized. ") 

81 scipy_status, scipy_message = ( 

82 scipy_statuses_messages.get(highs_status, unrecognized)) 

83 scipy_message = (f"{scipy_message}" 

84 f"(HiGHS Status {highs_status}: {highs_message})") 

85 return scipy_status, scipy_message 

86 

87 

88def _replace_inf(x): 

89 # Replace `np.inf` with CONST_INF 

90 infs = np.isinf(x) 

91 x[infs] = np.sign(x[infs])*CONST_INF 

92 return x 

93 

94 

95def _convert_to_highs_enum(option, option_str, choices): 

96 # If option is in the choices we can look it up, if not use 

97 # the default value taken from function signature and warn: 

98 try: 

99 return choices[option.lower()] 

100 except AttributeError: 

101 return choices[option] 

102 except KeyError: 

103 sig = inspect.signature(_linprog_highs) 

104 default_str = sig.parameters[option_str].default 

105 warn(f"Option {option_str} is {option}, but only values in " 

106 f"{set(choices.keys())} are allowed. Using default: " 

107 f"{default_str}.", 

108 OptimizeWarning, stacklevel=3) 

109 return choices[default_str] 

110 

111 

112def _linprog_highs(lp, solver, time_limit=None, presolve=True, 

113 disp=False, maxiter=None, 

114 dual_feasibility_tolerance=None, 

115 primal_feasibility_tolerance=None, 

116 ipm_optimality_tolerance=None, 

117 simplex_dual_edge_weight_strategy=None, 

118 mip_rel_gap=None, 

119 mip_max_nodes=None, 

120 **unknown_options): 

121 r""" 

122 Solve the following linear programming problem using one of the HiGHS 

123 solvers: 

124 

125 User-facing documentation is in _linprog_doc.py. 

126 

127 Parameters 

128 ---------- 

129 lp : _LPProblem 

130 A ``scipy.optimize._linprog_util._LPProblem`` ``namedtuple``. 

131 solver : "ipm" or "simplex" or None 

132 Which HiGHS solver to use. If ``None``, "simplex" will be used. 

133 

134 Options 

135 ------- 

136 maxiter : int 

137 The maximum number of iterations to perform in either phase. For 

138 ``solver='ipm'``, this does not include the number of crossover 

139 iterations. Default is the largest possible value for an ``int`` 

140 on the platform. 

141 disp : bool 

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

143 to the console each iteration; default ``False``. 

144 time_limit : float 

145 The maximum time in seconds allotted to solve the problem; default is 

146 the largest possible value for a ``double`` on the platform. 

147 presolve : bool 

148 Presolve attempts to identify trivial infeasibilities, 

149 identify trivial unboundedness, and simplify the problem before 

150 sending it to the main solver. It is generally recommended 

151 to keep the default setting ``True``; set to ``False`` if presolve is 

152 to be disabled. 

153 dual_feasibility_tolerance : double 

154 Dual feasibility tolerance. Default is 1e-07. 

155 The minimum of this and ``primal_feasibility_tolerance`` 

156 is used for the feasibility tolerance when ``solver='ipm'``. 

157 primal_feasibility_tolerance : double 

158 Primal feasibility tolerance. Default is 1e-07. 

159 The minimum of this and ``dual_feasibility_tolerance`` 

160 is used for the feasibility tolerance when ``solver='ipm'``. 

161 ipm_optimality_tolerance : double 

162 Optimality tolerance for ``solver='ipm'``. Default is 1e-08. 

163 Minimum possible value is 1e-12 and must be smaller than the largest 

164 possible value for a ``double`` on the platform. 

165 simplex_dual_edge_weight_strategy : str (default: None) 

166 Strategy for simplex dual edge weights. The default, ``None``, 

167 automatically selects one of the following. 

168 

169 ``'dantzig'`` uses Dantzig's original strategy of choosing the most 

170 negative reduced cost. 

171 

172 ``'devex'`` uses the strategy described in [15]_. 

173 

174 ``steepest`` uses the exact steepest edge strategy as described in 

175 [16]_. 

176 

177 ``'steepest-devex'`` begins with the exact steepest edge strategy 

178 until the computation is too costly or inexact and then switches to 

179 the devex method. 

180 

181 Curently, using ``None`` always selects ``'steepest-devex'``, but this 

182 may change as new options become available. 

183 

184 mip_max_nodes : int 

185 The maximum number of nodes allotted to solve the problem; default is 

186 the largest possible value for a ``HighsInt`` on the platform. 

187 Ignored if not using the MIP solver. 

188 unknown_options : dict 

189 Optional arguments not used by this particular solver. If 

190 ``unknown_options`` is non-empty, a warning is issued listing all 

191 unused options. 

192 

193 Returns 

194 ------- 

195 sol : dict 

196 A dictionary consisting of the fields: 

197 

198 x : 1D array 

199 The values of the decision variables that minimizes the 

200 objective function while satisfying the constraints. 

201 fun : float 

202 The optimal value of the objective function ``c @ x``. 

203 slack : 1D array 

204 The (nominally positive) values of the slack, 

205 ``b_ub - A_ub @ x``. 

206 con : 1D array 

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

208 ``b_eq - A_eq @ x``. 

209 success : bool 

210 ``True`` when the algorithm succeeds in finding an optimal 

211 solution. 

212 status : int 

213 An integer representing the exit status of the algorithm. 

214 

215 ``0`` : Optimization terminated successfully. 

216 

217 ``1`` : Iteration or time limit reached. 

218 

219 ``2`` : Problem appears to be infeasible. 

220 

221 ``3`` : Problem appears to be unbounded. 

222 

223 ``4`` : The HiGHS solver ran into a problem. 

224 

225 message : str 

226 A string descriptor of the exit status of the algorithm. 

227 nit : int 

228 The total number of iterations performed. 

229 For ``solver='simplex'``, this includes iterations in all 

230 phases. For ``solver='ipm'``, this does not include 

231 crossover iterations. 

232 crossover_nit : int 

233 The number of primal/dual pushes performed during the 

234 crossover routine for ``solver='ipm'``. This is ``0`` 

235 for ``solver='simplex'``. 

236 ineqlin : OptimizeResult 

237 Solution and sensitivity information corresponding to the 

238 inequality constraints, `b_ub`. A dictionary consisting of the 

239 fields: 

240 

241 residual : np.ndnarray 

242 The (nominally positive) values of the slack variables, 

243 ``b_ub - A_ub @ x``. This quantity is also commonly 

244 referred to as "slack". 

245 

246 marginals : np.ndarray 

247 The sensitivity (partial derivative) of the objective 

248 function with respect to the right-hand side of the 

249 inequality constraints, `b_ub`. 

250 

251 eqlin : OptimizeResult 

252 Solution and sensitivity information corresponding to the 

253 equality constraints, `b_eq`. A dictionary consisting of the 

254 fields: 

255 

256 residual : np.ndarray 

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

258 ``b_eq - A_eq @ x``. 

259 

260 marginals : np.ndarray 

261 The sensitivity (partial derivative) of the objective 

262 function with respect to the right-hand side of the 

263 equality constraints, `b_eq`. 

264 

265 lower, upper : OptimizeResult 

266 Solution and sensitivity information corresponding to the 

267 lower and upper bounds on decision variables, `bounds`. 

268 

269 residual : np.ndarray 

270 The (nominally positive) values of the quantity 

271 ``x - lb`` (lower) or ``ub - x`` (upper). 

272 

273 marginals : np.ndarray 

274 The sensitivity (partial derivative) of the objective 

275 function with respect to the lower and upper 

276 `bounds`. 

277 

278 mip_node_count : int 

279 The number of subproblems or "nodes" solved by the MILP 

280 solver. Only present when `integrality` is not `None`. 

281 

282 mip_dual_bound : float 

283 The MILP solver's final estimate of the lower bound on the 

284 optimal solution. Only present when `integrality` is not 

285 `None`. 

286 

287 mip_gap : float 

288 The difference between the final objective function value 

289 and the final dual bound, scaled by the final objective 

290 function value. Only present when `integrality` is not 

291 `None`. 

292 

293 Notes 

294 ----- 

295 The result fields `ineqlin`, `eqlin`, `lower`, and `upper` all contain 

296 `marginals`, or partial derivatives of the objective function with respect 

297 to the right-hand side of each constraint. These partial derivatives are 

298 also referred to as "Lagrange multipliers", "dual values", and 

299 "shadow prices". The sign convention of `marginals` is opposite that 

300 of Lagrange multipliers produced by many nonlinear solvers. 

301 

302 References 

303 ---------- 

304 .. [15] Harris, Paula MJ. "Pivot selection methods of the Devex LP code." 

305 Mathematical programming 5.1 (1973): 1-28. 

306 .. [16] Goldfarb, Donald, and John Ker Reid. "A practicable steepest-edge 

307 simplex algorithm." Mathematical Programming 12.1 (1977): 361-371. 

308 """ 

309 

310 _check_unknown_options(unknown_options) 

311 

312 # Map options to HiGHS enum values 

313 simplex_dual_edge_weight_strategy_enum = _convert_to_highs_enum( 

314 simplex_dual_edge_weight_strategy, 

315 'simplex_dual_edge_weight_strategy', 

316 choices={'dantzig': HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_DANTZIG, 

317 'devex': HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_DEVEX, 

318 'steepest-devex': HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_CHOOSE, 

319 'steepest': 

320 HIGHS_SIMPLEX_EDGE_WEIGHT_STRATEGY_STEEPEST_EDGE, 

321 None: None}) 

322 

323 c, A_ub, b_ub, A_eq, b_eq, bounds, x0, integrality = lp 

324 

325 lb, ub = bounds.T.copy() # separate bounds, copy->C-cntgs 

326 # highs_wrapper solves LHS <= A*x <= RHS, not equality constraints 

327 lhs_ub = -np.ones_like(b_ub)*np.inf # LHS of UB constraints is -inf 

328 rhs_ub = b_ub # RHS of UB constraints is b_ub 

329 lhs_eq = b_eq # Equality constaint is inequality 

330 rhs_eq = b_eq # constraint with LHS=RHS 

331 lhs = np.concatenate((lhs_ub, lhs_eq)) 

332 rhs = np.concatenate((rhs_ub, rhs_eq)) 

333 

334 if issparse(A_ub) or issparse(A_eq): 

335 A = vstack((A_ub, A_eq)) 

336 else: 

337 A = np.vstack((A_ub, A_eq)) 

338 A = csc_matrix(A) 

339 

340 options = { 

341 'presolve': presolve, 

342 'sense': HIGHS_OBJECTIVE_SENSE_MINIMIZE, 

343 'solver': solver, 

344 'time_limit': time_limit, 

345 'highs_debug_level': MESSAGE_LEVEL_NONE, 

346 'dual_feasibility_tolerance': dual_feasibility_tolerance, 

347 'ipm_optimality_tolerance': ipm_optimality_tolerance, 

348 'log_to_console': disp, 

349 'mip_max_nodes': mip_max_nodes, 

350 'output_flag': disp, 

351 'primal_feasibility_tolerance': primal_feasibility_tolerance, 

352 'simplex_dual_edge_weight_strategy': 

353 simplex_dual_edge_weight_strategy_enum, 

354 'simplex_strategy': HIGHS_SIMPLEX_STRATEGY_DUAL, 

355 'simplex_crash_strategy': HIGHS_SIMPLEX_CRASH_STRATEGY_OFF, 

356 'ipm_iteration_limit': maxiter, 

357 'simplex_iteration_limit': maxiter, 

358 'mip_rel_gap': mip_rel_gap, 

359 } 

360 

361 # np.inf doesn't work; use very large constant 

362 rhs = _replace_inf(rhs) 

363 lhs = _replace_inf(lhs) 

364 lb = _replace_inf(lb) 

365 ub = _replace_inf(ub) 

366 

367 if integrality is None or np.sum(integrality) == 0: 

368 integrality = np.empty(0) 

369 else: 

370 integrality = np.array(integrality) 

371 

372 res = _highs_wrapper(c, A.indptr, A.indices, A.data, lhs, rhs, 

373 lb, ub, integrality.astype(np.uint8), options) 

374 

375 # HiGHS represents constraints as lhs/rhs, so 

376 # Ax + s = b => Ax = b - s 

377 # and we need to split up s by A_ub and A_eq 

378 if 'slack' in res: 

379 slack = res['slack'] 

380 con = np.array(slack[len(b_ub):]) 

381 slack = np.array(slack[:len(b_ub)]) 

382 else: 

383 slack, con = None, None 

384 

385 # lagrange multipliers for equalities/inequalities and upper/lower bounds 

386 if 'lambda' in res: 

387 lamda = res['lambda'] 

388 marg_ineqlin = np.array(lamda[:len(b_ub)]) 

389 marg_eqlin = np.array(lamda[len(b_ub):]) 

390 marg_upper = np.array(res['marg_bnds'][1, :]) 

391 marg_lower = np.array(res['marg_bnds'][0, :]) 

392 else: 

393 marg_ineqlin, marg_eqlin = None, None 

394 marg_upper, marg_lower = None, None 

395 

396 # this needs to be updated if we start choosing the solver intelligently 

397 solvers = {"ipm": "highs-ipm", "simplex": "highs-ds", None: "highs-ds"} 

398 

399 # Convert to scipy-style status and message 

400 highs_status = res.get('status', None) 

401 highs_message = res.get('message', None) 

402 status, message = _highs_to_scipy_status_message(highs_status, 

403 highs_message) 

404 

405 x = np.array(res['x']) if 'x' in res else None 

406 sol = {'x': x, 

407 'slack': slack, 

408 'con': con, 

409 'ineqlin': OptimizeResult({ 

410 'residual': slack, 

411 'marginals': marg_ineqlin, 

412 }), 

413 'eqlin': OptimizeResult({ 

414 'residual': con, 

415 'marginals': marg_eqlin, 

416 }), 

417 'lower': OptimizeResult({ 

418 'residual': None if x is None else x - lb, 

419 'marginals': marg_lower, 

420 }), 

421 'upper': OptimizeResult({ 

422 'residual': None if x is None else ub - x, 

423 'marginals': marg_upper 

424 }), 

425 'fun': res.get('fun'), 

426 'status': status, 

427 'success': res['status'] == MODEL_STATUS_OPTIMAL, 

428 'message': message, 

429 'nit': res.get('simplex_nit', 0) or res.get('ipm_nit', 0), 

430 'crossover_nit': res.get('crossover_nit'), 

431 } 

432 

433 if np.any(x) and integrality is not None: 

434 sol.update({ 

435 'mip_node_count': res.get('mip_node_count', 0), 

436 'mip_dual_bound': res.get('mip_dual_bound', 0.0), 

437 'mip_gap': res.get('mip_gap', 0.0), 

438 }) 

439 

440 return sol