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) 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 (Fusion)", "MOSEK via 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 

520 # verbosity 

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

522 

523 # abs_prim_fsb_tol 

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

525 value = self.ext.options.abs_prim_fsb_tol 

526 

527 # Interior-point primal feasibility tolerances. 

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

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

530 

531 # Simplex primal feasibility tolerance. 

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

533 

534 # Mixed-integer (primal) feasibility tolerance. 

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

536 

537 # abs_dual_fsb_tol 

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

539 value = self.ext.options.abs_dual_fsb_tol 

540 

541 # Interior-point dual feasibility tolerances. 

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

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

544 

545 # Simplex dual feasibility (optimality) tolerance. 

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

547 

548 # rel_dual_fsb_tol 

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

550 # Simplex relative dual feasibility (optimality) tolerance. 

551 self.int.setSolverParam("basisRelTolS", 

552 self.ext.options.rel_dual_fsb_tol) 

553 

554 # rel_ipm_opt_tol 

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

556 value = self.ext.options.rel_ipm_opt_tol 

557 

558 # Interior-point primal feasibility tolerances. 

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

560 self.int.setSolverParam( 

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

562 

563 # abs_bnb_opt_tol 

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

565 self.int.setSolverParam("mioTolAbsGap", 

566 self.ext.options.abs_bnb_opt_tol) 

567 

568 # rel_bnb_opt_tol 

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

570 self.int.setSolverParam("mioTolRelGap", 

571 self.ext.options.rel_bnb_opt_tol) 

572 

573 # integrality_tol 

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

575 self.int.setSolverParam("mioTolAbsRelaxInt", 

576 self.ext.options.integrality_tol) 

577 

578 # max_iterations 

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

580 value = self.ext.options.max_iterations 

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

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

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

584 

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

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

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

588 _islp = isinstance( 

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

590 and all([isinstance(constraint, AffineConstraint) 

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

592 

593 _lpm = { 

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

595 "psimplex": "primalSimplex", 

596 "dsimplex": "dualSimplex"} 

597 

598 # lp_node_method 

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

600 value = self.ext.options.lp_node_method 

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

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

603 

604 # lp_root_method 

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

606 value = self.ext.options.lp_root_method 

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

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

609 

610 # timelimit 

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

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

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

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

615 

616 # max_fsb_nodes 

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

618 self.int.setSolverParam("mioMaxNumSolutions", 

619 self.ext.options.max_fsb_nodes) 

620 

621 # hotstart 

622 if self.ext.options.hotstart: 

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

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

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

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

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

628 self._import_variable_values(integralOnly=True) 

629 

630 # Handle MOSEK-specific options. 

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

632 try: 

633 self.int.setSolverParam(key, value) 

634 except msk.ParameterError as error: 

635 self._handle_bad_solver_specific_option_key(key, error) 

636 except ValueError as error: 

637 self._handle_bad_solver_specific_option_value(key, value, error) 

638 

639 # Handle unsupported options. 

640 self._handle_unsupported_option("treememory") 

641 

642 # Attempt to solve the problem. 

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

644 self.int.solve() 

645 

646 # Retrieve primals. 

647 primals = {} 

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

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

650 mosekVar = self.knownVariables[picosVar] 

651 try: 

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

653 except msk.SolutionError: 

654 primals[picosVar] = None 

655 

656 # Retrieve duals. 

657 duals = {} 

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

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

660 if isinstance(picosCon, DummyConstraint): 

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

662 continue 

663 

664 # Retrieve corresponding MOSEK constraint. 

665 mosekCon = self.knownConstraints[picosCon] 

666 

667 # Retrieve its dual. 

668 try: 

669 mosekDual = mosekCon.dual() 

670 except msk.SolutionError: 

671 duals[picosCon] = None 

672 continue 

673 

674 # Devectorize the dual. Note the change from row-major to 

675 # column-major order. 

676 # size = picosCon.size 

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

678 picosDual = cvxopt.matrix(mosekDual) 

679 

680 # Adjust the dual based on constraint type. 

681 if isinstance(picosCon, AffineConstraint) \ 

682 or isinstance(picosCon, LMIConstraint): 

683 if not picosCon.is_increasing(): 

684 picosDual = -picosDual 

685 elif isinstance(picosCon, SOCConstraint): 

686 picosDual = -picosDual 

687 elif isinstance(picosCon, RSOCConstraint): 

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

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

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

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

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

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

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

695 scale = 0.5**0.5 

696 alpha = scale * picosDual[0] 

697 beta = scale * picosDual[1] 

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

699 

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

701 # The first two vector elements of the rotated 

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

703 # shorter notation in the linear part of their dual 

704 # representation) even though their definition of the 

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

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

707 alpha = -alpha 

708 beta = -beta 

709 

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

711 else: 

712 assert False, \ 

713 "Constraint type belongs to unsupported problem type." 

714 

715 # Flip sign based on objective sense. 

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

717 picosDual = -picosDual 

718 

719 duals[picosCon] = picosDual 

720 

721 # Retrieve objective value. 

722 try: 

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

724 except msk.SolutionError: 

725 value = None 

726 

727 # Retrieve solution status. 

728 primalStatus = self._solution_status_pic2msk( 

729 self.int.getPrimalSolutionStatus()) 

730 dualStatus = self._solution_status_pic2msk( 

731 self.int.getDualSolutionStatus()) 

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

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

734 

735 # Correct two known bad solution states: 

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

737 primalStatus = SS_INFEASIBLE 

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

739 dualStatus = SS_INFEASIBLE 

740 

741 return self._make_solution( 

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

743 

744 def _solution_status_pic2msk(self, statusCode): 

745 from mosek.fusion import SolutionStatus as ss 

746 

747 map = { 

748 ss.Undefined: SS_EMPTY, 

749 ss.Unknown: SS_UNKNOWN, 

750 ss.Optimal: SS_OPTIMAL, 

751 ss.Feasible: SS_FEASIBLE, 

752 ss.Certificate: SS_INFEASIBLE, 

753 ss.IllposedCert: SS_UNKNOWN 

754 } 

755 

756 if self.ver <= 8: 

757 map.update({ 

758 ss.NearOptimal: SS_FEASIBLE, 

759 ss.NearFeasible: SS_UNKNOWN, 

760 ss.NearCertificate: SS_UNKNOWN, 

761 }) 

762 

763 try: 

764 return map[statusCode] 

765 except KeyError: 

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

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

768 return SS_UNKNOWN 

769 

770 def _problem_status_pic2msk(self, statusCode, integerProblem): 

771 from mosek.fusion import ProblemStatus as ps 

772 

773 try: 

774 return { 

775 ps.Unknown: PS_UNKNOWN, 

776 ps.PrimalAndDualFeasible: PS_FEASIBLE, 

777 ps.PrimalFeasible: 

778 PS_FEASIBLE if integerProblem else PS_UNKNOWN, 

779 ps.DualFeasible: PS_UNKNOWN, 

780 ps.PrimalInfeasible: PS_INFEASIBLE, 

781 ps.DualInfeasible: PS_UNBOUNDED, 

782 ps.PrimalAndDualInfeasible: PS_INFEASIBLE, 

783 ps.IllPosed: PS_ILLPOSED, 

784 ps.PrimalInfeasibleOrUnbounded: PS_INF_OR_UNB 

785 }[statusCode] 

786 except KeyError: 

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

788 " PICOS.".format(statusCode)) 

789 return PS_UNKNOWN 

790 

791 

792# -------------------------------------- 

793__all__ = api_end(_API_START, globals())