Skip to content

Commit

Permalink
Added local optimizer (#152)
Browse files Browse the repository at this point in the history
* local optimizer. base version

* Merged with current main + added some comments

* fixed names

* fixed names in solver.py
  • Loading branch information
MADZEROPIE authored Jul 10, 2023
1 parent 2639bb0 commit e0286a1
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 67 deletions.
150 changes: 150 additions & 0 deletions iOpt/method/local_optimizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import sys
from typing import List
from collections.abc import Callable

import scipy

from iOpt.method.optim_task import OptimizationTask
from iOpt.trial import Point, FunctionValue, FunctionType


class LocalTaskWrapper:
"""
Класс LocalTaskWrapper (название временное) оборачивает вычисление функции для дальнейшего применения локальных методов.
"""

def __init__(self, task: OptimizationTask, discrete_variables=None, max_calcs=-1):
self.discrete_variables = discrete_variables
self.task = task
self.calcs_count = 0
self.max_calcs = max_calcs # В globalizer используется именно ограничение по количеству вычислений функции

def evaluate_function(self, y: List[float]) -> float:
"""
Метод вычисляет значение целевой функции
:param y: Точка в которой нужно вычислить значение функции
:return: возвращает значение целевой функции или
sys.float_info.max, если:
точка лежит за областью поиска
ИЛИ не были выполнены ограничения
ИЛИ было выкинуто исключение (функция не может быть посчитана в этой точке)
ИЛИ число вычислений превысило лимит (если он задан)
"""
point = Point(y, self.discrete_variables)
function_value = FunctionValue(FunctionType.OBJECTIV)
if self.max_calcs != -1 and self.calcs_count >= self.max_calcs:
function_value.value = sys.float_info.max
return function_value.value
for i in range(self.task.problem.dimension):
if (y[i] < self.task.problem.lower_bound_of_float_variables[i]) \
or (y[i] > self.task.problem.upper_bound_of_float_variables[i]):
function_value.value = sys.float_info.max
return function_value.value

self.calcs_count += 1
try:
for i in range(self.task.problem.number_of_constraints):
function_constraint_value = FunctionValue(FunctionType.CONSTRAINT, i)
function_constraint_value = self.task.problem.calculate(point, function_constraint_value)
if function_constraint_value.value > 0:
function_value.value = sys.float_info.max
return function_value.value

function_value = self.task.problem.calculate(point, function_value)
except Exception:
function_value.value = sys.float_info.max

return function_value.value


class HookeJeevesOptimizer:
"""
Класс HookeJeevesOptimizer реализует метод Хука-Дживса.
"""
def __init__(self, func: Callable[[List[float]], float], start_point: List[float],
step_mult: float, eps: float, max_iter: float):
self.nfev = 0
self.cur_point = None
self.minf = None
self.best_point = None
self.pr_resdir = None
self.cur_resdir = None
self.dim = len(start_point)
self.f = func
self.start_point = start_point
self.max_iter = max_iter
self.eps = min(eps, 0.0001)
self.step = self.eps * 2
self.step_mult = step_mult

def minimize(self) -> List[float]:
need_restart: bool = True
# self.best_point = self.start_point
# self.minf = self.f(self.start_point)
k, i, curr_f = 0, 0, 0.0
while i < self.max_iter:
i += 1
if need_restart:
k = 0
self.cur_point = self.start_point.copy()
self.cur_resdir = self.start_point.copy()
curr_f = self.f(self.cur_point)
need_restart = False

self.pr_resdir = [el for el in self.cur_resdir]
self.cur_resdir = [el for el in self.cur_point]
next_f_value = self._make_research(self.cur_resdir)

if curr_f > next_f_value:
self._do_step()
k += 1
curr_f = next_f_value
elif self.step > self.eps:
if k != 0:
self.start_point = self.pr_resdir.copy()
else:
self.step /= self.step_mult
need_restart = True
else:
break

return self.pr_resdir

def _make_research(self, point) -> float:
best_value = self.f(point) # в globalizer сделано так, хотя это значение уже было вычислено...

for i in range(self.dim):
point[i] += self.step
right_f_val = self.f(point)
if right_f_val > best_value:
point[i] -= 2 * self.step
left_f_val = self.f(point)

if left_f_val > best_value:
point[i] += self.step
else:
best_value = left_f_val

else:
best_value = right_f_val

return best_value

def _do_step(self) -> None:
for i in range(self.dim):
self.cur_point[i] = (1 + self.step_mult) * self.cur_resdir[i] - self.step_mult * self.pr_resdir[i]


def local_optimize(task: OptimizationTask, method, start_point: Point, args: dict, max_calcs: int = -1) -> dict:

local_task = LocalTaskWrapper(task=task, discrete_variables=start_point.discrete_variables, max_calcs=max_calcs)
if method == 'Hooke-Jeeves':
best_point = HookeJeevesOptimizer(local_task.evaluate_function, start_point.float_variables.copy(),
**args).minimize()
else:
best_point = scipy.optimize.minimize(local_task.evaluate_function, x0=start_point.float_variables.copy(),
method=method, **args).x

return {"x": best_point, "fev": local_task.calcs_count}
70 changes: 25 additions & 45 deletions iOpt/method/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@

import traceback

import scipy
from scipy.optimize import Bounds

from iOpt.evolvent.evolvent import Evolvent
from iOpt.method.listener import Listener
from iOpt.method.local_optimizer import local_optimize
from iOpt.method.method import Method
from iOpt.method.optim_task import OptimizationTask
from iOpt.method.search_data import SearchData, SearchDataItem
Expand Down Expand Up @@ -57,7 +55,7 @@ def solve(self) -> Solution:
:return: Текущая оценка решения задачи оптимизации
"""

startTime = datetime.now()
start_time = datetime.now()

try:
while not self.method.check_stop_condition():
Expand All @@ -71,7 +69,7 @@ def solve(self) -> Solution:
self.do_local_refinement(self.parameters.local_method_iteration_count)

result = self.get_results()
result.solving_time = (datetime.now() - startTime).total_seconds()
result.solving_time = (datetime.now() - start_time).total_seconds()

for listener in self._listeners:
status = self.method.check_stop_condition()
Expand All @@ -86,11 +84,11 @@ def do_global_iteration(self, number: int = 1):
:param number: Количество итераций глобального поиска
"""
number_ = number
doneTrials = []
done_trials = []
if self._first_iteration is True:
for listener in self._listeners:
listener.before_method_start(self.method)
doneTrials = self.method.first_iteration()
done_trials = self.method.first_iteration()
self._first_iteration = False
number = number - 1

Expand All @@ -100,35 +98,10 @@ def do_global_iteration(self, number: int = 1):
self.method.update_optimum(newpoint)
self.method.renew_search_data(newpoint, oldpoint)
self.method.finalize_iteration()
doneTrials = self.search_data.get_last_items(self.parameters.number_of_parallel_points * number_)
done_trials = self.search_data.get_last_items(self.parameters.number_of_parallel_points * number_)

for listener in self._listeners:
listener.on_end_iteration(doneTrials, self.get_results())

def problem_calculate(self, y):
result = self.get_results()
point = Point(y, result.best_trials[0].point.discrete_variables)
functionValue = FunctionValue(FunctionType.OBJECTIV)

for i in range(self.task.problem.dimension):
if (y[i] < self.task.problem.lower_bound_of_float_variables[i]) \
or (y[i] > self.task.problem.upper_bound_of_float_variables[i]):
functionValue.value = sys.float_info.max
return functionValue.value

try:
for i in range(self.task.problem.number_of_constraints):
functionConstraintValue = FunctionValue(FunctionType.CONSTRAINT, i)
functionConstraintValue = self.task.problem.calculate(point, functionConstraintValue)
if functionConstraintValue.value > 0:
functionValue.value = sys.float_info.max
return functionValue.value

functionValue = self.task.problem.calculate(point, functionValue)
except Exception:
functionValue.value = sys.float_info.max

return functionValue.value
listener.on_end_iteration(done_trials, self.get_results())

def do_local_refinement(self, number: int = 1):
"""
Expand All @@ -139,23 +112,32 @@ def do_local_refinement(self, number: int = 1):
try:
local_method_iteration_count = number
if number == -1:
local_method_iteration_count = self.parameters.local_method_iteration_count
local_method_iteration_count = int(self.parameters.local_method_iteration_count)

result = self.get_results()
start_point = result.best_trials[0].point.float_variables

nelder_mead = scipy.optimize.minimize(self.problem_calculate, x0=start_point, method='Nelder-Mead',
options={'maxiter': local_method_iteration_count})
# start_point = result.bestTrials[0].point.floatVariables

local_solution = local_optimize(self.task,
method="Hooke-Jeeves", start_point=result.best_trials[0].point,
max_calcs=local_method_iteration_count,
args={"eps": self.parameters.eps / 100, "step_mult": 2,
"max_iter": local_method_iteration_count}
)
# scipy.optimize.minimize(self.problemCalculate, x0=start_point, method='Nelder-Mead',
# options={'maxiter': local_method_iteration_count})
# local_solution = LocalOptimize(LocalTaskWrapper(self.task, result.bestTrials[0].point.discreteVariables),
# method="Nelder-Mead", start_point=start_point,
# args={"options": {'maxiter': local_method_iteration_count}})

if local_method_iteration_count > 0:
result.best_trials[0].point.float_variables = nelder_mead.x
result.best_trials[0].point.float_variables = local_solution["x"]

point: SearchDataItem = SearchDataItem(result.best_trials[0].point,
self.evolvent.get_inverse_image(
result.best_trials[0].point.float_variables),
function_values=[FunctionValue()] *
(self.task.problem.number_of_constraints +
self.task.problem.number_of_objectives)
self.task.problem.number_of_objectives)
)

number_of_constraints = self.task.problem.number_of_constraints
Expand All @@ -167,15 +149,15 @@ def do_local_refinement(self, number: int = 1):
if point.get_z() > 0:
break
point.function_values[number_of_constraints] = FunctionValue(FunctionType.OBJECTIV,
number_of_constraints)
number_of_constraints)
point.function_values[number_of_constraints] = \
self.task.problem.calculate(point.point, point.function_values[number_of_constraints])
point.set_z(point.function_values[number_of_constraints].value)
point.set_index(number_of_constraints)

result.best_trials[0].function_values = point.function_values

result.number_of_local_trials = nelder_mead.nfev
result.number_of_local_trials = local_solution["fev"]
except Exception:
print("Local Refinement is not possible")

Expand All @@ -185,8 +167,6 @@ def get_results(self) -> Solution:
:return: Решение задачи оптимизации
"""
# ДА, ЭТО КОСТЫЛЬ. т.к. solution хранит trial
# self.search_data.solution.best_trials[0] = self.method.GetOptimumEstimation()
return self.search_data.solution

def save_progress(self, file_name: str) -> None:
Expand Down
27 changes: 14 additions & 13 deletions iOpt/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self,
self.problem = problem
self.parameters = parameters

Solver.chack_parameters(self.problem, self.parameters)
Solver.check_parameters(self.problem, self.parameters)

self.__listeners: List[Listener] = []

Expand All @@ -52,8 +52,8 @@ def solve(self) -> Solution:
:return: решение задачи оптимизации
"""
Solver.chack_parameters(self.problem, self.parameters)
sol: Solution = None;
Solver.check_parameters(self.problem, self.parameters)
sol: Solution = None
if self.parameters.timeout < 0:
sol = self.process.solve()
else:
Expand All @@ -77,7 +77,7 @@ def do_global_iteration(self, number: int = 1):
:param number: число итераций глобального поиска
"""
Solver.chack_parameters(self.problem, self.parameters)
Solver.check_parameters(self.problem, self.parameters)
self.process.do_global_iteration(number)

def do_local_refinement(self, number: int = 1):
Expand All @@ -86,7 +86,7 @@ def do_local_refinement(self, number: int = 1):
:param number: число итераций локального поиска
"""
Solver.chack_parameters(self.problem, self.parameters)
Solver.check_parameters(self.problem, self.parameters)
self.process.do_local_refinement(number)

def get_results(self) -> Solution:
Expand All @@ -111,7 +111,7 @@ def load_progress(self, file_name: str) -> None:
:param file_name: имя файла
"""
Solver.chack_parameters(self.problem, self.parameters)
Solver.check_parameters(self.problem, self.parameters)
self.process.load_progress(file_name=file_name)

def refresh_listener(self) -> None:
Expand All @@ -131,7 +131,7 @@ def add_listener(self, listener: Listener) -> None:
self.__listeners.append(listener)

@staticmethod
def chack_parameters(problem: Problem,
def check_parameters(problem: Problem,
parameters: SolverParameters = SolverParameters()) -> None:
"""
Проверяет параметры решателя
Expand Down Expand Up @@ -169,8 +169,9 @@ def chack_parameters(problem: Problem,
if len(problem.upper_bound_of_float_variables) != problem.number_of_float_variables:
raise Exception("List of upper bounds for float search variables defined incorrectly")

for lowerBound, upperBound in zip(problem.lower_bound_of_float_variables, problem.upper_bound_of_float_variables):
if lowerBound >= upperBound:
for lower_bound, upper_bound in zip(problem.lower_bound_of_float_variables,
problem.upper_bound_of_float_variables):
if lower_bound >= upper_bound:
raise Exception("For floating point search variables, "
"the upper search bound must be greater than the lower.")

Expand All @@ -188,8 +189,8 @@ def chack_parameters(problem: Problem,
if parameters.start_point.discrete_variables:
if len(parameters.start_point.discrete_variables) != problem.number_of_discrete_variables:
raise Exception("Incorrect start point discrete variables")
for lowerBound, upperBound, y in zip(problem.lower_bound_of_float_variables, problem.upper_bound_of_float_variables,
parameters.start_point.float_variables):
if y < lowerBound or y > upperBound:
for lower_bound, upper_bound, y in zip(problem.lower_bound_of_float_variables,
problem.upper_bound_of_float_variables,
parameters.start_point.float_variables):
if y < lower_bound or y > upper_bound:
raise Exception("Incorrect start point coordinate")

Loading

0 comments on commit e0286a1

Please sign in to comment.