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
« 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# ------------------------------------------------------------------------------
19"""Implementation of :class:`MOSEKFusionSolver`."""
21import sys
23import cvxopt
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
36_API_START = api_start(globals())
37# -------------------------------
40class MOSEKFusionSolver(Solver):
41 """Interface to the MOSEK solver via its high level Fusion API.
43 Supports both MOSEK 8 and 9.
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 """
50 SUPPORTED = Specification(
51 objectives=[
52 AffineExpression],
53 constraints=[
54 DummyConstraint,
55 AffineConstraint,
56 SOCConstraint,
57 RSOCConstraint,
58 LMIConstraint])
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
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
74 if footprint not in cls.SUPPORTED:
75 if explain:
76 return False, cls.SUPPORTED.mismatch_reason(footprint)
77 else:
78 return False
80 return (True, None) if explain else True
82 @classmethod
83 def default_penalty(cls):
84 """Implement :meth:`~.solver.Solver.default_penalty`."""
85 return 0.5 # Commercial solver with slower interface.
87 @classmethod
88 def test_availability(cls):
89 """Implement :meth:`~.solver.Solver.test_availability`."""
90 cls.check_import("mosek.fusion")
92 @classmethod
93 def names(cls):
94 """Implement :meth:`~.solver.Solver.names`."""
95 return "mskfsn", "MOSEK", "MOSEK", "Fusion API"
97 @classmethod
98 def is_free(cls):
99 """Implement :meth:`~.solver.Solver.is_free`."""
100 return False
102 def __init__(self, problem):
103 """Initialize a MOSEK (Fusion) solver interface.
105 :param ~picos.Problem problem: The problem to be solved.
106 """
107 super(MOSEKFusionSolver, self).__init__(problem)
109 # Maps PICOS variables to MOSEK variables and vice versa.
110 self.knownVariables = {}
112 # Maps PICOS constraints to MOSEK constraints and vice versa.
113 self.knownConstraints = {}
115 def __del__(self):
116 if self.int is not None:
117 self.int.dispose()
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()
127 @classmethod
128 def _get_major_version(cls):
129 if not hasattr(cls, "mosekVersion"):
130 import mosek
131 cls.mosekVersion = mosek.Env.getversion()
133 return cls.mosekVersion[0]
135 ver = property(lambda self: self.__class__._get_major_version())
136 """The major version of the available MOSEK library."""
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
149 return I, J, V
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
156 M = cvxoptMatrix
157 n, m = M.size
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.")
167 @classmethod
168 def _mosek_vstack(cls, *expressions):
169 """Vertically stack MOSEK expressions.
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
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)
184 def _affinexp_pic2msk(self, picosExpression):
185 """Transform an affine expression from PICOS to MOSEK.
187 Requries all contained variables to be known to MOSEK.
188 """
189 import mosek.fusion as msk
191 assert isinstance(picosExpression, AffineExpression)
193 vectorShape = [len(picosExpression), 1]
194 targetShape = list(picosExpression.size)
196 if self.ver < 9:
197 vectorShape = msk.Set.make(vectorShape)
198 targetShape = msk.Set.make(targetShape)
200 # Convert linear part of expression.
201 firstSummand = True
202 for picosVar, factor in picosExpression._linear_coefs.items():
203 mosekVar = self.knownVariables[picosVar]
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)
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))
217 if firstSummand:
218 mosekExpression = mosekConstant
219 else:
220 mosekExpression = msk.Expr.add(mosekExpression, mosekConstant)
221 elif firstSummand:
222 mosekExpression = msk.Expr.zeros(vectorShape)
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)
229 if self._debug():
230 self._debug(
231 "Affine expression converted: {} → {}".format(
232 repr(picosExpression), mosekExpression.toString()))
234 return mosekExpression
236 @classmethod
237 def _bounds_pic2msk(cls, picosVar, fixMOSEK9=False):
238 """Transform PICOS variable bounds to MOSEK matrices or scalars.
240 Scalars are returned in the case of homogenous bounds.
241 """
242 import mosek.fusion as msk
244 dim = picosVar.dim
245 lower, upper = picosVar.bound_dicts
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
251 for i, b in lower.items():
252 LV[i] = b
254 for i, b in upper.items():
255 UV[i] = b
256 else:
257 LV, LI = [], []
258 UV, UI = [], []
260 for i, b in lower.items():
261 LI.append(i)
262 LV.append(b)
264 for i, b in upper.items():
265 UI.append(i)
266 UV.append(b)
268 LJ = [0]*len(LV)
269 UJ = [0]*len(UV)
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)
278 return mosekBounds
280 def _import_variable(self, picosVar):
281 import mosek.fusion as msk
283 shape = [picosVar.dim, 1]
285 # Import variable bounds.
286 if not isinstance(picosVar, BinaryVariable):
287 # Retrieve lower and upper bounds.
288 lower, upper = self._bounds_pic2msk(picosVar)
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()
313 domain = msk.Domain.inRange(lower, upper, shape)
314 else:
315 domain = msk.Domain.inRange(lower, upper)
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)
323 # Create the MOSEK variable.
324 mosekVar = self.int.variable(picosVar.name, shape, domain)
326 # Map the PICOS variable to the MOSEK variable and vice versa.
327 self.knownVariables[picosVar] = mosekVar
328 self.knownVariables[mosekVar] = picosVar
330 if self._debug():
331 self._debug("Variable imported: {} → {}"
332 .format(picosVar, " ".join(mosekVar.toString().split())))
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
341 if picosVar.valued:
342 value = picosVar.internal_value
344 if isinstance(value, cvxopt.spmatrix):
345 value = cvxopt.matrix(value)
347 self.knownVariables[picosVar].setLevel(list(value))
349 def _import_linear_constraint(self, picosCon):
350 import mosek.fusion as msk
352 assert isinstance(picosCon, AffineConstraint)
354 # Separate constraint into a linear function and a constant.
355 linear, bound = picosCon.bounded_linear_form()
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)
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."
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)
377 def _import_socone_constraint(self, picosCon):
378 import mosek.fusion as msk
380 assert isinstance(picosCon, SOCConstraint)
382 coneElement = self._mosek_vstack(
383 msk.Expr.flatten(self._affinexp_pic2msk(picosCon.ub)),
384 msk.Expr.flatten(self._affinexp_pic2msk(picosCon.ne)))
386 # TODO: Remove zeros from coneElement[1:].
388 return self.int.constraint(coneElement, msk.Domain.inQCone())
390 def _import_rscone_constraint(self, picosCon):
391 import mosek.fusion as msk
393 assert isinstance(picosCon, RSOCConstraint)
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)))
406 # TODO: Remove zeros from coneElement[2:].
408 return self.int.constraint(coneElement, msk.Domain.inRotatedQCone())
410 def _import_sdp_constraint(self, picosCon):
411 import mosek.fusion as msk
412 assert isinstance(picosCon, LMIConstraint)
414 semiDefMatrix = self._affinexp_pic2msk(picosCon.psd)
416 return self.int.constraint(semiDefMatrix, msk.Domain.inPSDCone())
418 def _import_constraint(self, picosCon):
419 import mosek.fusion as msk
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)
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__)
438 self.int.setLogHandler(sys.stdout)
440 # Map the PICOS constraint to the MOSEK constraint and vice versa.
441 self.knownConstraints[picosCon] = mosekCon
442 self.knownConstraints[mosekCon] = picosCon
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))
449 def _import_objective(self):
450 import mosek.fusion as msk
452 picosSense, picosObjective = self.ext.no
454 if picosSense == "min":
455 mosekSense = msk.ObjectiveSense.Minimize
456 else:
457 assert picosSense == "max"
458 mosekSense = msk.ObjectiveSense.Maximize
460 mosekObjective = self._affinexp_pic2msk(picosObjective)
462 self.int.objective(mosekSense, mosekObjective)
464 if self._debug():
465 self._debug(
466 "Objective imported: {} {} → {} {}".format(
467 picosSense, picosObjective, mosekSense,
468 " ".join(mosekObjective.toString().split())))
470 def _import_problem(self):
471 import mosek.fusion as msk
473 # Create a problem instance.
474 self.int = msk.Model()
475 self.int.setLogHandler(sys.stdout)
477 # Import variables.
478 for variable in self.ext.variables.values():
479 self._import_variable(variable)
481 # Import constraints.
482 for constraint in self.ext.constraints.values():
483 if not isinstance(constraint, DummyConstraint):
484 self._import_constraint(constraint)
486 # Set objective.
487 self._import_objective()
489 def _update_problem(self):
490 for oldConstraint in self._removed_constraints():
491 raise ProblemUpdateError(
492 "MOSEK does not support removal of constraints.")
494 for oldVariable in self._removed_variables():
495 raise ProblemUpdateError(
496 "MOSEK does not support removal of variables.")
498 for newVariable in self._new_variables():
499 self._import_variable(newVariable)
501 for newConstraint in self._new_constraints():
502 self._import_constraint(newConstraint)
504 if self._objective_has_changed():
505 self._import_objective()
507 def _solve(self):
508 import mosek.fusion as msk
509 from mosek import objsense
511 # MOSEK 8 has additional parameters and status codes.
512 mosek8 = self.ver < 9
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("")
521 # verbosity
522 self.int.setSolverParam("log", max(0, self.verbosity()))
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
528 # Interior-point primal feasibility tolerances.
529 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()):
530 self.int.setSolverParam("intpnt{}TolPfeas".format(ptype), value)
532 # Simplex primal feasibility tolerance.
533 self.int.setSolverParam("basisTolX", value)
535 # Mixed-integer (primal) feasibility tolerance.
536 self.int.setSolverParam("mioTolFeas", value)
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
542 # Interior-point dual feasibility tolerances.
543 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()):
544 self.int.setSolverParam("intpnt{}TolDfeas".format(ptype), value)
546 # Simplex dual feasibility (optimality) tolerance.
547 self.int.setSolverParam("basisTolS", value)
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)
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
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)
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)
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)
574 # integrality_tol
575 if self.ext.options.integrality_tol is not None:
576 self.int.setSolverParam("mioTolAbsRelaxInt",
577 self.ext.options.integrality_tol)
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)
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()])
594 _lpm = {
595 "interior": "intpnt" if _islp else "conic",
596 "psimplex": "primalSimplex",
597 "dsimplex": "dualSimplex"}
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])
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])
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)
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)
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)
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)
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)
645 # Handle unsupported options.
646 self._handle_unsupported_option("treememory")
648 # Attempt to solve the problem.
649 with self._header(), self._stopwatch():
650 self.int.solve()
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
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
670 # Retrieve corresponding MOSEK constraint.
671 mosekCon = self.knownConstraints[picosCon]
673 # Retrieve its dual.
674 try:
675 mosekDual = mosekCon.dual()
676 except msk.SolutionError:
677 duals[picosCon] = None
678 continue
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
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:])
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
714 picosDual = cvxopt.matrix([alpha, beta] + z)
715 else:
716 assert False, \
717 "Constraint type belongs to unsupported problem type."
719 # Flip sign based on objective sense.
720 if (self.int.getTask().getobjsense() == objsense.minimize):
721 picosDual = -picosDual
723 duals[picosCon] = picosDual
725 # Retrieve objective value.
726 try:
727 value = float(self.int.primalObjValue())
728 except msk.SolutionError:
729 value = None
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())
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
745 return self._make_solution(
746 value, primals, duals, primalStatus, dualStatus, problemStatus)
748 def _solution_status_pic2msk(self, statusCode):
749 from mosek.fusion import SolutionStatus as ss
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 }
760 if self.ver <= 8:
761 map.update({
762 ss.NearOptimal: SS_FEASIBLE,
763 ss.NearFeasible: SS_UNKNOWN,
764 ss.NearCertificate: SS_UNKNOWN,
765 })
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
774 def _problem_status_pic2msk(self, statusCode, integerProblem):
775 from mosek.fusion import ProblemStatus as ps
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
796# --------------------------------------
797__all__ = api_end(_API_START, globals())