Coverage for picos/solvers/solver_gurobi.py : 11.46%

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) 2018-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:`GurobiSolver`."""
21from collections import namedtuple
23import cvxopt
25from ..apidoc import api_end, api_start
26from ..constraints import (AffineConstraint, ConvexQuadraticConstraint,
27 DummyConstraint, RSOCConstraint, SOCConstraint)
28from ..expressions import (CONTINUOUS_VARTYPES, AffineExpression,
29 BinaryVariable, IntegerVariable,
30 QuadraticExpression)
31from ..modeling.footprint import Specification
32from ..modeling.solution import (PS_FEASIBLE, PS_INF_OR_UNB, PS_INFEASIBLE,
33 PS_UNBOUNDED, PS_UNKNOWN, PS_UNSTABLE,
34 SS_EMPTY, SS_FEASIBLE, SS_INFEASIBLE,
35 SS_OPTIMAL, SS_PREMATURE, SS_UNKNOWN)
36from .solver import Solver
38_API_START = api_start(globals())
39# -------------------------------
42class GurobiSolver(Solver):
43 """Interface to the Gurobi solver via its official Python interface."""
45 # TODO: Allow nonconvex quadratic constraints in the integer case?
46 # TODO: Don't support (conic) quadratic constraints when duals are
47 # requested because their precision is bad and can't be controlled.
48 SUPPORTED = Specification(
49 objectives=[
50 AffineExpression,
51 QuadraticExpression],
52 constraints=[
53 DummyConstraint,
54 AffineConstraint,
55 SOCConstraint,
56 RSOCConstraint,
57 ConvexQuadraticConstraint])
59 @classmethod
60 def supports(cls, footprint, explain=False):
61 """Implement :meth:`~.solver.Solver.supports`."""
62 result = Solver.supports(footprint, explain)
63 if not result or (explain and not result[0]):
64 return result
66 if footprint.nonconvex_quadratic_objective and footprint.continuous:
67 if explain:
68 return (False,
69 "Continuous problems with nonconvex quadratic objectives.")
70 else:
71 return False
73 if footprint not in cls.SUPPORTED:
74 if explain:
75 return False, cls.SUPPORTED.mismatch_reason(footprint)
76 else:
77 return False
79 return (True, None) if explain else True
81 @classmethod
82 def default_penalty(cls):
83 """Implement :meth:`~.solver.Solver.default_penalty`."""
84 return 0.0 # Commercial solver.
86 @classmethod
87 def test_availability(cls):
88 """Implement :meth:`~.solver.Solver.test_availability`."""
89 cls.check_import("gurobipy")
91 @classmethod
92 def names(cls):
93 """Implement :meth:`~.solver.Solver.names`."""
94 return "gurobi", "Gurobi", "Gurobi Optimizer"
96 @classmethod
97 def is_free(cls):
98 """Implement :meth:`~.solver.Solver.is_free`."""
99 return False
101 GurobiSOCC = namedtuple("GurobiSOCC",
102 ("LHSVars", "RHSVar", "LHSCons", "RHSCon", "quadCon"))
104 GurobiRSOCC = namedtuple("GurobiRSOCC",
105 ("LHSVars", "RHSVars", "LHSCons", "RHSCons", "quadCon"))
107 def __init__(self, problem):
108 """Initialize a Gurobi solver interface.
110 :param ~picos.Problem problem: The problem to be solved.
111 """
112 super(GurobiSolver, self).__init__(problem)
114 self._gurobiVar = dict()
115 """Maps PICOS variable indices to Gurobi variables."""
117 self._gurobiLinearConstraints = dict()
118 """Maps a PICOS (multidimensional) linear constraint to a collection of
119 Gurobi (scalar) linear constraints."""
121 self._gurobiQuadConstraint = dict()
122 """Maps a PICOS quadratic constraint to a Gurobi quadr. constraint."""
124 self._gurobiSOCC = dict()
125 """Maps a PICOS second order cone constraint to its Gurobi
126 representation involving auxiliary variables and constraints."""
128 self._gurobiRSOCC = dict()
129 """Maps a PICOS rotated second order cone constraint to its Gurobi
130 representation involving auxiliary variables and constraints."""
132 def reset_problem(self):
133 """Implement :meth:`~.solver.Solver.reset_problem`."""
134 self.int = None
136 self._gurobiVar.clear()
137 self._gurobiLinearConstraints.clear()
138 self._gurobiQuadConstraint.clear()
139 self._gurobiSOCC.clear()
140 self._gurobiRSOCC.clear()
142 def _import_variable(self, picosVar):
143 import gurobipy as gurobi
145 dim = picosVar.dim
147 # Retrieve types.
148 if isinstance(picosVar, CONTINUOUS_VARTYPES):
149 gurobiVarType = gurobi.GRB.CONTINUOUS
150 elif isinstance(picosVar, IntegerVariable):
151 gurobiVarType = gurobi.GRB.INTEGER
152 elif isinstance(picosVar, BinaryVariable):
153 gurobiVarType = gurobi.GRB.BINARY
154 else:
155 assert False, "Unexpected variable type."
157 # Retrieve bounds.
158 lowerBounds = [-gurobi.GRB.INFINITY]*dim
159 upperBounds = [gurobi.GRB.INFINITY]*dim
160 lower, upper = picosVar.bound_dicts
161 for i, b in lower.items():
162 lowerBounds[i] = b
163 for i, b in upper.items():
164 upperBounds[i] = b
166 # Import variable.
167 # Note that Gurobi allows importing the objective function coefficients
168 # for the new variables here, but that is done later to streamline
169 # updates to the objective.
170 gurobiVars = self.int.addVars(dim, lb=lowerBounds, ub=upperBounds,
171 vtype=gurobiVarType, name=picosVar.name)
173 # Map PICOS variable indices to Gurobi variables.
174 for localIndex in range(dim):
175 self._gurobiVar[picosVar.id_at(localIndex)] = gurobiVars[localIndex]
177 def _remove_variable(self, picosVar):
178 gurobiVars = [self._gurobiVar.pop(picosVar.id_at(localIndex))
179 for localIndex in range(picosVar.dim)]
180 self.int.remove(gurobiVars)
182 def _import_variable_values(self):
183 for picosVar in self.ext.variables.values():
184 if picosVar.valued:
185 value = picosVar.internal_value
187 for localIndex in range(picosVar.dim):
188 gurobiVar = self._gurobiVar[picosVar.id_at(localIndex)]
189 gurobiVar.Start = value[localIndex]
191 def _reset_variable_values(self):
192 import gurobipy as gurobi
194 for gurobiVar in self._gurobiVar.values():
195 gurobiVar.Start = gurobi.GRB.UNDEFINED
197 def _affinexp_pic2grb(self, picosExpression):
198 import gurobipy as gurobi
200 for gurobiVars, coefficients, constant in picosExpression.sparse_rows(
201 None, indexFunction=lambda picosVar, localIndex:
202 self._gurobiVar[picosVar.id_at(localIndex)]):
203 gurobiExpression = gurobi.LinExpr(coefficients, gurobiVars)
204 gurobiExpression.addConstant(constant)
205 yield gurobiExpression
207 def _scalar_affinexp_pic2grb(self, picosExpression):
208 """Transform a scalar affine expression from PICOS to Gurobi.
210 :returns: A :class:`LinExpr <gurobipy.LinExpr>`.
211 """
212 assert len(picosExpression) == 1
213 return next(self._affinexp_pic2grb(picosExpression))
215 def _quadexp_pic2grb(self, picosExpression):
216 import gurobipy as gurobi
218 assert isinstance(picosExpression, QuadraticExpression)
220 # Import affine part of expression.
221 gurobiExpression = gurobi.QuadExpr(
222 self._scalar_affinexp_pic2grb(picosExpression.aff))
224 # Import quadratic form.
225 gurobiI, gurobiJ, gurobiV = [], [], []
226 for (picosVar1, picosVar2), picosCoefficients \
227 in picosExpression._sparse_quads.items():
228 for sparseIndex in range(len(picosCoefficients)):
229 localVar1Index = picosCoefficients.I[sparseIndex]
230 localVar2Index = picosCoefficients.J[sparseIndex]
231 localCoefficient = picosCoefficients.V[sparseIndex]
232 gurobiI.append(self._gurobiVar[picosVar1.id_at(localVar1Index)])
233 gurobiJ.append(self._gurobiVar[picosVar2.id_at(localVar2Index)])
234 gurobiV.append(localCoefficient)
235 gurobiExpression.addTerms(gurobiV, gurobiI, gurobiJ)
237 return gurobiExpression
239 def _import_linear_constraint(self, picosConstraint):
240 import gurobipy as gurobi
242 assert isinstance(picosConstraint, AffineConstraint)
244 # Retrieve sense.
245 if picosConstraint.is_increasing():
246 gurobiSense = gurobi.GRB.LESS_EQUAL
247 elif picosConstraint.is_decreasing():
248 gurobiSense = gurobi.GRB.GREATER_EQUAL
249 elif picosConstraint.is_equality():
250 gurobiSense = gurobi.GRB.EQUAL
251 else:
252 assert False, "Unexpected constraint relation."
254 # Append scalar constraints.
255 gurobiCons = []
256 for localConIndex, (gurobiLHS, gurobiRHS) in enumerate(zip(
257 self._affinexp_pic2grb(picosConstraint.lhs),
258 self._affinexp_pic2grb(picosConstraint.rhs))):
259 if picosConstraint.name:
260 gurobiName = "{}:{}".format(picosConstraint.name, localConIndex)
261 else:
262 gurobiName = ""
264 gurobiCons.append(self.int.addConstr(
265 gurobiLHS, gurobiSense, gurobiRHS, gurobiName))
267 return gurobiCons
269 def _import_quad_constraint(self, picosConstraint):
270 import gurobipy as gurobi
272 assert isinstance(picosConstraint, ConvexQuadraticConstraint)
274 gurobiLHS = self._quadexp_pic2grb(picosConstraint.le0)
275 gurobiRHS = -gurobiLHS.getLinExpr().getConstant()
276 if gurobiRHS:
277 gurobiLHS.getLinExpr().addConstant(gurobiRHS)
279 return self.int.addQConstr(
280 gurobiLHS, gurobi.GRB.LESS_EQUAL, gurobiRHS)
282 # TODO: Handle SOC → Quadratic via a reformulation.
283 def _import_socone_constraint(self, picosConstraint):
284 import gurobipy as gurobi
286 assert isinstance(picosConstraint, SOCConstraint)
288 picosLHS = picosConstraint.ne
289 picosRHS = picosConstraint.ub
290 picosLHSLen = len(picosLHS)
292 # Add auxiliary variables: One for every dimension of the left hand side
293 # of the PICOS constraint and one for its right hand side.
294 gurobiLHSVarsIndexed = self.int.addVars(
295 picosLHSLen, lb=-gurobi.GRB.INFINITY, ub=gurobi.GRB.INFINITY)
296 gurobiLHSVars = gurobiLHSVarsIndexed.values()
297 gurobiRHSVar = self.int.addVar(lb=0.0, ub=gurobi.GRB.INFINITY)
299 # Add constraints that identify the left hand side Gurobi auxiliary
300 # variables with their slice of the PICOS left hand side expression.
301 gurobiLHSSlices = dict()
302 for dimension, slice in enumerate(self._affinexp_pic2grb(picosLHS)):
303 gurobiLHSSlices[dimension] = slice
304 gurobiLHSCons = self.int.addConstrs(
305 (gurobiLHSSlices[dimension] - gurobiLHSVarsIndexed[dimension] == 0
306 for dimension in range(picosLHSLen))).values()
308 # Add a constraint that identifies the right hand side Gurobi auxiliary
309 # variable with the PICOS right hand side scalar expression.
310 gurobiRHSExp = self._scalar_affinexp_pic2grb(picosRHS)
311 gurobiRHSCon = self.int.addConstr(
312 gurobiRHSVar - gurobiRHSExp, gurobi.GRB.EQUAL, 0)
314 # Add a quadratic constraint over the auxiliary variables that
315 # represents the PICOS second order cone constraint itself.
316 quadExpr = gurobi.QuadExpr()
317 quadExpr.addTerms([1.0] * picosLHSLen, gurobiLHSVars, gurobiLHSVars)
318 gurobiName = picosConstraint.name if picosConstraint.name else ""
319 gurobiQuadCon = self.int.addQConstr(quadExpr, gurobi.GRB.LESS_EQUAL,
320 gurobiRHSVar * gurobiRHSVar, gurobiName)
322 gurobiMetaCon = self.GurobiSOCC(
323 LHSVars=gurobiLHSVars, RHSVar=gurobiRHSVar, LHSCons=gurobiLHSCons,
324 RHSCon=gurobiRHSCon, quadCon=gurobiQuadCon)
326 return gurobiMetaCon
328 # TODO: Handle RSOC → Quadratic via a reformulation.
329 def _import_rscone_constraint(self, picosConstraint):
330 import gurobipy as gurobi
332 assert isinstance(picosConstraint, RSOCConstraint)
334 picosLHS = picosConstraint.ne
335 picosRHS1 = picosConstraint.ub1
336 picosRHS2 = picosConstraint.ub2
337 picosLHSLen = len(picosLHS)
339 # Add auxiliary variables: One for every dimension of the left hand side
340 # of the PICOS constraint and one for its right hand side.
341 gurobiLHSVarsIndexed = self.int.addVars(
342 picosLHSLen, lb=-gurobi.GRB.INFINITY, ub=gurobi.GRB.INFINITY)
343 gurobiLHSVars = gurobiLHSVarsIndexed.values()
344 gurobiRHSVars = self.int.addVars(
345 2, lb=0.0, ub=gurobi.GRB.INFINITY).values()
347 # Add constraints that identify the left hand side Gurobi auxiliary
348 # variables with their slice of the PICOS left hand side expression.
349 gurobiLHSSlices = dict()
350 for dimension, slice in enumerate(self._affinexp_pic2grb(picosLHS)):
351 gurobiLHSSlices[dimension] = slice
352 gurobiLHSCons = self.int.addConstrs(
353 (gurobiLHSSlices[dimension] - gurobiLHSVarsIndexed[dimension] == 0
354 for dimension in range(picosLHSLen))).values()
356 # Add two constraints that identify the right hand side Gurobi auxiliary
357 # variables with the PICOS right hand side scalar expressions.
358 gurobiRHSExps = \
359 self._scalar_affinexp_pic2grb(picosRHS1), \
360 self._scalar_affinexp_pic2grb(picosRHS2)
361 gurobiRHSCons = self.int.addConstrs(
362 (gurobiRHSVars[i] - gurobiRHSExps[i] == 0 for i in (0, 1))).values()
364 # Add a quadratic constraint over the auxiliary variables that
365 # represents the PICOS second order cone constraint itself.
366 quadExpr = gurobi.QuadExpr()
367 quadExpr.addTerms([1.0] * picosLHSLen, gurobiLHSVars, gurobiLHSVars)
368 gurobiName = picosConstraint.name if picosConstraint.name else ""
369 gurobiQuadCon = self.int.addQConstr(quadExpr, gurobi.GRB.LESS_EQUAL,
370 gurobiRHSVars[0] * gurobiRHSVars[1], gurobiName)
372 gurobiMetaCon = self.GurobiRSOCC(
373 LHSVars=gurobiLHSVars, RHSVars=gurobiRHSVars, LHSCons=gurobiLHSCons,
374 RHSCons=gurobiRHSCons, quadCon=gurobiQuadCon)
376 return gurobiMetaCon
378 def _import_constraint(self, picosConstraint):
379 # Import constraint based on type.
380 if isinstance(picosConstraint, AffineConstraint):
381 self._gurobiLinearConstraints[picosConstraint] = \
382 self._import_linear_constraint(picosConstraint)
383 elif isinstance(picosConstraint, ConvexQuadraticConstraint):
384 self._gurobiQuadConstraint[picosConstraint] = \
385 self._import_quad_constraint(picosConstraint)
386 elif isinstance(picosConstraint, SOCConstraint):
387 self._gurobiSOCC[picosConstraint] = \
388 self._import_socone_constraint(picosConstraint)
389 elif isinstance(picosConstraint, RSOCConstraint):
390 self._gurobiRSOCC[picosConstraint] = \
391 self._import_rscone_constraint(picosConstraint)
392 else:
393 assert isinstance(picosConstraint, DummyConstraint), \
394 "Unexpected constraint type: {}".format(
395 picosConstraint.__class__.__name__)
397 def _remove_constraint(self, picosConstraint):
398 if isinstance(picosConstraint, AffineConstraint):
399 self.int.remove(
400 self._gurobiLinearConstraints.pop(picosConstraint))
401 elif isinstance(picosConstraint, ConvexQuadraticConstraint):
402 self.int.remove(
403 self._gurobiQuadConstraint.pop(picosConstraint))
404 elif isinstance(picosConstraint, SOCConstraint):
405 c = self._gurobiSOCC.pop(picosConstraint)
406 self.int.remove(c.gurobiLHSCons + [c.gurobiRHSCon]
407 + [c.gurobiQuadCon] + c.gurobiLHSVars + [c.gurobiRHSVar])
408 elif isinstance(picosConstraint, RSOCConstraint):
409 c = self._gurobiRSOCC.pop(picosConstraint)
410 self.int.remove(c.gurobiLHSCons + c.gurobiRHSCons
411 + [c.gurobiQuadCon] + c.gurobiLHSVars + c.gurobiRHSVars)
412 else:
413 assert isinstance(picosConstraint, DummyConstraint), \
414 "Unexpected constraint type: {}".format(
415 picosConstraint.__class__.__name__)
417 def _import_objective(self):
418 import gurobipy as gurobi
420 picosSense, picosObjective = self.ext.no
422 # Retrieve objective sense.
423 if picosSense == "min":
424 gurobiSense = gurobi.GRB.MINIMIZE
425 else:
426 assert picosSense == "max"
427 gurobiSense = gurobi.GRB.MAXIMIZE
429 # Retrieve objective function.
430 if isinstance(picosObjective, AffineExpression):
431 gurobiObjective = self._scalar_affinexp_pic2grb(picosObjective)
432 else:
433 assert isinstance(picosObjective, QuadraticExpression)
434 gurobiObjective = self._quadexp_pic2grb(picosObjective)
436 self.int.setObjective(gurobiObjective, gurobiSense)
438 def _import_problem(self):
439 import gurobipy as gurobi
441 # Create a problem instance.
442 if self._license_warnings:
443 self.int = gurobi.Model()
444 else:
445 with self._enforced_verbosity():
446 self.int = gurobi.Model()
448 # Import variables.
449 for variable in self.ext.variables.values():
450 self._import_variable(variable)
452 # Import constraints.
453 for constraint in self.ext.constraints.values():
454 self._import_constraint(constraint)
456 # Set objective.
457 self._import_objective()
459 def _update_problem(self):
460 for oldConstraint in self._removed_constraints():
461 self._remove_constraint(oldConstraint)
463 for oldVariable in self._removed_variables():
464 self._remove_variable(oldVariable)
466 for newVariable in self._new_variables():
467 self._import_variable(newVariable)
469 for newConstraint in self._new_constraints():
470 self._import_constraint(newConstraint)
472 if self._objective_has_changed():
473 self._import_objective()
475 def _solve(self):
476 import gurobipy as gurobi
478 # Reset options.
479 # NOTE: OutputFlag = 0 prevents resetParams from printing to console.
480 self.int.Params.OutputFlag = 0
481 self.int.resetParams()
483 # verbosity
484 self.int.Params.OutputFlag = 1 if self.verbosity() > 0 else 0
486 # abs_prim_fsb_tol
487 if self.ext.options.abs_prim_fsb_tol is not None:
488 self.int.Params.FeasibilityTol = self.ext.options.abs_prim_fsb_tol
490 # abs_dual_fsb_tol
491 if self.ext.options.abs_dual_fsb_tol is not None:
492 self.int.Params.OptimalityTol = self.ext.options.abs_dual_fsb_tol
494 # rel_ipm_opt_tol
495 if self.ext.options.rel_ipm_opt_tol is not None:
496 self.int.Params.BarConvTol = self.ext.options.rel_ipm_opt_tol
498 # HACK: Work around low precision (conic) quadratic duals.
499 self.int.Params.BarQCPConvTol = \
500 0.01 * self.ext.options.rel_ipm_opt_tol
502 # abs_bnb_opt_tol
503 if self.ext.options.abs_bnb_opt_tol is not None:
504 self.int.Params.MIPGapAbs = self.ext.options.abs_bnb_opt_tol
506 # rel_bnb_opt_tol
507 if self.ext.options.rel_bnb_opt_tol is not None:
508 self.int.Params.MIPGap = self.ext.options.rel_bnb_opt_tol
510 # integrality_tol
511 if self.ext.options.integrality_tol is not None:
512 self.int.Params.IntFeasTol = self.ext.options.integrality_tol
514 # markowitz_tol
515 if self.ext.options.markowitz_tol is not None:
516 self.int.Params.MarkowitzTol = self.ext.options.markowitz_tol
518 # max_iterations
519 if self.ext.options.max_iterations is not None:
520 self.int.Params.BarIterLimit = self.ext.options.max_iterations
521 self.int.Params.IterationLimit = self.ext.options.max_iterations
523 _lpm = {"interior": 2, "psimplex": 0, "dsimplex": 1}
525 # lp_node_method
526 if self.ext.options.lp_node_method is not None:
527 value = self.ext.options.lp_node_method
528 assert value in _lpm, "Unexpected lp_node_method value."
529 self.int.Params.SiftMethod = _lpm[value]
531 # lp_root_method
532 if self.ext.options.lp_root_method is not None:
533 value = self.ext.options.lp_root_method
534 assert value in _lpm, "Unexpected lp_root_method value."
535 self.int.Params.Method = _lpm[value]
537 # timelimit
538 if self.ext.options.timelimit is not None:
539 self.int.Params.TimeLimit = self.ext.options.timelimit
541 # max_fsb_nodes
542 if self.ext.options.max_fsb_nodes is not None:
543 self.int.Params.SolutionLimit = self.ext.options.max_fsb_nodes
545 # hotstart
546 if self.ext.options.hotstart:
547 self._import_variable_values()
548 else:
549 self._reset_variable_values()
551 # Handle Gurobi-specific options.
552 for key, value in self.ext.options.gurobi_params.items():
553 if not self.int.getParamInfo(key):
554 self._handle_bad_solver_specific_option_key(key)
556 try:
557 self.int.setParam(key, value)
558 except TypeError as error:
559 self._handle_bad_solver_specific_option_value(key, value, error)
561 # Handle unsupported options.
562 self._handle_unsupported_option("treememory")
564 # Compute duals also for QPs and QCPs.
565 if self.ext.options.duals is not False and self.ext.is_continuous():
566 self.int.setParam(gurobi.GRB.Param.QCPDual, 1)
568 # Attempt to solve the problem.
569 with self._header(), self._stopwatch():
570 try:
571 self.int.optimize()
572 except gurobi.GurobiError as error:
573 if error.errno == gurobi.GRB.Error.Q_NOT_PSD:
574 self._handle_continuous_nonconvex_error(error)
575 else:
576 raise
578 # Retrieve primals.
579 primals = {}
580 if self.ext.options.primals is not False:
581 for picosVar in self.ext.variables.values():
582 try:
583 value = []
584 for localIndex in range(picosVar.dim):
585 gurobiVar = self._gurobiVar[picosVar.id_at(localIndex)]
586 scalarValue = gurobiVar.getAttr(gurobi.GRB.Attr.X)
587 value.append(scalarValue)
588 except AttributeError:
589 primals[picosVar] = None
590 else:
591 primals[picosVar] = value
593 # Retrieve duals.
594 duals = {}
595 if self.ext.options.duals is not False and self.ext.is_continuous():
596 Pi = gurobi.GRB.Attr.Pi
598 for picosCon in self.ext.constraints.values():
599 if isinstance(picosCon, DummyConstraint):
600 duals[picosCon] = cvxopt.spmatrix([], [], [], picosCon.size)
601 continue
603 # HACK: Work around gurobiCon.getAttr(gurobi.GRB.Attr.Pi)
604 # printing a newline to console when it raises an
605 # AttributeError and OutputFlag is enabled. This is a
606 # WONTFIX on Gurobi's end (PICOS #264, Gurobi #14248).
607 oldOutput = self.int.Params.OutputFlag
608 self.int.Params.OutputFlag = 0
610 try:
611 if isinstance(picosCon, AffineConstraint):
612 gurobiCons = self._gurobiLinearConstraints[picosCon]
613 gurobiDuals = []
614 for gurobiCon in gurobiCons:
615 gurobiDuals.append(gurobiCon.getAttr(Pi))
616 picosDual = cvxopt.matrix(gurobiDuals, picosCon.size)
618 # Flip sign based on constraint relation.
619 if not picosCon.is_increasing():
620 picosDual = -picosDual
621 elif isinstance(picosCon, SOCConstraint):
622 gurobiMetaCon = self._gurobiSOCC[picosCon]
623 lb = gurobiMetaCon.RHSCon.getAttr(Pi)
624 z = [-constraint.getAttr(Pi)
625 for constraint in gurobiMetaCon.LHSCons]
626 picosDual = cvxopt.matrix([lb] + z)
627 elif isinstance(picosCon, RSOCConstraint):
628 gurobiMetaCon = self._gurobiRSOCC[picosCon]
629 ab = [constraint.getAttr(Pi)
630 for constraint in gurobiMetaCon.RHSCons]
631 z = [-constraint.getAttr(Pi)
632 for constraint in gurobiMetaCon.LHSCons]
633 picosDual = cvxopt.matrix(ab + z)
634 elif isinstance(picosCon, ConvexQuadraticConstraint):
635 picosDual = None
636 else:
637 assert isinstance(picosCon, DummyConstraint), \
638 "Unexpected constraint type: {}".format(
639 picosCon.__class__.__name__)
641 # Flip sign based on objective sense.
642 if picosDual and self.ext.no.direction == "min":
643 picosDual = -picosDual
644 except AttributeError:
645 duals[picosCon] = None
646 else:
647 duals[picosCon] = picosDual
649 # HACK: See above. Also: Silence Gurobi while enabling output.
650 if oldOutput != 0:
651 with self._enforced_verbosity(noStdOutAt=float("inf")):
652 self.int.Params.OutputFlag = oldOutput
654 # Retrieve objective value.
655 try:
656 value = self.int.getAttr(gurobi.GRB.Attr.ObjVal)
657 except AttributeError:
658 value = None
660 # Retrieve solution status.
661 statusCode = self.int.getAttr(gurobi.GRB.Attr.Status)
662 if statusCode == gurobi.GRB.Status.LOADED:
663 raise RuntimeError("Gurobi claims to have just loaded the problem "
664 "while PICOS expects the solution search to have terminated.")
665 elif statusCode == gurobi.GRB.Status.OPTIMAL:
666 primalStatus = SS_OPTIMAL
667 dualStatus = SS_OPTIMAL
668 problemStatus = PS_FEASIBLE
669 elif statusCode == gurobi.GRB.Status.INFEASIBLE:
670 primalStatus = SS_INFEASIBLE
671 dualStatus = SS_UNKNOWN
672 problemStatus = PS_INFEASIBLE
673 elif statusCode == gurobi.GRB.Status.INF_OR_UNBD:
674 primalStatus = SS_UNKNOWN
675 dualStatus = SS_UNKNOWN
676 problemStatus = PS_INF_OR_UNB
677 elif statusCode == gurobi.GRB.Status.UNBOUNDED:
678 primalStatus = SS_UNKNOWN
679 dualStatus = SS_INFEASIBLE
680 problemStatus = PS_UNBOUNDED
681 elif statusCode == gurobi.GRB.Status.CUTOFF:
682 # "Optimal objective for model was proven to be worse than the value
683 # specified in the Cutoff parameter. No solution information is
684 # available."
685 primalStatus = SS_PREMATURE
686 dualStatus = SS_PREMATURE
687 problemStatus = PS_UNKNOWN
688 elif statusCode == gurobi.GRB.Status.ITERATION_LIMIT:
689 primalStatus = SS_PREMATURE
690 dualStatus = SS_PREMATURE
691 problemStatus = PS_UNKNOWN
692 elif statusCode == gurobi.GRB.Status.NODE_LIMIT:
693 primalStatus = SS_PREMATURE
694 dualStatus = SS_EMPTY # Applies only to mixed integer problems.
695 problemStatus = PS_UNKNOWN
696 elif statusCode == gurobi.GRB.Status.TIME_LIMIT:
697 primalStatus = SS_PREMATURE
698 dualStatus = SS_PREMATURE
699 problemStatus = PS_UNKNOWN
700 elif statusCode == gurobi.GRB.Status.SOLUTION_LIMIT:
701 primalStatus = SS_PREMATURE
702 dualStatus = SS_PREMATURE
703 problemStatus = PS_UNKNOWN
704 elif statusCode == gurobi.GRB.Status.INTERRUPTED:
705 primalStatus = SS_PREMATURE
706 dualStatus = SS_PREMATURE
707 problemStatus = PS_UNKNOWN
708 elif statusCode == gurobi.GRB.Status.NUMERIC:
709 primalStatus = SS_UNKNOWN
710 dualStatus = SS_UNKNOWN
711 problemStatus = PS_UNSTABLE
712 elif statusCode == gurobi.GRB.Status.SUBOPTIMAL:
713 # "Unable to satisfy optimality tolerances; a sub-optimal solution
714 # is available."
715 primalStatus = SS_FEASIBLE
716 dualStatus = SS_FEASIBLE
717 problemStatus = PS_FEASIBLE
718 elif statusCode == gurobi.GRB.Status.INPROGRESS:
719 raise RuntimeError("Gurobi claims solution search to be 'in "
720 "progress' while PICOS expects it to have terminated.")
721 elif statusCode == gurobi.GRB.Status.USER_OBJ_LIMIT:
722 # "User specified an objective limit (a bound on either the best
723 # objective or the best bound), and that limit has been reached."
724 primalStatus = SS_FEASIBLE
725 dualStatus = SS_EMPTY # Applies only to mixed integer problems.
726 problemStatus = PS_FEASIBLE
727 else:
728 primalStatus = SS_UNKNOWN
729 dualStatus = SS_UNKNOWN
730 problemStatus = PS_UNKNOWN
732 return self._make_solution(
733 value, primals, duals, primalStatus, dualStatus, problemStatus)
736# --------------------------------------
737__all__ = api_end(_API_START, globals())