surrogates_interface 4.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ import importlib.metadata
2
+
3
+ __version__ = importlib.metadata.version(__name__)
@@ -0,0 +1,107 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ The classes contained in this module allow to find when a set of points is contained in the domain.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+
8
+ import h5py
9
+ import numpy as np
10
+
11
+ # String type for h5py.
12
+ H5_STR = h5py.special_dtype(vlen=str)
13
+
14
+
15
+ class GenericDomain(ABC):
16
+ """Base class for all domains."""
17
+
18
+ @property
19
+ @abstractmethod
20
+ def fields(self):
21
+ """List of fields in hdf5 group and GenericDomain object."""
22
+
23
+ @abstractmethod
24
+ def in_domain(self, x):
25
+ """Check that the input points are inside the domain."""
26
+
27
+ def _save_h5(self, group):
28
+ """Save this domain to a h5py `Group`.
29
+
30
+ Parameters
31
+ ----------
32
+ group : h5py.Group
33
+ Group where this object will be saved.
34
+ """
35
+ group.create_dataset("type", data=self.__class__.__name__, dtype=H5_STR)
36
+ for f in self.fields:
37
+ group.create_dataset(f, data=getattr(self, f))
38
+
39
+ @classmethod
40
+ def _load_h5(cls, group):
41
+ """
42
+ Load this domain from a h5py `Group`.
43
+
44
+ Parameters
45
+ ----------
46
+ group : h5py.Group
47
+ Group where this object has been saved.
48
+
49
+ Returns
50
+ -------
51
+ obj : GenericDomain
52
+ The saved domain.
53
+ """
54
+ # Protected because this function should be called by SurrogateModel.load_h5.
55
+ return cls(*[group[f][:] for f in cls.fields])
56
+
57
+
58
+ class BoxDomain(GenericDomain):
59
+ """Implements a n-dimensional box domain."""
60
+
61
+ fields = ["min", "max"]
62
+
63
+ def __init__(self, min, max):
64
+ """Create a new `BoxDomain`, based on the given min and max for each feature.
65
+
66
+ Parameters
67
+ ----------
68
+ min : array-like of shape (n_features)
69
+ Minimum for each feature.
70
+ max : array-like of shape (n_features)
71
+ Maximum for each feature.
72
+ """
73
+ self.min = np.asarray(min)
74
+ self.max = np.asarray(max)
75
+ assert np.all(np.greater_equal(self.max, self.min))
76
+
77
+ def in_domain(self, x):
78
+ """Check that the input points are inside the domain.
79
+
80
+ Parameters
81
+ ----------
82
+ x : array-like of shape (n_points, n_features)
83
+ Input points.
84
+
85
+ Returns
86
+ -------
87
+ y : array-like of shape (n_points, )
88
+ Array with `True` if a point is inside the domain and `False` otherwise.
89
+ """
90
+ # Test x >= min for each dimension.
91
+ test_min = np.greater_equal(x, self.min)
92
+
93
+ # Test x <= max for each dimension.
94
+ test_max = np.less_equal(x, self.max)
95
+
96
+ # Test min <= x <= max for each dimension.
97
+ test_minmax = np.logical_and(test_min, test_max)
98
+
99
+ # Test min <= x <= max for all dimensions.
100
+ return np.all(test_minmax, axis=1)
101
+
102
+ def __eq__(self, other):
103
+ if isinstance(other, self.__class__):
104
+ test_min = np.array_equal(self.min, other.min)
105
+ test_max = np.array_equal(self.max, other.max)
106
+ return test_min and test_max
107
+ return False
@@ -0,0 +1,389 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ In this module, we wrap the surrogate models into OpenMDAO components.
4
+ """
5
+
6
+ from enum import IntEnum, auto
7
+
8
+ import numpy as np
9
+ import openmdao.api as om
10
+
11
+ # from surrogates_interface.surrogates import SurrogateModel
12
+
13
+
14
+ class InputOutputType(IntEnum):
15
+ """
16
+ Enumeration used by `SurrogateModelComp` to determine the type of the input and output.
17
+ """
18
+
19
+ #: The input or output dimensions of a surrogate model are combined into one.
20
+ JOINED = auto()
21
+ #: The input or output dimensions of a surrogate model are kept separate.
22
+ SPLIT = auto()
23
+
24
+
25
+ class SurrogateModelCompFD(om.ExplicitComponent):
26
+ """
27
+ Wraps `SurrogateModel` into an OpenMDAO explicit component.
28
+ The gradient is approximated via finite differences.
29
+
30
+ Parameters
31
+ ----------
32
+ model : subclass of SurrogateModel
33
+ Surrogate model to be wrapped.
34
+ n_points : int
35
+ Number of points over which to evaluate the surrogate model.
36
+ input_type : InputOutputType
37
+ Type of the input.
38
+ - If `InputOutputType.JOINED` there will be only 1 input named `input_name`.
39
+ - If `InputOutputType.SPLIT` there will be 1 input per input channel. Their names will be taken from `model.input_names`.
40
+ output_type : InputOutputType
41
+ Type of the output.
42
+ - If `InputOutputType.JOINED` there will be only 1 output named `output_name`.
43
+ - If `InputOutputType.SPLIT` there will be 1 input per output channel. Their names will be taken from `model.output_names`.
44
+ input_name : str, default 'x'
45
+ Name of the input, that is used when `input_type = InputOutputType.JOINED`.
46
+ output_name : str, default 'y'
47
+ Name of the output, that is used when `output_type = InputOutputType.JOINED`.
48
+ """
49
+
50
+ @staticmethod
51
+ def _check_in_out_type(name, value):
52
+ """
53
+ Check that `value` is defined in `InputOutputType`.
54
+
55
+ Parameters
56
+ ----------
57
+ name : str
58
+ Name of the option.
59
+ value : InputOutputType
60
+ Value of the option.
61
+
62
+ Returns
63
+ -------
64
+ None.
65
+
66
+ Raises
67
+ ------
68
+ ValueError
69
+ If value is not valid for option.
70
+ """
71
+ if value not in iter(InputOutputType):
72
+ raise ValueError(
73
+ f"Option {name}, here {value}, must be defined in `InputOutputType`."
74
+ )
75
+
76
+ def initialize(self):
77
+ self.options.declare(
78
+ "model",
79
+ # types=SurrogateModel,
80
+ desc="Surrogate model to be wrapped.",
81
+ recordable=False,
82
+ ) # There is nothing to record here.
83
+
84
+ self.options.declare(
85
+ "n_points",
86
+ types=int,
87
+ desc="Number of points over which to evaluate the surrogate model.",
88
+ )
89
+
90
+ self.options.declare(
91
+ "input_type",
92
+ desc="""Type of the input.
93
+ - If `InputOutputType.JOINED` there will be only 1 input named `input_name`.
94
+ - If `InputOutputType.SPLIT` there will be 1 input per input channel. Their names will be taken from `model.input_names`.
95
+ """,
96
+ check_valid=self._check_in_out_type,
97
+ )
98
+
99
+ self.options.declare(
100
+ "output_type",
101
+ desc="""Type of the output.
102
+ - If `InputOutputType.JOINED` there will be only 1 output named `output_name`.
103
+ - If `InputOutputType.SPLIT` there will be 1 input per output channel. Their names will be taken from `model.output_names`.
104
+ """,
105
+ check_valid=self._check_in_out_type,
106
+ )
107
+
108
+ self.options.declare(
109
+ "input_name",
110
+ default="x",
111
+ desc="Name of the input, that is used when `input_type = InputOutputType.JOINED`.",
112
+ )
113
+
114
+ self.options.declare(
115
+ "output_name",
116
+ default="y",
117
+ desc="Name of the output, that is used when `output_type = InputOutputType.JOINED`.",
118
+ )
119
+
120
+ def setup(self):
121
+ if self.options["input_type"] is InputOutputType.JOINED:
122
+ self.add_input(
123
+ self.options["input_name"],
124
+ shape=(self.options["n_points"], self.options["model"].n_inputs),
125
+ )
126
+ else:
127
+ for name in self.options["model"].input_names:
128
+ self.add_input(name, shape=(self.options["n_points"]))
129
+
130
+ if self.options["output_type"] is InputOutputType.JOINED:
131
+ self.add_output(
132
+ self.options["output_name"],
133
+ shape=(self.options["n_points"], self.options["model"].n_outputs),
134
+ )
135
+ else:
136
+ for name in self.options["model"].output_names:
137
+ self.add_output(name, shape=(self.options["n_points"]))
138
+
139
+ def setup_partials(self):
140
+ """
141
+ Setup the partial derivatives.
142
+ """
143
+ self.declare_partials("*", "*", method="fd") # pragma: no cover
144
+
145
+ def compute(self, inputs, outputs):
146
+ """
147
+ Wrapper of `SurrogateModel.predict_output()`.
148
+
149
+ Parameters
150
+ ----------
151
+ inputs : dict
152
+ Inputs.
153
+ - If `input_type is InputOutputType.JOINED` we expect one input named `input_name` of shape `(n_points, n_inputs)`.
154
+ - If `input_type is InputOutputType.SPLIT` we expect one input per feature of shape `(n_points,)`.
155
+ The inputs will be stacked according to `input_names`.
156
+ outputs : dict
157
+ Outputs.
158
+ - If `output_type is InputOutputType.JOINED` we expect one output named `output_name` of shape `(n_points, n_outputs)`.
159
+ The outputs will be stacked according to `output_names`.
160
+ - If `output_type is InputOutputType.SPLIT` we expect one output per feature of shape `(n_points,)`.
161
+
162
+ Returns
163
+ -------
164
+ None.
165
+
166
+ """
167
+ # Compose the input.
168
+ if self.options["input_type"] is InputOutputType.JOINED:
169
+ x = inputs[self.options["input_name"]]
170
+
171
+ elif self.options["input_type"] is InputOutputType.SPLIT:
172
+ x = np.column_stack(
173
+ [inputs[key] for key in self.options["model"].input_names]
174
+ )
175
+
176
+ # Evaluate the model.
177
+ y = self.options["model"].predict_output(x)
178
+
179
+ # Compose the output.
180
+ if self.options["output_type"] is InputOutputType.JOINED:
181
+ outputs[self.options["output_name"]] = y
182
+
183
+ elif self.options["output_type"] is InputOutputType.SPLIT:
184
+ for i in range(self.options["model"].n_outputs):
185
+ outputs[self.options["model"].output_names[i]] = y[:, i]
186
+
187
+
188
+ class SurrogateModelComp(SurrogateModelCompFD):
189
+ """
190
+ Wraps `SurrogateModel` into an OpenMDAO explicit component.
191
+ The exact gradient is provided via `compute_partials`.
192
+ """
193
+
194
+ def setup_partials(self):
195
+ """
196
+ Setup the partial derivatives.
197
+
198
+ The format of the Jacobian depends if the user selected `JOINED` or `SPLIT` for the input and output.
199
+ Since the points are independent, the Jacobian is a block diagonal matrix
200
+ with shape `(n_points * n_outputs, n_points * n_inputs)`,
201
+ where each block has shape `(n_outputs, n_inputs)`.
202
+ If the user selected `SPLIT` than there is only 1 feature, while with `JOINED` all features are kept.
203
+
204
+ Note that in a time series model the points are dependent, and this code will be incorrect.
205
+ """
206
+
207
+ # The following code defines the sparsity pattern in COO format.
208
+
209
+ # Shorthand for the number of points.
210
+ ns = self.options["n_points"]
211
+
212
+ # Set the number of inputs.
213
+ if self.options["input_type"] is InputOutputType.JOINED:
214
+ ni = self.options["model"].n_inputs
215
+
216
+ elif self.options["input_type"] is InputOutputType.SPLIT:
217
+ ni = 1
218
+
219
+ # Set the number of outputs.
220
+ if self.options["output_type"] is InputOutputType.JOINED:
221
+ no = self.options["model"].n_outputs
222
+
223
+ elif self.options["output_type"] is InputOutputType.SPLIT:
224
+ no = 1
225
+
226
+ # Define the block-diagonal matrix.
227
+ self.declare_partials(
228
+ "*",
229
+ "*",
230
+ method="exact",
231
+ rows=np.arange(ns * no).repeat(ni),
232
+ cols=np.arange(ns * ni).reshape((ns, ni)).repeat(no, 0).flatten(),
233
+ )
234
+
235
+ def compute_partials(self, inputs, partials, discrete_inputs=None):
236
+ """
237
+ Wrapper of `SurrogateModel.predict_jacobian()`.
238
+
239
+ Parameters
240
+ ----------
241
+ inputs : dict
242
+ Inputs.
243
+ - If `input_type is InputOutputType.JOINED` we expect one input named `input_name` of shape `(n_points, n_inputs)`.
244
+ - If `input_type is InputOutputType.SPLIT` we expect one input per feature of shape `(n_points,)`.
245
+ The inputs will be stacked according to `input_names`.
246
+ partials : dict
247
+ Jacobian.
248
+ - If `output_type is InputOutputType.JOINED` we expect one Jacobian of shape `(n_points, n_outputs)`.
249
+ - If `output_type is InputOutputType.SPLIT` we expect `n_outputs` Jacobians of shape `(n_points,)`.
250
+
251
+ Returns
252
+ -------
253
+ None.
254
+
255
+ """
256
+ # Compose the input.
257
+ if self.options["input_type"] is InputOutputType.JOINED:
258
+ x = inputs[self.options["input_name"]]
259
+ elif self.options["input_type"] is InputOutputType.SPLIT:
260
+ x = np.column_stack(
261
+ [inputs[key] for key in self.options["model"].input_names]
262
+ )
263
+
264
+ # Evaluate the model.
265
+ jacobian = self.options["model"].predict_jacobian(x)
266
+
267
+ # Compose the output.
268
+ if self.options["output_type"] is InputOutputType.JOINED:
269
+ if self.options["input_type"] is InputOutputType.JOINED:
270
+ partials[self.options["output_name"], self.options["input_name"]] = (
271
+ jacobian.ravel()
272
+ )
273
+
274
+ elif self.options["input_type"] is InputOutputType.SPLIT:
275
+ for j in range(self.options["model"].n_inputs):
276
+ partials[
277
+ self.options["output_name"],
278
+ self.options["model"].input_names[j],
279
+ ] = jacobian[:, :, j].ravel()
280
+
281
+ elif self.options["output_type"] is InputOutputType.SPLIT:
282
+ if self.options["input_type"] is InputOutputType.JOINED:
283
+ for i in range(self.options["model"].n_outputs):
284
+ partials[
285
+ self.options["model"].output_names[i],
286
+ self.options["input_name"],
287
+ ] = jacobian[:, i, :].ravel()
288
+
289
+ elif self.options["input_type"] is InputOutputType.SPLIT:
290
+ for i in range(self.options["model"].n_outputs):
291
+ for j in range(self.options["model"].n_inputs):
292
+ partials[
293
+ self.options["model"].output_names[i],
294
+ self.options["model"].input_names[j],
295
+ ] = jacobian[:, i, j]
296
+
297
+
298
+ class SurrogateModelCompMatrixFree(SurrogateModelCompFD):
299
+ """
300
+ Wraps `SurrogateModel` into an OpenMDAO explicit component that uses the matrix-free API.
301
+ """
302
+
303
+ def setup_partials(self):
304
+ pass
305
+
306
+ def compute_jacvec_product(
307
+ self, inputs, d_inputs, d_outputs, mode, discrete_inputs=None
308
+ ):
309
+ # Compose the input.
310
+ if self.options["input_type"] is InputOutputType.JOINED:
311
+ x = inputs[self.options["input_name"]]
312
+ elif self.options["input_type"] is InputOutputType.SPLIT:
313
+ x = np.column_stack(
314
+ [inputs[key] for key in self.options["model"].input_names]
315
+ )
316
+
317
+ # Check for forward or reverse mode.
318
+ if mode == "fwd":
319
+ if self.options["output_type"] is InputOutputType.JOINED:
320
+ if self.options["input_type"] is InputOutputType.JOINED:
321
+ dx = d_inputs[self.options["input_name"]]
322
+ dy = self.options["model"].predict_jvp(x, dx)
323
+ d_outputs[self.options["output_name"]] = dy
324
+
325
+ elif self.options["input_type"] is InputOutputType.SPLIT:
326
+ for in_name in d_inputs:
327
+ i_in = self.options["model"].input_names.index(in_name)
328
+ dx = np.zeros(
329
+ (
330
+ self.options["n_points"],
331
+ self.options["model"].n_inputs,
332
+ )
333
+ )
334
+ dx[:, i_in] = d_inputs[in_name]
335
+ dy = self.options["model"].predict_jvp(x, dx)
336
+ d_outputs[self.options["output_name"]] += dy
337
+
338
+ elif self.options["output_type"] is InputOutputType.SPLIT:
339
+ if self.options["input_type"] is InputOutputType.JOINED:
340
+ dx = d_inputs[self.options["input_name"]]
341
+ dy = self.options["model"].predict_jvp(x, dx)
342
+ for out_name in d_outputs:
343
+ i_out = self.options["model"].output_names.index(out_name)
344
+ d_outputs[out_name] += dy[:, i_out]
345
+
346
+ elif self.options["input_type"] is InputOutputType.SPLIT:
347
+ for in_name in d_inputs:
348
+ i_in = self.options["model"].input_names.index(in_name)
349
+ dx = np.zeros(
350
+ (
351
+ self.options["n_points"],
352
+ self.options["model"].n_inputs,
353
+ )
354
+ )
355
+ dx[:, i_in] = d_inputs[in_name]
356
+ dy = self.options["model"].predict_jvp(x, dx)
357
+ for out_name in d_outputs:
358
+ i_out = self.options["model"].output_names.index(out_name)
359
+ d_outputs[out_name] += dy[:, i_out]
360
+
361
+ else: # mode == 'rev'
362
+ if self.options["output_type"] is InputOutputType.JOINED:
363
+ dy = d_outputs[self.options["output_name"]]
364
+ dx = self.options["model"].predict_vjp(x, dy)
365
+
366
+ if self.options["input_type"] is InputOutputType.JOINED:
367
+ d_inputs[self.options["input_name"]] += dx
368
+
369
+ elif self.options["input_type"] is InputOutputType.SPLIT:
370
+ for in_name in d_inputs:
371
+ i_in = self.options["model"].input_names.index(in_name)
372
+ d_inputs[in_name] += dx[:, i_in]
373
+
374
+ elif self.options["output_type"] is InputOutputType.SPLIT:
375
+ for out_name in d_outputs:
376
+ i_out = self.options["model"].output_names.index(out_name)
377
+ dy = np.zeros(
378
+ (self.options["n_points"], self.options["model"].n_outputs)
379
+ )
380
+ dy[:, i_out] = d_outputs[out_name]
381
+ dx = self.options["model"].predict_vjp(x, dy)
382
+
383
+ if self.options["input_type"] is InputOutputType.JOINED:
384
+ d_inputs[self.options["input_name"]] += dx
385
+
386
+ elif self.options["input_type"] is InputOutputType.SPLIT:
387
+ for in_name in d_inputs:
388
+ i_in = self.options["model"].input_names.index(in_name)
389
+ d_inputs[in_name] += dx[:, i_in]