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

404 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-12 07:53 +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 if self.ver >= 11: 

519 self.int.getTask().resetparameters() 

520 else: 

521 self.int.getTask().setdefaults() 

522 self.int.optserverHost("") 

523 

524 # verbosity 

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

526 

527 # abs_prim_fsb_tol 

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

529 value = self.ext.options.abs_prim_fsb_tol 

530 

531 # Interior-point primal feasibility tolerances. 

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

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

534 

535 # Simplex primal feasibility tolerance. 

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

537 

538 # Mixed-integer (primal) feasibility tolerance. 

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

540 

541 # abs_dual_fsb_tol 

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

543 value = self.ext.options.abs_dual_fsb_tol 

544 

545 # Interior-point dual feasibility tolerances. 

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

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

548 

549 # Simplex dual feasibility (optimality) tolerance. 

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

551 

552 # rel_dual_fsb_tol 

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

554 # Simplex relative dual feasibility (optimality) tolerance. 

555 self.int.setSolverParam("basisRelTolS", 

556 self.ext.options.rel_dual_fsb_tol) 

557 

558 # rel_ipm_opt_tol 

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

560 value = self.ext.options.rel_ipm_opt_tol 

561 

562 # Interior-point primal feasibility tolerances. 

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

564 self.int.setSolverParam( 

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

566 

567 # abs_bnb_opt_tol 

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

569 self.int.setSolverParam("mioTolAbsGap", 

570 self.ext.options.abs_bnb_opt_tol) 

571 

572 # rel_bnb_opt_tol 

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

574 self.int.setSolverParam("mioTolRelGap", 

575 self.ext.options.rel_bnb_opt_tol) 

576 

577 # integrality_tol 

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

579 self.int.setSolverParam("mioTolAbsRelaxInt", 

580 self.ext.options.integrality_tol) 

581 

582 # max_iterations 

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

584 value = self.ext.options.max_iterations 

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

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

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

588 

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

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

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

592 _islp = isinstance( 

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

594 and all([isinstance(constraint, AffineConstraint) 

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

596 

597 _lpm = { 

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

599 "psimplex": "primalSimplex", 

600 "dsimplex": "dualSimplex"} 

601 

602 # lp_node_method 

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

604 value = self.ext.options.lp_node_method 

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

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

607 

608 # lp_root_method 

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

610 value = self.ext.options.lp_root_method 

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

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

613 

614 # timelimit 

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

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

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

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

619 

620 # max_fsb_nodes 

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

622 self.int.setSolverParam("mioMaxNumSolutions", 

623 self.ext.options.max_fsb_nodes) 

624 

625 # hotstart 

626 if self.ext.options.hotstart: 

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

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

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

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

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

632 self._import_variable_values(integralOnly=True) 

633 

634 # Handle MOSEK-specific options. 

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

636 try: 

637 self.int.setSolverParam(key, value) 

638 except msk.ParameterError as error: 

639 self._handle_bad_solver_specific_option_key(key, error) 

640 except ValueError as error: 

641 self._handle_bad_solver_specific_option_value(key, value, error) 

642 

643 # Handle 'mosek_server' option. 

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

645 if self.ext.options.mosek_server: 

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

647 

648 # Handle unsupported options. 

649 self._handle_unsupported_option("treememory") 

650 

651 # Attempt to solve the problem. 

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

653 self.int.solve() 

654 

655 # Retrieve primals. 

656 primals = {} 

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

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

659 mosekVar = self.knownVariables[picosVar] 

660 try: 

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

662 except msk.SolutionError: 

663 primals[picosVar] = None 

664 

665 # Retrieve duals. 

666 duals = {} 

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

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

669 if isinstance(picosCon, DummyConstraint): 

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

671 continue 

672 

673 # Retrieve corresponding MOSEK constraint. 

674 mosekCon = self.knownConstraints[picosCon] 

675 

676 # Retrieve its dual. 

677 try: 

678 mosekDual = mosekCon.dual() 

679 except msk.SolutionError: 

680 duals[picosCon] = None 

681 continue 

682 

683 # Devectorize the dual. 

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

685 size = picosCon.size 

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

687 

688 # Adjust the dual based on constraint type. 

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

690 if not picosCon.is_increasing(): 

691 picosDual = -picosDual 

692 elif isinstance(picosCon, SOCConstraint): 

693 picosDual = -picosDual 

694 elif isinstance(picosCon, RSOCConstraint): 

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

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

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

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

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

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

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

702 scale = 0.5**0.5 

703 alpha = scale * picosDual[0] 

704 beta = scale * picosDual[1] 

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

706 

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

708 # The first two vector elements of the rotated 

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

710 # shorter notation in the linear part of their dual 

711 # representation) even though their definition of the 

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

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

714 alpha = -alpha 

715 beta = -beta 

716 

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

718 else: 

719 assert False, \ 

720 "Constraint type belongs to unsupported problem type." 

721 

722 # Flip sign based on objective sense. 

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

724 picosDual = -picosDual 

725 

726 duals[picosCon] = picosDual 

727 

728 # Retrieve objective value. 

729 try: 

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

731 except msk.SolutionError: 

732 value = None 

733 

734 # Retrieve solution status. 

735 primalStatus = self._solution_status_pic2msk( 

736 self.int.getPrimalSolutionStatus()) 

737 dualStatus = self._solution_status_pic2msk( 

738 self.int.getDualSolutionStatus()) 

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

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

741 

742 # Correct two known bad solution states: 

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

744 primalStatus = SS_INFEASIBLE 

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

746 dualStatus = SS_INFEASIBLE 

747 

748 return self._make_solution( 

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

750 

751 def _solution_status_pic2msk(self, statusCode): 

752 from mosek.fusion import SolutionStatus as ss 

753 

754 map = { 

755 ss.Undefined: SS_EMPTY, 

756 ss.Unknown: SS_UNKNOWN, 

757 ss.Optimal: SS_OPTIMAL, 

758 ss.Feasible: SS_FEASIBLE, 

759 ss.Certificate: SS_INFEASIBLE, 

760 ss.IllposedCert: SS_UNKNOWN 

761 } 

762 

763 if self.ver <= 8: 

764 map.update({ 

765 ss.NearOptimal: SS_FEASIBLE, 

766 ss.NearFeasible: SS_UNKNOWN, 

767 ss.NearCertificate: SS_UNKNOWN, 

768 }) 

769 

770 try: 

771 return map[statusCode] 

772 except KeyError: 

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

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

775 return SS_UNKNOWN 

776 

777 def _problem_status_pic2msk(self, statusCode, integerProblem): 

778 from mosek.fusion import ProblemStatus as ps 

779 

780 try: 

781 return { 

782 ps.Unknown: PS_UNKNOWN, 

783 ps.PrimalAndDualFeasible: PS_FEASIBLE, 

784 ps.PrimalFeasible: 

785 PS_FEASIBLE if integerProblem else PS_UNKNOWN, 

786 ps.DualFeasible: PS_UNKNOWN, 

787 ps.PrimalInfeasible: PS_INFEASIBLE, 

788 ps.DualInfeasible: PS_UNBOUNDED, 

789 ps.PrimalAndDualInfeasible: PS_INFEASIBLE, 

790 ps.IllPosed: PS_ILLPOSED, 

791 ps.PrimalInfeasibleOrUnbounded: PS_INF_OR_UNB 

792 }[statusCode] 

793 except KeyError: 

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

795 " PICOS.".format(statusCode)) 

796 return PS_UNKNOWN 

797 

798 

799# -------------------------------------- 

800__all__ = api_end(_API_START, globals())