Skip to content

Commit

Permalink
Add status to lp solvers (and default one to all solvers)
Browse files Browse the repository at this point in the history
Gurobi and mathopt give information about solver status. As for minizinc
and cpsat, we make it available on the wrapper level.

As we use the same enumeration `StatusSolver`, we move it to `solver_do`
module. And thus add for all solvers a `status_solver` attribute, by
default equal to unknown and which is used by `is_optimal()` method.

We remove `get_status_solver()` method which was only returning
`self.status_solver`.
  • Loading branch information
nhuet authored and g-poveda committed Oct 1, 2024
1 parent 625fb4f commit 0f4e7c2
Show file tree
Hide file tree
Showing 12 changed files with 60 additions and 50 deletions.
28 changes: 1 addition & 27 deletions discrete_optimization/generic_tools/cp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
CallbackList,
)
from discrete_optimization.generic_tools.do_problem import Solution
from discrete_optimization.generic_tools.do_solver import SolverDO
from discrete_optimization.generic_tools.do_solver import SolverDO, StatusSolver
from discrete_optimization.generic_tools.exceptions import SolveEarlyStop
from discrete_optimization.generic_tools.hyperparameters.hyperparameter import (
EnumHyperparameter,
Expand Down Expand Up @@ -180,13 +180,6 @@ class SignEnum(Enum):
UP = ">"


class StatusSolver(Enum):
SATISFIED = "SATISFIED"
UNSATISFIABLE = "UNSATISFIABLE"
OPTIMAL = "OPTIMAL"
UNKNOWN = "UNKNOWN"


map_mzn_status_to_do_status: dict[Status, StatusSolver] = {
Status.SATISFIED: StatusSolver.SATISFIED,
Status.UNSATISFIABLE: StatusSolver.UNSATISFIABLE,
Expand All @@ -200,22 +193,6 @@ class CPSolver(SolverDO):
Additional function to be implemented by a CP Solver.
"""

status_solver: Optional[StatusSolver] = None

def is_optimal(self) -> Optional[bool]:
"""Tell if found solution is supposed to be optimal.
To be called after a solve.
Returns:
optimality of the solution. If information missing, returns None instead.
"""
if self.status_solver is None or self.status_solver == StatusSolver.UNKNOWN:
return None
else:
return self.status_solver == StatusSolver.OPTIMAL

@abstractmethod
def init_model(self, **args: Any) -> None:
"""
Expand All @@ -235,9 +212,6 @@ def solve(
) -> ResultStorage:
...

def get_status_solver(self) -> Union[StatusSolver, None]:
return self.status_solver


class MinizincCPSolver(CPSolver):
"""CP solver wrapping a minizinc solver."""
Expand Down
14 changes: 13 additions & 1 deletion discrete_optimization/generic_tools/do_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from abc import ABC, abstractmethod
from collections.abc import Iterable
from enum import Enum
from typing import Any, Optional

from discrete_optimization.generic_tools.callbacks.callback import Callback
Expand All @@ -28,10 +29,18 @@
)


class StatusSolver(Enum):
SATISFIED = "SATISFIED"
UNSATISFIABLE = "UNSATISFIABLE"
OPTIMAL = "OPTIMAL"
UNKNOWN = "UNKNOWN"


class SolverDO(Hyperparametrizable, ABC):
"""Base class for a discrete-optimization solver."""

problem: Problem
status_solver: StatusSolver = StatusSolver.UNKNOWN

def __init__(
self,
Expand Down Expand Up @@ -102,7 +111,10 @@ def is_optimal(self) -> Optional[bool]:
optimality of the solution. If information missing, returns None instead.
"""
return None
if self.status_solver == StatusSolver.UNKNOWN:
return None
else:
return self.status_solver == StatusSolver.OPTIMAL

def get_model_objectives_available(self) -> list[str]:
"""List objectives available for lexico optimization
Expand Down
36 changes: 32 additions & 4 deletions discrete_optimization/generic_tools/lp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import datetime
import logging
from abc import abstractmethod
from collections import defaultdict
from collections.abc import Callable
from enum import Enum
from typing import Any, Optional, Union
Expand All @@ -23,7 +24,11 @@
Problem,
Solution,
)
from discrete_optimization.generic_tools.do_solver import SolverDO, WarmstartMixin
from discrete_optimization.generic_tools.do_solver import (
SolverDO,
StatusSolver,
WarmstartMixin,
)
from discrete_optimization.generic_tools.exceptions import SolveEarlyStop
from discrete_optimization.generic_tools.hyperparameters.hyperparameter import (
EnumHyperparameter,
Expand All @@ -41,7 +46,14 @@
gurobi_available = False
else:
gurobi_available = True
GRB = gurobipy.GRB
map_gurobi_status_to_do_status: dict[int, StatusSolver] = defaultdict(
lambda: StatusSolver.UNKNOWN,
{
gurobipy.GRB.status.OPTIMAL: StatusSolver.OPTIMAL,
gurobipy.GRB.status.INFEASIBLE: StatusSolver.UNSATISFIABLE,
gurobipy.GRB.status.SUBOPTIMAL: StatusSolver.SATISFIED,
},
)

try:
import docplex
Expand Down Expand Up @@ -511,6 +523,8 @@ def optimize_model(
cb=mathopt_cb,
)
self.termination = mathopt_res.termination
self.status_solver = map_mathopt_status_to_do_status[self.termination.reason]

logger.info(f"Solver found {len(mathopt_res.solutions)} solutions")
if mathopt_res.termination.reason in [
mathopt.TerminationReason.OPTIMAL,
Expand All @@ -520,6 +534,19 @@ def optimize_model(
return mathopt_res


map_mathopt_status_to_do_status: dict[mathopt.TerminationReason, StatusSolver] = {
mathopt.TerminationReason.OPTIMAL: StatusSolver.OPTIMAL,
mathopt.TerminationReason.INFEASIBLE: StatusSolver.UNSATISFIABLE,
mathopt.TerminationReason.INFEASIBLE_OR_UNBOUNDED: StatusSolver.UNKNOWN,
mathopt.TerminationReason.UNBOUNDED: StatusSolver.UNKNOWN,
mathopt.TerminationReason.FEASIBLE: StatusSolver.SATISFIED,
mathopt.TerminationReason.NO_SOLUTION_FOUND: StatusSolver.UNSATISFIABLE,
mathopt.TerminationReason.IMPRECISE: StatusSolver.UNKNOWN,
mathopt.TerminationReason.NUMERICAL_ERROR: StatusSolver.UNKNOWN,
mathopt.TerminationReason.OTHER_ERROR: StatusSolver.UNKNOWN,
}


def _mathopt_cb_get_obj_value_for_current_solution():
raise RuntimeError("Cannot retrieve objective!")

Expand Down Expand Up @@ -652,6 +679,7 @@ def optimize_model(
parameters_milp=parameters_milp, time_limit=time_limit, **kwargs
)
self.model.optimize()
self.status_solver = map_gurobi_status_to_do_status[self.model.Status]

logger.info(f"Problem has {self.model.NumObj} objectives")
logger.info(f"Solver found {self.model.SolCount} solutions")
Expand Down Expand Up @@ -724,13 +752,13 @@ def __init__(self, do_solver: GurobiMilpSolver, callback: Callback):
self.nb_solutions = 0

def __call__(self, model, where) -> None:
if where == GRB.Callback.MIPSOL:
if where == gurobipy.GRB.Callback.MIPSOL:
try:
# retrieve and store new solution
sol = self.do_solver.retrieve_current_solution(
get_var_value_for_current_solution=model.cbGetSolution,
get_obj_value_for_current_solution=lambda: model.cbGet(
GRB.Callback.MIPSOL_OBJ
gurobipy.GRB.Callback.MIPSOL_OBJ
),
)
fit = self.do_solver.aggreg_from_sol(sol)
Expand Down
7 changes: 2 additions & 5 deletions discrete_optimization/generic_tools/ortools_cpsat_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,9 @@
Callback,
CallbackList,
)
from discrete_optimization.generic_tools.cp_tools import (
CPSolver,
ParametersCP,
StatusSolver,
)
from discrete_optimization.generic_tools.cp_tools import CPSolver, ParametersCP
from discrete_optimization.generic_tools.do_problem import Solution
from discrete_optimization.generic_tools.do_solver import StatusSolver
from discrete_optimization.generic_tools.exceptions import SolveEarlyStop
from discrete_optimization.generic_tools.result_storage.result_storage import (
ResultStorage,
Expand Down
3 changes: 1 addition & 2 deletions discrete_optimization/rcpsp/solver/cpsat_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@
ObjLinearExprT,
)

from discrete_optimization.generic_tools.cp_tools import StatusSolver
from discrete_optimization.generic_tools.do_problem import ParamsObjectiveFunction
from discrete_optimization.generic_tools.do_solver import WarmstartMixin
from discrete_optimization.generic_tools.do_solver import StatusSolver, WarmstartMixin
from discrete_optimization.generic_tools.hyperparameters.hyperparameter import (
CategoricalHyperparameter,
)
Expand Down
4 changes: 2 additions & 2 deletions examples/coloring/coloring_cpspat_solver_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def run_cpsat_coloring():
callbacks=[NbIterationTracker(step_verbosity_level=logging.INFO)],
parameters_cp=p,
)
print("Status solver : ", solver.get_status_solver())
print("Status solver : ", solver.status_solver)
solution, fit = result_store.get_best_solution_fit()
plot_coloring_solution(solution)
plt.show()
Expand All @@ -65,7 +65,7 @@ def run_cpsat_coloring_with_constraints():
p = ParametersCP.default()
result_store = solver.solve(parameters_cp=p, time_limit=20)
solution, fit = result_store.get_best_solution_fit()
print("Status solver : ", solver.get_status_solver())
print("Status solver : ", solver.status_solver)
plot_coloring_solution(solution)
plt.show()
print(solution, fit)
Expand Down
2 changes: 1 addition & 1 deletion examples/jsp/jsp_cpsat_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def run_cpsat_jsp():
p.nb_process = 10
res = solver.solve(parameters_cp=p, time_limit=10)
sol = res.get_best_solution_fit()[0]
print(solver.get_status_solver())
print(solver.status_solver)
assert problem.satisfy(sol)
print(problem.evaluate(sol))

Expand Down
2 changes: 1 addition & 1 deletion examples/knapsack/knapsack_cpsat_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def run_cpsat_knapsack():
params_cp = ParametersCP.default()
result_storage = cp_model.solve(parameters_cp=params_cp, time_limit=10)
sol, fit = result_storage.get_best_solution_fit()
print("Status solver :", cp_model.get_status_solver())
print("Status solver :", cp_model.status_solver)
assert knapsack_model.satisfy(sol)


Expand Down
4 changes: 2 additions & 2 deletions examples/maximum_independent_set/lns_cpsat_mis.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def run_lns():
)
sol, fit = res.get_best_solution_fit()
print("Found by cpsat :", fit)
print("Status : ", solver.get_status_solver())
print("Status : ", solver.status_solver)

initial_solution_provider = TrivialInitialSolution(
solver.create_result_storage(
Expand Down Expand Up @@ -106,7 +106,7 @@ def run_lns_mix():
)
sol, fit = res.get_best_solution_fit()
print("Found by cpsat :", fit)
print("Status : ", solver.get_status_solver())
print("Status : ", solver.status_solver)

initial_solution_provider = TrivialInitialSolution(
solver.create_result_storage(
Expand Down
2 changes: 1 addition & 1 deletion examples/rcpsp_multiskill/mslib/mslib_parse_and_solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def example_mslib_cpsat():
],
ortools_cpsat_solver_kwargs={"log_search_progress": True},
)
print(solver.get_status_solver())
print(solver.status_solver)
from discrete_optimization.rcpsp_multiskill.plots.plot_solution import (
plot_resource_individual_gantt,
)
Expand Down
4 changes: 2 additions & 2 deletions examples/vrp/vrp_cpsat_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def run_cpsat_vrp():
p = ParametersCP.default_cpsat()
p.nb_process = 10
res = solver.solve(parameters_cp=p, time_limit=20)
print(solver.get_status_solver())
print(solver.status_solver)
sol, fit = res.get_best_solution_fit()
sol: VrpSolution
print(problem.evaluate(sol))
Expand Down Expand Up @@ -111,7 +111,7 @@ def warm_starting():
fix_variables_to_their_hinted_value=False, log_search_progress=True
),
)
print(solver.get_status_solver())
print(solver.status_solver)
# assert res[0][0].list_paths == start_solution.list_paths


Expand Down
4 changes: 2 additions & 2 deletions tests/coloring/test_coloring_lns_cpsat.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_lns_cpsat_coloring():
parameters_cp=p,
time_limit_subsolver=20,
)
print("Status solver : ", solver.get_status_solver())
print("Status solver : ", solver.status_solver)
solution, fit = result_store.get_best_solution_fit()
# plot_coloring_solution(solution)
# plt.show()
Expand Down Expand Up @@ -128,7 +128,7 @@ def test_lns_cpsat_coloring_with_constraints():
],
parameters_cp=p,
)
print("Status solver : ", solver.get_status_solver())
print("Status solver : ", solver.status_solver)
solution, fit = result_store.get_best_solution_fit()
# plot_coloring_solution(solution)
# plt.show()
Expand Down

0 comments on commit 0f4e7c2

Please sign in to comment.