Skip to content

Commit 39a9930

Browse files
authored
Egor driver (#6)
* Add initial basic implementation of Egor driver * Fix sego API change * Fix test due to remove file pb? * Add tests for Egor driver * Bump 1.1.0 * Add egobox in requirements * Re activate black check * black format * Use development mode for test * Make codacy happy
1 parent 4d67315 commit 39a9930

File tree

11 files changed

+419
-80
lines changed

11 files changed

+419
-80
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ jobs:
2626
python -m pip install --upgrade pip
2727
python -m pip install black pytest
2828
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
29-
pip install .
30-
# - name: Format with black
31-
# run: |
32-
# black --check .
29+
pip install -e .
30+
- name: Format with black
31+
run: |
32+
black --check .
3333
- name: Test with pytest
3434
run: |
3535
pytest

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,6 @@ venv.bak/
104104

105105
# mypy
106106
.mypy_cache/
107+
108+
# ignore openmdao reports
109+
**/reports

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
Set of specialized classes to handle specific methods using OpenMDAO framework.
88

9+
* <code>EgoboxEgorDriver</code> : an OpenMDAO driver for Egor optimizer from [Egobox](https://github.com/relf/egobox#egobox) library
910
* <code>OneraSegoDriver</code> : an OpenMDAO driver for Onera Super EGO optimizers
1011
* <code>SmtDoeDriver</code> : an OpenMDAO driver for sampling methods from surrogate [SMT](https://smt.readthedocs.io/en/latest/) library
1112
* <code>SalibDoeDriver</code> : an OpenMDAO driver for Morris or Saltelli DoE from sensitive analysis [SALib](https://salib.readthedocs.io/en/latest/) library
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import numpy as np
2+
import traceback
3+
4+
from openmdao.core.driver import Driver, RecordingDebugging
5+
from openmdao.core.analysis_error import AnalysisError
6+
7+
EGOBOX_NOT_INSTALLED = False
8+
try:
9+
import egobox as egx
10+
from egobox import Egor
11+
except ImportError:
12+
EGOBOX_NOT_INSTALLED = True
13+
14+
15+
def to_list(l, size):
16+
if not (isinstance(l, np.ndarray) or isinstance(l, list)):
17+
return [l] * size
18+
diff_len = len(l) - size
19+
if diff_len > 0:
20+
return l[0:size]
21+
elif diff_len < 0:
22+
return [l[0]] * size
23+
else:
24+
return l
25+
26+
27+
class EgoboxEgorDriver(Driver):
28+
"""OpenMDAO driver for egobox optimizer"""
29+
30+
def __init__(self, **kwargs):
31+
"""Initialize the driver with the given options."""
32+
super(EgoboxEgorDriver, self).__init__(**kwargs)
33+
34+
if EGOBOX_NOT_INSTALLED:
35+
raise RuntimeError("egobox library is not installed.")
36+
37+
# What we support
38+
self.supports["inequality_constraints"] = True
39+
self.supports["linear_constraints"] = True
40+
41+
# What we don't support
42+
self.supports["equality_constraints"] = False
43+
self.supports["two_sided_constraints"] = False
44+
self.supports["multiple_objectives"] = False
45+
self.supports["active_set"] = False
46+
self.supports["simultaneous_derivatives"] = False
47+
self.supports["total_jac_sparsity"] = False
48+
self.supports["gradients"] = False
49+
self.supports["integer_design_vars"] = False
50+
51+
self.opt_settings = {}
52+
53+
def _declare_options(self):
54+
self.options.declare(
55+
"optimizer",
56+
default="EGOR",
57+
values=["EGOR"],
58+
desc="Name of optimizer to use",
59+
)
60+
61+
def _setup_driver(self, problem):
62+
super(EgoboxEgorDriver, self)._setup_driver(problem)
63+
64+
self.comm = None
65+
66+
def run(self):
67+
model = self._problem().model
68+
69+
self.iter_count = 0
70+
self.name = "egobox_optimizer_egor"
71+
72+
# Initial Run
73+
with RecordingDebugging(
74+
self.options["optimizer"], self.iter_count, self
75+
) as rec:
76+
# Initial Run
77+
model._solve_nonlinear()
78+
rec.abs = 0.0
79+
rec.rel = 0.0
80+
self.iter_count += 1
81+
82+
# Format design variables to suit segomoe implementation
83+
self.xspecs = self._initialize_vars()
84+
85+
# Format constraints to suit segomoe implementation
86+
self.n_cstr = self._initialize_cons()
87+
88+
# Format option dictionary to suit Egor implementation
89+
optim_settings = {
90+
"cstr_tol": 1e-6,
91+
}
92+
n_iter = self.opt_settings["maxiter"]
93+
optim_settings.update(
94+
{k: v for k, v in self.opt_settings.items() if k != "maxiter"}
95+
)
96+
97+
dim = 0
98+
for name, meta in self._designvars.items():
99+
dim += meta["size"]
100+
print("Designvars dimension: ", dim)
101+
if dim > 10:
102+
self.optim_settings["kpls_dim"] = 3
103+
104+
# Instanciate a SEGO optimizer
105+
egor = Egor(
106+
self._objfunc,
107+
xspecs=self.xspecs,
108+
n_cstr=self.n_cstr,
109+
**optim_settings,
110+
)
111+
112+
# Run the optim
113+
res = egor.minimize(n_eval=n_iter)
114+
115+
# Set optimal parameters
116+
i = 0
117+
for name, meta in self._designvars.items():
118+
size = meta["size"]
119+
self.set_design_var(name, res.x_opt[i : i + size])
120+
i += size
121+
122+
with RecordingDebugging(
123+
self.options["optimizer"], self.iter_count, self
124+
) as rec:
125+
model._solve_nonlinear()
126+
rec.abs = 0.0
127+
rec.rel = 0.0
128+
self.iter_count += 1
129+
130+
return True
131+
132+
def _initialize_vars(self):
133+
variables = []
134+
desvars = self._designvars
135+
for _, meta in desvars.items():
136+
if meta["size"] > 1:
137+
if np.isscalar(meta["lower"]):
138+
variables += [
139+
egx.Vspec(
140+
egx.Vtype(egx.Vtype.FLOAT), [meta["lower"], meta["upper"]]
141+
)
142+
for i in range(meta["size"])
143+
]
144+
else:
145+
variables += [
146+
egx.Vspec(
147+
egx.Vtype(egx.Vtype.FLOAT), [meta["lower"], meta["upper"]]
148+
)
149+
for i in range(meta["size"])
150+
]
151+
else:
152+
variables += [
153+
egx.Vspec(
154+
egx.Vtype(egx.Vtype.FLOAT), [meta["lower"], meta["upper"]]
155+
)
156+
]
157+
return variables
158+
159+
def _initialize_cons(self, eq_tol=None, ieq_tol=None):
160+
"""Format OpenMDAO constraints to suit EGOR implementation
161+
162+
Parameters
163+
----------
164+
eq_tol: dict
165+
Dictionary to define specific tolerance for eq constraints
166+
{'[groupName]': [tol]} Default tol = 1e-5
167+
"""
168+
con_meta = self._cons
169+
170+
self.ieq_cons = {
171+
name: con for name, con in con_meta.items() if not con["equals"]
172+
}
173+
174+
# Inequality constraints
175+
n_cstr = 0
176+
for name in self.ieq_cons.keys():
177+
meta = con_meta[name]
178+
size = meta["size"]
179+
# Bounds - double sided is supported
180+
lower = to_list(meta["lower"], size)
181+
upper = to_list(meta["upper"], size)
182+
for k in range(size):
183+
if (lower[k] is None or lower[k] < -1e29) and upper[k] == 0.0:
184+
n_cstr += 1
185+
else:
186+
raise ValueError(
187+
f"Constraint {lower[k]} < g(x) < {upper[k]} not handled by Egor driver"
188+
)
189+
return n_cstr
190+
191+
def _objfunc(self, points):
192+
"""
193+
Function that evaluates and returns the objective function and the
194+
constraints. This function is called by SEGOMOE
195+
196+
Parameters
197+
----------
198+
point : numpy.ndarray
199+
point to evaluate
200+
201+
Returns
202+
-------
203+
func_dict : dict
204+
Dictionary of all functional variables evaluated at design point.
205+
fail : int
206+
0 for successful function evaluation
207+
1 for unsuccessful function evaluation
208+
"""
209+
res = np.zeros((points.shape[0], 1 + self.n_cstr))
210+
model = self._problem().model
211+
212+
for k, point in enumerate(points):
213+
try:
214+
# Pass in new parameters
215+
i = 0
216+
217+
for name, meta in self._designvars.items():
218+
size = meta["size"]
219+
self.set_design_var(name, point[i : i + size])
220+
i += size
221+
222+
# Execute the model
223+
with RecordingDebugging(
224+
self.options["optimizer"], self.iter_count, self
225+
) as _:
226+
self.iter_count += 1
227+
try:
228+
model.run_solve_nonlinear()
229+
230+
# Let the optimizer try to handle the error
231+
except AnalysisError:
232+
model._clear_iprint()
233+
234+
# Get the objective function evaluation - single obj support
235+
for obj in self.get_objective_values().values():
236+
res[k, 0] = obj
237+
238+
# Get the constraint evaluations
239+
j = 1
240+
for con_res in self.get_constraint_values().values():
241+
# Make sure con_res is array_like
242+
con_res = to_list(con_res, 1)
243+
# Perform mapping
244+
for i, _ in enumerate(con_res):
245+
res[k, j + i] = con_res[i]
246+
j += 1
247+
248+
except Exception as msg:
249+
tb = traceback.format_exc()
250+
print("Exception: %s" % str(msg))
251+
print(70 * "=", tb, 70 * "=")
252+
253+
return res

openmdao_extensions/onera_sego_driver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,13 @@ def run(self, path_hs="", eq_tol={}, ieq_tol={}):
168168
i = 0
169169
for name, meta in self._designvars.items():
170170
size = meta["size"]
171-
self.set_design_var(name, x_best[i : i + size])
171+
self.set_design_var(name, x_best[0][i : i + size])
172172
i += size
173173

174174
with RecordingDebugging(
175175
self.options["optimizer"], self.iter_count, self
176176
) as rec:
177-
model._solve_nonlinear()
177+
model.run_solve_nonlinear()
178178
rec.abs = 0.0
179179
rec.rel = 0.0
180180
self.iter_count += 1
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import numpy as np
2+
from math import cos, sin, pi
3+
from openmdao.api import (
4+
IndepVarComp,
5+
Group,
6+
ExplicitComponent,
7+
)
8+
9+
10+
class Branin(ExplicitComponent):
11+
def setup(self):
12+
self.add_input("x1", val=1.0)
13+
self.add_input("x2", val=1.0)
14+
self.add_output("obj", val=1.0)
15+
self.add_output("con", val=1.0)
16+
17+
@staticmethod
18+
def compute(inputs, outputs):
19+
x_1 = inputs["x1"][0]
20+
x_2 = inputs["x2"][0]
21+
# obj
22+
part1 = (x_2 - (5.1 * x_1**2) / (4.0 * pi**2) + 5.0 * x_1 / pi - 6.0) ** 2
23+
part2 = 10.0 * ((1.0 - 1.0 / (8.0 * pi)) * cos(x_1) + 1.0)
24+
part3 = (5.0 * x_1 + 25.0) / 15.0
25+
outputs["obj"] = part1 + part2 + part3
26+
27+
# con
28+
x_g1 = (x_1 - 2.5) / 7.5
29+
x_g2 = (x_2 - 7.5) / 7.5
30+
part1 = (4.0 - 2.1 * x_g1**2 + (x_g1**4) / 3.0) * x_g1**2
31+
part2 = x_g1 * x_g2
32+
part3 = (4.0 * x_g2**2 - 4.0) * x_g2**2
33+
part4 = 3.0 * sin(6.0 * (1.0 - x_g1))
34+
part5 = 3.0 * sin(6.0 * (1.0 - x_g2))
35+
outputs["con"] = -(part1 + part2 + part3 + part4 + part5 - 6.0)
36+
37+
38+
class BraninMDA(Group):
39+
def setup(self):
40+
indeps = self.add_subsystem("indeps", IndepVarComp(), promotes=["*"])
41+
indeps.add_output("x1", 9.1)
42+
indeps.add_output("x2", 4.75)
43+
44+
self.add_subsystem("Branin", Branin(), promotes=["*"])
45+
46+
47+
class Ackley(ExplicitComponent):
48+
def setup(self):
49+
self.add_input("x", val=[1.0, 1.0])
50+
self.add_output("obj", val=1.0)
51+
52+
@staticmethod
53+
def compute(inputs, outputs):
54+
dim = 2
55+
a = 20.0
56+
b = 0.2
57+
c = 2 * np.pi
58+
point = inputs["x"]
59+
outputs["obj"] = (
60+
-a * np.exp(-b * np.sqrt(1.0 / dim * np.sum(point**2)))
61+
- np.exp(1.0 / dim * np.sum(np.cos(c * point)))
62+
+ a
63+
+ np.exp(1)
64+
)
65+
66+
67+
class AckleyMDA(Group):
68+
def setup(self):
69+
indeps = self.add_subsystem("indeps", IndepVarComp(), promotes=["*"])
70+
indeps.add_output("x", [1.0, 1.0])
71+
72+
self.add_subsystem("Ackley", Ackley(), promotes=["*"])

0 commit comments

Comments
 (0)