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

106 statements  

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

1import warnings 

2import numpy as np 

3from scipy.sparse import csc_array, vstack 

4from ._highs._highs_wrapper import _highs_wrapper # type: ignore[import] 

5from ._constraints import LinearConstraint, Bounds 

6from ._optimize import OptimizeResult 

7from ._linprog_highs import _highs_to_scipy_status_message 

8 

9 

10def _constraints_to_components(constraints): 

11 """ 

12 Convert sequence of constraints to a single set of components A, b_l, b_u. 

13 

14 `constraints` could be 

15 

16 1. A LinearConstraint 

17 2. A tuple representing a LinearConstraint 

18 3. An invalid object 

19 4. A sequence of composed entirely of objects of type 1/2 

20 5. A sequence containing at least one object of type 3 

21 

22 We want to accept 1, 2, and 4 and reject 3 and 5. 

23 """ 

24 message = ("`constraints` (or each element within `constraints`) must be " 

25 "convertible into an instance of " 

26 "`scipy.optimize.LinearConstraint`.") 

27 As = [] 

28 b_ls = [] 

29 b_us = [] 

30 

31 # Accept case 1 by standardizing as case 4 

32 if isinstance(constraints, LinearConstraint): 

33 constraints = [constraints] 

34 else: 

35 # Reject case 3 

36 try: 

37 iter(constraints) 

38 except TypeError as exc: 

39 raise ValueError(message) from exc 

40 

41 # Accept case 2 by standardizing as case 4 

42 if len(constraints) == 3: 

43 # argument could be a single tuple representing a LinearConstraint 

44 try: 

45 constraints = [LinearConstraint(*constraints)] 

46 except (TypeError, ValueError, np.VisibleDeprecationWarning): 

47 # argument was not a tuple representing a LinearConstraint 

48 pass 

49 

50 # Address cases 4/5 

51 for constraint in constraints: 

52 # if it's not a LinearConstraint or something that represents a 

53 # LinearConstraint at this point, it's invalid 

54 if not isinstance(constraint, LinearConstraint): 

55 try: 

56 constraint = LinearConstraint(*constraint) 

57 except TypeError as exc: 

58 raise ValueError(message) from exc 

59 As.append(csc_array(constraint.A)) 

60 b_ls.append(np.atleast_1d(constraint.lb).astype(np.double)) 

61 b_us.append(np.atleast_1d(constraint.ub).astype(np.double)) 

62 

63 if len(As) > 1: 

64 A = vstack(As) 

65 b_l = np.concatenate(b_ls) 

66 b_u = np.concatenate(b_us) 

67 else: # avoid unnecessary copying 

68 A = As[0] 

69 b_l = b_ls[0] 

70 b_u = b_us[0] 

71 

72 return A, b_l, b_u 

73 

74 

75def _milp_iv(c, integrality, bounds, constraints, options): 

76 # objective IV 

77 c = np.atleast_1d(c).astype(np.double) 

78 if c.ndim != 1 or c.size == 0 or not np.all(np.isfinite(c)): 

79 message = ("`c` must be a one-dimensional array of finite numbers " 

80 "with at least one element.") 

81 raise ValueError(message) 

82 

83 # integrality IV 

84 message = ("`integrality` must contain integers 0-3 and be broadcastable " 

85 "to `c.shape`.") 

86 if integrality is None: 

87 integrality = 0 

88 try: 

89 integrality = np.broadcast_to(integrality, c.shape).astype(np.uint8) 

90 except ValueError: 

91 raise ValueError(message) 

92 if integrality.min() < 0 or integrality.max() > 3: 

93 raise ValueError(message) 

94 

95 # bounds IV 

96 if bounds is None: 

97 bounds = Bounds(0, np.inf) 

98 elif not isinstance(bounds, Bounds): 

99 message = ("`bounds` must be convertible into an instance of " 

100 "`scipy.optimize.Bounds`.") 

101 try: 

102 bounds = Bounds(*bounds) 

103 except TypeError as exc: 

104 raise ValueError(message) from exc 

105 

106 try: 

107 lb = np.broadcast_to(bounds.lb, c.shape).astype(np.double) 

108 ub = np.broadcast_to(bounds.ub, c.shape).astype(np.double) 

109 except (ValueError, TypeError) as exc: 

110 message = ("`bounds.lb` and `bounds.ub` must contain reals and " 

111 "be broadcastable to `c.shape`.") 

112 raise ValueError(message) from exc 

113 

114 # constraints IV 

115 if not constraints: 

116 constraints = [LinearConstraint(np.empty((0, c.size)), 

117 np.empty((0,)), np.empty((0,)))] 

118 try: 

119 A, b_l, b_u = _constraints_to_components(constraints) 

120 except ValueError as exc: 

121 message = ("`constraints` (or each element within `constraints`) must " 

122 "be convertible into an instance of " 

123 "`scipy.optimize.LinearConstraint`.") 

124 raise ValueError(message) from exc 

125 

126 if A.shape != (b_l.size, c.size): 

127 message = "The shape of `A` must be (len(b_l), len(c))." 

128 raise ValueError(message) 

129 indptr, indices, data = A.indptr, A.indices, A.data.astype(np.double) 

130 

131 # options IV 

132 options = options or {} 

133 supported_options = {'disp', 'presolve', 'time_limit', 'node_limit', 

134 'mip_rel_gap'} 

135 unsupported_options = set(options).difference(supported_options) 

136 if unsupported_options: 

137 message = (f"Unrecognized options detected: {unsupported_options}. " 

138 "These will be passed to HiGHS verbatim.") 

139 warnings.warn(message, RuntimeWarning, stacklevel=3) 

140 options_iv = {'log_to_console': options.pop("disp", False), 

141 'mip_max_nodes': options.pop("node_limit", None)} 

142 options_iv.update(options) 

143 

144 return c, integrality, lb, ub, indptr, indices, data, b_l, b_u, options_iv 

145 

146 

147def milp(c, *, integrality=None, bounds=None, constraints=None, options=None): 

148 r""" 

149 Mixed-integer linear programming 

150 

151 Solves problems of the following form: 

152 

153 .. math:: 

154 

155 \min_x \ & c^T x \\ 

156 \mbox{such that} \ & b_l \leq A x \leq b_u,\\ 

157 & l \leq x \leq u, \\ 

158 & x_i \in \mathbb{Z}, i \in X_i 

159 

160 where :math:`x` is a vector of decision variables; 

161 :math:`c`, :math:`b_l`, :math:`b_u`, :math:`l`, and :math:`u` are vectors; 

162 :math:`A` is a matrix, and :math:`X_i` is the set of indices of 

163 decision variables that must be integral. (In this context, a 

164 variable that can assume only integer values is said to be "integral"; 

165 it has an "integrality" constraint.) 

166 

167 Alternatively, that's: 

168 

169 minimize:: 

170 

171 c @ x 

172 

173 such that:: 

174 

175 b_l <= A @ x <= b_u 

176 l <= x <= u 

177 Specified elements of x must be integers 

178 

179 By default, ``l = 0`` and ``u = np.inf`` unless specified with 

180 ``bounds``. 

181 

182 Parameters 

183 ---------- 

184 c : 1D array_like 

185 The coefficients of the linear objective function to be minimized. 

186 `c` is converted to a double precision array before the problem is 

187 solved. 

188 integrality : 1D array_like, optional 

189 Indicates the type of integrality constraint on each decision variable. 

190 

191 ``0`` : Continuous variable; no integrality constraint. 

192 

193 ``1`` : Integer variable; decision variable must be an integer 

194 within `bounds`. 

195 

196 ``2`` : Semi-continuous variable; decision variable must be within 

197 `bounds` or take value ``0``. 

198 

199 ``3`` : Semi-integer variable; decision variable must be an integer 

200 within `bounds` or take value ``0``. 

201 

202 By default, all variables are continuous. `integrality` is converted 

203 to an array of integers before the problem is solved. 

204 

205 bounds : scipy.optimize.Bounds, optional 

206 Bounds on the decision variables. Lower and upper bounds are converted 

207 to double precision arrays before the problem is solved. The 

208 ``keep_feasible`` parameter of the `Bounds` object is ignored. If 

209 not specified, all decision variables are constrained to be 

210 non-negative. 

211 constraints : sequence of scipy.optimize.LinearConstraint, optional 

212 Linear constraints of the optimization problem. Arguments may be 

213 one of the following: 

214 

215 1. A single `LinearConstraint` object 

216 2. A single tuple that can be converted to a `LinearConstraint` object 

217 as ``LinearConstraint(*constraints)`` 

218 3. A sequence composed entirely of objects of type 1. and 2. 

219 

220 Before the problem is solved, all values are converted to double 

221 precision, and the matrices of constraint coefficients are converted to 

222 instances of `scipy.sparse.csc_array`. The ``keep_feasible`` parameter 

223 of `LinearConstraint` objects is ignored. 

224 options : dict, optional 

225 A dictionary of solver options. The following keys are recognized. 

226 

227 disp : bool (default: ``False``) 

228 Set to ``True`` if indicators of optimization status are to be 

229 printed to the console during optimization. 

230 node_limit : int, optional 

231 The maximum number of nodes (linear program relaxations) to solve 

232 before stopping. Default is no maximum number of nodes. 

233 presolve : bool (default: ``True``) 

234 Presolve attempts to identify trivial infeasibilities, 

235 identify trivial unboundedness, and simplify the problem before 

236 sending it to the main solver. 

237 time_limit : float, optional 

238 The maximum number of seconds allotted to solve the problem. 

239 Default is no time limit. 

240 mip_rel_gap : float, optional 

241 Termination criterion for MIP solver: solver will terminate when 

242 the gap between the primal objective value and the dual objective 

243 bound, scaled by the primal objective value, is <= mip_rel_gap. 

244 

245 Returns 

246 ------- 

247 res : OptimizeResult 

248 An instance of :class:`scipy.optimize.OptimizeResult`. The object 

249 is guaranteed to have the following attributes. 

250 

251 status : int 

252 An integer representing the exit status of the algorithm. 

253 

254 ``0`` : Optimal solution found. 

255 

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

257 

258 ``2`` : Problem is infeasible. 

259 

260 ``3`` : Problem is unbounded. 

261 

262 ``4`` : Other; see message for details. 

263 

264 success : bool 

265 ``True`` when an optimal solution is found and ``False`` otherwise. 

266 

267 message : str 

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

269 

270 The following attributes will also be present, but the values may be 

271 ``None``, depending on the solution status. 

272 

273 x : ndarray 

274 The values of the decision variables that minimize the 

275 objective function while satisfying the constraints. 

276 fun : float 

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

278 mip_node_count : int 

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

280 mip_dual_bound : float 

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

282 solution. 

283 mip_gap : float 

284 The difference between the primal objective value and the dual 

285 objective bound, scaled by the primal objective value. 

286 

287 Notes 

288 ----- 

289 `milp` is a wrapper of the HiGHS linear optimization software [1]_. The 

290 algorithm is deterministic, and it typically finds the global optimum of 

291 moderately challenging mixed-integer linear programs (when it exists). 

292 

293 References 

294 ---------- 

295 .. [1] Huangfu, Q., Galabova, I., Feldmeier, M., and Hall, J. A. J. 

296 "HiGHS - high performance software for linear optimization." 

297 https://highs.dev/ 

298 .. [2] Huangfu, Q. and Hall, J. A. J. "Parallelizing the dual revised 

299 simplex method." Mathematical Programming Computation, 10 (1), 

300 119-142, 2018. DOI: 10.1007/s12532-017-0130-5 

301 

302 Examples 

303 -------- 

304 Consider the problem at 

305 https://en.wikipedia.org/wiki/Integer_programming#Example, which is 

306 expressed as a maximization problem of two variables. Since `milp` requires 

307 that the problem be expressed as a minimization problem, the objective 

308 function coefficients on the decision variables are: 

309 

310 >>> import numpy as np 

311 >>> c = -np.array([0, 1]) 

312 

313 Note the negative sign: we maximize the original objective function 

314 by minimizing the negative of the objective function. 

315 

316 We collect the coefficients of the constraints into arrays like: 

317 

318 >>> A = np.array([[-1, 1], [3, 2], [2, 3]]) 

319 >>> b_u = np.array([1, 12, 12]) 

320 >>> b_l = np.full_like(b_u, -np.inf) 

321 

322 Because there is no lower limit on these constraints, we have defined a 

323 variable ``b_l`` full of values representing negative infinity. This may 

324 be unfamiliar to users of `scipy.optimize.linprog`, which only accepts 

325 "less than" (or "upper bound") inequality constraints of the form 

326 ``A_ub @ x <= b_u``. By accepting both ``b_l`` and ``b_u`` of constraints 

327 ``b_l <= A_ub @ x <= b_u``, `milp` makes it easy to specify "greater than" 

328 inequality constraints, "less than" inequality constraints, and equality 

329 constraints concisely. 

330 

331 These arrays are collected into a single `LinearConstraint` object like: 

332 

333 >>> from scipy.optimize import LinearConstraint 

334 >>> constraints = LinearConstraint(A, b_l, b_u) 

335 

336 The non-negativity bounds on the decision variables are enforced by 

337 default, so we do not need to provide an argument for `bounds`. 

338 

339 Finally, the problem states that both decision variables must be integers: 

340 

341 >>> integrality = np.ones_like(c) 

342 

343 We solve the problem like: 

344 

345 >>> from scipy.optimize import milp 

346 >>> res = milp(c=c, constraints=constraints, integrality=integrality) 

347 >>> res.x 

348 [1.0, 2.0] 

349 

350 Note that had we solved the relaxed problem (without integrality 

351 constraints): 

352 

353 >>> res = milp(c=c, constraints=constraints) # OR: 

354 >>> # from scipy.optimize import linprog; res = linprog(c, A, b_u) 

355 >>> res.x 

356 [1.8, 2.8] 

357 

358 we would not have obtained the correct solution by rounding to the nearest 

359 integers. 

360 

361 Other examples are given :ref:`in the tutorial <tutorial-optimize_milp>`. 

362 

363 """ 

364 args_iv = _milp_iv(c, integrality, bounds, constraints, options) 

365 c, integrality, lb, ub, indptr, indices, data, b_l, b_u, options = args_iv 

366 

367 highs_res = _highs_wrapper(c, indptr, indices, data, b_l, b_u, 

368 lb, ub, integrality, options) 

369 

370 res = {} 

371 

372 # Convert to scipy-style status and message 

373 highs_status = highs_res.get('status', None) 

374 highs_message = highs_res.get('message', None) 

375 status, message = _highs_to_scipy_status_message(highs_status, 

376 highs_message) 

377 res['status'] = status 

378 res['message'] = message 

379 res['success'] = (status == 0) 

380 x = highs_res.get('x', None) 

381 res['x'] = np.array(x) if x is not None else None 

382 res['fun'] = highs_res.get('fun', None) 

383 res['mip_node_count'] = highs_res.get('mip_node_count', None) 

384 res['mip_dual_bound'] = highs_res.get('mip_dual_bound', None) 

385 res['mip_gap'] = highs_res.get('mip_gap', None) 

386 

387 return OptimizeResult(res)