Skip to content

Commit d92ecaa

Browse files
committed
Feat: Feat: Add COBYLA optimizer (2 / n)
- Add merit function - Add docstrings
1 parent ba4ea47 commit d92ecaa

File tree

3 files changed

+93
-61
lines changed

3 files changed

+93
-61
lines changed

examples/optimization_algorithms/cobyla.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def sphere_function(para: np.array):
99
return -(x * x + y * y)
1010

1111
def constraint_1(para):
12-
return para[0] > -5
12+
return para[0] + 5
1313

1414
search_space = {
1515
"x": np.arange(-10, 10, 0.1),

src/gradient_free_optimizers/optimizers/core_optimizer/core_optimizer.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def __init__(
3737

3838
self.search_space = search_space
3939
self.initialize = initialize
40-
print(constraints)
4140
self.constraints = constraints
4241
self.random_state = random_state
4342
self.rand_rest_p = rand_rest_p

src/gradient_free_optimizers/optimizers/local_opt/COBYLA.py

Lines changed: 92 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
# copyright: hyperactive developers, MIT License (see LICENSE file)
2-
3-
# todo: write an informative docstring for the file or module, remove the above
4-
5-
__author__ = ["SimonBlanke", "yashnator"]
6-
71
from ..base_optimizer import BaseOptimizer
82

93
import numpy as np
@@ -57,15 +51,58 @@ class COBYLA(BaseOptimizer):
5751
... )
5852
>>> opt.search(sphere_function, n_iter=10)
5953
"""
54+
55+
name = "COBYLA"
56+
_name_ = "COBYLA"
57+
__name__ = "COBYLA"
6058

61-
_tags = {
62-
"authors": ["SimonBlanke", "yashnator"],
63-
}
59+
optimizer_type = "local"
60+
computationally_expensive = False
61+
62+
def __init__(
63+
self,
64+
search_space,
65+
x_0: np.array,
66+
rho_beg: int,
67+
rho_end: int,
68+
initialize={"grid": 4, "random": 2, "vertices": 4},
69+
constraints=[],
70+
random_state=None,
71+
rand_rest_p=0,
72+
nth_process=None,
73+
alpha = 0.25,
74+
beta = 2.1,
75+
):
76+
super().__init__(
77+
search_space=search_space,
78+
initialize=initialize,
79+
constraints=constraints,
80+
random_state=random_state,
81+
rand_rest_p=rand_rest_p,
82+
nth_process=nth_process,
83+
)
84+
self.dim = np.shape(x_0)[0]
85+
self.simplex = self._generate_initial_simplex(x_0, rho_beg)
86+
self.rho_beg = rho_beg
87+
self.rho_end = rho_end
88+
self._rho = rho_beg
89+
90+
self._mu = 0
91+
self.state = 0
92+
self.FLAG = 0
93+
94+
if alpha <= 0 or alpha >= 1:
95+
raise ValueError("Parameter alpha must belong to the range (0, 1)")
96+
97+
if beta <= 1:
98+
raise ValueError("Parameter beta must belong to the range (1, ∞)")
99+
100+
self.alpha = alpha
101+
self.beta = beta
64102

65103
def _generate_initial_simplex(self, x_0_initial, rho_beg):
66104
n = x_0_initial.shape[0]
67105
arr = np.ones((n + 1, 1)) * x_0_initial + rho_beg * np.eye(n + 1, n)
68-
print(arr)
69106
return arr
70107

71108
def _vertices_to_oppsite_face_distances(self):
@@ -76,9 +113,6 @@ def _vertices_to_oppsite_face_distances(self):
76113
For each vertex, the opposite hyperplane is obtained after removing the current
77114
vertex and then finding the projection on the subspace spanned by the hyperplane.
78115
The distance is then the L2 norm between projection and the current vertex.
79-
80-
Args:
81-
self: instance of current COBYLA class
82116
83117
Returns:
84118
distances: (n+1,) array of distances from each vertex to its opposite face.
@@ -96,66 +130,65 @@ def _vertices_to_oppsite_face_distances(self):
96130
return distances
97131

98132
def _is_simplex_acceptable(self):
133+
"""
134+
Determine whether the current simplex is acceptable according to COBYLA criteria.
135+
136+
In COBYLA, a simplex is acceptable if:
137+
1. The distances between each vertex and the first vertex (η_j) do not exceed
138+
a threshold defined by `beta * rho`, controlling simplex edge lengths.
139+
2. The distance from each vertex to its opposite (n-1)-dimensional face (σ_j)
140+
is not too small, ensuring the simplex is well-shaped. These distances must
141+
exceed a threshold given by `alpha * rho`.
142+
143+
This function enforces these two geometric conditions to ensure that the simplex
144+
maintains good numerical stability and approximation quality.
145+
146+
Returns:
147+
bool: True if both η and σ conditions are satisfied, False otherwise.
148+
"""
99149
eta = [np.linalg.norm(x - self.simplex[0]) for x in self.simplex]
100150
eta_constraint = self.beta * self._rho
101151
for eta_j in eta:
102152
if eta_j > eta_constraint:
103153
return False
104154
sigma = self._vertices_to_oppsite_face_distances()
105155
sigma_constraint = self.alpha * self._rho
106-
print(sigma)
107156
for sigma_j in sigma:
108157
if sigma_j < sigma_constraint:
109158
return False
110159
return True
111160

112161
def _eval_constraints(self, pos):
113-
# TODO: evalute constraints in optimized way
114-
115-
return None
162+
"""
163+
Evaluates constraints for the given position
164+
165+
Returns:
166+
np.array: array containing the evaluated value of constraints
167+
"""
168+
return [np.clip(f(pos), 0, np.inf) for f in self.constraints]
116169

117-
def _merit_value(self, pos):
118-
# TODO: write the merit function using the _eval_constraints
119-
return 0
170+
def _phi(self, pos):
171+
"""
172+
Compute the merit function Φ used in COBYLA to evaluate candidate points. Given by:
120173
121-
def __init__(
122-
self,
123-
search_space,
124-
x_0: np.array,
125-
rho_beg: int,
126-
rho_end: int,
127-
initialize={"grid": 4, "random": 2, "vertices": 4},
128-
constraints=[],
129-
random_state=None,
130-
rand_rest_p=0,
131-
nth_process=None,
132-
alpha = 0.25,
133-
beta = 2.1
134-
):
135-
super().__init__(
136-
search_space=search_space,
137-
initialize=initialize,
138-
constraints=constraints,
139-
random_state=random_state,
140-
rand_rest_p=rand_rest_p,
141-
nth_process=nth_process,
142-
)
143-
self.dim = np.shape(x_0)[0]
144-
self.simplex = self._generate_initial_simplex(x_0, rho_beg)
145-
self.rho_beg = rho_beg
146-
self.rho_end = rho_end
147-
self._rho = rho_beg
148-
self.state = 0
149-
self.FLAG = 0
150-
151-
if alpha <= 0 or alpha >= 1:
152-
raise ValueError("Parameter alpha must belong to the range (0, 1)")
153-
154-
if beta <= 1:
155-
raise ValueError("Parameter beta must belong to the range (1, ∞)")
156-
157-
self.alpha = alpha
158-
self.beta = beta
174+
Φ(x) = f(x) + μ * max_i(max(c_i(x), 0))
175+
176+
Args:
177+
pos (np.array): The point in parameter space at which to evaluate the merit function.
178+
179+
Returns:
180+
float: The value of the merit function Φ at the given point.
181+
"""
182+
c = self._eval_constraints(pos)
183+
return self.objective_function(pos) + self._mu * np.max(c)
184+
185+
def _rearrange_optimum_to_top(self):
186+
"""
187+
Rearrages simplex vertices such that first row is the optimal position
188+
"""
189+
opt_idx = np.argmin([self._phi(vert) for vert in self.simplex])
190+
if opt_idx != 0:
191+
self.simplex[[0, opt_idx]] = self.simplex[[opt_idx, 0]]
159192

160193
def iterate(self):
161194
# TODO: Impl
@@ -167,7 +200,7 @@ def _score(self, pos):
167200

168201
def evaluate(self, pos):
169202
# TODO: Impl
170-
return self._merit_value(pos)
203+
return self._phi(self.simplex[0])
171204

172205
def finish_search(self):
173206
# TODO: Impl

0 commit comments

Comments
 (0)