Skip to content

Commit

Permalink
Gurobipy solver changes.
Browse files Browse the repository at this point in the history
Refactored the solver interface for Gurobipy to be a class instead of a
function returning a function. The solver is now initialized and used
as follows:

```
solver = GurobipySolver(problem)
result = solver.solve("f_1")
```
  • Loading branch information
gialmisi committed Apr 25, 2024
1 parent c022cdd commit 68555b4
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 85 deletions.
4 changes: 2 additions & 2 deletions desdeo/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"add_stom_sf_nondiff",
"add_weighted_sums",
"available_nevergrad_optimizers",
"create_gurobipy_solver",
"GurobipySolver",
"create_ng_generic_solver",
"create_pyomo_bonmin_solver",
"create_pyomo_ipopt_solver",
Expand All @@ -37,7 +37,7 @@
from desdeo.tools.generics import CreateSolverType, SolverOptions, SolverResults
from desdeo.tools.gurobipy_solver_interfaces import (
PersistentGurobipySolver,
create_gurobipy_solver,
GurobipySolver,
)
from desdeo.tools.ng_solver_interfaces import (
NevergradGenericOptions,
Expand Down
100 changes: 51 additions & 49 deletions desdeo/tools/gurobipy_solver_interfaces.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Defines solver interfaces for gurobipy."""

from collections.abc import Callable

import gurobipy as gp

from desdeo.problem import Constraint, GurobipyEvaluator, Objective, Problem, ScalarizationFunction, Variable
Expand All @@ -10,14 +8,13 @@
# forward typehints
create_gurobipy_solver: CreateSolverType

def parse_gurobipy_optimizer_results(
problem: Problem, evaluator: GurobipyEvaluator
) -> SolverResults:
"""Parses results from GurobipyEvaluator's model into DESDEO SolverResutls.

def parse_gurobipy_optimizer_results(problem: Problem, evaluator: GurobipyEvaluator) -> SolverResults:
"""Parses results from GurobipyEvaluator's model into DESDEO SolverResults.
Args:
problem (Problem): the problem being solved.
evaluator (GurobipyEvaluator): the evalutor utilized to solve the problem.
evaluator (GurobipyEvaluator): the evaluator utilized to solve the problem.
Returns:
SolverResults: DESDEO solver results.
Expand All @@ -27,7 +24,7 @@ def parse_gurobipy_optimizer_results(
variable_values = {var.symbol: results[var.symbol] for var in problem.variables}
objective_values = {obj.symbol: results[obj.symbol] for obj in problem.objectives}
constraint_values = {con.symbol: results[con.symbol] for con in problem.constraints}
success = ( evaluator.model.status == gp.GRB.OPTIMAL )
success = evaluator.model.status == gp.GRB.OPTIMAL
if evaluator.model.status == gp.GRB.OPTIMAL:
status = "Optimal solution found."
elif evaluator.model.status == gp.GRB.INFEASIBLE:
Expand All @@ -38,9 +35,7 @@ def parse_gurobipy_optimizer_results(
status = "Model is either infeasible or unbounded."
else:
status = f"Optimization ended with status: {evaluator.model.status}"
msg = (
f"Gurobipy solver status is: '{status}'"
)
msg = f"Gurobipy solver status is: '{status}'"

return SolverResults(
optimal_variables=variable_values,
Expand All @@ -50,48 +45,55 @@ def parse_gurobipy_optimizer_results(
message=msg,
)

def create_gurobipy_solver(
problem: Problem, options: dict[str,any]|None = None
) -> Callable[[str], SolverResults]:
"""Creates a gurobipy solver that utilizes gurobi's own python implementation.

Unlike with Pyomo you do not need to have gurobi installed on your system
for this to work. Suitable for solving mixed-integer linear and quadratic optimization
problems.
class GurobipySolver:
"""Creates a gurobipy solver that utilizes gurobi's own Python implementation."""

Args:
problem (Problem): the problem to be solved.
options (dict[str,any]): Dictionary of Gurobi parameters to set.
You probably don't need to set any of these and can just use the defaults.
For available parameters see https://www.gurobi.com/documentation/current/refman/parameters.html
def __init__(self, problem: Problem, options: dict[str, any] | None = None):
"""The solver is initialized by supplying a problem and options.
Returns:
Callable[[str], SolverResults]: returns a callable function that takes
as its argument one of the symbols defined for a function expression in
problem.
"""
evaluator = GurobipyEvaluator(problem)
if options is not None:
for key, value in options.items():
evaluator.model.setParam(key,value)
Unlike with Pyomo you do not need to have gurobi installed on your system
for this to work. Suitable for solving mixed-integer linear and quadratic optimization
problems.
Args:
problem (Problem): the problem to be solved.
options (dict[str,any]): Dictionary of Gurobi parameters to set.
You probably don't need to set any of these and can just use the defaults.
For available parameters see https://www.gurobi.com/documentation/current/refman/parameters.html
"""
self.evaluator = GurobipyEvaluator(problem)
self.problem = problem

if options is not None:
for key, value in options.items():
self.evaluator.model.setParam(key, value)

def solver(target: str) -> SolverResults:
evaluator.set_optimization_target(target)
evaluator.model.optimize()
return parse_gurobipy_optimizer_results(problem, evaluator)
def solve(self, target: str) -> SolverResults:
"""Solve the problem for the given target.
Args:
target (str): the symbol of the function to be optimized, and which is
defined in the problem given when initializing the solver.
Returns:
SolverResults: the results of the optimization.
"""
self.evaluator.set_optimization_target(target)
self.evaluator.model.optimize()
return parse_gurobipy_optimizer_results(self.problem, self.evaluator)

return solver

class PersistentGurobipySolver(PersistentSolver):
"""A persistent solver class utlizing gurobipy.
Use this instead of create_gurobipy_solver when reinitializing the
Use this instead of create_gurobipy_solver when re-initializing the
solver every time the problem is changed is not practical.
"""

evaluator: GurobipyEvaluator

def __init__(self, problem: Problem, options: dict[str,any]|None = None):
def __init__(self, problem: Problem, options: dict[str, any] | None = None):
"""Initializer for the persistent solver.
Args:
Expand All @@ -104,9 +106,9 @@ def __init__(self, problem: Problem, options: dict[str,any]|None = None):
self.evaluator = GurobipyEvaluator(problem)
if options is not None:
for key, value in options.items():
self.evaluator.model.setParam(key,value)
self.evaluator.model.setParam(key, value)

def add_constraint(self, constraint: Constraint|list[Constraint]) -> gp.Constr|list[gp.Constr]:
def add_constraint(self, constraint: Constraint | list[Constraint]) -> gp.Constr | list[gp.Constr]:
"""Add one or more constraint expressions to the solver.
If adding a lot of constraints or dealing with a large model, this function
Expand All @@ -124,15 +126,15 @@ def add_constraint(self, constraint: Constraint|list[Constraint]) -> gp.Constr|l
gurobipy.Constr: The gurobipy constraint that was added or a list of gurobipy
constraints if the constraint argument was a list.
"""
if isinstance(constraint,list):
if isinstance(constraint, list):
cons_list = list[gp.Constr]
for cons in constraint:
cons_list.append(self.evaluator.add_constraint(cons))
return cons_list

return self.evaluator.add_constraint(constraint)

def add_objective(self, objective: Objective|list[Objective]):
def add_objective(self, objective: Objective | list[Objective]):
"""Adds an objective function expression to the solver.
Does not yet add any actual gurobipy optimization objectives, only adds them to the dict
Expand All @@ -143,13 +145,13 @@ def add_objective(self, objective: Objective|list[Objective]):
objective (Objective): an objective function expression or a list of objective function
expressions to be added.
"""
if not isinstance(objective,list):
if not isinstance(objective, list):
objective = [objective]

for obj in objective:
self.evaluator.add_objective(obj)

def add_scalarization_function(self, scalarization: ScalarizationFunction|list[ScalarizationFunction]):
def add_scalarization_function(self, scalarization: ScalarizationFunction | list[ScalarizationFunction]):
"""Adds a scalrization expression to the solver.
Scalarizations work identically to objectives, except they are stored in a different
Expand All @@ -166,7 +168,7 @@ def add_scalarization_function(self, scalarization: ScalarizationFunction|list[S
for scal in scalarization:
self.evaluator.add_scalarization_function(scal)

def add_variable(self, variable: Variable|list[Variable]) -> gp.Var|list[gp.Var]:
def add_variable(self, variable: Variable | list[Variable]) -> gp.Var | list[gp.Var]:
"""Add one or more variables to the solver.
If adding a lot of variables or dealing with a large model, this function
Expand All @@ -192,7 +194,7 @@ def add_variable(self, variable: Variable|list[Variable]) -> gp.Var|list[gp.Var]

return self.evaluator.add_variable(variable)

def remove_constraint(self, symbol: str|list[str]):
def remove_constraint(self, symbol: str | list[str]):
"""Removes a constraint from the solver.
If removing a lot of constraints or dealing with a very large model this function
Expand All @@ -203,12 +205,12 @@ def remove_constraint(self, symbol: str|list[str]):
symbol (str): a str representing the symbol of the constraint to be removed.
Can also be a list of multiple symbols.
"""
if not isinstance(symbol,list):
if not isinstance(symbol, list):
symbol = [symbol]
for s in symbol:
self.evaluator.remove_constraint(s)

def remove_variable(self, symbol: str|list[str]):
def remove_variable(self, symbol: str | list[str]):
"""Removes a variable from the model.
If removing a lot of variables or dealing with a very large model this function
Expand Down
5 changes: 5 additions & 0 deletions docs/api/desdeo_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
options:
heading_level: 3

## Gurobipy solver interfaces
::: desdeo.tools.gurobipy_solver_interfaces
options:
heading_level: 3

## Nevergrad solver interfaces
::: desdeo.tools.ng_solver_interfaces
options:
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ plugins:
show_root_heading: true
separate_signature: true
heading_level: 2
merge_init_into_class: true
merge_init_into_class: false
show_root_toc_entry: false
group_by_category: true
show_category_heading: false
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ markers = [
"nimbus: tests related to the NIMBUS method",
"gurobipy: tests related to the gurobipy solver"
]
addopts = "-m 'not performance' -m 'not nautilus'"
pythonpath = "."

[build-system]
Expand Down
46 changes: 14 additions & 32 deletions tests/test_gurobipy_solver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the gurobipy solver."""

import pytest

import gurobipy as gp
Expand All @@ -11,21 +12,19 @@
ScalarizationFunction,
simple_linear_test_problem,
Variable,
VariableTypeEnum
)
from desdeo.tools import (
create_gurobipy_solver,
PersistentGurobipySolver
VariableTypeEnum,
)
from desdeo.tools import GurobipySolver, PersistentGurobipySolver


@pytest.mark.slow
@pytest.mark.gurobipy
def test_gurobipy_solver():
"""Tests the bonmin solver."""
problem = simple_linear_test_problem()
solver = create_gurobipy_solver(problem)
solver = GurobipySolver(problem)

results = solver("f_1")
results = solver.solve("f_1")

assert results.success

Expand All @@ -49,40 +48,23 @@ def test_gurobipy_persistent_solver():
assert np.isclose(xs["x_1"], 4.2, atol=1e-8)
assert np.isclose(xs["x_2"], 2.1, atol=1e-8)

testvar = Variable(
name="test_y",
symbol="y",
variable_type=VariableTypeEnum.integer,
lowerbound=-20,
upperbound=30
)
testvar = Variable(name="test_y", symbol="y", variable_type=VariableTypeEnum.integer, lowerbound=-20, upperbound=30)
solver.add_variable(testvar)
assert isinstance(solver.evaluator.get_expression_by_name("y"),gp.Var)
assert isinstance(solver.evaluator.get_expression_by_name("y"), gp.Var)

testconstr = Constraint(
name="testconstraint",
symbol="c_test",
cons_type=ConstraintTypeEnum.EQ,
func=["Add","x_1","x_2","y",-20]
name="testconstraint", symbol="c_test", cons_type=ConstraintTypeEnum.EQ, func=["Add", "x_1", "x_2", "y", -20]
)
solver.add_constraint(testconstr)
assert solver.evaluator.model.getConstrByName("c_test") is not None

testobjective = Objective(
name="testobjective",
symbol="f_test",
func=["Add","y"]
)
testobjective = Objective(name="testobjective", symbol="f_test", func=["Add", "y"])
solver.add_objective(testobjective)
assert isinstance(solver.evaluator.get_expression_by_name("f_test"),gp.Var)
assert isinstance(solver.evaluator.get_expression_by_name("f_test"), gp.Var)

testscal = ScalarizationFunction(
name="test scalarization function",
symbol="scal",
func=["Add","f_test","f_1"]
)
testscal = ScalarizationFunction(name="test scalarization function", symbol="scal", func=["Add", "f_test", "f_1"])
solver.evaluator.add_scalarization_function(testscal)
assert isinstance(solver.evaluator.get_expression_by_name("scal"),gp.LinExpr)
assert isinstance(solver.evaluator.get_expression_by_name("scal"), gp.LinExpr)

solver.solve("scal")
assert np.isclose(solver.evaluator.get_expression_by_name("scal").getValue(), 20)
Expand All @@ -101,4 +83,4 @@ def test_gurobipy_persistent_solver():

xs = results.optimal_variables
assert np.isclose(xs["x_1"], 4.2, atol=1e-8)
assert np.isclose(xs["x_2"], 2.1, atol=1e-8)
assert np.isclose(xs["x_2"], 2.1, atol=1e-8)

0 comments on commit 68555b4

Please sign in to comment.