1# PuLP : Python LP Modeler 2# Version 1.4.2 3 4# Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) 5# Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) 6# $Id:solvers.py 1791 2008-04-23 22:54:34Z smit023 $ 7 8# Permission is hereby granted, free of charge, to any person obtaining a 9# copy of this software and associated documentation files (the 10# "Software"), to deal in the Software without restriction, including 11# without limitation the rights to use, copy, modify, merge, publish, 12# distribute, sublicense, and/or sell copies of the Software, and to 13# permit persons to whom the Software is furnished to do so, subject to 14# the following conditions: 15 16# The above copyright notice and this permission notice shall be included 17# in all copies or substantial portions of the Software. 18 19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 22# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 24# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 25# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" 26 27from .core import LpSolver, PulpSolverError 28from .. import constants 29import sys 30 31 32class MOSEK(LpSolver): 33 """Mosek lp and mip solver (via Mosek Optimizer API).""" 34 35 name = "MOSEK" 36 try: 37 global mosek 38 import mosek 39 40 env = mosek.Env() 41 except ImportError: 42 43 def available(self): 44 """True if Mosek is available.""" 45 return False 46 47 def actualSolve(self, lp, callback=None): 48 """Solves a well-formulated lp problem.""" 49 raise PulpSolverError("MOSEK : Not Available") 50 51 else: 52 53 def __init__( 54 self, 55 mip=True, 56 msg=True, 57 options={}, 58 task_file_name="", 59 sol_type=mosek.soltype.bas, 60 ): 61 """Initializes the Mosek solver. 62 63 Keyword arguments: 64 65 @param mip: If False, then solve MIP as LP. 66 67 @param msg: Enable Mosek log output. 68 69 @param options: Accepts a dictionary of Mosek solver parameters. Ignore to 70 use default parameter values. Eg: options = {mosek.dparam.mio_max_time:30} 71 sets the maximum time spent by the Mixed Integer optimizer to 30 seconds. 72 Equivalently, one could also write: options = {"MSK_DPAR_MIO_MAX_TIME":30} 73 which uses the generic parameter name as used within the solver, instead of 74 using an object from the Mosek Optimizer API (Python), as before. 75 76 @param task_file_name: Writes a Mosek task file of the given name. By default, 77 no task file will be written. Eg: task_file_name = "eg1.opf". 78 79 @param sol_type: Mosek supports three types of solutions: mosek.soltype.bas 80 (Basic solution, default), mosek.soltype.itr (Interior-point 81 solution) and mosek.soltype.itg (Integer solution). 82 83 For a full list of Mosek parameters (for the Mosek Optimizer API) and supported task file 84 formats, please see https://docs.mosek.com/9.1/pythonapi/parameters.html#doc-all-parameter-list. 85 """ 86 self.mip = mip 87 self.msg = msg 88 self.task_file_name = task_file_name 89 self.solution_type = sol_type 90 self.options = options 91 92 def available(self): 93 """True if Mosek is available.""" 94 return True 95 96 def setOutStream(self, text): 97 """Sets the log-output stream.""" 98 sys.stdout.write(text) 99 sys.stdout.flush() 100 101 def buildSolverModel(self, lp, inf=1e20): 102 """Translate the problem into a Mosek task object.""" 103 self.cons = lp.constraints 104 self.numcons = len(self.cons) 105 self.cons_dict = {} 106 i = 0 107 for c in self.cons: 108 self.cons_dict[c] = i 109 i = i + 1 110 self.vars = list(lp.variables()) 111 self.numvars = len(self.vars) 112 self.var_dict = {} 113 # Checking for repeated names 114 lp.checkDuplicateVars() 115 self.task = MOSEK.env.Task() 116 self.task.appendcons(self.numcons) 117 self.task.appendvars(self.numvars) 118 if self.msg: 119 self.task.set_Stream(mosek.streamtype.log, self.setOutStream) 120 # Adding variables 121 for i in range(self.numvars): 122 vname = self.vars[i].name 123 self.var_dict[vname] = i 124 self.task.putvarname(i, vname) 125 # Variable type (Default: Continuous) 126 if self.mip & (self.vars[i].cat == constants.LpInteger): 127 self.task.putvartype(i, mosek.variabletype.type_int) 128 self.solution_type = mosek.soltype.itg 129 # Variable bounds 130 vbkey = mosek.boundkey.fr 131 vup = inf 132 vlow = -inf 133 if self.vars[i].lowBound != None: 134 vlow = self.vars[i].lowBound 135 if self.vars[i].upBound != None: 136 vup = self.vars[i].upBound 137 vbkey = mosek.boundkey.ra 138 else: 139 vbkey = mosek.boundkey.lo 140 elif self.vars[i].upBound != None: 141 vup = self.vars[i].upBound 142 vbkey = mosek.boundkey.up 143 self.task.putvarbound(i, vbkey, vlow, vup) 144 # Objective coefficient for the current variable. 145 self.task.putcj(i, lp.objective.get(self.vars[i], 0.0)) 146 # Coefficient matrix 147 self.A_rows, self.A_cols, self.A_vals = zip( 148 *[ 149 [self.cons_dict[row], self.var_dict[col], coeff] 150 for col, row, coeff in lp.coefficients() 151 ] 152 ) 153 self.task.putaijlist(self.A_rows, self.A_cols, self.A_vals) 154 # Constraints 155 self.constraint_data_list = [] 156 for c in self.cons: 157 cname = self.cons[c].name 158 if cname != None: 159 self.task.putconname(self.cons_dict[c], cname) 160 else: 161 self.task.putconname(self.cons_dict[c], c) 162 csense = self.cons[c].sense 163 cconst = -self.cons[c].constant 164 clow = -inf 165 cup = inf 166 # Constraint bounds 167 if csense == constants.LpConstraintEQ: 168 cbkey = mosek.boundkey.fx 169 clow = cconst 170 cup = cconst 171 elif csense == constants.LpConstraintGE: 172 cbkey = mosek.boundkey.lo 173 clow = cconst 174 elif csense == constants.LpConstraintLE: 175 cbkey = mosek.boundkey.up 176 cup = cconst 177 else: 178 raise PulpSolverError("Invalid constraint type.") 179 self.constraint_data_list.append([self.cons_dict[c], cbkey, clow, cup]) 180 self.cons_id_list, self.cbkey_list, self.clow_list, self.cup_list = zip( 181 *self.constraint_data_list 182 ) 183 self.task.putconboundlist( 184 self.cons_id_list, self.cbkey_list, self.clow_list, self.cup_list 185 ) 186 # Objective sense 187 if lp.sense == constants.LpMaximize: 188 self.task.putobjsense(mosek.objsense.maximize) 189 else: 190 self.task.putobjsense(mosek.objsense.minimize) 191 192 def findSolutionValues(self, lp): 193 """ 194 Read the solution values and status from the Mosek task object. Note: Since the status 195 map from mosek.solsta to LpStatus is not exact, it is recommended that one enables the 196 log output and then refer to Mosek documentation for a better understanding of the 197 solution (especially in the case of mip problems). 198 """ 199 self.solsta = self.task.getsolsta(self.solution_type) 200 self.solution_status_dict = { 201 mosek.solsta.optimal: constants.LpStatusOptimal, 202 mosek.solsta.prim_infeas_cer: constants.LpStatusInfeasible, 203 mosek.solsta.dual_infeas_cer: constants.LpStatusUnbounded, 204 mosek.solsta.unknown: constants.LpStatusUndefined, 205 mosek.solsta.integer_optimal: constants.LpStatusOptimal, 206 mosek.solsta.prim_illposed_cer: constants.LpStatusNotSolved, 207 mosek.solsta.dual_illposed_cer: constants.LpStatusNotSolved, 208 mosek.solsta.prim_feas: constants.LpStatusNotSolved, 209 mosek.solsta.dual_feas: constants.LpStatusNotSolved, 210 mosek.solsta.prim_and_dual_feas: constants.LpStatusNotSolved, 211 } 212 # Variable values. 213 try: 214 self.xx = [0.0] * self.numvars 215 self.task.getxx(self.solution_type, self.xx) 216 for var in lp.variables(): 217 var.varValue = self.xx[self.var_dict[var.name]] 218 except mosek.Error: 219 pass 220 # Constraint slack variables. 221 try: 222 self.xc = [0.0] * self.numcons 223 self.task.getxc(self.solution_type, self.xc) 224 for con in lp.constraints: 225 lp.constraints[con].slack = -( 226 self.cons[con].constant + self.xc[self.cons_dict[con]] 227 ) 228 except mosek.Error: 229 pass 230 # Reduced costs. 231 if self.solution_type != mosek.soltype.itg: 232 try: 233 self.x_rc = [0.0] * self.numvars 234 self.task.getreducedcosts( 235 self.solution_type, 0, self.numvars, self.x_rc 236 ) 237 for var in lp.variables(): 238 var.dj = self.x_rc[self.var_dict[var.name]] 239 except mosek.Error: 240 pass 241 # Constraint Pi variables. 242 try: 243 self.y = [0.0] * self.numcons 244 self.task.gety(self.solution_type, self.y) 245 for con in lp.constraints: 246 lp.constraints[con].pi = self.y[self.cons_dict[con]] 247 except mosek.Error: 248 pass 249 250 def putparam(self, par, val): 251 """ 252 Pass the values of valid parameters to Mosek. 253 """ 254 if isinstance(par, mosek.dparam): 255 self.task.putdouparam(par, val) 256 elif isinstance(par, mosek.iparam): 257 self.task.putintparam(par, val) 258 elif isinstance(par, mosek.sparam): 259 self.task.putstrparam(par, val) 260 elif isinstance(par, str): 261 if par.startswith("MSK_DPAR_"): 262 self.task.putnadouparam(par, val) 263 elif par.startswith("MSK_IPAR_"): 264 self.task.putnaintparam(par, val) 265 elif par.startswith("MSK_SPAR_"): 266 self.task.putnastrparam(par, val) 267 else: 268 raise PulpSolverError( 269 "Invalid MOSEK parameter: '{}'. Check MOSEK documentation for a list of valid parameters.".format( 270 par 271 ) 272 ) 273 274 def actualSolve(self, lp): 275 """ 276 Solve a well-formulated lp problem. 277 """ 278 self.buildSolverModel(lp) 279 # Set solver parameters 280 for msk_par in self.options: 281 self.putparam(msk_par, self.options[msk_par]) 282 # Task file 283 if self.task_file_name: 284 self.task.writedata(self.task_file_name) 285 # Optimize 286 self.task.optimize() 287 # Mosek solver log (default: standard output stream) 288 if self.msg: 289 self.task.solutionsummary(mosek.streamtype.msg) 290 self.findSolutionValues(lp) 291 lp.assignStatus(self.solution_status_dict[self.solsta]) 292 for var in lp.variables(): 293 var.modified = False 294 for con in lp.constraints.values(): 295 con.modified = False 296 return lp.status 297 298 def actualResolve(self, lp, inf=1e20, **kwargs): 299 """ 300 Modify constraints and re-solve an lp. The Mosek task object created in the first solve is used. 301 """ 302 for c in self.cons: 303 if self.cons[c].modified: 304 csense = self.cons[c].sense 305 cconst = -self.cons[c].constant 306 clow = -inf 307 cup = inf 308 # Constraint bounds 309 if csense == constants.LpConstraintEQ: 310 cbkey = mosek.boundkey.fx 311 clow = cconst 312 cup = cconst 313 elif csense == constants.LpConstraintGE: 314 cbkey = mosek.boundkey.lo 315 clow = cconst 316 elif csense == constants.LpConstraintLE: 317 cbkey = mosek.boundkey.up 318 cup = cconst 319 else: 320 raise PulpSolverError("Invalid constraint type.") 321 self.task.putconbound(self.cons_dict[c], cbkey, clow, cup) 322 # Re-solve 323 self.task.optimize() 324 self.findSolutionValues(lp) 325 lp.assignStatus(self.solution_status_dict[self.solsta]) 326 for var in lp.variables(): 327 var.modified = False 328 for con in lp.constraints.values(): 329 con.modified = False 330 return lp.status 331