Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2# Copyright (C) 2018-2019 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 

24 

25from ..apidoc import api_end, api_start 

26from ..constraints import (AffineConstraint, ConvexQuadraticConstraint, 

27 DummyConstraint, RSOCConstraint, SOCConstraint) 

28from ..expressions import (CONTINUOUS_VARTYPES, AffineExpression, 

29 BinaryVariable, IntegerVariable, 

30 QuadraticExpression) 

31from ..modeling.footprint import Specification 

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

33 PS_UNBOUNDED, PS_UNKNOWN, PS_UNSTABLE, 

34 SS_EMPTY, SS_FEASIBLE, SS_INFEASIBLE, 

35 SS_OPTIMAL, SS_PREMATURE, SS_UNKNOWN) 

36from .solver import Solver 

37 

38_API_START = api_start(globals()) 

39# ------------------------------- 

40 

41 

42class GurobiSolver(Solver): 

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

44 

45 # TODO: Allow nonconvex quadratic constraints in the integer case? 

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

47 # requested because their precision is bad and can't be controlled. 

48 SUPPORTED = Specification( 

49 objectives=[ 

50 AffineExpression, 

51 QuadraticExpression], 

52 constraints=[ 

53 DummyConstraint, 

54 AffineConstraint, 

55 SOCConstraint, 

56 RSOCConstraint, 

57 ConvexQuadraticConstraint]) 

58 

59 @classmethod 

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

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

62 result = Solver.supports(footprint, explain) 

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

64 return result 

65 

66 if footprint not in cls.SUPPORTED: 

67 if explain: 

68 return False, cls.SUPPORTED.mismatch_reason(footprint) 

69 else: 

70 return False 

71 

72 return (True, None) if explain else True 

73 

74 @classmethod 

75 def default_penalty(cls): 

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

77 return 0.0 # Commercial solver. 

78 

79 @classmethod 

80 def test_availability(cls): 

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

82 cls.check_import("gurobipy") 

83 

84 @classmethod 

85 def names(cls): 

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

87 return "gurobi", "Gurobi", "Gurobi Optimizer" 

88 

89 @classmethod 

90 def is_free(cls): 

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

92 return False 

93 

94 GurobiSOCC = namedtuple("GurobiSOCC", 

95 ("LHSVars", "RHSVar", "LHSCons", "RHSCon", "quadCon")) 

96 

97 GurobiRSOCC = namedtuple("GurobiRSOCC", 

98 ("LHSVars", "RHSVars", "LHSCons", "RHSCons", "quadCon")) 

99 

100 def __init__(self, problem): 

101 """Initialize a Gurobi solver interface. 

102 

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

104 """ 

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

106 

107 self._gurobiVar = dict() 

108 """Maps PICOS variable indices to Gurobi variables.""" 

109 

110 self._gurobiLinearConstraints = dict() 

111 """Maps a PICOS (multidimensional) linear constraint to a collection of 

112 Gurobi (scalar) linear constraints.""" 

113 

114 self._gurobiQuadConstraint = dict() 

115 """Maps a PICOS quadratic constraint to a Gurobi quadr. constraint.""" 

116 

117 self._gurobiSOCC = dict() 

118 """Maps a PICOS second order cone constraint to its Gurobi 

119 representation involving auxiliary variables and constraints.""" 

120 

121 self._gurobiRSOCC = dict() 

122 """Maps a PICOS rotated second order cone constraint to its Gurobi 

123 representation involving auxiliary variables and constraints.""" 

124 

125 def reset_problem(self): 

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

127 self.int = None 

128 

129 self._gurobiVar.clear() 

130 self._gurobiLinearConstraints.clear() 

131 self._gurobiQuadConstraint.clear() 

132 self._gurobiSOCC.clear() 

133 self._gurobiRSOCC.clear() 

134 

135 def _import_variable(self, picosVar): 

136 import gurobipy as gurobi 

137 

138 dim = picosVar.dim 

139 

140 # Retrieve types. 

141 if isinstance(picosVar, CONTINUOUS_VARTYPES): 

142 gurobiVarType = gurobi.GRB.CONTINUOUS 

143 elif isinstance(picosVar, IntegerVariable): 

144 gurobiVarType = gurobi.GRB.INTEGER 

145 elif isinstance(picosVar, BinaryVariable): 

146 gurobiVarType = gurobi.GRB.BINARY 

147 else: 

148 assert False, "Unexpected variable type." 

149 

150 # Retrieve bounds. 

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

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

153 lower, upper = picosVar.bound_dicts 

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

155 lowerBounds[i] = b 

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

157 upperBounds[i] = b 

158 

159 # Import variable. 

160 # Note that Gurobi allows importing the objective function coefficients 

161 # for the new variables here, but that is done later to streamline 

162 # updates to the objective. 

163 gurobiVars = self.int.addVars(dim, lb=lowerBounds, ub=upperBounds, 

164 vtype=gurobiVarType, name=picosVar.name) 

165 

166 # Map PICOS variable indices to Gurobi variables. 

167 for localIndex in range(dim): 

168 self._gurobiVar[picosVar.id_at(localIndex)] = gurobiVars[localIndex] 

169 

170 def _remove_variable(self, picosVar): 

171 gurobiVars = [self._gurobiVar.pop(picosVar.id_at(localIndex)) 

172 for localIndex in range(picosVar.dim)] 

173 self.int.remove(gurobiVars) 

174 

175 def _import_variable_values(self): 

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

177 if picosVar.valued: 

178 value = picosVar.internal_value 

179 

180 for localIndex in range(picosVar.dim): 

181 gurobiVar = self._gurobiVar[picosVar.id_at(localIndex)] 

182 gurobiVar.Start = value[localIndex] 

183 

184 def _reset_variable_values(self): 

185 import gurobipy as gurobi 

186 

187 for gurobiVar in self._gurobiVar.values(): 

188 gurobiVar.Start = gurobi.GRB.UNDEFINED 

189 

190 def _affinexp_pic2grb(self, picosExpression): 

191 import gurobipy as gurobi 

192 

193 for gurobiVars, coefficients, constant in picosExpression.sparse_rows( 

194 None, indexFunction=lambda picosVar, localIndex: 

195 self._gurobiVar[picosVar.id_at(localIndex)]): 

196 gurobiExpression = gurobi.LinExpr(coefficients, gurobiVars) 

197 gurobiExpression.addConstant(constant) 

198 yield gurobiExpression 

199 

200 def _scalar_affinexp_pic2grb(self, picosExpression): 

201 """Transform a scalar affine expression from PICOS to Gurobi. 

202 

203 :returns: A :class:`LinExpr <gurobipy.LinExpr>`. 

204 """ 

205 assert len(picosExpression) == 1 

206 return next(self._affinexp_pic2grb(picosExpression)) 

207 

208 def _quadexp_pic2grb(self, picosExpression): 

209 import gurobipy as gurobi 

210 

211 assert isinstance(picosExpression, QuadraticExpression) 

212 

213 # Import affine part of expression. 

214 gurobiExpression = gurobi.QuadExpr( 

215 self._scalar_affinexp_pic2grb(picosExpression.aff)) 

216 

217 # Import quadratic form. 

218 gurobiI, gurobiJ, gurobiV = [], [], [] 

219 for (picosVar1, picosVar2), picosCoefficients \ 

220 in picosExpression._sparse_quads.items(): 

221 for sparseIndex in range(len(picosCoefficients)): 

222 localVar1Index = picosCoefficients.I[sparseIndex] 

223 localVar2Index = picosCoefficients.J[sparseIndex] 

224 localCoefficient = picosCoefficients.V[sparseIndex] 

225 gurobiI.append(self._gurobiVar[picosVar1.id_at(localVar1Index)]) 

226 gurobiJ.append(self._gurobiVar[picosVar2.id_at(localVar2Index)]) 

227 gurobiV.append(localCoefficient) 

228 gurobiExpression.addTerms(gurobiV, gurobiI, gurobiJ) 

229 

230 return gurobiExpression 

231 

232 def _import_linear_constraint(self, picosConstraint): 

233 import gurobipy as gurobi 

234 

235 assert isinstance(picosConstraint, AffineConstraint) 

236 

237 # Retrieve sense. 

238 if picosConstraint.is_increasing(): 

239 gurobiSense = gurobi.GRB.LESS_EQUAL 

240 elif picosConstraint.is_decreasing(): 

241 gurobiSense = gurobi.GRB.GREATER_EQUAL 

242 elif picosConstraint.is_equality(): 

243 gurobiSense = gurobi.GRB.EQUAL 

244 else: 

245 assert False, "Unexpected constraint relation." 

246 

247 # Append scalar constraints. 

248 gurobiCons = [] 

249 for localConIndex, (gurobiLHS, gurobiRHS) in enumerate(zip( 

250 self._affinexp_pic2grb(picosConstraint.lhs), 

251 self._affinexp_pic2grb(picosConstraint.rhs))): 

252 if picosConstraint.name: 

253 gurobiName = "{}:{}".format(picosConstraint.name, localConIndex) 

254 else: 

255 gurobiName = "" 

256 

257 gurobiCons.append(self.int.addConstr( 

258 gurobiLHS, gurobiSense, gurobiRHS, gurobiName)) 

259 

260 return gurobiCons 

261 

262 def _import_quad_constraint(self, picosConstraint): 

263 import gurobipy as gurobi 

264 

265 assert isinstance(picosConstraint, ConvexQuadraticConstraint) 

266 

267 gurobiLHS = self._quadexp_pic2grb(picosConstraint.le0) 

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

269 if gurobiRHS: 

270 gurobiLHS.getLinExpr().addConstant(gurobiRHS) 

271 

272 return self.int.addQConstr( 

273 gurobiLHS, gurobi.GRB.LESS_EQUAL, gurobiRHS) 

274 

275 # TODO: Handle SOC → Quadratic via a reformulation. 

276 def _import_socone_constraint(self, picosConstraint): 

277 import gurobipy as gurobi 

278 

279 assert isinstance(picosConstraint, SOCConstraint) 

280 

281 picosLHS = picosConstraint.ne 

282 picosRHS = picosConstraint.ub 

283 picosLHSLen = len(picosLHS) 

284 

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

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

287 gurobiLHSVarsIndexed = self.int.addVars( 

288 picosLHSLen, lb=-gurobi.GRB.INFINITY, ub=gurobi.GRB.INFINITY) 

289 gurobiLHSVars = gurobiLHSVarsIndexed.values() 

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

291 

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

293 # variables with their slice of the PICOS left hand side expression. 

294 gurobiLHSSlices = dict() 

295 for dimension, slice in enumerate(self._affinexp_pic2grb(picosLHS)): 

296 gurobiLHSSlices[dimension] = slice 

297 gurobiLHSCons = self.int.addConstrs( 

298 (gurobiLHSSlices[dimension] - gurobiLHSVarsIndexed[dimension] == 0 

299 for dimension in range(picosLHSLen))).values() 

300 

301 # Add a constraint that identifies the right hand side Gurobi auxiliary 

302 # variable with the PICOS right hand side scalar expression. 

303 gurobiRHSExp = self._scalar_affinexp_pic2grb(picosRHS) 

304 gurobiRHSCon = self.int.addConstr( 

305 gurobiRHSVar - gurobiRHSExp, gurobi.GRB.EQUAL, 0) 

306 

307 # Add a quadratic constraint over the auxiliary variables that 

308 # represents the PICOS second order cone constraint itself. 

309 quadExpr = gurobi.QuadExpr() 

310 quadExpr.addTerms([1.0] * picosLHSLen, gurobiLHSVars, gurobiLHSVars) 

311 gurobiName = picosConstraint.name if picosConstraint.name else "" 

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

313 gurobiRHSVar * gurobiRHSVar, gurobiName) 

314 

315 gurobiMetaCon = self.GurobiSOCC( 

316 LHSVars=gurobiLHSVars, RHSVar=gurobiRHSVar, LHSCons=gurobiLHSCons, 

317 RHSCon=gurobiRHSCon, quadCon=gurobiQuadCon) 

318 

319 return gurobiMetaCon 

320 

321 # TODO: Handle RSOC → Quadratic via a reformulation. 

322 def _import_rscone_constraint(self, picosConstraint): 

323 import gurobipy as gurobi 

324 

325 assert isinstance(picosConstraint, RSOCConstraint) 

326 

327 picosLHS = picosConstraint.ne 

328 picosRHS1 = picosConstraint.ub1 

329 picosRHS2 = picosConstraint.ub2 

330 picosLHSLen = len(picosLHS) 

331 

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

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

334 gurobiLHSVarsIndexed = self.int.addVars( 

335 picosLHSLen, lb=-gurobi.GRB.INFINITY, ub=gurobi.GRB.INFINITY) 

336 gurobiLHSVars = gurobiLHSVarsIndexed.values() 

337 gurobiRHSVars = self.int.addVars( 

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

339 

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

341 # variables with their slice of the PICOS left hand side expression. 

342 gurobiLHSSlices = dict() 

343 for dimension, slice in enumerate(self._affinexp_pic2grb(picosLHS)): 

344 gurobiLHSSlices[dimension] = slice 

345 gurobiLHSCons = self.int.addConstrs( 

346 (gurobiLHSSlices[dimension] - gurobiLHSVarsIndexed[dimension] == 0 

347 for dimension in range(picosLHSLen))).values() 

348 

349 # Add two constraints that identify the right hand side Gurobi auxiliary 

350 # variables with the PICOS right hand side scalar expressions. 

351 gurobiRHSExps = \ 

352 self._scalar_affinexp_pic2grb(picosRHS1), \ 

353 self._scalar_affinexp_pic2grb(picosRHS2) 

354 gurobiRHSCons = self.int.addConstrs( 

355 (gurobiRHSVars[i] - gurobiRHSExps[i] == 0 for i in (0, 1))).values() 

356 

357 # Add a quadratic constraint over the auxiliary variables that 

358 # represents the PICOS second order cone constraint itself. 

359 quadExpr = gurobi.QuadExpr() 

360 quadExpr.addTerms([1.0] * picosLHSLen, gurobiLHSVars, gurobiLHSVars) 

361 gurobiName = picosConstraint.name if picosConstraint.name else "" 

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

363 gurobiRHSVars[0] * gurobiRHSVars[1], gurobiName) 

364 

365 gurobiMetaCon = self.GurobiRSOCC( 

366 LHSVars=gurobiLHSVars, RHSVars=gurobiRHSVars, LHSCons=gurobiLHSCons, 

367 RHSCons=gurobiRHSCons, quadCon=gurobiQuadCon) 

368 

369 return gurobiMetaCon 

370 

371 def _import_constraint(self, picosConstraint): 

372 # Import constraint based on type. 

373 if isinstance(picosConstraint, AffineConstraint): 

374 self._gurobiLinearConstraints[picosConstraint] = \ 

375 self._import_linear_constraint(picosConstraint) 

376 elif isinstance(picosConstraint, ConvexQuadraticConstraint): 

377 self._gurobiQuadConstraint[picosConstraint] = \ 

378 self._import_quad_constraint(picosConstraint) 

379 elif isinstance(picosConstraint, SOCConstraint): 

380 self._gurobiSOCC[picosConstraint] = \ 

381 self._import_socone_constraint(picosConstraint) 

382 elif isinstance(picosConstraint, RSOCConstraint): 

383 self._gurobiRSOCC[picosConstraint] = \ 

384 self._import_rscone_constraint(picosConstraint) 

385 else: 

386 assert isinstance(picosConstraint, DummyConstraint), \ 

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

388 picosConstraint.__class__.__name__) 

389 

390 def _remove_constraint(self, picosConstraint): 

391 if isinstance(picosConstraint, AffineConstraint): 

392 self.int.remove( 

393 self._gurobiLinearConstraints.pop(picosConstraint)) 

394 elif isinstance(picosConstraint, ConvexQuadraticConstraint): 

395 self.int.remove( 

396 self._gurobiQuadConstraint.pop(picosConstraint)) 

397 elif isinstance(picosConstraint, SOCConstraint): 

398 c = self._gurobiSOCC.pop(picosConstraint) 

399 self.int.remove(c.gurobiLHSCons + [c.gurobiRHSCon] 

400 + [c.gurobiQuadCon] + c.gurobiLHSVars + [c.gurobiRHSVar]) 

401 elif isinstance(picosConstraint, RSOCConstraint): 

402 c = self._gurobiRSOCC.pop(picosConstraint) 

403 self.int.remove(c.gurobiLHSCons + c.gurobiRHSCons 

404 + [c.gurobiQuadCon] + c.gurobiLHSVars + c.gurobiRHSVars) 

405 else: 

406 assert isinstance(picosConstraint, DummyConstraint), \ 

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

408 picosConstraint.__class__.__name__) 

409 

410 def _import_objective(self): 

411 import gurobipy as gurobi 

412 

413 picosSense, picosObjective = self.ext.no 

414 

415 # Retrieve objective sense. 

416 if picosSense == "min": 

417 gurobiSense = gurobi.GRB.MINIMIZE 

418 else: 

419 assert picosSense == "max" 

420 gurobiSense = gurobi.GRB.MAXIMIZE 

421 

422 # Retrieve objective function. 

423 if isinstance(picosObjective, AffineExpression): 

424 gurobiObjective = self._scalar_affinexp_pic2grb(picosObjective) 

425 else: 

426 assert isinstance(picosObjective, QuadraticExpression) 

427 gurobiObjective = self._quadexp_pic2grb(picosObjective) 

428 

429 self.int.setObjective(gurobiObjective, gurobiSense) 

430 

431 def _import_problem(self): 

432 import gurobipy as gurobi 

433 

434 # Create a problem instance. 

435 if self._license_warnings: 

436 self.int = gurobi.Model() 

437 else: 

438 with self._enforced_verbosity(): 

439 self.int = gurobi.Model() 

440 

441 # Import variables. 

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

443 self._import_variable(variable) 

444 

445 # Import constraints. 

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

447 self._import_constraint(constraint) 

448 

449 # Set objective. 

450 self._import_objective() 

451 

452 def _update_problem(self): 

453 for oldConstraint in self._removed_constraints(): 

454 self._remove_constraint(oldConstraint) 

455 

456 for oldVariable in self._removed_variables(): 

457 self._remove_variable(oldVariable) 

458 

459 for newVariable in self._new_variables(): 

460 self._import_variable(newVariable) 

461 

462 for newConstraint in self._new_constraints(): 

463 self._import_constraint(newConstraint) 

464 

465 if self._objective_has_changed(): 

466 self._import_objective() 

467 

468 def _solve(self): 

469 import gurobipy as gurobi 

470 

471 # Reset options. 

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

473 self.int.Params.OutputFlag = 0 

474 self.int.resetParams() 

475 

476 # verbosity 

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

478 

479 # abs_prim_fsb_tol 

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

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

482 

483 # abs_dual_fsb_tol 

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

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

486 

487 # rel_ipm_opt_tol 

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

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

490 

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

492 self.int.Params.BarQCPConvTol = \ 

493 0.01 * self.ext.options.rel_ipm_opt_tol 

494 

495 # abs_bnb_opt_tol 

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

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

498 

499 # rel_bnb_opt_tol 

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

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

502 

503 # integrality_tol 

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

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

506 

507 # markowitz_tol 

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

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

510 

511 # max_iterations 

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

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

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

515 

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

517 

518 # lp_node_method 

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

520 value = self.ext.options.lp_node_method 

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

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

523 

524 # lp_root_method 

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

526 value = self.ext.options.lp_root_method 

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

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

529 

530 # timelimit 

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

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

533 

534 # max_fsb_nodes 

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

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

537 

538 # hotstart 

539 if self.ext.options.hotstart: 

540 self._import_variable_values() 

541 else: 

542 self._reset_variable_values() 

543 

544 # Handle Gurobi-specific options. 

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

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

547 self._handle_bad_solver_specific_option_key(key) 

548 

549 try: 

550 self.int.setParam(key, value) 

551 except TypeError as error: 

552 self._handle_bad_solver_specific_option_value(key, value, error) 

553 

554 # Handle unsupported options. 

555 self._handle_unsupported_option("treememory") 

556 

557 # Extend functionality for continuous problems. 

558 if self.ext.is_continuous(): 

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

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

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

562 

563 # Allow nonconvex quadratic objectives. 

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

565 if self.ext.footprint.nonconvex_quadratic_objective: 

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

567 

568 # Attempt to solve the problem. 

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

570 try: 

571 self.int.optimize() 

572 except gurobi.GurobiError as error: 

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

574 self._handle_continuous_nonconvex_error(error) 

575 else: 

576 raise 

577 

578 # Retrieve primals. 

579 primals = {} 

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

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

582 try: 

583 value = [] 

584 for localIndex in range(picosVar.dim): 

585 gurobiVar = self._gurobiVar[picosVar.id_at(localIndex)] 

586 scalarValue = gurobiVar.getAttr(gurobi.GRB.Attr.X) 

587 value.append(scalarValue) 

588 except AttributeError: 

589 primals[picosVar] = None 

590 else: 

591 primals[picosVar] = value 

592 

593 # Retrieve duals. 

594 duals = {} 

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

596 Pi = gurobi.GRB.Attr.Pi 

597 

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

599 if isinstance(picosCon, DummyConstraint): 

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

601 continue 

602 

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

604 # printing a newline to console when it raises an 

605 # AttributeError and OutputFlag is enabled. This is a 

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

607 oldOutput = self.int.Params.OutputFlag 

608 self.int.Params.OutputFlag = 0 

609 

610 try: 

611 if isinstance(picosCon, AffineConstraint): 

612 gurobiCons = self._gurobiLinearConstraints[picosCon] 

613 gurobiDuals = [] 

614 for gurobiCon in gurobiCons: 

615 gurobiDuals.append(gurobiCon.getAttr(Pi)) 

616 picosDual = cvxopt.matrix(gurobiDuals, picosCon.size) 

617 

618 # Flip sign based on constraint relation. 

619 if not picosCon.is_increasing(): 

620 picosDual = -picosDual 

621 elif isinstance(picosCon, SOCConstraint): 

622 gurobiMetaCon = self._gurobiSOCC[picosCon] 

623 lb = gurobiMetaCon.RHSCon.getAttr(Pi) 

624 z = [-constraint.getAttr(Pi) 

625 for constraint in gurobiMetaCon.LHSCons] 

626 picosDual = cvxopt.matrix([lb] + z) 

627 elif isinstance(picosCon, RSOCConstraint): 

628 gurobiMetaCon = self._gurobiRSOCC[picosCon] 

629 ab = [constraint.getAttr(Pi) 

630 for constraint in gurobiMetaCon.RHSCons] 

631 z = [-constraint.getAttr(Pi) 

632 for constraint in gurobiMetaCon.LHSCons] 

633 picosDual = cvxopt.matrix(ab + z) 

634 elif isinstance(picosCon, ConvexQuadraticConstraint): 

635 picosDual = None 

636 else: 

637 assert isinstance(picosCon, DummyConstraint), \ 

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

639 picosCon.__class__.__name__) 

640 

641 # Flip sign based on objective sense. 

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

643 picosDual = -picosDual 

644 except AttributeError: 

645 duals[picosCon] = None 

646 else: 

647 duals[picosCon] = picosDual 

648 

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

650 if oldOutput != 0: 

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

652 self.int.Params.OutputFlag = oldOutput 

653 

654 # Retrieve objective value. 

655 try: 

656 value = self.int.getAttr(gurobi.GRB.Attr.ObjVal) 

657 except AttributeError: 

658 value = None 

659 

660 # Retrieve solution status. 

661 statusCode = self.int.getAttr(gurobi.GRB.Attr.Status) 

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

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

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

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

666 primalStatus = SS_OPTIMAL 

667 dualStatus = SS_OPTIMAL 

668 problemStatus = PS_FEASIBLE 

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

670 primalStatus = SS_INFEASIBLE 

671 dualStatus = SS_UNKNOWN 

672 problemStatus = PS_INFEASIBLE 

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

674 primalStatus = SS_UNKNOWN 

675 dualStatus = SS_UNKNOWN 

676 problemStatus = PS_INF_OR_UNB 

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

678 primalStatus = SS_UNKNOWN 

679 dualStatus = SS_INFEASIBLE 

680 problemStatus = PS_UNBOUNDED 

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

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

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

684 # available." 

685 primalStatus = SS_PREMATURE 

686 dualStatus = SS_PREMATURE 

687 problemStatus = PS_UNKNOWN 

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

689 primalStatus = SS_PREMATURE 

690 dualStatus = SS_PREMATURE 

691 problemStatus = PS_UNKNOWN 

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

693 primalStatus = SS_PREMATURE 

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

695 problemStatus = PS_UNKNOWN 

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

697 primalStatus = SS_PREMATURE 

698 dualStatus = SS_PREMATURE 

699 problemStatus = PS_UNKNOWN 

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

701 primalStatus = SS_PREMATURE 

702 dualStatus = SS_PREMATURE 

703 problemStatus = PS_UNKNOWN 

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

705 primalStatus = SS_PREMATURE 

706 dualStatus = SS_PREMATURE 

707 problemStatus = PS_UNKNOWN 

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

709 primalStatus = SS_UNKNOWN 

710 dualStatus = SS_UNKNOWN 

711 problemStatus = PS_UNSTABLE 

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

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

714 # is available." 

715 primalStatus = SS_FEASIBLE 

716 dualStatus = SS_FEASIBLE 

717 problemStatus = PS_FEASIBLE 

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

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

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

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

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

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

724 primalStatus = SS_FEASIBLE 

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

726 problemStatus = PS_FEASIBLE 

727 else: 

728 primalStatus = SS_UNKNOWN 

729 dualStatus = SS_UNKNOWN 

730 problemStatus = PS_UNKNOWN 

731 

732 return self._make_solution( 

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

734 

735 

736# -------------------------------------- 

737__all__ = api_end(_API_START, globals())