From b0856c2c2ae9e1c4e2d3df5f0637363129743896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Gonz=C3=A1lez-Santander=20de=20la=20Cruz?= Date: Fri, 12 Jan 2024 10:22:20 +0100 Subject: [PATCH] Change in highs api and status handling in both cases (#682) * Change in highs api and status handling in both cases * Use a mapping dictionary with the tuples * Small change * Added new test to take into account that a time limit can be reached without solution. Test available for HiGHS and CBC for now * Wrong mapping --------- Co-authored-by: pchtsp --- pulp/apis/highs_api.py | 104 ++++++++++++++++++++++++++++++++-------- pulp/tests/test_pulp.py | 21 +++++++- 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/pulp/apis/highs_api.py b/pulp/apis/highs_api.py index 4948dd54..5491a644 100644 --- a/pulp/apis/highs_api.py +++ b/pulp/apis/highs_api.py @@ -1,5 +1,6 @@ # PuLP : Python LP Modeler # Version 2.4 +from math import inf # Copyright (c) 2002-2005, Jean-Sebastien Roy (js@jeannot.org) # Modifications Copyright (c) 2007- Stuart Anthony Mitchell (s.mitchell@auckland.ac.nz) @@ -186,12 +187,12 @@ def actualSolve(self, lp): elif model_status.lower() == "infeasible": # infeasible status, status_sol = ( constants.LpStatusInfeasible, - constants.LpSolutionNoSolutionFound, + constants.LpSolutionInfeasible, ) elif model_status.lower() == "unbounded": # unbounded status, status_sol = ( constants.LpStatusUnbounded, - constants.LpSolutionNoSolutionFound, + constants.LpSolutionUnbounded, ) else: # no solution status, status_sol = ( @@ -390,39 +391,98 @@ def buildSolverModel(self, lp): def findSolutionValues(self, lp): status = lp.solverModel.getModelStatus() + obj_value = lp.solverModel.getObjectiveValue() + solution = lp.solverModel.getSolution() HighsModelStatus = highspy.HighsModelStatus + status_dict = { - HighsModelStatus.kNotset: constants.LpStatusNotSolved, - HighsModelStatus.kLoadError: constants.LpStatusNotSolved, - HighsModelStatus.kModelError: constants.LpStatusNotSolved, - HighsModelStatus.kPresolveError: constants.LpStatusNotSolved, - HighsModelStatus.kSolveError: constants.LpStatusNotSolved, - HighsModelStatus.kPostsolveError: constants.LpStatusNotSolved, - HighsModelStatus.kModelEmpty: constants.LpStatusNotSolved, - HighsModelStatus.kOptimal: constants.LpStatusOptimal, - HighsModelStatus.kInfeasible: constants.LpStatusInfeasible, - HighsModelStatus.kUnboundedOrInfeasible: constants.LpStatusInfeasible, - HighsModelStatus.kUnbounded: constants.LpStatusUnbounded, - HighsModelStatus.kObjectiveBound: constants.LpStatusNotSolved, - HighsModelStatus.kObjectiveTarget: constants.LpStatusNotSolved, - HighsModelStatus.kTimeLimit: constants.LpStatusNotSolved, - HighsModelStatus.kIterationLimit: constants.LpStatusNotSolved, - HighsModelStatus.kUnknown: constants.LpStatusNotSolved, + HighsModelStatus.kNotset: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kLoadError: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kModelError: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kPresolveError: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kSolveError: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kPostsolveError: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kModelEmpty: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), + HighsModelStatus.kOptimal: ( + constants.LpStatusOptimal, + constants.LpSolutionOptimal, + ), + HighsModelStatus.kInfeasible: ( + constants.LpStatusInfeasible, + constants.LpSolutionInfeasible, + ), + HighsModelStatus.kUnboundedOrInfeasible: ( + constants.LpStatusInfeasible, + constants.LpSolutionInfeasible, + ), + HighsModelStatus.kUnbounded: ( + constants.LpStatusUnbounded, + constants.LpSolutionUnbounded, + ), + HighsModelStatus.kObjectiveBound: ( + constants.LpStatusOptimal, + constants.LpSolutionIntegerFeasible, + ), + HighsModelStatus.kObjectiveTarget: ( + constants.LpStatusOptimal, + constants.LpSolutionIntegerFeasible, + ), + HighsModelStatus.kTimeLimit: ( + constants.LpStatusOptimal, + constants.LpSolutionIntegerFeasible, + ), + HighsModelStatus.kIterationLimit: ( + constants.LpStatusOptimal, + constants.LpSolutionIntegerFeasible, + ), + HighsModelStatus.kUnknown: ( + constants.LpStatusNotSolved, + constants.LpSolutionNoSolutionFound, + ), } col_values = list(solution.col_value) + + # Assign values to the variables as with lp.assignVarsVals() for var in lp.variables(): var.varValue = col_values[var.index] - return status_dict[status] + if obj_value == float(inf) and status in ( + HighsModelStatus.kTimeLimit, + HighsModelStatus.kIterationLimit, + ): + return constants.LpStatusNotSolved, constants.LpSolutionNoSolutionFound + else: + return status_dict[status] def actualSolve(self, lp): self.createAndConfigureSolver(lp) self.buildSolverModel(lp) self.callSolver(lp) - solutionStatus = self.findSolutionValues(lp) + status, sol_status = self.findSolutionValues(lp) for var in lp.variables(): var.modified = False @@ -430,7 +490,9 @@ def actualSolve(self, lp): for constraint in lp.constraints.values(): constraint.modifier = False - return solutionStatus + lp.assignStatus(status, sol_status) + + return status def actualResolve(self, lp, **kwargs): raise PulpSolverError("HiGHS: Resolving is not supported") diff --git a/pulp/tests/test_pulp.py b/pulp/tests/test_pulp.py index 49f3948a..b70dba94 100644 --- a/pulp/tests/test_pulp.py +++ b/pulp/tests/test_pulp.py @@ -1291,7 +1291,8 @@ def test_measuring_solving_time(self): return prob = create_bin_packing_problem(bins=bins, seed=99) self.solver.timeLimit = time_limit - prob.solve(self.solver) + status = prob.solve(self.solver) + delta = 20 reported_time = prob.solutionTime if self.solver.name in ["PULP_CBC_CMD", "COIN_CMD"]: @@ -1304,9 +1305,27 @@ def test_measuring_solving_time(self): msg=f"optimization time for solver {self.solver.name}", ) self.assertTrue(prob.objective.value() is not None) + self.assertEqual(status, const.LpStatusOptimal) for v in prob.variables(): self.assertTrue(v.varValue is not None) + @gurobi_test + def test_time_limit_no_solution(self): + print("\t Test time limit with no solution") + + time_limit = 1 + solver_settings = dict(HiGHS=50, PULP_CBC_CMD=30, COIN_CMD=30) + bins = solver_settings.get(self.solver.name) + if bins is None: + # not all solvers have timeLimit support + return + prob = create_bin_packing_problem(bins=bins, seed=99) + self.solver.timeLimit = time_limit + status = prob.solve(self.solver) + self.assertEqual(prob.status, const.LpStatusNotSolved) + self.assertEqual(status, const.LpStatusNotSolved) + self.assertEqual(prob.sol_status, const.LpSolutionNoSolutionFound) + def test_invalid_var_names(self): prob = LpProblem(self._testMethodName, const.LpMinimize) x = LpVariable("a")