Skip to content

Commit

Permalink
Scipy solver interface changes.
Browse files Browse the repository at this point in the history
Finished refactoring all the solvers. Utilizing them now follows the
same pattern for all solvers.
  • Loading branch information
gialmisi committed Apr 25, 2024
1 parent ca1da4a commit 5534f8c
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 138 deletions.
11 changes: 4 additions & 7 deletions desdeo/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"PyomoBonminSolver",
"PyomoGurobiSolver",
"PyomoIpoptSolver",
"ScipyDeSolver",
"ScipyMinimizeSolver",
"SolverOptions",
"SolverResults",
"ScalarizationError",
Expand All @@ -28,11 +30,6 @@
"add_stom_sf_nondiff",
"add_weighted_sums",
"available_nevergrad_optimizers",
"create_pyomo_bonmin_solver",
"create_pyomo_ipopt_solver",
"create_pyomo_gurobi_solver",
"create_scipy_de_solver",
"create_scipy_minimize_solver",
"get_corrected_ideal_and_nadir",
"get_corrected_reference_point",
"guess_best_solver",
Expand Down Expand Up @@ -72,8 +69,8 @@
add_weighted_sums,
)
from desdeo.tools.scipy_solver_interfaces import (
create_scipy_de_solver,
create_scipy_minimize_solver,
ScipyDeSolver,
ScipyMinimizeSolver,
)
from desdeo.tools.utils import (
get_corrected_ideal_and_nadir,
Expand Down
268 changes: 148 additions & 120 deletions desdeo/tools/scipy_solver_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
These solvers can solve various scalarized problems of multiobjective optimization problems.
"""

from collections.abc import Callable
from enum import Enum
from typing import Callable

import numpy as np
from scipy.optimize import NonlinearConstraint
Expand All @@ -13,11 +13,7 @@
from scipy.optimize import minimize as _scipy_minimize

from desdeo.problem import ConstraintTypeEnum, GenericEvaluator, Problem
from desdeo.tools.generics import CreateSolverType, SolverError, SolverResults

# forward typehints
create_scipy_de_solver: CreateSolverType
create_scipy_minimize_solver: CreateSolverType
from desdeo.tools.generics import SolverError, SolverResults


class EvalTargetEnum(str, Enum):
Expand Down Expand Up @@ -217,141 +213,173 @@ def parse_scipy_optimization_result(
)


def create_scipy_minimize_solver(
problem: Problem,
initial_guess: dict[str, float | None] | None = None,
method: str | None = None,
method_kwargs: dict | None = None,
tol: float | None = None,
options: dict | None = None,
subscriber: str | None = None,
) -> Callable[[str], SolverResults]:
"""Creates a solver that utilizes the `scipy.optimize.minimize` routine.
The `scipy.optimize.minimze` routine is fully accessible through this function.
For additional details and explanation of some of the argumetns, see
https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html#scipy.optimize.minimize
class ScipyMinimizeSolver:
"""Creates a scipy solver that utilizes the `minimization` routine."""

Args:
problem (Problem): the multiobjective optimization problem to be solved.
initial_guess (dict[str, float, None] | None, optional): The initial
guess to be utilized in the solver. For variables with a None as their
initial guess, the mid-point of the variable's lower and upper bound is
utilzied as the initial guess. If None, it is assumed that there are
no initial guesses for any of the variables. Defaults to None.
method (str | None, optional): the scipy.optimize.minimize method to be
used. If None, a method is selected automatically based on the
properties of the objective (does it have constraints?). Defaults to
None.
method_kwargs (dict | None, optional): the keyword arguments passed to
the scipy.optimize.minimize method. Defaults to None.
tol (float | None, optional): the tolerance for termination. Defaults to None.
subscriber (str | None, optional): not used right now. WIP. Defaults to None.
def __init__(
self,
problem: Problem,
initial_guess: dict[str, float | None] | None = None,
method: str | None = None,
method_kwargs: dict | None = None,
tol: float | None = None,
options: dict | None = None,
):
"""Initializes a solver that utilizes the `scipy.optimize.minimize` routine.
Returns:
Callable[[str], SolverResults]: results a callable function that can be
called with a target specifying the scalarization function to be used as
the objective of the single-objective optimization.
"""
# variables bounds as (min, max pairs)
bounds = get_variable_bounds_pairs(problem)
The `scipy.optimize.minimze` routine is fully accessible through this function.
For additional details and explanation of some of the argumetns, see
https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html#scipy.optimize.minimize
Args:
problem (Problem): the multiobjective optimization problem to be solved.
initial_guess (dict[str, float, None] | None, optional): The initial
guess to be utilized in the solver. For variables with a None as their
initial guess, the mid-point of the variable's lower and upper bound is
utilzied as the initial guess. If None, it is assumed that there are
no initial guesses for any of the variables. Defaults to None.
method (str | None, optional): the scipy.optimize.minimize method to be
used. If None, a method is selected automatically based on the
properties of the objective (does it have constraints?). Defaults to
None.
method_kwargs (dict | None, optional): the keyword arguments passed to
the scipy.optimize.minimize method. Defaults to None.
tol (float | None, optional): the tolerance for termination. Defaults to None.
subscriber (str | None, optional): not used right now. WIP. Defaults to None.
# the initial guess as a simple sequence. If no initial value is set for some variable,
# then the initial value defaults to middle of the upper and lower bounds.
x0 = set_initial_guess(problem)
"""
self.problem = problem
self.method = method
self.method_kwargs = method_kwargs
self.tol = tol
self.options = options

# variables bounds as (min, max pairs)
self.bounds = get_variable_bounds_pairs(problem)

# the initial guess as a simple sequence. If no initial value is set for some variable,
# then the initial value defaults to middle of the upper and lower bounds.
if initial_guess is not None:
self.initial_guess = initial_guess
else:
self.initial_guess = set_initial_guess(problem)

self.evaluator = GenericEvaluator(problem)

self.constraints = (
create_scipy_dict_constraints(self.problem, self.evaluator)
if self.problem.constraints is not None
else None
)

evaluator = GenericEvaluator(problem)
def solve(self, target: str) -> SolverResults:
"""Solves the problem for a given target.
Args:
target (str): the sumbol of the objective function to be optimized.
def solver(target: str) -> SolverResults:
Returns:
SolverResults: results of the optimization.
"""
# add constraints if there are any
constraints = create_scipy_dict_constraints(problem, evaluator) if problem.constraints is not None else None

optimization_result: _ScipyOptimizeResult = _scipy_minimize(
get_scipy_eval(problem, evaluator, target, EvalTargetEnum.objective),
x0,
method=method,
bounds=bounds,
constraints=constraints,
options=options,
tol=tol,
get_scipy_eval(self.problem, self.evaluator, target, EvalTargetEnum.objective),
self.initial_guess,
method=self.method,
bounds=self.bounds,
constraints=self.constraints,
options=self.options,
tol=self.tol,
)

# grab the results
return parse_scipy_optimization_result(optimization_result, problem, evaluator)
# pare and return the results
return parse_scipy_optimization_result(optimization_result, self.problem, self.evaluator)

return solver

class ScipyDeSolver:
"""Creates a scipy solver that utilizes differential evolution."""

def create_scipy_de_solver(
problem: Problem,
initial_guess: dict[str, float | None] | None = None,
de_kwargs: dict | None = None,
subscriber: str | None = None,
) -> Callable[[str], SolverResults]:
"""Creates a solver that utilizes the `scipy.optimize.differential_evolution` routine.
def __init__(
self,
problem: Problem,
initial_guess: dict[str, float | None] | None = None,
de_kwargs: dict | None = None,
):
"""Creates a solver that utilizes the `scipy.optimize.differential_evolution` routine.
The `scipy.optimize.differential_evolution` routine is fully accessible through this function.
For additional details and explanation of some of the argumetns, see
https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html
The `scipy.optimize.differential_evolution` routine is fully accessible through this function.
For additional details and explanation of some of the argumetns, see
https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.differential_evolution.html
Args:
problem (Problem): the multiobjective optimization problem to be solved.
initial_guess (dict[str, float, None] | None, optional): The initial
guess to be utilized in the solver. For variables with a None as their
initial guess, the mid-point of the variable's lower and upper bound is
utilzied as the initial guess. If None, it is assumed that there are
no initial guesses for any of the variables. Defaults to None.
de_kwargs (dict | None, optional): custom keyword arguments to be forwarded to
`scipy.optimize.differential_evolution`. Defaults to None.
subscriber (str | None, optional): not used right now. WIP. Defaults to None.
Returns:
Callable[[str], SolverResults]: results a callable function that can be
called with a target specifying the scalarization function to be used as
the objective of the single-objective optimization.
"""
if de_kwargs is None:
de_kwargs = {
"strategy": "best1bin",
"maxiter": 1000,
"popsize": 15,
"tol": 0.01,
"mutation": (0.5, 1),
"recombination": 0.7,
"seed": None,
"callback": None,
"disp": False,
"polish": True,
"init": "latinhypercube",
"atol": 0,
"updating": "deferred",
"workers": 1,
"integrality": None,
"vectorized": True, # the constraints for scipy_de need to be fixed first for this to work
}

# variable bounds
bounds = get_variable_bounds_pairs(problem)
Args:
problem (Problem): the multiobjective optimization problem to be solved.
initial_guess (dict[str, float, None] | None, optional): The initial
guess to be utilized in the solver. For variables with a None as their
initial guess, the mid-point of the variable's lower and upper bound is
utilzied as the initial guess. If None, it is assumed that there are
no initial guesses for any of the variables. Defaults to None.
de_kwargs (dict | None, optional): custom keyword arguments to be forwarded to
`scipy.optimize.differential_evolution`. Defaults to None.
subscriber (str | None, optional): not used right now. WIP. Defaults to None.
"""
self.problem = problem
if de_kwargs is None:
de_kwargs = {
"strategy": "best1bin",
"maxiter": 1000,
"popsize": 15,
"tol": 0.01,
"mutation": (0.5, 1),
"recombination": 0.7,
"seed": None,
"callback": None,
"disp": False,
"polish": True,
"init": "latinhypercube",
"atol": 0,
"updating": "deferred",
"workers": 1,
"integrality": None,
"vectorized": True, # the constraints for scipy_de need to be fixed first for this to work
}
self.de_kwargs = de_kwargs

# variable bounds
self.bounds = get_variable_bounds_pairs(problem)

# initial guess. If no guess is present for a variable, said variable's mid point of its
# lower abd upper bound is used instead
if initial_guess is None:
self.initial_guess = set_initial_guess(problem)
else:
self.initial_guess = initial_guess

self.evaluator = GenericEvaluator(problem)
self.constraints = (
create_scipy_object_constraints(self.problem, self.evaluator)
if self.problem.constraints is not None
else ()
)

# initial guess. If no guess is present for a variable, said variable's mid point of its
# lower abd upper bound is used instead
x0 = set_initial_guess(problem)
def solve(self, target: str) -> SolverResults:
"""Solve the problem for a given target.
evaluator = GenericEvaluator(problem)
Args:
target (str): the symbol of the objective function to be optimized.
def solver(target: str) -> SolverResults:
Returns:
SolverResults: results of the optimization.
"""
# add constraints if there are any
constraints = create_scipy_object_constraints(problem, evaluator) if problem.constraints is not None else ()

optimization_result: _ScipyOptimizeResult = _scipy_de(
get_scipy_eval(problem, evaluator, target, EvalTargetEnum.objective),
bounds=bounds,
x0=x0,
constraints=constraints,
**de_kwargs,
get_scipy_eval(self.problem, self.evaluator, target, EvalTargetEnum.objective),
bounds=self.bounds,
x0=self.initial_guess,
constraints=self.constraints,
**self.de_kwargs,
)

# parse the results
return parse_scipy_optimization_result(optimization_result, problem, evaluator)

return solver
return parse_scipy_optimization_result(optimization_result, self.problem, self.evaluator)
8 changes: 4 additions & 4 deletions desdeo/tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from .generics import CreateSolverType
from .proximal_solver import ProximalSolver
from .scipy_solver_interfaces import (
create_scipy_de_solver,
create_scipy_minimize_solver,
ScipyDeSolver,
ScipyMinimizeSolver,
)

available_solvers = {
"scipy_minimize": create_scipy_minimize_solver,
"scipy_de": create_scipy_de_solver,
"scipy_minimize": ScipyMinimizeSolver,
"scipy_de": ScipyDeSolver,
"proximal": ProximalSolver,
}

Expand Down
6 changes: 3 additions & 3 deletions tests/test_scalarization.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
NevergradGenericOptions,
NevergradGenericSolver,
PyomoBonminSolver,
create_scipy_minimize_solver,
ScipyMinimizeSolver,
)
from desdeo.tools.scalarization import (
ScalarizationError,
Expand Down Expand Up @@ -156,9 +156,9 @@ def test_add_epsilon_constraint_and_solve():
problem, target, eps_symbols, objective_symbol, epsilons
)

solver = create_scipy_minimize_solver(problem_w_cons)
solver = ScipyMinimizeSolver(problem_w_cons)

res = solver(target)
res = solver.solve(target)

# check that constraints are ok
cons_values = [res.constraint_values[s] for s in eps_symbols]
Expand Down

0 comments on commit 5534f8c

Please sign in to comment.