Bases: Generator, StateOwner
A generator that runs a sequential optimization algorithm, such as Nelder-Mead, extremum seeking, RCDS, etc.
Generally, these algorithms need an internal state to keep track of the optimization process.
Additionally, users cannot interrupt the optimization process to add new points.
These algorithms will start from the last point in the history.
Source code in xopt/generators/sequential/sequential_generator.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205 | class SequentialGenerator(Generator, StateOwner):
"""
A generator that runs a sequential optimization algorithm, such as Nelder-Mead, extremum seeking, RCDS, etc.
Generally, these algorithms need an internal state to keep track of the optimization process.
Additionally, users cannot interrupt the optimization process to add new points.
These algorithms will start from the last point in the history.
"""
is_active: bool = False
_last_candidate: Optional[List[Dict[str, float]]] = None
_data_set: bool = False
def add_data(self, new_data: pd.DataFrame):
"""
Add new data to the generator.
Parameters
----------
new_data : pd.DataFrame
The new data to add.
Raises
------
ValueError
If the generator is active but no candidate was generated, or if the new data does not contain the last candidate.
"""
# if the generator is active then the new data must contain the last candidate
if self.is_active:
if self._last_candidate is None:
raise SeqGeneratorError(
"Generator is active, but no candidate was generated. Cannot add data."
)
if len(new_data) > 1:
raise SeqGeneratorError(
"Cannot add more than one data point when generator is active."
)
else:
# check if the last candidate is in the new data
self.validate_point(new_data.iloc[0].to_dict())
# do not call super, this will likely need to be customized for some generators
if self.data is not None:
self.data = pd.concat([self.data, new_data], axis=0, ignore_index=True)
else:
self.data = new_data
# update internal state of the generator
self._add_data(new_data)
def validate_point(self, point: Dict[str, float]):
"""determine if an input point was generated by the generator"""
last_candidate = np.array(
[self._last_candidate[0][ele] for ele in self.vocs.variable_names]
)
point_variables = np.array(
[point[ele] for ele in self.vocs.variable_names]
).flatten()
if not np.allclose(last_candidate, point_variables, atol=0.0, rtol=1e-6):
raise SeqGeneratorError(
"Cannot add data that was not generated by the generator when generator is active. "
"Call reset() to reset the generator first in order to add data via other methods."
)
@abstractmethod
def _add_data(self, new_data: pd.DataFrame):
"""
Customization of adding data to the algorithm.
Parameters
----------
new_data : pd.DataFrame
The new data to add.
Raises
------
NotImplementedError
If the method is not implemented by the subclass.
"""
pass # pragma: no cover
def set_data(self, data: pd.DataFrame):
"""
Set the full dataset for the generator. Typically only used when loading from a save file. This skips active
generator lockout.
Parameters
----------
data : pd.DataFrame
The data to set.
"""
# TODO: make a flag for generator that support multiple data sets
if self._data_set:
raise SeqGeneratorError(
"Data has already been initialized for this generator."
)
self._set_data(data)
self._data_set = True
@abstractmethod
def _set_data(self, data: pd.DataFrame):
pass # pragma: no cover
def generate(self, n_candidates: int = 1) -> dict:
"""
Generate a new candidate point.
Parameters
----------
n_candidates : int, optional
Number of candidates to generate, by default 1.
Returns
-------
dict
A dictionary representing the candidate point.
Raises
------
ValueError
If more than one candidate is requested.
"""
# we cannot generate more than one candidate at a time
if n_candidates > 1:
raise SeqGeneratorError(
"Sequential generators can only generate one candidate at a time."
)
# if the generator is not active, we need to start it
if not self.is_active:
candidate = self._generate(True)
self.is_active = True
else:
candidate = self._generate()
# need to store the candidate to validate adding data to the generator
self._last_candidate = candidate
return candidate
@abstractmethod
def _generate(self, first_gen: bool = False) -> Optional[List[Dict[str, float]]]:
"""
Generate a new candidate point.
Returns
-------
dict
A dictionary representing the candidate point.
Raises
------
NotImplementedError
If the method is not implemented by the subclass.
"""
pass # pragma: no cover
def reset(self):
"""
Reset the generator.
"""
self.is_active = False
self._last_candidate = None
self._reset()
def _reset(self):
"""
Reset the generator.
"""
pass # pragma: no cover
def _get_initial_point(self) -> Tuple[np.ndarray, np.ndarray]:
"""
Get the initial x0, f0 value from data.
Returns
-------
np.ndarray
The initial point as a NumPy array.
np.ndarray
The corresponding function value as a NumPy array.
Raises
------
ValueError
If there are no points in the data.
"""
# get the initial x0 value from data
if self.data is None or len(self.data) == 0:
raise ValueError(
f"At least one point is required to start {self.__class__.__name__}, add data manually or via Xopt.random_evaluate() or Xopt.evaluate_data()"
)
x0 = self.data.iloc[-1][self.vocs.variable_names].to_numpy(dtype=float)
f0 = self.data.iloc[-1][self.vocs.objective_names].to_numpy(dtype=float)
return x0, f0
|
__init__(**kwargs)
Initialize the generator.
Source code in xopt/generator.py
| def __init__(self, **kwargs):
"""
Initialize the generator.
"""
super().__init__(**kwargs)
logger.info(f"Initialized generator {self.name}")
|
add_data(new_data)
Add new data to the generator.
Parameters:
| Name |
Type |
Description |
Default |
new_data
|
DataFrame
|
|
required
|
Raises:
| Type |
Description |
ValueError
|
If the generator is active but no candidate was generated, or if the new data does not contain the last candidate.
|
Source code in xopt/generators/sequential/sequential_generator.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58 | def add_data(self, new_data: pd.DataFrame):
"""
Add new data to the generator.
Parameters
----------
new_data : pd.DataFrame
The new data to add.
Raises
------
ValueError
If the generator is active but no candidate was generated, or if the new data does not contain the last candidate.
"""
# if the generator is active then the new data must contain the last candidate
if self.is_active:
if self._last_candidate is None:
raise SeqGeneratorError(
"Generator is active, but no candidate was generated. Cannot add data."
)
if len(new_data) > 1:
raise SeqGeneratorError(
"Cannot add more than one data point when generator is active."
)
else:
# check if the last candidate is in the new data
self.validate_point(new_data.iloc[0].to_dict())
# do not call super, this will likely need to be customized for some generators
if self.data is not None:
self.data = pd.concat([self.data, new_data], axis=0, ignore_index=True)
else:
self.data = new_data
# update internal state of the generator
self._add_data(new_data)
|
generate(n_candidates=1)
Generate a new candidate point.
Parameters:
| Name |
Type |
Description |
Default |
n_candidates
|
int
|
Number of candidates to generate, by default 1.
|
1
|
Returns:
| Type |
Description |
dict
|
A dictionary representing the candidate point.
|
Raises:
| Type |
Description |
ValueError
|
If more than one candidate is requested.
|
Source code in xopt/generators/sequential/sequential_generator.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148 | def generate(self, n_candidates: int = 1) -> dict:
"""
Generate a new candidate point.
Parameters
----------
n_candidates : int, optional
Number of candidates to generate, by default 1.
Returns
-------
dict
A dictionary representing the candidate point.
Raises
------
ValueError
If more than one candidate is requested.
"""
# we cannot generate more than one candidate at a time
if n_candidates > 1:
raise SeqGeneratorError(
"Sequential generators can only generate one candidate at a time."
)
# if the generator is not active, we need to start it
if not self.is_active:
candidate = self._generate(True)
self.is_active = True
else:
candidate = self._generate()
# need to store the candidate to validate adding data to the generator
self._last_candidate = candidate
return candidate
|
model_dump(*args, **kwargs)
overwrite model dump to remove faux class attrs
Source code in xopt/generator.py
152
153
154
155
156
157
158
159
160 | def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
"""overwrite model dump to remove faux class attrs"""
res = super().model_dump(*args, **kwargs)
res.pop("supports_batch_generation", None)
res.pop("supports_multi_objective", None)
return res
|
reset()
Reset the generator.
Source code in xopt/generators/sequential/sequential_generator.py
167
168
169
170
171
172
173 | def reset(self):
"""
Reset the generator.
"""
self.is_active = False
self._last_candidate = None
self._reset()
|
set_data(data)
Set the full dataset for the generator. Typically only used when loading from a save file. This skips active
generator lockout.
Parameters:
| Name |
Type |
Description |
Default |
data
|
DataFrame
|
|
required
|
Source code in xopt/generators/sequential/sequential_generator.py
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107 | def set_data(self, data: pd.DataFrame):
"""
Set the full dataset for the generator. Typically only used when loading from a save file. This skips active
generator lockout.
Parameters
----------
data : pd.DataFrame
The data to set.
"""
# TODO: make a flag for generator that support multiple data sets
if self._data_set:
raise SeqGeneratorError(
"Data has already been initialized for this generator."
)
self._set_data(data)
self._data_set = True
|
validate_point(point)
determine if an input point was generated by the generator
Source code in xopt/generators/sequential/sequential_generator.py
60
61
62
63
64
65
66
67
68
69
70
71
72 | def validate_point(self, point: Dict[str, float]):
"""determine if an input point was generated by the generator"""
last_candidate = np.array(
[self._last_candidate[0][ele] for ele in self.vocs.variable_names]
)
point_variables = np.array(
[point[ele] for ele in self.vocs.variable_names]
).flatten()
if not np.allclose(last_candidate, point_variables, atol=0.0, rtol=1e-6):
raise SeqGeneratorError(
"Cannot add data that was not generated by the generator when generator is active. "
"Call reset() to reset the generator first in order to add data via other methods."
)
|
yaml(**kwargs)
serialize first then dump to yaml string
Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238 | def yaml(self, **kwargs):
"""serialize first then dump to yaml string"""
output = json.loads(
self.to_json(
**kwargs,
)
)
return yaml.dump(output)
|