Coverage for picos/solvers/solver_mskfsn.py: 83.62%

403 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +0000

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

2# Copyright (C) 2017-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:`MOSEKFusionSolver`.""" 

20 

21import sys 

22 

23import cvxopt 

24 

25from ..apidoc import api_end, api_start 

26from ..constraints import (AffineConstraint, DummyConstraint, LMIConstraint, 

27 RSOCConstraint, SOCConstraint) 

28from ..expressions import AffineExpression, BinaryVariable, IntegerVariable 

29from ..modeling.footprint import Specification 

30from ..modeling.solution import (PS_FEASIBLE, PS_ILLPOSED, PS_INF_OR_UNB, 

31 PS_INFEASIBLE, PS_UNBOUNDED, PS_UNKNOWN, 

32 SS_EMPTY, SS_FEASIBLE, SS_INFEASIBLE, 

33 SS_OPTIMAL, SS_UNKNOWN) 

34from .solver import ProblemUpdateError, Solver 

35 

36_API_START = api_start(globals()) 

37# ------------------------------- 

38 

39 

40class MOSEKFusionSolver(Solver): 

41 """Interface to the MOSEK solver via its high level Fusion API. 

42 

43 Supports both MOSEK 8 and 9. 

44 

45 The Fusion API is currently much slower than MOSEK's low level Python API. 

46 If this changes in the future, the Fusion API would be the prefered 

47 interface. 

48 """ 

49 

50 SUPPORTED = Specification( 

51 objectives=[ 

52 AffineExpression], 

53 constraints=[ 

54 DummyConstraint, 

55 AffineConstraint, 

56 SOCConstraint, 

57 RSOCConstraint, 

58 LMIConstraint]) 

59 

60 @classmethod 

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

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

63 result = Solver.supports(footprint, explain) 

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

65 return result 

66 

67 # No integer SDPs. 

68 if footprint.integer and ("con", LMIConstraint) in footprint: 

69 if explain: 

70 return False, "Integer Semidefinite Programs." 

71 else: 

72 return False 

73 

74 if footprint not in cls.SUPPORTED: 

75 if explain: 

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

77 else: 

78 return False 

79 

80 return (True, None) if explain else True 

81 

82 @classmethod 

83 def default_penalty(cls): 

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

85 return 0.5 # Commercial solver with slower interface. 

86 

87 @classmethod 

88 def test_availability(cls): 

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

90 cls.check_import("mosek.fusion") 

91 

92 @classmethod 

93 def names(cls): 

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

95 return "mskfsn", "MOSEK", "MOSEK", "Fusion API" 

96 

97 @classmethod 

98 def is_free(cls): 

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

100 return False 

101 

102 def __init__(self, problem): 

103 """Initialize a MOSEK (Fusion) solver interface. 

104 

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

106 """ 

107 super(MOSEKFusionSolver, self).__init__(problem) 

108 

109 # Maps PICOS variables to MOSEK variables and vice versa. 

110 self.knownVariables = {} 

111 

112 # Maps PICOS constraints to MOSEK constraints and vice versa. 

113 self.knownConstraints = {} 

114 

115 def __del__(self): 

116 if self.int is not None: 

117 self.int.dispose() 

118 

119 def reset_problem(self): 

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

121 if self.int is not None: 

122 self.int.dispose() 

123 self.int = None 

124 self.knownVariables.clear() 

125 self.knownConstraints.clear() 

126 

127 @classmethod 

128 def _get_major_version(cls): 

129 if not hasattr(cls, "mosekVersion"): 

130 import mosek 

131 cls.mosekVersion = mosek.Env.getversion() 

132 

133 return cls.mosekVersion[0] 

134 

135 ver = property(lambda self: self.__class__._get_major_version()) 

136 """The major version of the available MOSEK library.""" 

137 

138 @classmethod 

139 def _mosek_sparse_triple(cls, I, J, V): 

140 """Transform a sparse triple (e.g. from CVXOPT) for use with MOSEK.""" 

141 if cls._get_major_version() >= 9: 

142 IJV = list(IJV for IJV in zip(I, J, V) if IJV[2] != 0) 

143 I, J, V = (list(X) for X in zip(*IJV)) if IJV else ([], [], []) 

144 else: 

145 I = list(I) if not isinstance(I, list) else I 

146 J = list(J) if not isinstance(J, list) else J 

147 V = list(V) if not isinstance(V, list) else V 

148 

149 return I, J, V 

150 

151 @classmethod 

152 def _matrix_cvx2msk(cls, cvxoptMatrix): 

153 """Transform a CVXOPT (sparse) matrix into a MOSEK (sparse) matrix.""" 

154 import mosek.fusion as msk 

155 

156 M = cvxoptMatrix 

157 n, m = M.size 

158 

159 if type(M) is cvxopt.spmatrix: 

160 return msk.Matrix.sparse( 

161 n, m, *cls._mosek_sparse_triple(M.I, M.J, M.V)) 

162 elif type(M) is cvxopt.matrix: 

163 return msk.Matrix.dense(n, m, list(M.T)) 

164 else: 

165 raise ValueError("Argument must be a CVXOPT matrix.") 

166 

167 @classmethod 

168 def _mosek_vstack(cls, *expressions): 

169 """Vertically stack MOSEK expressions. 

170 

171 This is a wrapper around MOSEK's :func:`vstack 

172 <mosek.fusion.Expr.vstack>` function that silences a FutureWarning. 

173 """ 

174 import mosek.fusion as msk 

175 

176 if cls._get_major_version() >= 9: 

177 return msk.Expr.vstack(*expressions) 

178 else: 

179 import warnings 

180 with warnings.catch_warnings(): 

181 warnings.simplefilter("ignore", FutureWarning) 

182 return msk.Expr.vstack(*expressions) 

183 

184 def _affinexp_pic2msk(self, picosExpression): 

185 """Transform an affine expression from PICOS to MOSEK. 

186 

187 Requries all contained variables to be known to MOSEK. 

188 """ 

189 import mosek.fusion as msk 

190 

191 assert isinstance(picosExpression, AffineExpression) 

192 

193 vectorShape = [len(picosExpression), 1] 

194 targetShape = list(picosExpression.size) 

195 

196 if self.ver < 9: 

197 vectorShape = msk.Set.make(vectorShape) 

198 targetShape = msk.Set.make(targetShape) 

199 

200 # Convert linear part of expression. 

201 firstSummand = True 

202 for picosVar, factor in picosExpression._linear_coefs.items(): 

203 mosekVar = self.knownVariables[picosVar] 

204 

205 summand = msk.Expr.mul(self._matrix_cvx2msk(factor), mosekVar) 

206 if firstSummand: 

207 mosekExpression = summand 

208 firstSummand = False 

209 else: 

210 mosekExpression = msk.Expr.add(mosekExpression, summand) 

211 

212 # Convert constant term of expression. 

213 if picosExpression.constant is not None: 

214 mosekConstant = msk.Expr.constTerm( 

215 self._matrix_cvx2msk(picosExpression._constant_coef)) 

216 

217 if firstSummand: 

218 mosekExpression = mosekConstant 

219 else: 

220 mosekExpression = msk.Expr.add(mosekExpression, mosekConstant) 

221 elif firstSummand: 

222 mosekExpression = msk.Expr.zeros(vectorShape) 

223 

224 # Restore the expression's original shape. 

225 # NOTE: Transposition due to differing major orders. 

226 mosekExpression = msk.Expr.reshape( 

227 msk.Expr.transpose(mosekExpression), targetShape) 

228 

229 if self._debug(): 

230 self._debug( 

231 "Affine expression converted: {} → {}".format( 

232 repr(picosExpression), mosekExpression.toString())) 

233 

234 return mosekExpression 

235 

236 @classmethod 

237 def _bounds_pic2msk(cls, picosVar, fixMOSEK9=False): 

238 """Transform PICOS variable bounds to MOSEK matrices or scalars. 

239 

240 Scalars are returned in the case of homogenous bounds. 

241 """ 

242 import mosek.fusion as msk 

243 

244 dim = picosVar.dim 

245 lower, upper = picosVar.bound_dicts 

246 

247 if fixMOSEK9: 

248 LV, LI, LJ = [-1e20]*dim, list(range(dim)), [0]*dim 

249 UV, UI, UJ = [+1e20]*dim, list(range(dim)), [0]*dim 

250 

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

252 LV[i] = b 

253 

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

255 UV[i] = b 

256 else: 

257 LV, LI = [], [] 

258 UV, UI = [], [] 

259 

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

261 LI.append(i) 

262 LV.append(b) 

263 

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

265 UI.append(i) 

266 UV.append(b) 

267 

268 LJ = [0]*len(LV) 

269 UJ = [0]*len(UV) 

270 

271 mosekBounds = [None, None] 

272 for side, I, J, V in ((0, LI, LJ, LV), (1, UI, UJ, UV)): 

273 if len(V) == dim and len(set(V)) == 1: 

274 mosekBounds[side] = V[0] 

275 elif V: 

276 mosekBounds[side] = msk.Matrix.sparse(dim, 1, I, J, V) 

277 

278 return mosekBounds 

279 

280 def _import_variable(self, picosVar): 

281 import mosek.fusion as msk 

282 

283 shape = [picosVar.dim, 1] 

284 

285 # Import variable bounds. 

286 if not isinstance(picosVar, BinaryVariable): 

287 # Retrieve lower and upper bounds. 

288 lower, upper = self._bounds_pic2msk(picosVar) 

289 

290 # Convert bounds to a domain. 

291 if lower is None and upper is None: 

292 domain = msk.Domain.unbounded() 

293 elif lower is not None and upper is None: 

294 domain = msk.Domain.greaterThan(lower) 

295 elif lower is None and upper is not None: 

296 domain = msk.Domain.lessThan(upper) 

297 elif lower is not None and upper is not None: 

298 if lower == upper: 

299 domain = msk.Domain.equalsTo(lower) 

300 elif self.ver >= 9: 

301 # HACK: MOSEK 9 does not accept sparse (partial) range 

302 # domains anymore. The workaround triggers a MOSEK 

303 # warning, but there is no other way to pass such 

304 # variable bounds directly. 

305 if isinstance(lower, msk.Matrix) \ 

306 or isinstance(upper, msk.Matrix): 

307 lower, upper = self._bounds_pic2msk(picosVar, True) 

308 if isinstance(lower, msk.Matrix): 

309 lower = lower.getDataAsArray() 

310 if isinstance(upper, msk.Matrix): 

311 upper = upper.getDataAsArray() 

312 

313 domain = msk.Domain.inRange(lower, upper, shape) 

314 else: 

315 domain = msk.Domain.inRange(lower, upper) 

316 

317 # Refine the domain with the variable's type. 

318 if isinstance(picosVar, BinaryVariable): 

319 domain = msk.Domain.binary() 

320 elif isinstance(picosVar, IntegerVariable): 

321 domain = msk.Domain.integral(domain) 

322 

323 # Create the MOSEK variable. 

324 mosekVar = self.int.variable(picosVar.name, shape, domain) 

325 

326 # Map the PICOS variable to the MOSEK variable and vice versa. 

327 self.knownVariables[picosVar] = mosekVar 

328 self.knownVariables[mosekVar] = picosVar 

329 

330 if self._debug(): 

331 self._debug("Variable imported: {} → {}" 

332 .format(picosVar, " ".join(mosekVar.toString().split()))) 

333 

334 # TODO: This needs a test. 

335 def _import_variable_values(self, integralOnly=False): 

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

337 if integralOnly and not isinstance( 

338 picosVar, (BinaryVariable, IntegerVariable)): 

339 continue 

340 

341 if picosVar.valued: 

342 value = picosVar.internal_value 

343 

344 if isinstance(value, cvxopt.spmatrix): 

345 value = cvxopt.matrix(value) 

346 

347 self.knownVariables[picosVar].setLevel(list(value)) 

348 

349 def _import_linear_constraint(self, picosCon): 

350 import mosek.fusion as msk 

351 

352 assert isinstance(picosCon, AffineConstraint) 

353 

354 # Separate constraint into a linear function and a constant. 

355 linear, bound = picosCon.bounded_linear_form() 

356 

357 # Rewrite constraint in MOSEK types: The linear function is represented 

358 # as a MOSEK expression while the constant term becomes a MOSEK domain. 

359 linear = self._affinexp_pic2msk(linear[:]) 

360 bound = self._matrix_cvx2msk(bound._constant_coef) 

361 

362 if picosCon.is_increasing(): 

363 domain = msk.Domain.lessThan(bound) 

364 elif picosCon.is_decreasing(): 

365 domain = msk.Domain.greaterThan(bound) 

366 elif picosCon.is_equality(): 

367 domain = msk.Domain.equalsTo(bound) 

368 else: 

369 assert False, "Unexpected constraint relation." 

370 

371 # Import the constraint. 

372 if picosCon.name is None: 

373 return self.int.constraint(linear, domain) 

374 else: 

375 return self.int.constraint(picosCon.name, linear, domain) 

376 

377 def _import_socone_constraint(self, picosCon): 

378 import mosek.fusion as msk 

379 

380 assert isinstance(picosCon, SOCConstraint) 

381 

382 coneElement = self._mosek_vstack( 

383 msk.Expr.flatten(self._affinexp_pic2msk(picosCon.ub)), 

384 msk.Expr.flatten(self._affinexp_pic2msk(picosCon.ne))) 

385 

386 # TODO: Remove zeros from coneElement[1:]. 

387 

388 return self.int.constraint(coneElement, msk.Domain.inQCone()) 

389 

390 def _import_rscone_constraint(self, picosCon): 

391 import mosek.fusion as msk 

392 

393 assert isinstance(picosCon, RSOCConstraint) 

394 

395 # MOSEK handles the vector [x₁; x₂; x₃] as input for a constraint of the 

396 # form ‖x₃‖² ≤ 2x₁x₂ whereas PICOS handles the expressions e₁, e₂ and e₃ 

397 # for a constraint of the form ‖e₁‖² ≤ e₂e₃. 

398 # Neutralize MOSEK's additional factor of two by scaling e₂ and e₃ by 

399 # sqrt(0.5) each to obtain x₁ and x₂ respectively. 

400 scale = 0.5**0.5 

401 coneElement = self._mosek_vstack( 

402 msk.Expr.flatten(self._affinexp_pic2msk(scale * picosCon.ub1)), 

403 msk.Expr.flatten(self._affinexp_pic2msk(scale * picosCon.ub2)), 

404 msk.Expr.flatten(self._affinexp_pic2msk(picosCon.ne))) 

405 

406 # TODO: Remove zeros from coneElement[2:]. 

407 

408 return self.int.constraint(coneElement, msk.Domain.inRotatedQCone()) 

409 

410 def _import_sdp_constraint(self, picosCon): 

411 import mosek.fusion as msk 

412 assert isinstance(picosCon, LMIConstraint) 

413 

414 semiDefMatrix = self._affinexp_pic2msk(picosCon.psd) 

415 

416 return self.int.constraint(semiDefMatrix, msk.Domain.inPSDCone()) 

417 

418 def _import_constraint(self, picosCon): 

419 import mosek.fusion as msk 

420 

421 # HACK: Work around faulty MOSEK warnings (warning 705). 

422 import os 

423 with open(os.devnull, "w") as devnull: 

424 self.int.setLogHandler(devnull) 

425 

426 if isinstance(picosCon, AffineConstraint): 

427 mosekCon = self._import_linear_constraint(picosCon) 

428 elif isinstance(picosCon, SOCConstraint): 

429 mosekCon = self._import_socone_constraint(picosCon) 

430 elif isinstance(picosCon, RSOCConstraint): 

431 mosekCon = self._import_rscone_constraint(picosCon) 

432 elif isinstance(picosCon, LMIConstraint): 

433 mosekCon = self._import_sdp_constraint(picosCon) 

434 else: 

435 assert False, "Unexpected constraint type: {}".format( 

436 picosCon.__class__.__name__) 

437 

438 self.int.setLogHandler(sys.stdout) 

439 

440 # Map the PICOS constraint to the MOSEK constraint and vice versa. 

441 self.knownConstraints[picosCon] = mosekCon 

442 self.knownConstraints[mosekCon] = picosCon 

443 

444 if self._debug(): 

445 self._debug("Constraint imported: {} → {}".format(picosCon, 

446 " ".join(mosekCon.toString().split()) if not isinstance( 

447 mosekCon, msk.PSDConstraint) else mosekCon)) 

448 

449 def _import_objective(self): 

450 import mosek.fusion as msk 

451 

452 picosSense, picosObjective = self.ext.no 

453 

454 if picosSense == "min": 

455 mosekSense = msk.ObjectiveSense.Minimize 

456 else: 

457 assert picosSense == "max" 

458 mosekSense = msk.ObjectiveSense.Maximize 

459 

460 mosekObjective = self._affinexp_pic2msk(picosObjective) 

461 

462 self.int.objective(mosekSense, mosekObjective) 

463 

464 if self._debug(): 

465 self._debug( 

466 "Objective imported: {} {} → {} {}".format( 

467 picosSense, picosObjective, mosekSense, 

468 " ".join(mosekObjective.toString().split()))) 

469 

470 def _import_problem(self): 

471 import mosek.fusion as msk 

472 

473 # Create a problem instance. 

474 self.int = msk.Model() 

475 self.int.setLogHandler(sys.stdout) 

476 

477 # Import variables. 

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

479 self._import_variable(variable) 

480 

481 # Import constraints. 

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

483 if not isinstance(constraint, DummyConstraint): 

484 self._import_constraint(constraint) 

485 

486 # Set objective. 

487 self._import_objective() 

488 

489 def _update_problem(self): 

490 for oldConstraint in self._removed_constraints(): 

491 raise ProblemUpdateError( 

492 "MOSEK does not support removal of constraints.") 

493 

494 for oldVariable in self._removed_variables(): 

495 raise ProblemUpdateError( 

496 "MOSEK does not support removal of variables.") 

497 

498 for newVariable in self._new_variables(): 

499 self._import_variable(newVariable) 

500 

501 for newConstraint in self._new_constraints(): 

502 self._import_constraint(newConstraint) 

503 

504 if self._objective_has_changed(): 

505 self._import_objective() 

506 

507 def _solve(self): 

508 import mosek.fusion as msk 

509 from mosek import objsense 

510 

511 # MOSEK 8 has additional parameters and status codes. 

512 mosek8 = self.ver < 9 

513 

514 # Reset options. 

515 # HACK: This is a direct access to MOSEK's internal Task object, which 

516 # is necessary as the Fusion API has no call to reset options. 

517 # TODO: As soon as the Fusion API offers option reset, use it instead. 

518 self.int.getTask().setdefaults() 

519 self.int.optserverHost("") 

520 

521 # verbosity 

522 self.int.setSolverParam("log", max(0, self.verbosity())) 

523 

524 # abs_prim_fsb_tol 

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

526 value = self.ext.options.abs_prim_fsb_tol 

527 

528 # Interior-point primal feasibility tolerances. 

529 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()): 

530 self.int.setSolverParam("intpnt{}TolPfeas".format(ptype), value) 

531 

532 # Simplex primal feasibility tolerance. 

533 self.int.setSolverParam("basisTolX", value) 

534 

535 # Mixed-integer (primal) feasibility tolerance. 

536 self.int.setSolverParam("mioTolFeas", value) 

537 

538 # abs_dual_fsb_tol 

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

540 value = self.ext.options.abs_dual_fsb_tol 

541 

542 # Interior-point dual feasibility tolerances. 

543 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()): 

544 self.int.setSolverParam("intpnt{}TolDfeas".format(ptype), value) 

545 

546 # Simplex dual feasibility (optimality) tolerance. 

547 self.int.setSolverParam("basisTolS", value) 

548 

549 # rel_dual_fsb_tol 

550 if self.ext.options.rel_dual_fsb_tol is not None: 

551 # Simplex relative dual feasibility (optimality) tolerance. 

552 self.int.setSolverParam("basisRelTolS", 

553 self.ext.options.rel_dual_fsb_tol) 

554 

555 # rel_ipm_opt_tol 

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

557 value = self.ext.options.rel_ipm_opt_tol 

558 

559 # Interior-point primal feasibility tolerances. 

560 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()): 

561 self.int.setSolverParam( 

562 "intpnt{}TolRelGap".format(ptype), value) 

563 

564 # abs_bnb_opt_tol 

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

566 self.int.setSolverParam("mioTolAbsGap", 

567 self.ext.options.abs_bnb_opt_tol) 

568 

569 # rel_bnb_opt_tol 

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

571 self.int.setSolverParam("mioTolRelGap", 

572 self.ext.options.rel_bnb_opt_tol) 

573 

574 # integrality_tol 

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

576 self.int.setSolverParam("mioTolAbsRelaxInt", 

577 self.ext.options.integrality_tol) 

578 

579 # max_iterations 

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

581 value = self.ext.options.max_iterations 

582 self.int.setSolverParam("biMaxIterations", value) 

583 self.int.setSolverParam("intpntMaxIterations", value) 

584 self.int.setSolverParam("simMaxIterations", value) 

585 

586 if self.ext.options.lp_node_method is not None \ 

587 or self.ext.options.lp_root_method is not None: 

588 # TODO: Give Problem an interface for checks like this. 

589 _islp = isinstance( 

590 self.ext.no.function, AffineExpression) \ 

591 and all([isinstance(constraint, AffineConstraint) 

592 for constraint in self.ext.constraints.values()]) 

593 

594 _lpm = { 

595 "interior": "intpnt" if _islp else "conic", 

596 "psimplex": "primalSimplex", 

597 "dsimplex": "dualSimplex"} 

598 

599 # lp_node_method 

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

601 value = self.ext.options.lp_node_method 

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

603 self.int.setSolverParam("mioNodeOptimizer", _lpm[value]) 

604 

605 # lp_root_method 

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

607 value = self.ext.options.lp_root_method 

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

609 self.int.setSolverParam("mioRootOptimizer", _lpm[value]) 

610 

611 # timelimit 

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

613 value = float(self.ext.options.timelimit) 

614 self.int.setSolverParam("optimizerMaxTime", value) 

615 self.int.setSolverParam("mioMaxTime", value) 

616 

617 # max_fsb_nodes 

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

619 self.int.setSolverParam("mioMaxNumSolutions", 

620 self.ext.options.max_fsb_nodes) 

621 

622 # hotstart 

623 if self.ext.options.hotstart: 

624 # TODO: Check if valued variables (i.e. a hotstart) are utilized by 

625 # MOSEK beyond mioConstructSol, and whether it makes sense to 

626 # (1) also value continuous variables and (2) reset variable 

627 # values when hotstart gets disabled again (see Gurobi). 

628 self.int.setSolverParam("mioConstructSol", "on") 

629 self._import_variable_values(integralOnly=True) 

630 

631 # Handle MOSEK-specific options. 

632 for key, value in self.ext.options.mskfsn_params.items(): 

633 try: 

634 self.int.setSolverParam(key, value) 

635 except msk.ParameterError as error: 

636 self._handle_bad_solver_specific_option_key(key, error) 

637 except ValueError as error: 

638 self._handle_bad_solver_specific_option_value(key, value, error) 

639 

640 # Handle 'mosek_server' option. 

641 # FIXME: This produces unsolicited console output with MOSEK 9.2. 

642 if self.ext.options.mosek_server: 

643 self.int.optserverHost(self.ext.options.mosek_server) 

644 

645 # Handle unsupported options. 

646 self._handle_unsupported_option("treememory") 

647 

648 # Attempt to solve the problem. 

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

650 self.int.solve() 

651 

652 # Retrieve primals. 

653 primals = {} 

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

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

656 mosekVar = self.knownVariables[picosVar] 

657 try: 

658 primals[picosVar] = list(mosekVar.level()) 

659 except msk.SolutionError: 

660 primals[picosVar] = None 

661 

662 # Retrieve duals. 

663 duals = {} 

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

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

666 if isinstance(picosCon, DummyConstraint): 

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

668 continue 

669 

670 # Retrieve corresponding MOSEK constraint. 

671 mosekCon = self.knownConstraints[picosCon] 

672 

673 # Retrieve its dual. 

674 try: 

675 mosekDual = mosekCon.dual() 

676 except msk.SolutionError: 

677 duals[picosCon] = None 

678 continue 

679 

680 # Devectorize the dual. 

681 # NOTE: Change from row-major to column-major order. 

682 size = picosCon.size 

683 picosDual = cvxopt.matrix(mosekDual, (size[1], size[0])).T 

684 

685 # Adjust the dual based on constraint type. 

686 if isinstance(picosCon, (AffineConstraint, LMIConstraint)): 

687 if not picosCon.is_increasing(): 

688 picosDual = -picosDual 

689 elif isinstance(picosCon, SOCConstraint): 

690 picosDual = -picosDual 

691 elif isinstance(picosCon, RSOCConstraint): 

692 # MOSEK handles the vector [x₁; x₂; x₃] as input for a 

693 # constraint of the form ‖x₃‖² ≤ 2x₁x₂ whereas PICOS handles 

694 # the expressions e₁, e₂ and e₃ for a constraint of the form 

695 # ‖e₁‖² ≤ e₂e₃. MOSEK's additional factor of two was 

696 # neutralized on import by scaling e₂ and e₃ by sqrt(0.5) 

697 # each to obtain x₁ and x₂ respectively. Scale now also the 

698 # dual returned by MOSEK to make up for this. 

699 scale = 0.5**0.5 

700 alpha = scale * picosDual[0] 

701 beta = scale * picosDual[1] 

702 z = list(-picosDual[2:]) 

703 

704 # HACK: Work around a potential documentation bug in MOSEK: 

705 # The first two vector elements of the rotated 

706 # quadratic cone dual are non-positive (allowing for a 

707 # shorter notation in the linear part of their dual 

708 # representation) even though their definition of the 

709 # (self-dual) rotated quadratic cone explicitly states 

710 # that they are non-negative (as in PICOS). 

711 alpha = -alpha 

712 beta = -beta 

713 

714 picosDual = cvxopt.matrix([alpha, beta] + z) 

715 else: 

716 assert False, \ 

717 "Constraint type belongs to unsupported problem type." 

718 

719 # Flip sign based on objective sense. 

720 if (self.int.getTask().getobjsense() == objsense.minimize): 

721 picosDual = -picosDual 

722 

723 duals[picosCon] = picosDual 

724 

725 # Retrieve objective value. 

726 try: 

727 value = float(self.int.primalObjValue()) 

728 except msk.SolutionError: 

729 value = None 

730 

731 # Retrieve solution status. 

732 primalStatus = self._solution_status_pic2msk( 

733 self.int.getPrimalSolutionStatus()) 

734 dualStatus = self._solution_status_pic2msk( 

735 self.int.getDualSolutionStatus()) 

736 problemStatus = self._problem_status_pic2msk(self.int.getProblemStatus( 

737 msk.SolutionType.Default), not self.ext.is_continuous()) 

738 

739 # Correct two known bad solution states: 

740 if problemStatus == PS_INFEASIBLE and primalStatus == SS_UNKNOWN: 

741 primalStatus = SS_INFEASIBLE 

742 if problemStatus == PS_UNBOUNDED and dualStatus == SS_UNKNOWN: 

743 dualStatus = SS_INFEASIBLE 

744 

745 return self._make_solution( 

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

747 

748 def _solution_status_pic2msk(self, statusCode): 

749 from mosek.fusion import SolutionStatus as ss 

750 

751 map = { 

752 ss.Undefined: SS_EMPTY, 

753 ss.Unknown: SS_UNKNOWN, 

754 ss.Optimal: SS_OPTIMAL, 

755 ss.Feasible: SS_FEASIBLE, 

756 ss.Certificate: SS_INFEASIBLE, 

757 ss.IllposedCert: SS_UNKNOWN 

758 } 

759 

760 if self.ver <= 8: 

761 map.update({ 

762 ss.NearOptimal: SS_FEASIBLE, 

763 ss.NearFeasible: SS_UNKNOWN, 

764 ss.NearCertificate: SS_UNKNOWN, 

765 }) 

766 

767 try: 

768 return map[statusCode] 

769 except KeyError: 

770 self._warn("The MOSEK Fusion solution status code {} is not known " 

771 "to PICOS.".format(statusCode)) 

772 return SS_UNKNOWN 

773 

774 def _problem_status_pic2msk(self, statusCode, integerProblem): 

775 from mosek.fusion import ProblemStatus as ps 

776 

777 try: 

778 return { 

779 ps.Unknown: PS_UNKNOWN, 

780 ps.PrimalAndDualFeasible: PS_FEASIBLE, 

781 ps.PrimalFeasible: 

782 PS_FEASIBLE if integerProblem else PS_UNKNOWN, 

783 ps.DualFeasible: PS_UNKNOWN, 

784 ps.PrimalInfeasible: PS_INFEASIBLE, 

785 ps.DualInfeasible: PS_UNBOUNDED, 

786 ps.PrimalAndDualInfeasible: PS_INFEASIBLE, 

787 ps.IllPosed: PS_ILLPOSED, 

788 ps.PrimalInfeasibleOrUnbounded: PS_INF_OR_UNB 

789 }[statusCode] 

790 except KeyError: 

791 self._warn("The MOSEK Fusion problem status code {} is not known to" 

792 " PICOS.".format(statusCode)) 

793 return PS_UNKNOWN 

794 

795 

796# -------------------------------------- 

797__all__ = api_end(_API_START, globals())