From 6a8e4631fdc09575834b2017e339c74017a5ac07 Mon Sep 17 00:00:00 2001 From: Maciej Date: Sat, 15 Jul 2023 11:05:35 +0200 Subject: [PATCH] Add more config parameters to HiGHS python API + set up logging (#606) * Add callback tuple + recognise common options * Simplify options handling * last touch-ups --- pulp/apis/highs_api.py | 51 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/pulp/apis/highs_api.py b/pulp/apis/highs_api.py index 062fb6e5..22048f9f 100644 --- a/pulp/apis/highs_api.py +++ b/pulp/apis/highs_api.py @@ -248,17 +248,41 @@ def actualSolve(self, lp, callback=None): raise PulpSolverError("HiGHS: Not Available") else: + # Note(maciej): It was surprising to me that higshpy wasn't logging out of the box, + # even with the different logging options set. This callback seems to work, but there + # are probably better ways of doing this ¯\_(ツ)_/¯ + DEFAULT_CALLBACK = lambda logType, logMsg, callbackValue: print( + f"[{logType.name}] {logMsg}" + ) + DEFAULT_CALLBACK_VALUE = "" def __init__( self, mip=True, msg=True, + callbackTuple=None, + gapAbs=None, + gapRel=None, + threads=None, timeLimit=None, - warmStart=False, - logPath=None, **solverParams, ): - super().__init__(mip, msg, timeLimit=timeLimit, **solverParams) + """ + :param bool mip: if False, assume LP even if integer variables + :param bool msg: if False, no log is shown + :param tuple callbackTuple: Tuple of log callback function (see DEFAULT_CALLBACK above for definition) + and callbackValue (tag embedded in every callback) + :param float gapRel: relative gap tolerance for the solver to stop (in fraction) + :param float gapAbs: absolute gap tolerance for the solver to stop + :param int threads: sets the maximum number of threads + :param float timeLimit: maximum time for solver (in seconds) + :param dict solverParams: list of named options to pass directly to the HiGHS solver + """ + super().__init__(mip=mip, msg=msg, timeLimit=timeLimit, **solverParams) + self.callbackTuple = callbackTuple + self.gapAbs = gapAbs + self.gapRel = gapRel + self.threads = threads def available(self): return True @@ -266,11 +290,24 @@ def available(self): def callSolver(self, lp): lp.solverModel.run() - def buildSolverModel(self, lp): + def createAndConfigureSolver(self, lp): lp.solverModel = highspy.Highs() - gapRel = self.optionsDict.get("gapRel", 0) - lp.solverModel.setOptionValue("mip_rel_gap", gapRel) + if self.msg or self.callbackTuple: + callbackTuple = self.callbackTuple or ( + HiGHS.DEFAULT_CALLBACK, + HiGHS.DEFAULT_CALLBACK_VALUE, + ) + lp.solverModel.setLogCallback(*callbackTuple) + + if self.gapRel is not None: + lp.solverModel.setOptionValue("mip_rel_gap", self.gapRel) + + if self.gapAbs is not None: + lp.solverModel.setOptionValue("mip_abs_gap", self.gapAbs) + + if self.threads is not None: + lp.solverModel.setOptionValue("threads", self.threads) if self.timeLimit is not None: lp.solverModel.setOptionValue("time_limit", float(self.timeLimit)) @@ -279,6 +316,7 @@ def buildSolverModel(self, lp): for key, value in self.optionsDict.items(): lp.solverModel.setOptionValue(key, value) + def buildSolverModel(self, lp): inf = highspy.kHighsInf obj_mult = -1 if lp.sense == constants.LpMaximize else 1 @@ -353,6 +391,7 @@ def findSolutionValues(self, lp): return status_dict[status] def actualSolve(self, lp): + self.createAndConfigureSolver(lp) self.buildSolverModel(lp) self.callSolver(lp)