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
« 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# ------------------------------------------------------------------------------
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 if self.ver >= 11:
519 self.int.getTask().resetparameters()
520 else:
521 self.int.getTask().setdefaults()
522 self.int.optserverHost("")
524 # verbosity
525 self.int.setSolverParam("log", max(0, self.verbosity()))
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
531 # Interior-point primal feasibility tolerances.
532 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()):
533 self.int.setSolverParam("intpnt{}TolPfeas".format(ptype), value)
535 # Simplex primal feasibility tolerance.
536 self.int.setSolverParam("basisTolX", value)
538 # Mixed-integer (primal) feasibility tolerance.
539 self.int.setSolverParam("mioTolFeas", value)
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
545 # Interior-point dual feasibility tolerances.
546 for ptype in ("", "Co") + (("Qo",) if mosek8 else ()):
547 self.int.setSolverParam("intpnt{}TolDfeas".format(ptype), value)
549 # Simplex dual feasibility (optimality) tolerance.
550 self.int.setSolverParam("basisTolS", value)
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)
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
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)
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)
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)
577 # integrality_tol
578 if self.ext.options.integrality_tol is not None:
579 self.int.setSolverParam("mioTolAbsRelaxInt",
580 self.ext.options.integrality_tol)
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)
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()])
597 _lpm = {
598 "interior": "intpnt" if _islp else "conic",
599 "psimplex": "primalSimplex",
600 "dsimplex": "dualSimplex"}
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])
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])
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)
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)
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)
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)
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)
648 # Handle unsupported options.
649 self._handle_unsupported_option("treememory")
651 # Attempt to solve the problem.
652 with self._header(), self._stopwatch():
653 self.int.solve()
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
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
673 # Retrieve corresponding MOSEK constraint.
674 mosekCon = self.knownConstraints[picosCon]
676 # Retrieve its dual.
677 try:
678 mosekDual = mosekCon.dual()
679 except msk.SolutionError:
680 duals[picosCon] = None
681 continue
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
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:])
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
717 picosDual = cvxopt.matrix([alpha, beta] + z)
718 else:
719 assert False, \
720 "Constraint type belongs to unsupported problem type."
722 # Flip sign based on objective sense.
723 if (self.int.getTask().getobjsense() == objsense.minimize):
724 picosDual = -picosDual
726 duals[picosCon] = picosDual
728 # Retrieve objective value.
729 try:
730 value = float(self.int.primalObjValue())
731 except msk.SolutionError:
732 value = None
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())
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
748 return self._make_solution(
749 value, primals, duals, primalStatus, dualStatus, problemStatus)
751 def _solution_status_pic2msk(self, statusCode):
752 from mosek.fusion import SolutionStatus as ss
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 }
763 if self.ver <= 8:
764 map.update({
765 ss.NearOptimal: SS_FEASIBLE,
766 ss.NearFeasible: SS_UNKNOWN,
767 ss.NearCertificate: SS_UNKNOWN,
768 })
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
777 def _problem_status_pic2msk(self, statusCode, integerProblem):
778 from mosek.fusion import ProblemStatus as ps
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
799# --------------------------------------
800__all__ = api_end(_API_START, globals())