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