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.nonconvex_quadratic_objective and footprint.continuous: 

67 if explain: 

68 return (False, 

69 "Continuous problems with nonconvex quadratic objectives.") 

70 else: 

71 return False 

72 

73 if footprint not in cls.SUPPORTED: 

74 if explain: 

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

76 else: 

77 return False 

78 

79 return (True, None) if explain else True 

80 

81 @classmethod 

82 def default_penalty(cls): 

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

84 return 0.0 # Commercial solver. 

85 

86 @classmethod 

87 def test_availability(cls): 

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

89 cls.check_import("gurobipy") 

90 

91 @classmethod 

92 def names(cls): 

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

94 return "gurobi", "Gurobi", "Gurobi Optimizer" 

95 

96 @classmethod 

97 def is_free(cls): 

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

99 return False 

100 

101 GurobiSOCC = namedtuple("GurobiSOCC", 

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

103 

104 GurobiRSOCC = namedtuple("GurobiRSOCC", 

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

106 

107 def __init__(self, problem): 

108 """Initialize a Gurobi solver interface. 

109 

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

111 """ 

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

113 

114 self._gurobiVar = dict() 

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

116 

117 self._gurobiLinearConstraints = dict() 

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

119 Gurobi (scalar) linear constraints.""" 

120 

121 self._gurobiQuadConstraint = dict() 

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

123 

124 self._gurobiSOCC = dict() 

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

126 representation involving auxiliary variables and constraints.""" 

127 

128 self._gurobiRSOCC = dict() 

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

130 representation involving auxiliary variables and constraints.""" 

131 

132 def reset_problem(self): 

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

134 self.int = None 

135 

136 self._gurobiVar.clear() 

137 self._gurobiLinearConstraints.clear() 

138 self._gurobiQuadConstraint.clear() 

139 self._gurobiSOCC.clear() 

140 self._gurobiRSOCC.clear() 

141 

142 def _import_variable(self, picosVar): 

143 import gurobipy as gurobi 

144 

145 dim = picosVar.dim 

146 

147 # Retrieve types. 

148 if isinstance(picosVar, CONTINUOUS_VARTYPES): 

149 gurobiVarType = gurobi.GRB.CONTINUOUS 

150 elif isinstance(picosVar, IntegerVariable): 

151 gurobiVarType = gurobi.GRB.INTEGER 

152 elif isinstance(picosVar, BinaryVariable): 

153 gurobiVarType = gurobi.GRB.BINARY 

154 else: 

155 assert False, "Unexpected variable type." 

156 

157 # Retrieve bounds. 

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

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

160 lower, upper = picosVar.bound_dicts 

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

162 lowerBounds[i] = b 

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

164 upperBounds[i] = b 

165 

166 # Import variable. 

167 # Note that Gurobi allows importing the objective function coefficients 

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

169 # updates to the objective. 

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

171 vtype=gurobiVarType, name=picosVar.name) 

172 

173 # Map PICOS variable indices to Gurobi variables. 

174 for localIndex in range(dim): 

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

176 

177 def _remove_variable(self, picosVar): 

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

179 for localIndex in range(picosVar.dim)] 

180 self.int.remove(gurobiVars) 

181 

182 def _import_variable_values(self): 

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

184 if picosVar.valued: 

185 value = picosVar.internal_value 

186 

187 for localIndex in range(picosVar.dim): 

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

189 gurobiVar.Start = value[localIndex] 

190 

191 def _reset_variable_values(self): 

192 import gurobipy as gurobi 

193 

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

195 gurobiVar.Start = gurobi.GRB.UNDEFINED 

196 

197 def _affinexp_pic2grb(self, picosExpression): 

198 import gurobipy as gurobi 

199 

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

201 None, indexFunction=lambda picosVar, localIndex: 

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

203 gurobiExpression = gurobi.LinExpr(coefficients, gurobiVars) 

204 gurobiExpression.addConstant(constant) 

205 yield gurobiExpression 

206 

207 def _scalar_affinexp_pic2grb(self, picosExpression): 

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

209 

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

211 """ 

212 assert len(picosExpression) == 1 

213 return next(self._affinexp_pic2grb(picosExpression)) 

214 

215 def _quadexp_pic2grb(self, picosExpression): 

216 import gurobipy as gurobi 

217 

218 assert isinstance(picosExpression, QuadraticExpression) 

219 

220 # Import affine part of expression. 

221 gurobiExpression = gurobi.QuadExpr( 

222 self._scalar_affinexp_pic2grb(picosExpression.aff)) 

223 

224 # Import quadratic form. 

225 gurobiI, gurobiJ, gurobiV = [], [], [] 

226 for (picosVar1, picosVar2), picosCoefficients \ 

227 in picosExpression._sparse_quads.items(): 

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

229 localVar1Index = picosCoefficients.I[sparseIndex] 

230 localVar2Index = picosCoefficients.J[sparseIndex] 

231 localCoefficient = picosCoefficients.V[sparseIndex] 

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

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

234 gurobiV.append(localCoefficient) 

235 gurobiExpression.addTerms(gurobiV, gurobiI, gurobiJ) 

236 

237 return gurobiExpression 

238 

239 def _import_linear_constraint(self, picosConstraint): 

240 import gurobipy as gurobi 

241 

242 assert isinstance(picosConstraint, AffineConstraint) 

243 

244 # Retrieve sense. 

245 if picosConstraint.is_increasing(): 

246 gurobiSense = gurobi.GRB.LESS_EQUAL 

247 elif picosConstraint.is_decreasing(): 

248 gurobiSense = gurobi.GRB.GREATER_EQUAL 

249 elif picosConstraint.is_equality(): 

250 gurobiSense = gurobi.GRB.EQUAL 

251 else: 

252 assert False, "Unexpected constraint relation." 

253 

254 # Append scalar constraints. 

255 gurobiCons = [] 

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

257 self._affinexp_pic2grb(picosConstraint.lhs), 

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

259 if picosConstraint.name: 

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

261 else: 

262 gurobiName = "" 

263 

264 gurobiCons.append(self.int.addConstr( 

265 gurobiLHS, gurobiSense, gurobiRHS, gurobiName)) 

266 

267 return gurobiCons 

268 

269 def _import_quad_constraint(self, picosConstraint): 

270 import gurobipy as gurobi 

271 

272 assert isinstance(picosConstraint, ConvexQuadraticConstraint) 

273 

274 gurobiLHS = self._quadexp_pic2grb(picosConstraint.le0) 

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

276 if gurobiRHS: 

277 gurobiLHS.getLinExpr().addConstant(gurobiRHS) 

278 

279 return self.int.addQConstr( 

280 gurobiLHS, gurobi.GRB.LESS_EQUAL, gurobiRHS) 

281 

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

283 def _import_socone_constraint(self, picosConstraint): 

284 import gurobipy as gurobi 

285 

286 assert isinstance(picosConstraint, SOCConstraint) 

287 

288 picosLHS = picosConstraint.ne 

289 picosRHS = picosConstraint.ub 

290 picosLHSLen = len(picosLHS) 

291 

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

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

294 gurobiLHSVarsIndexed = self.int.addVars( 

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

296 gurobiLHSVars = gurobiLHSVarsIndexed.values() 

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

298 

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

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

301 gurobiLHSSlices = dict() 

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

303 gurobiLHSSlices[dimension] = slice 

304 gurobiLHSCons = self.int.addConstrs( 

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

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

307 

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

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

310 gurobiRHSExp = self._scalar_affinexp_pic2grb(picosRHS) 

311 gurobiRHSCon = self.int.addConstr( 

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

313 

314 # Add a quadratic constraint over the auxiliary variables that 

315 # represents the PICOS second order cone constraint itself. 

316 quadExpr = gurobi.QuadExpr() 

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

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

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

320 gurobiRHSVar * gurobiRHSVar, gurobiName) 

321 

322 gurobiMetaCon = self.GurobiSOCC( 

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

324 RHSCon=gurobiRHSCon, quadCon=gurobiQuadCon) 

325 

326 return gurobiMetaCon 

327 

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

329 def _import_rscone_constraint(self, picosConstraint): 

330 import gurobipy as gurobi 

331 

332 assert isinstance(picosConstraint, RSOCConstraint) 

333 

334 picosLHS = picosConstraint.ne 

335 picosRHS1 = picosConstraint.ub1 

336 picosRHS2 = picosConstraint.ub2 

337 picosLHSLen = len(picosLHS) 

338 

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

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

341 gurobiLHSVarsIndexed = self.int.addVars( 

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

343 gurobiLHSVars = gurobiLHSVarsIndexed.values() 

344 gurobiRHSVars = self.int.addVars( 

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

346 

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

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

349 gurobiLHSSlices = dict() 

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

351 gurobiLHSSlices[dimension] = slice 

352 gurobiLHSCons = self.int.addConstrs( 

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

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

355 

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

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

358 gurobiRHSExps = \ 

359 self._scalar_affinexp_pic2grb(picosRHS1), \ 

360 self._scalar_affinexp_pic2grb(picosRHS2) 

361 gurobiRHSCons = self.int.addConstrs( 

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

363 

364 # Add a quadratic constraint over the auxiliary variables that 

365 # represents the PICOS second order cone constraint itself. 

366 quadExpr = gurobi.QuadExpr() 

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

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

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

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

371 

372 gurobiMetaCon = self.GurobiRSOCC( 

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

374 RHSCons=gurobiRHSCons, quadCon=gurobiQuadCon) 

375 

376 return gurobiMetaCon 

377 

378 def _import_constraint(self, picosConstraint): 

379 # Import constraint based on type. 

380 if isinstance(picosConstraint, AffineConstraint): 

381 self._gurobiLinearConstraints[picosConstraint] = \ 

382 self._import_linear_constraint(picosConstraint) 

383 elif isinstance(picosConstraint, ConvexQuadraticConstraint): 

384 self._gurobiQuadConstraint[picosConstraint] = \ 

385 self._import_quad_constraint(picosConstraint) 

386 elif isinstance(picosConstraint, SOCConstraint): 

387 self._gurobiSOCC[picosConstraint] = \ 

388 self._import_socone_constraint(picosConstraint) 

389 elif isinstance(picosConstraint, RSOCConstraint): 

390 self._gurobiRSOCC[picosConstraint] = \ 

391 self._import_rscone_constraint(picosConstraint) 

392 else: 

393 assert isinstance(picosConstraint, DummyConstraint), \ 

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

395 picosConstraint.__class__.__name__) 

396 

397 def _remove_constraint(self, picosConstraint): 

398 if isinstance(picosConstraint, AffineConstraint): 

399 self.int.remove( 

400 self._gurobiLinearConstraints.pop(picosConstraint)) 

401 elif isinstance(picosConstraint, ConvexQuadraticConstraint): 

402 self.int.remove( 

403 self._gurobiQuadConstraint.pop(picosConstraint)) 

404 elif isinstance(picosConstraint, SOCConstraint): 

405 c = self._gurobiSOCC.pop(picosConstraint) 

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

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

408 elif isinstance(picosConstraint, RSOCConstraint): 

409 c = self._gurobiRSOCC.pop(picosConstraint) 

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

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

412 else: 

413 assert isinstance(picosConstraint, DummyConstraint), \ 

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

415 picosConstraint.__class__.__name__) 

416 

417 def _import_objective(self): 

418 import gurobipy as gurobi 

419 

420 picosSense, picosObjective = self.ext.no 

421 

422 # Retrieve objective sense. 

423 if picosSense == "min": 

424 gurobiSense = gurobi.GRB.MINIMIZE 

425 else: 

426 assert picosSense == "max" 

427 gurobiSense = gurobi.GRB.MAXIMIZE 

428 

429 # Retrieve objective function. 

430 if isinstance(picosObjective, AffineExpression): 

431 gurobiObjective = self._scalar_affinexp_pic2grb(picosObjective) 

432 else: 

433 assert isinstance(picosObjective, QuadraticExpression) 

434 gurobiObjective = self._quadexp_pic2grb(picosObjective) 

435 

436 self.int.setObjective(gurobiObjective, gurobiSense) 

437 

438 def _import_problem(self): 

439 import gurobipy as gurobi 

440 

441 # Create a problem instance. 

442 if self._license_warnings: 

443 self.int = gurobi.Model() 

444 else: 

445 with self._enforced_verbosity(): 

446 self.int = gurobi.Model() 

447 

448 # Import variables. 

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

450 self._import_variable(variable) 

451 

452 # Import constraints. 

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

454 self._import_constraint(constraint) 

455 

456 # Set objective. 

457 self._import_objective() 

458 

459 def _update_problem(self): 

460 for oldConstraint in self._removed_constraints(): 

461 self._remove_constraint(oldConstraint) 

462 

463 for oldVariable in self._removed_variables(): 

464 self._remove_variable(oldVariable) 

465 

466 for newVariable in self._new_variables(): 

467 self._import_variable(newVariable) 

468 

469 for newConstraint in self._new_constraints(): 

470 self._import_constraint(newConstraint) 

471 

472 if self._objective_has_changed(): 

473 self._import_objective() 

474 

475 def _solve(self): 

476 import gurobipy as gurobi 

477 

478 # Reset options. 

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

480 self.int.Params.OutputFlag = 0 

481 self.int.resetParams() 

482 

483 # verbosity 

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

485 

486 # abs_prim_fsb_tol 

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

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

489 

490 # abs_dual_fsb_tol 

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

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

493 

494 # rel_ipm_opt_tol 

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

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

497 

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

499 self.int.Params.BarQCPConvTol = \ 

500 0.01 * self.ext.options.rel_ipm_opt_tol 

501 

502 # abs_bnb_opt_tol 

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

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

505 

506 # rel_bnb_opt_tol 

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

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

509 

510 # integrality_tol 

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

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

513 

514 # markowitz_tol 

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

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

517 

518 # max_iterations 

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

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

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

522 

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

524 

525 # lp_node_method 

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

527 value = self.ext.options.lp_node_method 

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

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

530 

531 # lp_root_method 

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

533 value = self.ext.options.lp_root_method 

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

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

536 

537 # timelimit 

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

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

540 

541 # max_fsb_nodes 

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

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

544 

545 # hotstart 

546 if self.ext.options.hotstart: 

547 self._import_variable_values() 

548 else: 

549 self._reset_variable_values() 

550 

551 # Handle Gurobi-specific options. 

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

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

554 self._handle_bad_solver_specific_option_key(key) 

555 

556 try: 

557 self.int.setParam(key, value) 

558 except TypeError as error: 

559 self._handle_bad_solver_specific_option_value(key, value, error) 

560 

561 # Handle unsupported options. 

562 self._handle_unsupported_option("treememory") 

563 

564 # Compute duals also for QPs and QCPs. 

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

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

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())