Coverage for picos/solvers/solver_gurobi.py: 77.35%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

543 statements  

1# ------------------------------------------------------------------------------ 

2# Copyright (C) 2018-2022 Maximilian Stahlberg 

3# 

4# This file is part of PICOS. 

5# 

6# PICOS is free software: you can redistribute it and/or modify it under the 

7# terms of the GNU General Public License as published by the Free Software 

8# Foundation, either version 3 of the License, or (at your option) any later 

9# version. 

10# 

11# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY 

12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

13# A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

14# 

15# You should have received a copy of the GNU General Public License along with 

16# this program. If not, see <http://www.gnu.org/licenses/>. 

17# ------------------------------------------------------------------------------ 

18 

19"""Implementation of :class:`GurobiSolver`.""" 

20 

21from collections import namedtuple 

22 

23import cvxopt 

24import numpy 

25 

26from .. import settings 

27from ..apidoc import api_end, api_start 

28from ..constraints import (AffineConstraint, ConvexQuadraticConstraint, 

29 DummyConstraint, NonconvexQuadraticConstraint, 

30 RSOCConstraint, SOCConstraint) 

31from ..expressions import (CONTINUOUS_VARTYPES, AffineExpression, 

32 BinaryVariable, IntegerVariable, 

33 QuadraticExpression) 

34from ..expressions.data import cvx2csr, cvx2np 

35from ..modeling.footprint import Specification 

36from ..modeling.solution import (PS_FEASIBLE, PS_INF_OR_UNB, PS_INFEASIBLE, 

37 PS_UNBOUNDED, PS_UNKNOWN, PS_UNSTABLE, 

38 SS_EMPTY, SS_FEASIBLE, SS_INFEASIBLE, 

39 SS_OPTIMAL, SS_PREMATURE, SS_UNKNOWN) 

40from .solver import Solver 

41 

42_API_START = api_start(globals()) 

43# ------------------------------- 

44 

45 

46class GurobiSolver(Solver): 

47 """Interface to the Gurobi solver via its official Python interface.""" 

48 

49 # TODO: Don't support (conic) quadratic constraints when duals are 

50 # requested because their precision is bad and can't be controlled? 

51 SUPPORTED_8 = Specification( 

52 objectives=[ 

53 AffineExpression, 

54 QuadraticExpression], 

55 constraints=[ 

56 DummyConstraint, 

57 AffineConstraint, 

58 SOCConstraint, 

59 RSOCConstraint, 

60 ConvexQuadraticConstraint]) 

61 

62 SUPPORTED_9 = Specification( 

63 objectives=[ 

64 AffineExpression, 

65 QuadraticExpression], 

66 constraints=[ 

67 DummyConstraint, 

68 AffineConstraint, 

69 SOCConstraint, 

70 RSOCConstraint, 

71 NonconvexQuadraticConstraint]) 

72 

73 @classmethod 

74 def _gurobi9(cls): 

75 try: 

76 import gurobipy as gurobi 

77 except ImportError: 

78 # This method should be used only after test_availability confirmed 

79 # that gurobipy is available, however that method does not actually 

80 # perform an import and so it could still fail here due to a bad 

81 # installation. In this case an exception will be raised when Gurobi 

82 # is actually selected and we just return False as a dummy here. 

83 return False 

84 else: 

85 return hasattr(gurobi, "MVar") 

86 

87 @classmethod 

88 def supports(cls, footprint, explain=False): 

89 """Implement :meth:`~.solver.Solver.supports`.""" 

90 result = Solver.supports(footprint, explain) 

91 if not result or (explain and not result[0]): 

92 return result 

93 

94 supported = cls.SUPPORTED_9 if cls._gurobi9() else cls.SUPPORTED_8 

95 

96 if footprint not in supported: 

97 if explain: 

98 return False, supported.mismatch_reason(footprint) 

99 else: 

100 return False 

101 

102 return (True, None) if explain else True 

103 

104 @classmethod 

105 def default_penalty(cls): 

106 """Implement :meth:`~.solver.Solver.default_penalty`.""" 

107 return 0.0 # Commercial solver. 

108 

109 @classmethod 

110 def test_availability(cls): 

111 """Implement :meth:`~.solver.Solver.test_availability`.""" 

112 cls.check_import("gurobipy") 

113 

114 @classmethod 

115 def names(cls): 

116 """Implement :meth:`~.solver.Solver.names`.""" 

117 return "gurobi", "Gurobi", "Gurobi Optimizer", None 

118 

119 @classmethod 

120 def is_free(cls): 

121 """Implement :meth:`~.solver.Solver.is_free`.""" 

122 return False 

123 

124 GurobiMetaConstraint = namedtuple( 

125 "GurobiMetaConstraint", ("auxCons", "auxVars")) 

126 

127 def __init__(self, problem): 

128 """Initialize a Gurobi solver interface. 

129 

130 :param ~picos.Problem problem: The problem to be solved. 

131 """ 

132 super(GurobiSolver, self).__init__(problem) 

133 

134 self._matint_decision = None 

135 

136 self._gurobiVar = dict() 

137 """Maps PICOS variable indices to Gurobi variables. (Matrix interf.)""" 

138 

139 self._gurobiVars = [] 

140 """A list of all Gurobi variables added. (Legacy interface.)""" 

141 

142 self._gurobiVarOffset = dict() 

143 """Maps PICOS variables to Gurobi variable offsets. (Legacy interf.)""" 

144 

145 self._gurobiLinCon = dict() 

146 """Maps a PICOS linear constraint to (a) Gurobi linear constraint(s).""" 

147 

148 self._gurobiQuadCon = dict() 

149 """Maps a PICOS quadr. constraint to (a) Gurobi quadr. constraint(s).""" 

150 

151 self._gurobiConicCon = dict() 

152 """Maps a PICOS quadr. constraint to its Gurobi representation.""" 

153 

154 def _make_matint_decision(self): 

155 default = settings.PREFER_GUROBI_MATRIX_INTERFACE 

156 choice = self.ext.options.gurobi_matint 

157 

158 if not choice and (not default or choice is not None): 

159 return False 

160 

161 if not self._gurobi9(): 

162 if choice: 

163 raise RuntimeError( 

164 "Gurobi's matrix interface should be used by user's choice " 

165 "but Gurobi < 9 appears to be installed. More precisely, " 

166 "gurobipy.Mvar is not available.") 

167 else: 

168 return False 

169 

170 try: 

171 self.check_import("scipy.sparse") 

172 except ModuleNotFoundError as error: 

173 if choice: 

174 raise RuntimeError( 

175 "Gurobi's matrix interface should be used by user's choice " 

176 "but this requires SciPy, which was not found.") from error 

177 else: 

178 return False 

179 

180 assert choice or (choice is None and default) 

181 return True 

182 

183 @property 

184 def matint(self): 

185 """Whether Gurobi's matrix interface is in use.""" 

186 if self._matint_decision is None: 

187 self._matint_decision = self._make_matint_decision() 

188 

189 decision = self._matint_decision 

190 choice = self.ext.options.gurobi_matint 

191 

192 if (choice and not decision) \ 

193 or (decision and not choice and choice is not None): 

194 raise NotImplementedError( 

195 "The user's choice with respect to using Gurobi's matrix " 

196 "interface has changed between solution attempts. This is not " 

197 "supported. To re-load the problem with the other interface, " 

198 "you must manually reset your problem's solution strategy.") 

199 

200 return decision 

201 

202 def reset_problem(self): 

203 """Implement :meth:`~.solver.Solver.reset_problem`.""" 

204 self.int = None 

205 

206 self._gurobiVar.clear() 

207 self._gurobiVars.clear() 

208 self._gurobiVarOffset.clear() 

209 

210 self._gurobiLinCon.clear() 

211 self._gurobiQuadCon.clear() 

212 self._gurobiConicCon.clear() 

213 

214 def _import_variable(self, picosVar): 

215 import gurobipy as gurobi 

216 

217 dim = picosVar.dim 

218 

219 # Retrieve types. 

220 if isinstance(picosVar, CONTINUOUS_VARTYPES): 

221 gurobiVarType = gurobi.GRB.CONTINUOUS 

222 elif isinstance(picosVar, IntegerVariable): 

223 gurobiVarType = gurobi.GRB.INTEGER 

224 elif isinstance(picosVar, BinaryVariable): 

225 gurobiVarType = gurobi.GRB.BINARY 

226 else: 

227 assert False, "Unexpected variable type." 

228 

229 # Retrieve bounds. 

230 lowerBounds = [-gurobi.GRB.INFINITY]*dim 

231 upperBounds = [gurobi.GRB.INFINITY]*dim 

232 lower, upper = picosVar.bound_dicts 

233 for i, b in lower.items(): 

234 lowerBounds[i] = b 

235 for i, b in upper.items(): 

236 upperBounds[i] = b 

237 

238 # Import the variable. 

239 if self.matint: 

240 gurobiVar = self.int.addMVar(dim, lb=lowerBounds, ub=upperBounds, 

241 vtype=gurobiVarType, name=picosVar.name) 

242 

243 self._gurobiVar[picosVar] = gurobiVar 

244 else: 

245 gurobiVarsDict = self.int.addVars( 

246 dim, lb=lowerBounds, ub=upperBounds, vtype=gurobiVarType) 

247 gurobiVars = [gurobiVarsDict[i] for i in range(dim)] 

248 

249 self._gurobiVarOffset[picosVar] = len(self._gurobiVars) 

250 self._gurobiVars.extend(gurobiVars) 

251 

252 def _remove_variable(self, picosVar): 

253 if self.matint: 

254 gurobiVar = self._gurobiVar.pop(picosVar) 

255 

256 self.int.remove(gurobiVar) 

257 else: 

258 offset = self._gurobiVarOffset[picosVar] 

259 dim = picosVar.dim 

260 

261 gurobiVars = self._gurobiVars[offset:offset + dim] 

262 

263 self._gurobiVars = ( 

264 self._gurobiVars[:offset] + self._gurobiVars[offset + dim:]) 

265 

266 for other in self._gurobiVarOffset: 

267 if self._gurobiVarOffset[other] > offset: 

268 self._gurobiVarOffset[other] -= dim 

269 

270 self.int.remove(gurobiVars) 

271 

272 def _import_variable_values(self): 

273 for picosVar in self.ext.variables.values(): 

274 if picosVar.valued: 

275 value = picosVar.internal_value 

276 

277 if self.matint: 

278 gurobiVar = self._gurobiVar[picosVar] 

279 gurobiVar.Start = value 

280 else: 

281 offset = self._gurobiVarOffset[picosVar] 

282 dim = picosVar.dim 

283 

284 gurobiVars = self._gurobiVars[offset, offset + picosVar.dim] 

285 

286 for localIndex in range(dim): 

287 gurobiVars[localIndex].Start = value[localIndex] 

288 

289 def _reset_variable_values(self): 

290 import gurobipy as gurobi 

291 

292 if self.matint: 

293 gurobiVars = self._gurobiVar.values() 

294 else: 

295 gurobiVars = self._gurobiVars 

296 

297 for gurobiVar in gurobiVars: 

298 gurobiVar.Start = gurobi.GRB.UNDEFINED 

299 

300 def _affexp_pic2grb_matint(self, picosExpression): 

301 assert self.matint 

302 

303 # NOTE: Constant Gurobi matrix expressions don't exist; return thus 

304 # constant PICOS expressions as NumPy arrays. 

305 gurobiExpression = numpy.ravel(cvx2np( 

306 picosExpression._constant_coef)) 

307 

308 for picosVar, coef in picosExpression._linear_coefs.items(): 

309 A = cvx2csr(coef) 

310 x = self._gurobiVar[picosVar] 

311 

312 # NOTE: Using __(r)matmul__ as PICOS supports Python 3.4 and the 

313 # @-operator was implemented in Python 3.5. 

314 gurobiExpression += x.__rmatmul__(A) 

315 

316 return gurobiExpression 

317 

318 def _affexp_pic2grb_legacy(self, picosExpression): 

319 import gurobipy as gurobi 

320 

321 assert not self.matint 

322 

323 for J, V, c in picosExpression.sparse_rows(self._gurobiVarOffset): 

324 gurobiVars = [self._gurobiVars[j] for j in J] 

325 gurobiExpression = gurobi.LinExpr(V, gurobiVars) 

326 gurobiExpression.addConstant(c) 

327 

328 yield gurobiExpression 

329 

330 def _scalar_affexp_pic2grb(self, picosExpression): 

331 assert len(picosExpression) == 1 

332 

333 if self.matint: 

334 return self._affexp_pic2grb_matint(picosExpression) 

335 else: 

336 return next(self._affexp_pic2grb_legacy(picosExpression)) 

337 

338 def _quadexp_pic2grb(self, picosExpression): 

339 import gurobipy as gurobi 

340 

341 assert isinstance(picosExpression, QuadraticExpression) 

342 

343 if self.matint: 

344 # Import affine part. 

345 gurobiExpression = self._affexp_pic2grb_matint(picosExpression.aff) 

346 

347 # Import quadratic part. 

348 for picosVars, coef in picosExpression._sparse_quads.items(): 

349 Q = cvx2csr(coef) 

350 x = self._gurobiVar[picosVars[0]] 

351 y = self._gurobiVar[picosVars[1]] 

352 

353 gurobiExpression += x.__matmul__(Q).__matmul__(y) 

354 else: 

355 # Import affine part. 

356 gurobiExpression = gurobi.QuadExpr( 

357 self._scalar_affexp_pic2grb(picosExpression.aff)) 

358 

359 # Import quadratic part. 

360 V, I, J = [], [], [] 

361 for (x, y), Q in picosExpression._sparse_quads.items(): 

362 xOffset = self._gurobiVarOffset[x] 

363 yOffset = self._gurobiVarOffset[y] 

364 

365 V.extend(Q.V) 

366 I.extend(self._gurobiVars[i] for i in Q.I + xOffset) 

367 J.extend(self._gurobiVars[j] for j in Q.J + yOffset) 

368 

369 gurobiExpression.addTerms(V, I, J) 

370 

371 return gurobiExpression 

372 

373 def _import_linear_constraint(self, picosCon): 

374 import gurobipy as gurobi 

375 

376 assert isinstance(picosCon, AffineConstraint) 

377 

378 if self.matint: 

379 gurobiLHS = self._affexp_pic2grb_matint(picosCon.lhs) 

380 gurobiRHS = self._affexp_pic2grb_matint(picosCon.rhs) 

381 

382 # HACK: Fallback to the legacy interface for constant constraints. 

383 # NOTE: This happens to work with remove_constraint since 

384 # gurobipy.Model.remove accepts both lists and constraints. 

385 if isinstance(gurobiLHS, numpy.ndarray) \ 

386 and isinstance(gurobiRHS, numpy.ndarray): 

387 if picosCon.is_increasing(): 

388 gurobiSense = gurobi.GRB.LESS_EQUAL 

389 elif picosCon.is_decreasing(): 

390 gurobiSense = gurobi.GRB.GREATER_EQUAL 

391 elif picosCon.is_equality(): 

392 gurobiSense = gurobi.GRB.EQUAL 

393 else: 

394 assert False, "Unexpected constraint relation." 

395 

396 return [self.int.addLConstr(a, gurobiSense, b) 

397 for a, b in zip(gurobiLHS, gurobiRHS)] 

398 

399 # Construct the constraint. 

400 if picosCon.is_increasing(): 

401 gurobiCon = gurobiLHS <= gurobiRHS 

402 elif picosCon.is_decreasing(): 

403 gurobiCon = gurobiLHS >= gurobiRHS 

404 elif picosCon.is_equality(): 

405 gurobiCon = gurobiLHS == gurobiRHS 

406 else: 

407 assert False, "Unexpected constraint relation." 

408 

409 # Add the constraint. 

410 gurobiCon = self.int.addConstr(gurobiCon) 

411 

412 return gurobiCon 

413 else: 

414 # Retrieve sense. 

415 if picosCon.is_increasing(): 

416 gurobiSense = gurobi.GRB.LESS_EQUAL 

417 elif picosCon.is_decreasing(): 

418 gurobiSense = gurobi.GRB.GREATER_EQUAL 

419 elif picosCon.is_equality(): 

420 gurobiSense = gurobi.GRB.EQUAL 

421 else: 

422 assert False, "Unexpected constraint relation." 

423 

424 # Append scalar constraints. 

425 gurobiCons = [self.int.addLConstr(gurobiLHS, gurobiSense, 0.0) 

426 for gurobiLHS in self._affexp_pic2grb_legacy(picosCon.lmr)] 

427 

428 return gurobiCons 

429 

430 def _import_quad_constraint(self, picosCon): 

431 import gurobipy as gurobi 

432 

433 # NOTE: NonconvexQuadraticConstraint includes ConvexQuadraticConstraint. 

434 assert isinstance(picosCon, NonconvexQuadraticConstraint) 

435 

436 if self.matint: 

437 gurobiLE0 = self._quadexp_pic2grb(picosCon.le0) 

438 gurobiCon = self.int.addConstr(gurobiLE0 <= 0) 

439 else: 

440 gurobiLHS = self._quadexp_pic2grb(picosCon.le0) 

441 gurobiRHS = -gurobiLHS.getLinExpr().getConstant() 

442 

443 if gurobiRHS: 

444 gurobiLHS.getLinExpr().addConstant(gurobiRHS) 

445 

446 gurobiCon = self.int.addQConstr( 

447 gurobiLHS, gurobi.GRB.LESS_EQUAL, gurobiRHS) 

448 

449 return gurobiCon 

450 

451 def _import_socone_constraint(self, picosCon): 

452 import gurobipy as gurobi 

453 

454 assert isinstance(picosCon, SOCConstraint) 

455 

456 n = len(picosCon.ne) 

457 

458 # Load defining expressions. 

459 gurobiRHS = self._scalar_affexp_pic2grb(picosCon.ub) 

460 

461 if self.matint: 

462 # Load defining expressions. 

463 gurobiLHS = self._affexp_pic2grb_matint(picosCon.ne) 

464 

465 # Add auxiliary variables for both sides. 

466 gurobiLHSVar = self.int.addMVar(n, lb=-gurobi.GRB.INFINITY) 

467 gurobiRHSVar = self.int.addMVar(1) 

468 

469 # Add constraints to identify auxiliary variables with expressions. 

470 gurobiLHSCon = self.int.addConstr(gurobiLHSVar == gurobiLHS) 

471 gurobiRHSCon = self.int.addConstr(gurobiRHSVar == gurobiRHS) 

472 

473 # Add a quadratic constraint over the auxiliary variables that 

474 # represents the PICOS second order cone constraint itself. 

475 gurobiQuadLHS = gurobiLHSVar.__matmul__(gurobiLHSVar) 

476 gurobiQuadRHS = gurobiRHSVar.__matmul__(gurobiRHSVar) 

477 gurobiQuadCon = self.int.addConstr(gurobiQuadLHS <= gurobiQuadRHS) 

478 

479 # Collect auxiliary objects. 

480 auxCons = [gurobiLHSCon, gurobiRHSCon, gurobiQuadCon] 

481 auxVars = [gurobiLHSVar, gurobiRHSVar] 

482 else: 

483 # Load defining expressions. 

484 gurobiLHS = self._affexp_pic2grb_legacy(picosCon.ne) 

485 

486 # Add auxiliary variables: One for every dimension of the left hand 

487 # side of the PICOS constraint and one for its right hand side. 

488 gurobiLHSVarsDict = self.int.addVars( 

489 n, lb=-gurobi.GRB.INFINITY, ub=gurobi.GRB.INFINITY) 

490 gurobiLHSVars = gurobiLHSVarsDict.values() 

491 gurobiRHSVar = self.int.addVar(lb=0.0, ub=gurobi.GRB.INFINITY) 

492 

493 # Add constraints that identify the left hand side Gurobi auxiliary 

494 # variables with entries of the PICOS left hand side expression. 

495 gurobiLHSDict = dict(enumerate(gurobiLHS)) 

496 gurobiLHSConsDict = self.int.addConstrs( 

497 gurobiLHSVarsDict[d] == gurobiLHSDict[d] for d in range(n)) 

498 gurobiLHSCons = gurobiLHSConsDict.values() 

499 

500 # Add a constraint that identifies the right hand side Gurobi 

501 # auxiliary variable with the PICOS right hand side expression. 

502 gurobiRHSCon = self.int.addLConstr( 

503 gurobiRHSVar, gurobi.GRB.EQUAL, gurobiRHS) 

504 

505 # Add a quadratic constraint over the auxiliary variables that 

506 # represents the PICOS second order cone constraint itself. 

507 quadExpr = gurobi.QuadExpr() 

508 quadExpr.addTerms([1.0] * n, gurobiLHSVars, gurobiLHSVars) 

509 gurobiQuadCon = self.int.addQConstr( 

510 quadExpr, gurobi.GRB.LESS_EQUAL, gurobiRHSVar * gurobiRHSVar) 

511 

512 # Collect auxiliary objects. 

513 auxCons = list(gurobiLHSCons) + [gurobiRHSCon, gurobiQuadCon] 

514 auxVars = list(gurobiLHSVars) + [gurobiRHSVar] 

515 

516 return self.GurobiMetaConstraint(auxCons=auxCons, auxVars=auxVars) 

517 

518 def _import_rscone_constraint(self, picosCon): 

519 import gurobipy as gurobi 

520 

521 assert isinstance(picosCon, RSOCConstraint) 

522 

523 n = len(picosCon.ne) 

524 

525 # Load defining expressions. 

526 gurobiRHS = ( 

527 self._scalar_affexp_pic2grb(picosCon.ub1), 

528 self._scalar_affexp_pic2grb(picosCon.ub2)) 

529 

530 if self.matint: 

531 # Load defining expressions. 

532 gurobiLHS = self._affexp_pic2grb_matint(picosCon.ne) 

533 

534 # Add auxiliary variables for both sides. 

535 gurobiLHSVar = self.int.addMVar(n, lb=-gurobi.GRB.INFINITY) 

536 gurobiRHSVars = (self.int.addMVar(1), self.int.addMVar(1)) 

537 

538 # Add constraints to identify auxiliary variables with expressions. 

539 gurobiLHSCon = self.int.addConstr(gurobiLHSVar == gurobiLHS) 

540 gurobiRHSConsDict = self.int.addConstrs( 

541 gurobiRHSVars[i] == gurobiRHS[i] for i in range(2)) 

542 gurobiRHSCons = gurobiRHSConsDict.values() 

543 

544 # Add a quadratic constraint over the auxiliary variables that 

545 # represents the PICOS rotated second order cone constraint itself. 

546 gurobiQuadLHS = gurobiLHSVar.__matmul__(gurobiLHSVar) 

547 gurobiQuadRHS = gurobiRHSVars[0].__matmul__(gurobiRHSVars[1]) 

548 gurobiQuadCon = self.int.addConstr(gurobiQuadLHS <= gurobiQuadRHS) 

549 

550 # Collect auxiliary objects. 

551 auxCons = [gurobiLHSCon] + list(gurobiRHSCons) + [gurobiQuadCon] 

552 auxVars = [gurobiLHSVar] + list(gurobiRHSVars) 

553 else: 

554 # Load defining expressions. 

555 gurobiLHS = self._affexp_pic2grb_legacy(picosCon.ne) 

556 

557 # Add auxiliary variables: One for every dimension of the left hand 

558 # side of the PICOS constraint and one for its right hand side. 

559 gurobiLHSVarsDict = self.int.addVars( 

560 n, lb=-gurobi.GRB.INFINITY, ub=gurobi.GRB.INFINITY) 

561 gurobiLHSVars = gurobiLHSVarsDict.values() 

562 gurobiRHSVars = self.int.addVars( 

563 2, lb=0.0, ub=gurobi.GRB.INFINITY).values() 

564 

565 # Add constraints that identify the left hand side Gurobi auxiliary 

566 # variables with entries of the PICOS left hand side expression. 

567 gurobiLHSDict = dict(enumerate(gurobiLHS)) 

568 gurobiLHSConsDict = self.int.addConstrs( 

569 gurobiLHSVarsDict[d] == gurobiLHSDict[d] for d in range(n)) 

570 gurobiLHSCons = gurobiLHSConsDict.values() 

571 

572 # Add two constraints that identify the right hand side Gurobi 

573 # auxiliary variables with the PICOS right hand side expressions. 

574 gurobiRHSConsDict = self.int.addConstrs( 

575 gurobiRHSVars[i] == gurobiRHS[i] for i in (0, 1)) 

576 gurobiRHSCons = gurobiRHSConsDict.values() 

577 

578 # Add a quadratic constraint over the auxiliary variables that 

579 # represents the PICOS second order cone constraint itself. 

580 quadExpr = gurobi.QuadExpr() 

581 quadExpr.addTerms([1.0] * n, gurobiLHSVars, gurobiLHSVars) 

582 gurobiQuadCon = self.int.addQConstr(quadExpr, gurobi.GRB.LESS_EQUAL, 

583 gurobiRHSVars[0] * gurobiRHSVars[1]) 

584 

585 # Collect auxiliary objects. 

586 auxCons = ( 

587 list(gurobiLHSCons) + list(gurobiRHSCons) + [gurobiQuadCon]) 

588 auxVars = list(gurobiLHSVars) + list(gurobiRHSVars) 

589 

590 return self.GurobiMetaConstraint(auxCons=auxCons, auxVars=auxVars) 

591 

592 def _import_constraint(self, picosCon): 

593 if isinstance(picosCon, AffineConstraint): 

594 self._gurobiLinCon[picosCon] = \ 

595 self._import_linear_constraint(picosCon) 

596 elif isinstance(picosCon, NonconvexQuadraticConstraint): 

597 self._gurobiQuadCon[picosCon] = \ 

598 self._import_quad_constraint(picosCon) 

599 elif isinstance(picosCon, SOCConstraint): 

600 self._gurobiConicCon[picosCon] = \ 

601 self._import_socone_constraint(picosCon) 

602 elif isinstance(picosCon, RSOCConstraint): 

603 self._gurobiConicCon[picosCon] = \ 

604 self._import_rscone_constraint(picosCon) 

605 else: 

606 assert isinstance(picosCon, DummyConstraint), \ 

607 "Unexpected constraint type: {}".format( 

608 picosCon.__class__.__name__) 

609 

610 def _remove_constraint(self, picosCon): 

611 if isinstance(picosCon, AffineConstraint): 

612 self.int.remove(self._gurobiLinCon.pop(picosCon)) 

613 elif isinstance(picosCon, NonconvexQuadraticConstraint): 

614 self.int.remove(self._gurobiQuadCon.pop(picosCon)) 

615 elif isinstance(picosCon, (SOCConstraint, RSOCConstraint)): 

616 metaCon = self._gurobiConicCon.pop(picosCon) 

617 

618 self.int.remove(metaCon.auxCons) 

619 self.int.remove(metaCon.auxVars) 

620 else: 

621 assert isinstance(picosCon, DummyConstraint), \ 

622 "Unexpected constraint type: {}".format( 

623 picosCon.__class__.__name__) 

624 

625 def _import_objective(self): 

626 import gurobipy as gurobi 

627 

628 picosSense, picosObjective = self.ext.no 

629 

630 # Retrieve objective sense. 

631 if picosSense == "min": 

632 gurobiSense = gurobi.GRB.MINIMIZE 

633 else: 

634 assert picosSense == "max" 

635 gurobiSense = gurobi.GRB.MAXIMIZE 

636 

637 # Retrieve objective function. 

638 if isinstance(picosObjective, AffineExpression): 

639 gurobiObjective = self._scalar_affexp_pic2grb(picosObjective) 

640 else: 

641 assert isinstance(picosObjective, QuadraticExpression) 

642 gurobiObjective = self._quadexp_pic2grb(picosObjective) 

643 

644 self.int.setObjective(gurobiObjective, gurobiSense) 

645 

646 def _import_problem(self): 

647 import gurobipy as gurobi 

648 

649 # Create a problem instance. 

650 if self._license_warnings: 

651 self.int = gurobi.Model() 

652 else: 

653 with self._enforced_verbosity(): 

654 self.int = gurobi.Model() 

655 

656 # Import variables. 

657 for variable in self.ext.variables.values(): 

658 self._import_variable(variable) 

659 

660 # Import constraints. 

661 for constraint in self.ext.constraints.values(): 

662 self._import_constraint(constraint) 

663 

664 # Set objective. 

665 self._import_objective() 

666 

667 def _update_problem(self): 

668 for oldConstraint in self._removed_constraints(): 

669 self._remove_constraint(oldConstraint) 

670 

671 for oldVariable in self._removed_variables(): 

672 self._remove_variable(oldVariable) 

673 

674 for newVariable in self._new_variables(): 

675 self._import_variable(newVariable) 

676 

677 for newConstraint in self._new_constraints(): 

678 self._import_constraint(newConstraint) 

679 

680 if self._objective_has_changed(): 

681 self._import_objective() 

682 

683 def _solve(self): 

684 import gurobipy as gurobi 

685 

686 # Reset options. 

687 # NOTE: OutputFlag = 0 prevents resetParams from printing to console. 

688 self.int.Params.OutputFlag = 0 

689 self.int.resetParams() 

690 

691 # verbosity 

692 self.int.Params.OutputFlag = 1 if self.verbosity() > 0 else 0 

693 

694 # abs_prim_fsb_tol 

695 if self.ext.options.abs_prim_fsb_tol is not None: 

696 self.int.Params.FeasibilityTol = self.ext.options.abs_prim_fsb_tol 

697 

698 # abs_dual_fsb_tol 

699 if self.ext.options.abs_dual_fsb_tol is not None: 

700 self.int.Params.OptimalityTol = self.ext.options.abs_dual_fsb_tol 

701 

702 # rel_ipm_opt_tol 

703 if self.ext.options.rel_ipm_opt_tol is not None: 

704 self.int.Params.BarConvTol = self.ext.options.rel_ipm_opt_tol 

705 

706 # HACK: Work around low precision (conic) quadratic duals. 

707 self.int.Params.BarQCPConvTol = \ 

708 0.01 * self.ext.options.rel_ipm_opt_tol 

709 

710 # abs_bnb_opt_tol 

711 if self.ext.options.abs_bnb_opt_tol is not None: 

712 self.int.Params.MIPGapAbs = self.ext.options.abs_bnb_opt_tol 

713 

714 # rel_bnb_opt_tol 

715 if self.ext.options.rel_bnb_opt_tol is not None: 

716 self.int.Params.MIPGap = self.ext.options.rel_bnb_opt_tol 

717 

718 # integrality_tol 

719 if self.ext.options.integrality_tol is not None: 

720 self.int.Params.IntFeasTol = self.ext.options.integrality_tol 

721 

722 # markowitz_tol 

723 if self.ext.options.markowitz_tol is not None: 

724 self.int.Params.MarkowitzTol = self.ext.options.markowitz_tol 

725 

726 # max_iterations 

727 if self.ext.options.max_iterations is not None: 

728 self.int.Params.BarIterLimit = self.ext.options.max_iterations 

729 self.int.Params.IterationLimit = self.ext.options.max_iterations 

730 

731 _lpm = {"interior": 2, "psimplex": 0, "dsimplex": 1} 

732 

733 # lp_node_method 

734 if self.ext.options.lp_node_method is not None: 

735 value = self.ext.options.lp_node_method 

736 assert value in _lpm, "Unexpected lp_node_method value." 

737 self.int.Params.SiftMethod = _lpm[value] 

738 

739 # lp_root_method 

740 if self.ext.options.lp_root_method is not None: 

741 value = self.ext.options.lp_root_method 

742 assert value in _lpm, "Unexpected lp_root_method value." 

743 self.int.Params.Method = _lpm[value] 

744 

745 # timelimit 

746 if self.ext.options.timelimit is not None: 

747 self.int.Params.TimeLimit = self.ext.options.timelimit 

748 

749 # max_fsb_nodes 

750 if self.ext.options.max_fsb_nodes is not None: 

751 self.int.Params.SolutionLimit = self.ext.options.max_fsb_nodes 

752 

753 # hotstart 

754 if self.ext.options.hotstart: 

755 self._import_variable_values() 

756 else: 

757 self._reset_variable_values() 

758 

759 # Handle Gurobi-specific options. 

760 for key, value in self.ext.options.gurobi_params.items(): 

761 if not self.int.getParamInfo(key): 

762 self._handle_bad_solver_specific_option_key(key) 

763 

764 try: 

765 self.int.setParam(key, value) 

766 except TypeError as error: 

767 self._handle_bad_solver_specific_option_value(key, value, error) 

768 

769 # Handle unsupported options. 

770 self._handle_unsupported_option("treememory") 

771 

772 # Extend functionality for continuous problems. 

773 if self.ext.is_continuous(): 

774 # Compute duals also for QPs and QC(Q)Ps. 

775 if self.ext.options.duals is not False: 

776 self.int.setParam(gurobi.GRB.Param.QCPDual, 1) 

777 

778 # Allow nonconvex quadratic objectives. 

779 # TODO: Allow querying self.ext.objective directly. 

780 # TODO: Check if this should/must be set also for Gurobi >= 9. 

781 if self.ext.footprint.nonconvex_quadratic_objective: 

782 self.int.setParam(gurobi.GRB.Param.NonConvex, 2) 

783 

784 # Attempt to solve the problem. 

785 with self._header(), self._stopwatch(): 

786 try: 

787 self.int.optimize() 

788 except gurobi.GurobiError as error: 

789 if error.errno == gurobi.GRB.Error.Q_NOT_PSD: 

790 self._handle_continuous_nonconvex_error(error) 

791 else: 

792 raise 

793 

794 # Retrieve primals. 

795 primals = {} 

796 if self.ext.options.primals is not False: 

797 for picosVar in self.ext.variables.values(): 

798 try: 

799 if self.matint: 

800 value = cvxopt.matrix(self._gurobiVar[picosVar].X) 

801 else: 

802 o = self._gurobiVarOffset[picosVar] 

803 d = picosVar.dim 

804 

805 value = [v.X for v in self._gurobiVars[o:o + d]] 

806 except (AttributeError, gurobi.GurobiError): 

807 # NOTE: AttributeError is raised for gurobipy.Var, 

808 # gurobi.GurobiError for gurobipy.MVar. 

809 primals[picosVar] = None 

810 else: 

811 primals[picosVar] = value 

812 

813 # Retrieve duals. 

814 duals = {} 

815 if self.ext.options.duals is not False and self.ext.is_continuous(): 

816 for picosCon in self.ext.constraints.values(): 

817 if isinstance(picosCon, DummyConstraint): 

818 duals[picosCon] = cvxopt.spmatrix([], [], [], picosCon.size) 

819 continue 

820 

821 # HACK: Work around gurobiCon.getAttr(gurobi.GRB.Attr.Pi) 

822 # printing a newline to console when it raises an 

823 # AttributeError and OutputFlag is enabled. This is a 

824 # WONTFIX on Gurobi's end (PICOS #264, Gurobi #14248). 

825 # TODO: Check if this also happens for urobiCon.Pi, which is now 

826 # used for both interfaces. 

827 oldOutput = self.int.Params.OutputFlag 

828 self.int.Params.OutputFlag = 0 

829 

830 try: 

831 if isinstance(picosCon, AffineConstraint): 

832 gurobiCon = self._gurobiLinCon[picosCon] 

833 

834 # HACK: Seee _import_linear_constraint. 

835 if not self.matint or isinstance(gurobiCon, list): 

836 gurobiDual = [c.Pi for c in gurobiCon] 

837 else: 

838 gurobiDual = gurobiCon.Pi 

839 

840 picosDual = cvxopt.matrix(gurobiDual, picosCon.size) 

841 

842 if not picosCon.is_increasing(): 

843 picosDual = -picosDual 

844 elif isinstance(picosCon, SOCConstraint): 

845 gurobiMetaCon = self._gurobiConicCon[picosCon] 

846 

847 if self.matint: 

848 ne, ub, _ = gurobiMetaCon.auxCons 

849 dual = numpy.hstack([ub.Pi, ne.Pi]) 

850 picosDual = cvxopt.matrix(dual) 

851 else: 

852 n = len(picosCon.ne) 

853 assert len(gurobiMetaCon.auxCons) == n + 2 

854 

855 ne = gurobiMetaCon.auxCons[:n] 

856 ub = gurobiMetaCon.auxCons[n] 

857 

858 z, lbd = [c.Pi for c in ne], ub.Pi 

859 picosDual = cvxopt.matrix([lbd] + z) 

860 elif isinstance(picosCon, RSOCConstraint): 

861 gurobiMetaCon = self._gurobiConicCon[picosCon] 

862 

863 if self.matint: 

864 ne, ub1, ub2, _ = gurobiMetaCon.auxCons 

865 dual = numpy.hstack([ub1.Pi, ub2.Pi, ne.Pi]) 

866 picosDual = cvxopt.matrix(dual) 

867 else: 

868 n = len(picosCon.ne) 

869 assert len(gurobiMetaCon.auxCons) == n + 3 

870 

871 ne = gurobiMetaCon.auxCons[:n] 

872 ub1 = gurobiMetaCon.auxCons[n] 

873 ub2 = gurobiMetaCon.auxCons[n + 1] 

874 

875 z, a, b = [c.Pi for c in ne], ub1.Pi, ub2.Pi 

876 picosDual = cvxopt.matrix([a] + [b] + z) 

877 elif isinstance(picosCon, NonconvexQuadraticConstraint): 

878 picosDual = None 

879 else: 

880 assert isinstance(picosCon, DummyConstraint), \ 

881 "Unexpected constraint type: {}".format( 

882 picosCon.__class__.__name__) 

883 

884 # Flip sign based on objective sense. 

885 if picosDual and self.ext.no.direction == "min": 

886 picosDual = -picosDual 

887 except (AttributeError, gurobi.GurobiError): 

888 # NOTE: AttributeError is raised for gurobipy.Constr, 

889 # gurobi.GurobiError for gurobipy.MConstr. 

890 duals[picosCon] = None 

891 else: 

892 duals[picosCon] = picosDual 

893 

894 # HACK: See above. Also: Silence Gurobi while enabling output. 

895 if oldOutput != 0: 

896 with self._enforced_verbosity(noStdOutAt=float("inf")): 

897 self.int.Params.OutputFlag = oldOutput 

898 

899 # Retrieve objective value. 

900 try: 

901 value = self.int.ObjVal 

902 except AttributeError: 

903 value = None 

904 

905 # Retrieve solution status. 

906 statusCode = self.int.Status 

907 if statusCode == gurobi.GRB.Status.LOADED: 

908 raise RuntimeError("Gurobi claims to have just loaded the problem " 

909 "while PICOS expects the solution search to have terminated.") 

910 elif statusCode == gurobi.GRB.Status.OPTIMAL: 

911 primalStatus = SS_OPTIMAL 

912 dualStatus = SS_OPTIMAL 

913 problemStatus = PS_FEASIBLE 

914 elif statusCode == gurobi.GRB.Status.INFEASIBLE: 

915 primalStatus = SS_INFEASIBLE 

916 dualStatus = SS_UNKNOWN 

917 problemStatus = PS_INFEASIBLE 

918 elif statusCode == gurobi.GRB.Status.INF_OR_UNBD: 

919 primalStatus = SS_UNKNOWN 

920 dualStatus = SS_UNKNOWN 

921 problemStatus = PS_INF_OR_UNB 

922 elif statusCode == gurobi.GRB.Status.UNBOUNDED: 

923 primalStatus = SS_UNKNOWN 

924 dualStatus = SS_INFEASIBLE 

925 problemStatus = PS_UNBOUNDED 

926 elif statusCode == gurobi.GRB.Status.CUTOFF: 

927 # "Optimal objective for model was proven to be worse than the value 

928 # specified in the Cutoff parameter. No solution information is 

929 # available." 

930 primalStatus = SS_PREMATURE 

931 dualStatus = SS_PREMATURE 

932 problemStatus = PS_UNKNOWN 

933 elif statusCode == gurobi.GRB.Status.ITERATION_LIMIT: 

934 primalStatus = SS_PREMATURE 

935 dualStatus = SS_PREMATURE 

936 problemStatus = PS_UNKNOWN 

937 elif statusCode == gurobi.GRB.Status.NODE_LIMIT: 

938 primalStatus = SS_PREMATURE 

939 dualStatus = SS_EMPTY # Applies only to mixed integer problems. 

940 problemStatus = PS_UNKNOWN 

941 elif statusCode == gurobi.GRB.Status.TIME_LIMIT: 

942 primalStatus = SS_PREMATURE 

943 dualStatus = SS_PREMATURE 

944 problemStatus = PS_UNKNOWN 

945 elif statusCode == gurobi.GRB.Status.SOLUTION_LIMIT: 

946 primalStatus = SS_PREMATURE 

947 dualStatus = SS_PREMATURE 

948 problemStatus = PS_UNKNOWN 

949 elif statusCode == gurobi.GRB.Status.INTERRUPTED: 

950 primalStatus = SS_PREMATURE 

951 dualStatus = SS_PREMATURE 

952 problemStatus = PS_UNKNOWN 

953 elif statusCode == gurobi.GRB.Status.NUMERIC: 

954 primalStatus = SS_UNKNOWN 

955 dualStatus = SS_UNKNOWN 

956 problemStatus = PS_UNSTABLE 

957 elif statusCode == gurobi.GRB.Status.SUBOPTIMAL: 

958 # "Unable to satisfy optimality tolerances; a sub-optimal solution 

959 # is available." 

960 primalStatus = SS_FEASIBLE 

961 dualStatus = SS_FEASIBLE 

962 problemStatus = PS_FEASIBLE 

963 elif statusCode == gurobi.GRB.Status.INPROGRESS: 

964 raise RuntimeError("Gurobi claims solution search to be 'in " 

965 "progress' while PICOS expects it to have terminated.") 

966 elif statusCode == gurobi.GRB.Status.USER_OBJ_LIMIT: 

967 # "User specified an objective limit (a bound on either the best 

968 # objective or the best bound), and that limit has been reached." 

969 primalStatus = SS_FEASIBLE 

970 dualStatus = SS_EMPTY # Applies only to mixed integer problems. 

971 problemStatus = PS_FEASIBLE 

972 else: 

973 primalStatus = SS_UNKNOWN 

974 dualStatus = SS_UNKNOWN 

975 problemStatus = PS_UNKNOWN 

976 

977 return self._make_solution( 

978 value, primals, duals, primalStatus, dualStatus, problemStatus) 

979 

980 

981# -------------------------------------- 

982__all__ = api_end(_API_START, globals())