Skip to content

Commit

Permalink
Proximal solver changes.
Browse files Browse the repository at this point in the history
Refactored the solver into a class. It is now utilized as follows:

```
solver = ProximalSolver(problem)
result = solver.solve("f_1")
```

Many tests are not passing right now, but this will be fixed once the
other solvers have been refactored into classes as well.
  • Loading branch information
gialmisi committed Apr 25, 2024
1 parent 0e36c8d commit 2ec0ac6
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 63 deletions.
12 changes: 6 additions & 6 deletions desdeo/mcdm/nautili.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def solve_reachable_bounds(
}

# if a solver creator was provided, use that, else, guess the best one
_create_solver = guess_best_solver(problem) if create_solver is None else create_solver
solver_init = guess_best_solver(problem) if create_solver is None else create_solver

lower_bounds = {}
upper_bounds = {}
Expand All @@ -93,8 +93,8 @@ def solve_reachable_bounds(
)

# solve
solver = _create_solver(eps_problem)
res = solver(target)
solver = solver_init(eps_problem)
res = solver.solve(target)

if not res.success:
# could not optimize eps problem
Expand Down Expand Up @@ -152,7 +152,7 @@ def solve_reachable_solution(
SolverResults: the results of the projection.
"""
# check solver
_create_solver = guess_best_solver(problem) if create_solver is None else create_solver
init_solver = guess_best_solver(problem) if create_solver is None else create_solver

# create and add scalarization function
# previous_nav_point = objective_dict_to_numpy_array(problem, previous_nav_point).tolist()
Expand All @@ -175,8 +175,8 @@ def solve_reachable_solution(
)

# solve the problem
solver = _create_solver(problem_w_asf)
return solver(target)
solver = init_solver(problem_w_asf)
return solver.solve(target)


def nautili_init(problem: Problem, create_solver: CreateSolverType | None = None) -> NAUTILI_Response:
Expand Down
6 changes: 3 additions & 3 deletions desdeo/mcdm/nautilus.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def solve_reachable_solution(
SolverResults: the results of the projection.
"""
# check solver
_create_solver = guess_best_solver(problem) if create_solver is None else create_solver
init_solver = guess_best_solver(problem) if create_solver is None else create_solver

# need to convert the preferences to preferential factors?

Expand All @@ -106,8 +106,8 @@ def solve_reachable_solution(
)

# solve the problem
solver = _create_solver(problem_w_asf)
return solver(target)
solver = init_solver(problem_w_asf)
return solver.solve(target)


# NAUTILUS initializer and steppers
Expand Down
16 changes: 8 additions & 8 deletions desdeo/mcdm/nautilus_navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def solve_reachable_bounds(
}

# if a solver creator was provided, use that, else, guess the best one
_create_solver = guess_best_solver(problem) if create_solver is None else create_solver
solver_init = guess_best_solver(problem) if create_solver is None else create_solver

lower_bounds = {}
upper_bounds = {}
Expand Down Expand Up @@ -154,8 +154,8 @@ def solve_reachable_bounds(
eps_problem = eps_problem.add_constraints(bound_constraints)

# solve
solver = _create_solver(eps_problem)
res = solver(target)
solver = solver_init(eps_problem)
res = solver.solve(target)

if not res.success:
# could not optimize eps problem
Expand Down Expand Up @@ -212,8 +212,8 @@ def solve_reachable_bounds(
eps_problem = eps_problem.add_constraints([bound_to_nav_constraint])

# solve
solver = _create_solver(eps_problem)
res = solver(target)
solver = solver_init(eps_problem)
res = solver.solve(target)
if not res.success:
# could not optimize eps problem
msg = (
Expand Down Expand Up @@ -267,7 +267,7 @@ def solve_reachable_solution(
SolverResults: the results of the projection.
"""
# check solver
_create_solver = guess_best_solver(problem) if create_solver is None else create_solver
init_solver = guess_best_solver(problem) if create_solver is None else create_solver

# create and add scalarization function
problem_w_asf, target = add_asf_nondiff(
Expand All @@ -289,8 +289,8 @@ def solve_reachable_solution(
)

# solve the problem
solver = _create_solver(problem_w_asf)
return solver(target)
solver = init_solver(problem_w_asf)
return solver.solve(target)


def calculate_distance_to_front(
Expand Down
4 changes: 3 additions & 1 deletion desdeo/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
"IpoptOptions",
"CreateSolverType",
"GurobipySolver",
"NevergradGenericOptions",
"NevergradGenericSolver",
"PersistentGurobipySolver",
"ProximalSolver",
"SolverOptions",
"SolverResults",
"NevergradGenericOptions",
"ScalarizationError",
"add_asf_diff",
"add_asf_generic_nondiff",
Expand Down Expand Up @@ -44,6 +45,7 @@
NevergradGenericSolver,
available_nevergrad_optimizers,
)
from desdeo.tools.proximal_solver import ProximalSolver
from desdeo.tools.pyomo_solver_interfaces import (
BonminOptions,
IpoptOptions,
Expand Down
5 changes: 1 addition & 4 deletions desdeo/tools/gurobipy_solver_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import gurobipy as gp

from desdeo.problem import Constraint, GurobipyEvaluator, Objective, Problem, ScalarizationFunction, Variable
from desdeo.tools.generics import CreateSolverType, PersistentSolver, SolverResults

# forward typehints
create_gurobipy_solver: CreateSolverType
from desdeo.tools.generics import PersistentSolver, SolverResults


def parse_gurobipy_optimizer_results(problem: Problem, evaluator: GurobipyEvaluator) -> SolverResults:
Expand Down
5 changes: 1 addition & 4 deletions desdeo/tools/ng_solver_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
from pydantic import BaseModel, Field

from desdeo.problem import Problem, SympyEvaluator
from desdeo.tools.generics import CreateSolverType, SolverResults

# forward typehints
NevergradGenericSolver: CreateSolverType
from desdeo.tools.generics import SolverResults

available_nevergrad_optimizers = [
"NGOpt",
Expand Down
63 changes: 38 additions & 25 deletions desdeo/tools/proximal_solver.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
"""Defines solvers meant to be utilized with Problems with discrete representations."""
from collections.abc import Callable

import polars as pl

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

# forward typehints
create_proximal_solver: CreateSolverType

class ProximalSolver:
"""Creates a solver that finds the closest solution given a fully discrete problem.
def create_proximal_solver(problem: Problem) -> Callable[[str], SolverResults]:
"""Creates a solver that assumes the problem being a fully discrete one.
Note:
This solver is extremely naive. It will optimize the problem and the result will
be a point defined for a discrete problem that is closest (Euclidean
distance) to the optimum. The result may be wildly inaccurate depending on how
representative the discrete points are of the original problem.
"""

Assumes that problem has only data-based objectives and a discrete definition
that fully defines all the objectives.
def __init__(self, problem: Problem):
"""Creates a solver that assumes the problem being a fully discrete one.
Args:
problem (Problem): the problem being solved.
Assumes that problem has only data-based objectives and a discrete definition
that fully defines all the objectives.
Returns:
Callable[[str], SolverResults]: a solver that can be called with a target to be optimized.
This function then returns the results of the optimization as in a `SolverResults` dataclass.
"""
evaluator = GenericEvaluator(problem, evaluator_mode=EvaluatorModesEnum.discrete)
Args:
problem (Problem): the problem being solved.
Returns:
Callable[[str], SolverResults]: a solver that can be called with a target to be optimized.
This function then returns the results of the optimization as in a `SolverResults` dataclass.
"""
self.problem = problem
self.evaluator = GenericEvaluator(problem, evaluator_mode=EvaluatorModesEnum.discrete)

def solver(target: str) -> SolverResults:
results_df = evaluator.evaluate()
def solve(self, target: str) -> SolverResults:
"""Solve the problem for the given target.
Args:
target (str): the symbol of the objective function to be optimized.
Returns:
SolverResults: the results fo the optimization.
"""
results_df = self.evaluator.evaluate()

# check constraint values if problem has constraints
if problem.constraints is not None:
if self.problem.constraints is not None:
cons_condition = pl.lit(True)
for constraint in problem.constraints:
for constraint in self.problem.constraints:
cons_condition = cons_condition & (results_df[constraint.symbol] <= 0)

results_df = results_df.filter(cons_condition)
Expand All @@ -40,11 +55,11 @@ def solver(target: str) -> SolverResults:
closest = results_df.sort(target).head(1)

# extract relevant results, extract them as disc for easier jsonification
variable_values = {variable.symbol: closest[variable.symbol][0] for variable in problem.variables}
objective_values = {objective.symbol: closest[objective.symbol][0] for objective in problem.objectives}
variable_values = {variable.symbol: closest[variable.symbol][0] for variable in self.problem.variables}
objective_values = {objective.symbol: closest[objective.symbol][0] for objective in self.problem.objectives}
constraint_values = (
{constraint.symbol: closest[constraint.symbol][0] for constraint in problem.constraints}
if problem.constraints is not None
{constraint.symbol: closest[constraint.symbol][0] for constraint in self.problem.constraints}
if self.problem.constraints is not None
else None
)
message = f"Optimal value found from tabular data minimizing the column '{target}'."
Expand All @@ -58,5 +73,3 @@ def solver(target: str) -> SolverResults:
success=success,
message=message,
)

return solver
4 changes: 2 additions & 2 deletions desdeo/tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from desdeo.problem import ObjectiveTypeEnum, Problem
from .generics import CreateSolverType
from .proximal_solver import create_proximal_solver
from .proximal_solver import ProximalSolver
from .scipy_solver_interfaces import (
create_scipy_de_solver,
create_scipy_minimize_solver,
Expand All @@ -11,7 +11,7 @@
available_solvers = {
"scipy_minimize": create_scipy_minimize_solver,
"scipy_de": create_scipy_de_solver,
"proximal": create_proximal_solver,
"proximal": ProximalSolver,
}


Expand Down
20 changes: 10 additions & 10 deletions tests/test_proximal_solver.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Tests the proximal solver."""

import numpy.testing as npt

from desdeo.problem import simple_data_problem
from desdeo.tools.proximal_solver import create_proximal_solver
from desdeo.tools.proximal_solver import ProximalSolver
from desdeo.tools.scalarization import (
add_asf_nondiff,
add_scalarization_function,
add_weighted_sums,
)

Expand All @@ -32,9 +32,9 @@ def test_proximal_with_simple_data_problem():

problem, target = add_weighted_sums(problem, symbol="ws_1", weights={"g_1": 1, "g_2": 0, "g_3": 0})

solver = create_proximal_solver(problem)
solver = ProximalSolver(problem)

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

npt.assert_array_almost_equal([res.optimal_objectives[symbol] for symbol in objective_symbols], obj_should_be)
npt.assert_array_almost_equal(res.constraint_values[const_symbol], const_should_be)
Expand All @@ -46,9 +46,9 @@ def test_proximal_with_simple_data_problem():

problem, target = add_weighted_sums(problem, symbol="ws_2", weights={"g_1": 0, "g_2": 1, "g_3": 0})

solver = create_proximal_solver(problem)
solver = ProximalSolver(problem)

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

npt.assert_array_almost_equal([res.optimal_objectives[symbol] for symbol in objective_symbols], obj_should_be)
npt.assert_array_almost_equal(res.constraint_values[const_symbol], const_should_be)
Expand All @@ -60,9 +60,9 @@ def test_proximal_with_simple_data_problem():

problem, target = add_weighted_sums(problem, symbol="ws_3", weights={"g_1": 0, "g_2": 0, "g_3": 1})

solver = create_proximal_solver(problem)
solver = ProximalSolver(problem)

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

npt.assert_array_almost_equal([res.optimal_objectives[symbol] for symbol in objective_symbols], obj_should_be)
npt.assert_array_almost_equal(res.constraint_values[const_symbol], const_should_be)
Expand All @@ -75,9 +75,9 @@ def test_proximal_with_simple_data_problem():

problem, target = add_asf_nondiff(problem, symbol="asf", reference_point=reference_point)

solver = create_proximal_solver(problem)
solver = ProximalSolver(problem)

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

npt.assert_array_almost_equal([res.optimal_objectives[symbol] for symbol in objective_symbols], obj_should_be)
npt.assert_array_almost_equal(res.constraint_values[const_symbol], const_should_be)

0 comments on commit 2ec0ac6

Please sign in to comment.