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.
- surrogates_interface/__init__.py +3 -0
- surrogates_interface/domains.py +107 -0
- surrogates_interface/openmdao.py +389 -0
- surrogates_interface/surrogates.py +1027 -0
- surrogates_interface/test/__init__.py +0 -0
- surrogates_interface/test/test_domains.py +44 -0
- surrogates_interface/test/test_init.py +12 -0
- surrogates_interface/test/test_pca_jacobian.py +137 -0
- surrogates_interface/test/test_smt.py +258 -0
- surrogates_interface/test/test_surrogates.py +22 -0
- surrogates_interface/test/test_tensorflow_and_openmdao.py +1220 -0
- surrogates_interface/test/test_transformers.py +617 -0
- surrogates_interface/transformers.py +816 -0
- surrogates_interface-4.0.1.dist-info/LICENSE +25 -0
- surrogates_interface-4.0.1.dist-info/METADATA +66 -0
- surrogates_interface-4.0.1.dist-info/RECORD +17 -0
- surrogates_interface-4.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
import h5py
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.testing as npt
|
|
10
|
+
import openmdao.api as om
|
|
11
|
+
import pytest
|
|
12
|
+
import tensorflow as tf
|
|
13
|
+
from sklearn.preprocessing import MinMaxScaler, StandardScaler
|
|
14
|
+
|
|
15
|
+
from surrogates_interface.domains import BoxDomain
|
|
16
|
+
from surrogates_interface.openmdao import (
|
|
17
|
+
InputOutputType,
|
|
18
|
+
SurrogateModelComp,
|
|
19
|
+
SurrogateModelCompMatrixFree,
|
|
20
|
+
)
|
|
21
|
+
from surrogates_interface.surrogates import (
|
|
22
|
+
H5_STR,
|
|
23
|
+
ADMode,
|
|
24
|
+
SurrogateModel,
|
|
25
|
+
TensorFlowModel,
|
|
26
|
+
)
|
|
27
|
+
from surrogates_interface.transformers import MinMaxScaler as MinMaxScalerSI
|
|
28
|
+
from surrogates_interface.transformers import Transformer
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def set_tf_seed():
|
|
32
|
+
"""
|
|
33
|
+
Set several seeds used by TensorFlow, to ensure the reproducibility of the results.
|
|
34
|
+
"""
|
|
35
|
+
os.environ["PYTHONHASHSEED"] = str(0)
|
|
36
|
+
random.seed(1)
|
|
37
|
+
np.random.seed(2)
|
|
38
|
+
tf.random.set_seed(3)
|
|
39
|
+
try:
|
|
40
|
+
tf.config.threading.set_inter_op_parallelism_threads(1)
|
|
41
|
+
tf.config.threading.set_intra_op_parallelism_threads(1)
|
|
42
|
+
except RuntimeError:
|
|
43
|
+
# It has probably already been set.
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# %% Test TensorFlowModel.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_init_n_features():
|
|
51
|
+
obj = TensorFlowModel(
|
|
52
|
+
tf.keras.Sequential(),
|
|
53
|
+
input_names=("x1", "x2"),
|
|
54
|
+
output_names=("y1",),
|
|
55
|
+
)
|
|
56
|
+
assert obj.n_inputs == 2
|
|
57
|
+
assert obj.n_outputs == 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_init_channel_names():
|
|
61
|
+
obj = TensorFlowModel(tf.keras.Sequential(), n_inputs=2, n_outputs=1)
|
|
62
|
+
assert obj.input_names == ["x_0", "x_1"]
|
|
63
|
+
assert obj.output_names == ["y_0"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_init_features_in_mismatch():
|
|
67
|
+
with pytest.raises(AssertionError):
|
|
68
|
+
TensorFlowModel(tf.keras.Sequential(), input_names=("x1", "x2"), n_inputs=5)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_init_features_out_mismatch():
|
|
72
|
+
with pytest.raises(AssertionError):
|
|
73
|
+
TensorFlowModel(tf.keras.Sequential(), output_names=("y1",), n_outputs=4)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_surrogate_model_jvp():
|
|
77
|
+
obj = SurrogateModel(lambda x: x)
|
|
78
|
+
with pytest.raises(NotImplementedError):
|
|
79
|
+
obj._predict_model_jvp(np.array([0.0]), np.array([0.0]))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_surrogate_model_vjp():
|
|
83
|
+
obj = SurrogateModel(lambda x: x)
|
|
84
|
+
with pytest.raises(NotImplementedError):
|
|
85
|
+
obj._predict_model_vjp(np.array([0.0]), np.array([0.0]))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_init_features_inout_match():
|
|
89
|
+
obj = TensorFlowModel(
|
|
90
|
+
tf.keras.Sequential(),
|
|
91
|
+
input_names=("x1", "x2"),
|
|
92
|
+
n_inputs=2,
|
|
93
|
+
output_names=("y1",),
|
|
94
|
+
n_outputs=1,
|
|
95
|
+
)
|
|
96
|
+
assert obj.input_names == ("x1", "x2")
|
|
97
|
+
assert obj.output_names == ("y1",)
|
|
98
|
+
assert obj.n_inputs == 2
|
|
99
|
+
assert obj.n_outputs == 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# These tests are meant to cover the following cases:
|
|
103
|
+
# - Single Input Single Output
|
|
104
|
+
# - Multiple Input Single Output
|
|
105
|
+
# - Single Input Multiple Output
|
|
106
|
+
# - Multiple Input Multiple Output
|
|
107
|
+
# In all cases, the input and output transformers are included.
|
|
108
|
+
# The tests focus on predict_output() and predict_jacobian(),
|
|
109
|
+
# where the latter is checked with finite differences against the model output.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.fixture
|
|
113
|
+
def make_data_siso():
|
|
114
|
+
# Build input and output.
|
|
115
|
+
domain = BoxDomain([-5.0], [10.0])
|
|
116
|
+
input = np.linspace(domain.min[0], domain.max[0], 1001, dtype=np.single).reshape(
|
|
117
|
+
-1, 1
|
|
118
|
+
)
|
|
119
|
+
output = input**2
|
|
120
|
+
# Build input transformers.
|
|
121
|
+
sklearn_input_transformers = [
|
|
122
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
123
|
+
MinMaxScaler(feature_range=(-2.0, +2.0), copy=True, clip=False),
|
|
124
|
+
]
|
|
125
|
+
input_transformed = input.copy()
|
|
126
|
+
for transformer in sklearn_input_transformers:
|
|
127
|
+
input_transformed = transformer.fit_transform(input_transformed)
|
|
128
|
+
si_input_transformers = [
|
|
129
|
+
Transformer.from_sklearn(t) for t in sklearn_input_transformers
|
|
130
|
+
]
|
|
131
|
+
# Build output transformers.
|
|
132
|
+
sklearn_output_transformers = [
|
|
133
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
134
|
+
MinMaxScaler(feature_range=(-1.0, +1.0), copy=True, clip=False),
|
|
135
|
+
]
|
|
136
|
+
output_transformed = output.copy()
|
|
137
|
+
for transformer in sklearn_output_transformers:
|
|
138
|
+
output_transformed = transformer.fit_transform(output_transformed)
|
|
139
|
+
si_output_transformers = [
|
|
140
|
+
Transformer.from_sklearn(t) for t in sklearn_output_transformers
|
|
141
|
+
]
|
|
142
|
+
# Train Artificial Neural Network.
|
|
143
|
+
# We only need a smooth result, and therefore train a simple model for only a few epochs.
|
|
144
|
+
n_sample_total = input.shape[0]
|
|
145
|
+
n_inputs = input.shape[1]
|
|
146
|
+
n_outputs = output.shape[1]
|
|
147
|
+
ratio_train = 0.7
|
|
148
|
+
ratio_test = 0.2
|
|
149
|
+
ratio_validation = 1.0 - (ratio_train + ratio_test)
|
|
150
|
+
i_sample = np.arange(n_sample_total)
|
|
151
|
+
rng = np.random.default_rng(seed=12345)
|
|
152
|
+
rng.shuffle(i_sample)
|
|
153
|
+
n_train = round(ratio_train * n_sample_total)
|
|
154
|
+
n_validation = round(ratio_validation * n_sample_total)
|
|
155
|
+
all_points_parts = np.split(i_sample, (n_train, n_train + n_validation))
|
|
156
|
+
(
|
|
157
|
+
sample_training_set,
|
|
158
|
+
sample_validation_set,
|
|
159
|
+
sample_testing_set,
|
|
160
|
+
) = all_points_parts
|
|
161
|
+
set_tf_seed()
|
|
162
|
+
tf_model = tf.keras.Sequential(
|
|
163
|
+
[
|
|
164
|
+
tf.keras.layers.Dense(5, activation="tanh", input_shape=(n_inputs,)),
|
|
165
|
+
tf.keras.layers.Dense(5, activation="tanh"),
|
|
166
|
+
tf.keras.layers.Dense(n_outputs),
|
|
167
|
+
]
|
|
168
|
+
)
|
|
169
|
+
tf_model.compile(
|
|
170
|
+
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-2),
|
|
171
|
+
loss=tf.keras.losses.MeanSquaredError(),
|
|
172
|
+
)
|
|
173
|
+
hist = tf_model.fit(
|
|
174
|
+
x=input[sample_training_set, :],
|
|
175
|
+
y=output[sample_training_set, :],
|
|
176
|
+
epochs=400,
|
|
177
|
+
batch_size=n_train,
|
|
178
|
+
verbose=0,
|
|
179
|
+
validation_data=(
|
|
180
|
+
input[sample_validation_set, :],
|
|
181
|
+
output[sample_validation_set, :],
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
history = hist.history
|
|
185
|
+
output_predicted_transformed = tf_model.predict(
|
|
186
|
+
input_transformed, batch_size=n_sample_total, verbose=0
|
|
187
|
+
)
|
|
188
|
+
output_predicted = output_predicted_transformed.copy()
|
|
189
|
+
for transformer in reversed(si_output_transformers):
|
|
190
|
+
transformer.inverse_transform(output_predicted, inplace=True)
|
|
191
|
+
if False:
|
|
192
|
+
import matplotlib.pyplot as plt
|
|
193
|
+
|
|
194
|
+
# Plot history.
|
|
195
|
+
fig_name = "training_history"
|
|
196
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
197
|
+
ax.set_xlabel("Epoch")
|
|
198
|
+
ax.set_ylabel("Mean Squared Error [-]")
|
|
199
|
+
ax.set_yscale("log")
|
|
200
|
+
ax.grid(True)
|
|
201
|
+
ax.plot(history["loss"], label="Training")
|
|
202
|
+
ax.plot(history["val_loss"], label="Validation")
|
|
203
|
+
ax.legend(loc="upper right")
|
|
204
|
+
# Plot function.
|
|
205
|
+
fig_name = "function"
|
|
206
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
207
|
+
ax.set_xlabel("x")
|
|
208
|
+
ax.set_ylabel("y")
|
|
209
|
+
ax.grid(True)
|
|
210
|
+
ax.plot(input.ravel(), output.ravel(), label="Exact")
|
|
211
|
+
ax.plot(input.ravel(), output_predicted.ravel(), label="ANN")
|
|
212
|
+
ax.legend()
|
|
213
|
+
si_model = TensorFlowModel(
|
|
214
|
+
tf_model,
|
|
215
|
+
si_input_transformers,
|
|
216
|
+
si_output_transformers,
|
|
217
|
+
n_inputs=1,
|
|
218
|
+
n_outputs=1,
|
|
219
|
+
domain=domain,
|
|
220
|
+
)
|
|
221
|
+
return (
|
|
222
|
+
si_model,
|
|
223
|
+
input[sample_testing_set, :],
|
|
224
|
+
output_predicted[sample_testing_set, :],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_predict_output_siso(make_data_siso):
|
|
229
|
+
si_model, input, output = make_data_siso
|
|
230
|
+
output_si = si_model.predict_output(input)
|
|
231
|
+
npt.assert_array_almost_equal_nulp(output_si, output, nulp=10)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_predict_jacobian_siso(make_data_siso):
|
|
235
|
+
si_model, input, _ = make_data_siso
|
|
236
|
+
# Compute jacobian using the chain rule and TensorFlow.
|
|
237
|
+
jacobian_si = si_model.predict_jacobian(input)
|
|
238
|
+
# Compute jacobian using central finite differences.
|
|
239
|
+
h = 1e-3
|
|
240
|
+
jacobian_fd = np.zeros(jacobian_si.shape)
|
|
241
|
+
jacobian_fd[:, 0, 0] = (
|
|
242
|
+
si_model.predict_output(input + h).ravel()
|
|
243
|
+
- si_model.predict_output(input - h).ravel()
|
|
244
|
+
) / (2.0 * h)
|
|
245
|
+
if False:
|
|
246
|
+
# Sensitivity study on the step size.
|
|
247
|
+
import matplotlib.pyplot as plt
|
|
248
|
+
|
|
249
|
+
fig_name = "jacobian"
|
|
250
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
251
|
+
ax.set_xlabel("x")
|
|
252
|
+
ax.set_ylabel("f'(x)")
|
|
253
|
+
ax.grid(True)
|
|
254
|
+
input = np.sort(input, axis=0)
|
|
255
|
+
# Compute central finite differences.
|
|
256
|
+
for h in [1e-3, 1e-2, 1e-1]:
|
|
257
|
+
yl = si_model.predict_output(input - h)
|
|
258
|
+
yr = si_model.predict_output(input + h)
|
|
259
|
+
jacobian_fd = (yr - yl) / (2.0 * h)
|
|
260
|
+
ax.plot(input.ravel(), jacobian_fd.ravel(), label=f"Central, h = {h:.0e}")
|
|
261
|
+
# Compute forward finite differences.
|
|
262
|
+
yc = si_model.predict_output(input)
|
|
263
|
+
for h in [1e-3, 1e-2, 1e-1]:
|
|
264
|
+
yr = si_model.predict_output(input + h)
|
|
265
|
+
jacobian_fd = (yr - yc) / h
|
|
266
|
+
ax.plot(
|
|
267
|
+
input.ravel(),
|
|
268
|
+
jacobian_fd.ravel(),
|
|
269
|
+
"--",
|
|
270
|
+
label=f"Forward, h = {h:.0e}",
|
|
271
|
+
)
|
|
272
|
+
ax.legend()
|
|
273
|
+
npt.assert_allclose(jacobian_si, jacobian_fd, rtol=1e-1, atol=1e-1)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@pytest.fixture
|
|
277
|
+
def make_data_miso():
|
|
278
|
+
# Build input and output.
|
|
279
|
+
domain = BoxDomain([-5.0, -10.0], [10.0, 2.0])
|
|
280
|
+
input = np.column_stack(
|
|
281
|
+
(
|
|
282
|
+
np.linspace(domain.min[0], domain.max[0], 1001, dtype=np.single),
|
|
283
|
+
np.linspace(domain.min[1], domain.max[1], 1001, dtype=np.single),
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
output = np.sum(input**2, axis=1, keepdims=True)
|
|
287
|
+
# Build input transformers.
|
|
288
|
+
sklearn_input_transformers = [
|
|
289
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
290
|
+
MinMaxScaler(feature_range=(-2.0, +2.0), copy=True, clip=False),
|
|
291
|
+
]
|
|
292
|
+
input_transformed = input.copy()
|
|
293
|
+
for transformer in sklearn_input_transformers:
|
|
294
|
+
input_transformed = transformer.fit_transform(input_transformed)
|
|
295
|
+
si_input_transformers = [
|
|
296
|
+
Transformer.from_sklearn(t) for t in sklearn_input_transformers
|
|
297
|
+
]
|
|
298
|
+
# Build output transformers.
|
|
299
|
+
sklearn_output_transformers = [
|
|
300
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
301
|
+
MinMaxScaler(feature_range=(-1.0, +1.0), copy=True, clip=False),
|
|
302
|
+
]
|
|
303
|
+
output_transformed = output.copy()
|
|
304
|
+
for transformer in sklearn_output_transformers:
|
|
305
|
+
output_transformed = transformer.fit_transform(output_transformed)
|
|
306
|
+
si_output_transformers = [
|
|
307
|
+
Transformer.from_sklearn(t) for t in sklearn_output_transformers
|
|
308
|
+
]
|
|
309
|
+
# Train Artificial Neural Network.
|
|
310
|
+
# We only need a smooth result, and therefore train a simple model for only a few epochs.
|
|
311
|
+
n_sample_total = input.shape[0]
|
|
312
|
+
n_inputs = input.shape[1]
|
|
313
|
+
n_outputs = output.shape[1]
|
|
314
|
+
ratio_train = 0.7
|
|
315
|
+
ratio_test = 0.2
|
|
316
|
+
ratio_validation = 1.0 - (ratio_train + ratio_test)
|
|
317
|
+
i_sample = np.arange(n_sample_total)
|
|
318
|
+
rng = np.random.default_rng(seed=12345)
|
|
319
|
+
rng.shuffle(i_sample)
|
|
320
|
+
n_train = round(ratio_train * n_sample_total)
|
|
321
|
+
n_validation = round(ratio_validation * n_sample_total)
|
|
322
|
+
all_points_parts = np.split(i_sample, (n_train, n_train + n_validation))
|
|
323
|
+
(
|
|
324
|
+
sample_training_set,
|
|
325
|
+
sample_validation_set,
|
|
326
|
+
sample_testing_set,
|
|
327
|
+
) = all_points_parts
|
|
328
|
+
set_tf_seed()
|
|
329
|
+
tf_model = tf.keras.Sequential(
|
|
330
|
+
[
|
|
331
|
+
tf.keras.layers.Dense(5, activation="tanh", input_shape=(n_inputs,)),
|
|
332
|
+
tf.keras.layers.Dense(5, activation="tanh"),
|
|
333
|
+
tf.keras.layers.Dense(n_outputs),
|
|
334
|
+
]
|
|
335
|
+
)
|
|
336
|
+
tf_model.compile(
|
|
337
|
+
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-2),
|
|
338
|
+
loss=tf.keras.losses.MeanSquaredError(),
|
|
339
|
+
)
|
|
340
|
+
hist = tf_model.fit(
|
|
341
|
+
x=input[sample_training_set, :],
|
|
342
|
+
y=output[sample_training_set, :],
|
|
343
|
+
epochs=400,
|
|
344
|
+
batch_size=n_train,
|
|
345
|
+
verbose=0,
|
|
346
|
+
validation_data=(
|
|
347
|
+
input[sample_validation_set, :],
|
|
348
|
+
output[sample_validation_set, :],
|
|
349
|
+
),
|
|
350
|
+
)
|
|
351
|
+
history = hist.history
|
|
352
|
+
output_predicted_transformed = tf_model.predict(
|
|
353
|
+
input_transformed, batch_size=n_sample_total, verbose=0
|
|
354
|
+
)
|
|
355
|
+
output_predicted = output_predicted_transformed.copy()
|
|
356
|
+
for transformer in reversed(si_output_transformers):
|
|
357
|
+
transformer.inverse_transform(output_predicted, inplace=True)
|
|
358
|
+
if False:
|
|
359
|
+
import matplotlib.pyplot as plt
|
|
360
|
+
|
|
361
|
+
# Plot history.
|
|
362
|
+
fig_name = "training_history"
|
|
363
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
364
|
+
ax.set_xlabel("Epoch")
|
|
365
|
+
ax.set_ylabel("Mean Squared Error [-]")
|
|
366
|
+
ax.set_yscale("log")
|
|
367
|
+
ax.grid(True)
|
|
368
|
+
ax.plot(history["loss"], label="Training")
|
|
369
|
+
ax.plot(history["val_loss"], label="Validation")
|
|
370
|
+
ax.legend(loc="upper right")
|
|
371
|
+
# Plot 1 to 1.
|
|
372
|
+
fig_name = "one_to_one"
|
|
373
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
374
|
+
ax.set_xlabel("Exact Output")
|
|
375
|
+
ax.set_ylabel("Predicted Output")
|
|
376
|
+
ax.grid(True)
|
|
377
|
+
ax.axline((0.0, 0.0), slope=1.0, color="k", linestyle="--")
|
|
378
|
+
ax.scatter(output.ravel(), output_predicted.ravel())
|
|
379
|
+
si_model = TensorFlowModel(
|
|
380
|
+
tf_model,
|
|
381
|
+
si_input_transformers,
|
|
382
|
+
si_output_transformers,
|
|
383
|
+
n_inputs=2,
|
|
384
|
+
n_outputs=1,
|
|
385
|
+
domain=domain,
|
|
386
|
+
)
|
|
387
|
+
return (
|
|
388
|
+
si_model,
|
|
389
|
+
input[sample_testing_set, :],
|
|
390
|
+
output_predicted[sample_testing_set, :],
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def test_predict_output_miso(make_data_miso):
|
|
395
|
+
si_model, input, output = make_data_miso
|
|
396
|
+
output_si = si_model.predict_output(input)
|
|
397
|
+
npt.assert_array_almost_equal_nulp(output_si, output, nulp=10)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_predict_output_outside_of_domain_miso(make_data_miso):
|
|
401
|
+
si_model, _, _ = make_data_miso
|
|
402
|
+
input = np.column_stack(
|
|
403
|
+
(
|
|
404
|
+
np.linspace(-6.0, 20.0, 4, dtype=np.single),
|
|
405
|
+
np.linspace(-2.0, 1.0, 4, dtype=np.single),
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
with pytest.warns(
|
|
409
|
+
UserWarning, match="75.0% of points are outside of the input domain."
|
|
410
|
+
):
|
|
411
|
+
si_model.predict_output(input)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_predict_jacobian_miso(make_data_miso):
|
|
415
|
+
si_model, input, _ = make_data_miso
|
|
416
|
+
# Compute jacobian using the chain rule and TensorFlow.
|
|
417
|
+
jacobian_si = si_model.predict_jacobian(input)
|
|
418
|
+
# Compute jacobian using central finite differences.
|
|
419
|
+
jacobian_fd = np.zeros(jacobian_si.shape)
|
|
420
|
+
h = 1e-3
|
|
421
|
+
for i in range(input.shape[1]):
|
|
422
|
+
xm = input.copy()
|
|
423
|
+
xp = input.copy()
|
|
424
|
+
xm[:, i] -= h
|
|
425
|
+
xp[:, i] += h
|
|
426
|
+
jacobian_fd[:, 0, i] = (
|
|
427
|
+
si_model.predict_output(xp).ravel() - si_model.predict_output(xm).ravel()
|
|
428
|
+
)
|
|
429
|
+
jacobian_fd /= 2.0 * h
|
|
430
|
+
npt.assert_allclose(jacobian_si, jacobian_fd, rtol=1e-1, atol=1e-1)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_predict_jacobian_outside_of_domain_miso(make_data_miso):
|
|
434
|
+
si_model, _, _ = make_data_miso
|
|
435
|
+
input = np.column_stack(
|
|
436
|
+
(
|
|
437
|
+
np.linspace(-6.0, 20.0, 4, dtype=np.single),
|
|
438
|
+
np.linspace(-2.0, 1.0, 4, dtype=np.single),
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
with pytest.warns(
|
|
442
|
+
UserWarning, match="75.0% of points are outside of the input domain."
|
|
443
|
+
):
|
|
444
|
+
si_model.predict_jacobian(input)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@pytest.fixture
|
|
448
|
+
def make_data_simo():
|
|
449
|
+
# Build input and output.
|
|
450
|
+
domain = BoxDomain([-5.0], [10.0])
|
|
451
|
+
input = np.linspace(domain.min[0], domain.max[0], 1001, dtype=np.single).reshape(
|
|
452
|
+
-1, 1
|
|
453
|
+
)
|
|
454
|
+
output = np.column_stack((input**2, 0.5 * input**2))
|
|
455
|
+
# Build input transformers.
|
|
456
|
+
sklearn_input_transformers = [
|
|
457
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
458
|
+
MinMaxScaler(feature_range=(-2.0, +2.0), copy=True, clip=False),
|
|
459
|
+
]
|
|
460
|
+
input_transformed = input.copy()
|
|
461
|
+
for transformer in sklearn_input_transformers:
|
|
462
|
+
input_transformed = transformer.fit_transform(input_transformed)
|
|
463
|
+
si_input_transformers = [
|
|
464
|
+
Transformer.from_sklearn(t) for t in sklearn_input_transformers
|
|
465
|
+
]
|
|
466
|
+
# Build output transformers.
|
|
467
|
+
sklearn_output_transformers = [
|
|
468
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
469
|
+
MinMaxScaler(feature_range=(-1.0, +1.0), copy=True, clip=False),
|
|
470
|
+
]
|
|
471
|
+
output_transformed = output.copy()
|
|
472
|
+
for transformer in sklearn_output_transformers:
|
|
473
|
+
output_transformed = transformer.fit_transform(output_transformed)
|
|
474
|
+
si_output_transformers = [
|
|
475
|
+
Transformer.from_sklearn(t) for t in sklearn_output_transformers
|
|
476
|
+
]
|
|
477
|
+
# Train Artificial Neural Network.
|
|
478
|
+
# We only need a smooth result, and therefore train a simple model for only a few epochs.
|
|
479
|
+
n_sample_total = input.shape[0]
|
|
480
|
+
n_inputs = input.shape[1]
|
|
481
|
+
n_outputs = output.shape[1]
|
|
482
|
+
ratio_train = 0.7
|
|
483
|
+
ratio_test = 0.2
|
|
484
|
+
ratio_validation = 1.0 - (ratio_train + ratio_test)
|
|
485
|
+
i_sample = np.arange(n_sample_total)
|
|
486
|
+
rng = np.random.default_rng(seed=12345)
|
|
487
|
+
rng.shuffle(i_sample)
|
|
488
|
+
n_train = round(ratio_train * n_sample_total)
|
|
489
|
+
n_validation = round(ratio_validation * n_sample_total)
|
|
490
|
+
all_points_parts = np.split(i_sample, (n_train, n_train + n_validation))
|
|
491
|
+
(
|
|
492
|
+
sample_training_set,
|
|
493
|
+
sample_validation_set,
|
|
494
|
+
sample_testing_set,
|
|
495
|
+
) = all_points_parts
|
|
496
|
+
set_tf_seed()
|
|
497
|
+
tf_model = tf.keras.Sequential(
|
|
498
|
+
[
|
|
499
|
+
tf.keras.layers.Dense(5, activation="tanh", input_shape=(n_inputs,)),
|
|
500
|
+
tf.keras.layers.Dense(5, activation="tanh"),
|
|
501
|
+
tf.keras.layers.Dense(n_outputs),
|
|
502
|
+
]
|
|
503
|
+
)
|
|
504
|
+
tf_model.compile(
|
|
505
|
+
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-2),
|
|
506
|
+
loss=tf.keras.losses.MeanSquaredError(),
|
|
507
|
+
)
|
|
508
|
+
hist = tf_model.fit(
|
|
509
|
+
x=input[sample_training_set, :],
|
|
510
|
+
y=output[sample_training_set, :],
|
|
511
|
+
epochs=400,
|
|
512
|
+
batch_size=n_train,
|
|
513
|
+
verbose=0,
|
|
514
|
+
validation_data=(
|
|
515
|
+
input[sample_validation_set, :],
|
|
516
|
+
output[sample_validation_set, :],
|
|
517
|
+
),
|
|
518
|
+
)
|
|
519
|
+
history = hist.history
|
|
520
|
+
output_predicted_transformed = tf_model.predict(
|
|
521
|
+
input_transformed, batch_size=n_sample_total, verbose=0
|
|
522
|
+
)
|
|
523
|
+
output_predicted = output_predicted_transformed.copy()
|
|
524
|
+
for transformer in reversed(si_output_transformers):
|
|
525
|
+
transformer.inverse_transform(output_predicted, inplace=True)
|
|
526
|
+
if False:
|
|
527
|
+
import matplotlib.pyplot as plt
|
|
528
|
+
|
|
529
|
+
# Plot history.
|
|
530
|
+
fig_name = "training_history"
|
|
531
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
532
|
+
ax.set_xlabel("Epoch")
|
|
533
|
+
ax.set_ylabel("Mean Squared Error [-]")
|
|
534
|
+
ax.set_yscale("log")
|
|
535
|
+
ax.grid(True)
|
|
536
|
+
ax.plot(history["loss"], label="Training")
|
|
537
|
+
ax.plot(history["val_loss"], label="Validation")
|
|
538
|
+
ax.legend(loc="upper right")
|
|
539
|
+
# Plot function.
|
|
540
|
+
fig_name = "function"
|
|
541
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
542
|
+
ax.set_xlabel("x")
|
|
543
|
+
ax.set_ylabel("y")
|
|
544
|
+
ax.grid(True)
|
|
545
|
+
for i in range(n_outputs):
|
|
546
|
+
ax.plot(input.ravel(), output[:, i], color=f"C{i}", label=f"Exact {i}")
|
|
547
|
+
ax.plot(
|
|
548
|
+
input.ravel(),
|
|
549
|
+
output_predicted[:, i].ravel(),
|
|
550
|
+
"--",
|
|
551
|
+
color=f"C{i}",
|
|
552
|
+
label=f"ANN {i}",
|
|
553
|
+
)
|
|
554
|
+
ax.legend()
|
|
555
|
+
si_model = TensorFlowModel(
|
|
556
|
+
tf_model,
|
|
557
|
+
si_input_transformers,
|
|
558
|
+
si_output_transformers,
|
|
559
|
+
n_inputs=1,
|
|
560
|
+
n_outputs=2,
|
|
561
|
+
domain=domain,
|
|
562
|
+
)
|
|
563
|
+
return (
|
|
564
|
+
si_model,
|
|
565
|
+
input[sample_testing_set, :],
|
|
566
|
+
output_predicted[sample_testing_set, :],
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def test_predict_output_simo(make_data_simo):
|
|
571
|
+
si_model, input, output = make_data_simo
|
|
572
|
+
output_si = si_model.predict_output(input)
|
|
573
|
+
npt.assert_array_almost_equal_nulp(output_si, output, nulp=40)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def test_predict_jacobian_simo(make_data_simo):
|
|
577
|
+
si_model, input, _ = make_data_simo
|
|
578
|
+
# Compute jacobian using the chain rule and TensorFlow.
|
|
579
|
+
jacobian_si = si_model.predict_jacobian(input)
|
|
580
|
+
# Compute jacobian using central finite differences.
|
|
581
|
+
h = 1e-3
|
|
582
|
+
jacobian_fd = np.zeros(jacobian_si.shape)
|
|
583
|
+
jacobian_fd[:, :, 0] = (
|
|
584
|
+
si_model.predict_output(input + h) - si_model.predict_output(input - h)
|
|
585
|
+
) / (2.0 * h)
|
|
586
|
+
npt.assert_allclose(jacobian_si, jacobian_fd, rtol=1e-1, atol=1e-1)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@pytest.fixture
|
|
590
|
+
def make_data_mimo():
|
|
591
|
+
# Build input and output.
|
|
592
|
+
domain = BoxDomain([-5.0, -10.0], [10.0, 2.0])
|
|
593
|
+
input = np.column_stack(
|
|
594
|
+
(
|
|
595
|
+
np.linspace(domain.min[0], domain.max[0], 1001, dtype=np.single),
|
|
596
|
+
np.linspace(domain.min[1], domain.max[1], 1001, dtype=np.single),
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
output = np.column_stack(
|
|
600
|
+
(0.8 * input[:, 0] ** 2, 0.5 * input[:, 1] ** 2, np.sum(input**2, axis=1))
|
|
601
|
+
)
|
|
602
|
+
# Build input transformers.
|
|
603
|
+
sklearn_input_transformers = [
|
|
604
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
605
|
+
MinMaxScaler(feature_range=(-2.0, +2.0), copy=True, clip=False),
|
|
606
|
+
]
|
|
607
|
+
input_transformed = input.copy()
|
|
608
|
+
for transformer in sklearn_input_transformers:
|
|
609
|
+
input_transformed = transformer.fit_transform(input_transformed)
|
|
610
|
+
si_input_transformers = [
|
|
611
|
+
Transformer.from_sklearn(t) for t in sklearn_input_transformers
|
|
612
|
+
]
|
|
613
|
+
# Build output transformers.
|
|
614
|
+
sklearn_output_transformers = [
|
|
615
|
+
StandardScaler(copy=True, with_mean=True, with_std=True),
|
|
616
|
+
MinMaxScaler(feature_range=(-1.0, +1.0), copy=True, clip=False),
|
|
617
|
+
]
|
|
618
|
+
output_transformed = output.copy()
|
|
619
|
+
for transformer in sklearn_output_transformers:
|
|
620
|
+
output_transformed = transformer.fit_transform(output_transformed)
|
|
621
|
+
si_output_transformers = [
|
|
622
|
+
Transformer.from_sklearn(t) for t in sklearn_output_transformers
|
|
623
|
+
]
|
|
624
|
+
# Train Artificial Neural Network.
|
|
625
|
+
# We only need a smooth result, and therefore train a simple model for only a few epochs.
|
|
626
|
+
n_sample_total = input.shape[0]
|
|
627
|
+
n_inputs = input.shape[1]
|
|
628
|
+
n_outputs = output.shape[1]
|
|
629
|
+
ratio_train = 0.7
|
|
630
|
+
ratio_test = 0.2
|
|
631
|
+
ratio_validation = 1.0 - (ratio_train + ratio_test)
|
|
632
|
+
i_sample = np.arange(n_sample_total)
|
|
633
|
+
rng = np.random.default_rng(seed=12345)
|
|
634
|
+
rng.shuffle(i_sample)
|
|
635
|
+
n_train = round(ratio_train * n_sample_total)
|
|
636
|
+
n_validation = round(ratio_validation * n_sample_total)
|
|
637
|
+
all_points_parts = np.split(i_sample, (n_train, n_train + n_validation))
|
|
638
|
+
(
|
|
639
|
+
sample_training_set,
|
|
640
|
+
sample_validation_set,
|
|
641
|
+
sample_testing_set,
|
|
642
|
+
) = all_points_parts
|
|
643
|
+
set_tf_seed()
|
|
644
|
+
tf_model = tf.keras.Sequential(
|
|
645
|
+
[
|
|
646
|
+
tf.keras.layers.Dense(5, activation="tanh", input_shape=(n_inputs,)),
|
|
647
|
+
tf.keras.layers.Dense(5, activation="tanh"),
|
|
648
|
+
tf.keras.layers.Dense(n_outputs),
|
|
649
|
+
]
|
|
650
|
+
)
|
|
651
|
+
tf_model.compile(
|
|
652
|
+
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-2),
|
|
653
|
+
loss=tf.keras.losses.MeanSquaredError(),
|
|
654
|
+
)
|
|
655
|
+
hist = tf_model.fit(
|
|
656
|
+
x=input[sample_training_set, :],
|
|
657
|
+
y=output[sample_training_set, :],
|
|
658
|
+
epochs=400,
|
|
659
|
+
batch_size=n_train,
|
|
660
|
+
verbose=0,
|
|
661
|
+
validation_data=(
|
|
662
|
+
input[sample_validation_set, :],
|
|
663
|
+
output[sample_validation_set, :],
|
|
664
|
+
),
|
|
665
|
+
)
|
|
666
|
+
history = hist.history
|
|
667
|
+
output_predicted_transformed = tf_model.predict(
|
|
668
|
+
input_transformed, batch_size=n_sample_total, verbose=0
|
|
669
|
+
)
|
|
670
|
+
output_predicted = output_predicted_transformed.copy()
|
|
671
|
+
for transformer in reversed(si_output_transformers):
|
|
672
|
+
transformer.inverse_transform(output_predicted, inplace=True)
|
|
673
|
+
if False:
|
|
674
|
+
import matplotlib.pyplot as plt
|
|
675
|
+
|
|
676
|
+
# Plot history.
|
|
677
|
+
fig_name = "training_history"
|
|
678
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
679
|
+
ax.set_xlabel("Epoch")
|
|
680
|
+
ax.set_ylabel("Mean Squared Error [-]")
|
|
681
|
+
ax.set_yscale("log")
|
|
682
|
+
ax.grid(True)
|
|
683
|
+
ax.plot(history["loss"], label="Training")
|
|
684
|
+
ax.plot(history["val_loss"], label="Validation")
|
|
685
|
+
ax.legend(loc="upper right")
|
|
686
|
+
# Plot 1 to 1.
|
|
687
|
+
fig_name = "one_to_one"
|
|
688
|
+
fig, ax = plt.subplots(num=fig_name, dpi=300)
|
|
689
|
+
ax.set_xlabel("Exact Output")
|
|
690
|
+
ax.set_ylabel("Predicted Output")
|
|
691
|
+
ax.grid(True)
|
|
692
|
+
ax.axline((0.0, 0.0), slope=1.0, color="k", linestyle="--")
|
|
693
|
+
ax.scatter(output.ravel(), output_predicted.ravel())
|
|
694
|
+
si_model = TensorFlowModel(
|
|
695
|
+
tf_model,
|
|
696
|
+
input_transformers=si_input_transformers,
|
|
697
|
+
output_transformers=si_output_transformers,
|
|
698
|
+
input_names=[f"x{i}" for i in range(n_inputs)],
|
|
699
|
+
output_names=[f"y{i}" for i in range(n_outputs)],
|
|
700
|
+
metadata={"Wöhler exponents": rng.uniform(0.0, 10.0, n_outputs)},
|
|
701
|
+
domain=domain,
|
|
702
|
+
)
|
|
703
|
+
return (
|
|
704
|
+
si_model,
|
|
705
|
+
input[sample_testing_set, :],
|
|
706
|
+
output_predicted[sample_testing_set, :],
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def test_predict_output_mimo(make_data_mimo):
|
|
711
|
+
si_model, input, output = make_data_mimo
|
|
712
|
+
output_si = si_model.predict_output(input)
|
|
713
|
+
npt.assert_array_almost_equal_nulp(output_si, output, nulp=10)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def test_predict_jacobian_mimo_automatic(make_data_mimo):
|
|
717
|
+
# The AD mode is chosen automatically based on the number of inputs and outputs.
|
|
718
|
+
si_model, input, _ = make_data_mimo
|
|
719
|
+
# Compute jacobian using the chain rule and TensorFlow.
|
|
720
|
+
jacobian_si = si_model.predict_jacobian(input)
|
|
721
|
+
# Compute jacobian using central finite differences.
|
|
722
|
+
jacobian_fd = np.zeros(jacobian_si.shape)
|
|
723
|
+
h = 1e-3
|
|
724
|
+
for i in range(input.shape[1]):
|
|
725
|
+
xm = input.copy()
|
|
726
|
+
xp = input.copy()
|
|
727
|
+
xm[:, i] -= h
|
|
728
|
+
xp[:, i] += h
|
|
729
|
+
jacobian_fd[:, :, i] = si_model.predict_output(xp) - si_model.predict_output(xm)
|
|
730
|
+
jacobian_fd /= 2.0 * h
|
|
731
|
+
npt.assert_allclose(jacobian_si, jacobian_fd, rtol=1e-1, atol=1e-1)
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def test_predict_jacobian_mimo_fwd(make_data_mimo):
|
|
735
|
+
# Compute the Jacobian using forward mode AD.
|
|
736
|
+
si_model, input, _ = make_data_mimo
|
|
737
|
+
# Compute jacobian using the chain rule and TensorFlow.
|
|
738
|
+
jacobian_si = si_model.predict_jacobian(input, ad_mode=ADMode.FWD)
|
|
739
|
+
# Compute jacobian using central finite differences.
|
|
740
|
+
jacobian_fd = np.zeros(jacobian_si.shape)
|
|
741
|
+
h = 1e-3
|
|
742
|
+
for i in range(input.shape[1]):
|
|
743
|
+
xm = input.copy()
|
|
744
|
+
xp = input.copy()
|
|
745
|
+
xm[:, i] -= h
|
|
746
|
+
xp[:, i] += h
|
|
747
|
+
jacobian_fd[:, :, i] = si_model.predict_output(xp) - si_model.predict_output(xm)
|
|
748
|
+
jacobian_fd /= 2.0 * h
|
|
749
|
+
npt.assert_allclose(jacobian_si, jacobian_fd, rtol=1e-1, atol=1e-1)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def test_predict_jacobian_mimo_rev(make_data_mimo):
|
|
753
|
+
# Compute the Jacobian using reverse mode AD.
|
|
754
|
+
si_model, input, _ = make_data_mimo
|
|
755
|
+
# Compute jacobian using the chain rule and TensorFlow.
|
|
756
|
+
jacobian_si = si_model.predict_jacobian(input, ad_mode=ADMode.REV)
|
|
757
|
+
# Compute jacobian using central finite differences.
|
|
758
|
+
jacobian_fd = np.zeros(jacobian_si.shape)
|
|
759
|
+
h = 1e-3
|
|
760
|
+
for i in range(input.shape[1]):
|
|
761
|
+
xm = input.copy()
|
|
762
|
+
xp = input.copy()
|
|
763
|
+
xm[:, i] -= h
|
|
764
|
+
xp[:, i] += h
|
|
765
|
+
jacobian_fd[:, :, i] = si_model.predict_output(xp) - si_model.predict_output(xm)
|
|
766
|
+
jacobian_fd /= 2.0 * h
|
|
767
|
+
npt.assert_allclose(jacobian_si, jacobian_fd, rtol=1e-1, atol=1e-1)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def test_save_load_h5(make_data_mimo):
|
|
771
|
+
si_model, _, _ = make_data_mimo
|
|
772
|
+
model_path = f"./{str(uuid.uuid4())}-model.h5"
|
|
773
|
+
extra_data_path = f"./{str(uuid.uuid4())}-extra-data.h5"
|
|
774
|
+
si_model.save_h5(model_path, extra_data_path)
|
|
775
|
+
si_model_loaded = TensorFlowModel.load_h5(model_path, extra_data_path)
|
|
776
|
+
assert si_model == si_model_loaded
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def test_save_load_h5_without_domain(make_data_mimo):
|
|
780
|
+
model, _, _ = make_data_mimo
|
|
781
|
+
model.domain = None
|
|
782
|
+
model_path = f"./{str(uuid.uuid4())}-model.h5"
|
|
783
|
+
extra_data_path = f"./{str(uuid.uuid4())}-extra-data.h5"
|
|
784
|
+
model.save_h5(model_path, extra_data_path)
|
|
785
|
+
model_loaded = TensorFlowModel.load_h5(model_path, extra_data_path)
|
|
786
|
+
assert model == model_loaded
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def test_save_load_h5_without_n_in_out(make_data_mimo):
|
|
790
|
+
model, _, _ = make_data_mimo
|
|
791
|
+
# model.n_inputs = None
|
|
792
|
+
# model.n_outputs = None
|
|
793
|
+
model_path = f"./{str(uuid.uuid4())}-model.h5"
|
|
794
|
+
extra_data_path = f"./{str(uuid.uuid4())}-extra-data.h5"
|
|
795
|
+
model.save_h5(model_path, extra_data_path)
|
|
796
|
+
# Delete n_inputs and n_outputs from the saved file.
|
|
797
|
+
# They will be reconstructed during loading from the input and output names.
|
|
798
|
+
with h5py.File(extra_data_path, "a") as fid:
|
|
799
|
+
del fid["n_inputs"]
|
|
800
|
+
del fid["n_outputs"]
|
|
801
|
+
model_loaded = TensorFlowModel.load_h5(model_path, extra_data_path)
|
|
802
|
+
assert model == model_loaded
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def test_save_load_h5_without_n_in_out_and_names(make_data_mimo):
|
|
806
|
+
model, _, _ = make_data_mimo
|
|
807
|
+
# model.n_inputs = None
|
|
808
|
+
# model.n_outputs = None
|
|
809
|
+
model_path = f"./{str(uuid.uuid4())}-model.h5"
|
|
810
|
+
extra_data_path = f"./{str(uuid.uuid4())}-extra-data.h5"
|
|
811
|
+
model.save_h5(model_path, extra_data_path)
|
|
812
|
+
# Delete n_inputs and n_outputs from the saved file.
|
|
813
|
+
# Delete also the input and output names.
|
|
814
|
+
with h5py.File(extra_data_path, "a") as fid:
|
|
815
|
+
del fid["n_inputs"]
|
|
816
|
+
del fid["n_outputs"]
|
|
817
|
+
del fid["input_names"]
|
|
818
|
+
del fid["output_names"]
|
|
819
|
+
fid.create_dataset("input_names", data=[], dtype=H5_STR)
|
|
820
|
+
fid.create_dataset("output_names", data=[], dtype=H5_STR)
|
|
821
|
+
with pytest.warns(UserWarning):
|
|
822
|
+
TensorFlowModel.load_h5(model_path, extra_data_path)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def test_eq_wrong_metadata(make_data_siso, make_data_mimo):
|
|
826
|
+
# This test will fail because of the different metadata.
|
|
827
|
+
model_siso, _, _ = make_data_siso
|
|
828
|
+
model_mimo, _, _ = make_data_mimo
|
|
829
|
+
with pytest.raises(AssertionError):
|
|
830
|
+
assert model_siso == model_mimo
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def test_eq_missing_domain(make_data_siso):
|
|
834
|
+
model_siso, _, _ = make_data_siso
|
|
835
|
+
obj = SurrogateModel(lambda x: x + 2.0)
|
|
836
|
+
with pytest.raises(AssertionError):
|
|
837
|
+
assert model_siso == obj
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def test_save_load_h5_with_wrong_model(make_data_mimo):
|
|
841
|
+
si_model, _, _ = make_data_mimo
|
|
842
|
+
model_path = f"./{str(uuid.uuid4())}-model.h5"
|
|
843
|
+
extra_data_path = f"./{str(uuid.uuid4())}-extra-data.h5"
|
|
844
|
+
si_model.save_h5(model_path, extra_data_path)
|
|
845
|
+
with pytest.raises(
|
|
846
|
+
ValueError,
|
|
847
|
+
match=re.escape(
|
|
848
|
+
"The model type in the extra data file (TensorFlowModel) does not match the current one (SurrogateModel)."
|
|
849
|
+
),
|
|
850
|
+
):
|
|
851
|
+
SurrogateModel.load_h5(model_path, extra_data_path)
|
|
852
|
+
os.remove(model_path)
|
|
853
|
+
os.remove(extra_data_path)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# We exclude this test until we will be able to drop TensorFlow 2.10.
|
|
857
|
+
def _test_save_h5_extra_data_to_non_h5_model_error(make_data_mimo):
|
|
858
|
+
si_model, _, _ = make_data_mimo
|
|
859
|
+
model_path = f"./{str(uuid.uuid4())}-model.keras"
|
|
860
|
+
with pytest.raises(
|
|
861
|
+
ValueError,
|
|
862
|
+
match="The extra data cannot be appended to the model file, because it is not a hdf5 file. Please specify the extra_data_path argument",
|
|
863
|
+
):
|
|
864
|
+
si_model.save_h5(model_path, save_format="keras")
|
|
865
|
+
|
|
866
|
+
os.remove(model_path)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def test_eq_wrong_type(make_data_mimo):
|
|
870
|
+
si_model, _, _ = make_data_mimo
|
|
871
|
+
obj_2 = MinMaxScalerSI(np.array([1.1, 10.4, 3.7]), np.array([-3.0, -50.0, +21.0]))
|
|
872
|
+
assert not si_model == obj_2
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
# %% Test SurrogateModelComp.
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def test_om_compute_array(make_data_mimo):
|
|
879
|
+
# Get the data.
|
|
880
|
+
si_model, input_test, output_test = make_data_mimo
|
|
881
|
+
# Make the OpenMDAO problem.
|
|
882
|
+
component = SurrogateModelComp(
|
|
883
|
+
model=si_model,
|
|
884
|
+
input_type=InputOutputType.JOINED,
|
|
885
|
+
output_type=InputOutputType.JOINED,
|
|
886
|
+
n_points=input_test.shape[0],
|
|
887
|
+
)
|
|
888
|
+
problem = om.Problem()
|
|
889
|
+
problem.model.add_subsystem(
|
|
890
|
+
"surrogate", component, promotes_inputs=["x"], promotes_outputs=["y"]
|
|
891
|
+
)
|
|
892
|
+
problem.setup()
|
|
893
|
+
# Set the input.
|
|
894
|
+
problem.set_val("x", input_test)
|
|
895
|
+
# Evaluate the model.
|
|
896
|
+
problem.run_model()
|
|
897
|
+
# Check the output.
|
|
898
|
+
npt.assert_allclose(problem.get_val("y"), output_test, rtol=1e-6)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def test_om_compute_dict(make_data_mimo):
|
|
902
|
+
# Get the data.
|
|
903
|
+
si_model, input_test, output_test = make_data_mimo
|
|
904
|
+
# Make the OpenMDAO problem.
|
|
905
|
+
component = SurrogateModelComp(
|
|
906
|
+
model=si_model,
|
|
907
|
+
input_type=InputOutputType.SPLIT,
|
|
908
|
+
output_type=InputOutputType.SPLIT,
|
|
909
|
+
n_points=input_test.shape[0],
|
|
910
|
+
)
|
|
911
|
+
problem = om.Problem()
|
|
912
|
+
problem.model.add_subsystem(
|
|
913
|
+
"surrogate", component, promotes_inputs=["*"], promotes_outputs=["*"]
|
|
914
|
+
)
|
|
915
|
+
problem.setup()
|
|
916
|
+
# Set the input.
|
|
917
|
+
for i in range(si_model.n_inputs):
|
|
918
|
+
problem.set_val(si_model.input_names[i], input_test[:, i])
|
|
919
|
+
# Evaluate the model.
|
|
920
|
+
problem.run_model()
|
|
921
|
+
# Get the output.
|
|
922
|
+
y = np.column_stack([problem.get_val(key) for key in si_model.output_names])
|
|
923
|
+
# Check the output.
|
|
924
|
+
npt.assert_allclose(y, output_test, rtol=1e-6)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def test_om_setup_wrong_input_type():
|
|
928
|
+
with pytest.raises(ValueError):
|
|
929
|
+
SurrogateModelComp(
|
|
930
|
+
model=TensorFlowModel(None, n_inputs=2, n_outputs=1),
|
|
931
|
+
input_type=-5,
|
|
932
|
+
output_type=InputOutputType.SPLIT,
|
|
933
|
+
n_points=10,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def test_om_setup_wrong_output_type():
|
|
938
|
+
with pytest.raises(ValueError):
|
|
939
|
+
SurrogateModelComp(
|
|
940
|
+
model=TensorFlowModel(None, n_inputs=2, n_outputs=1),
|
|
941
|
+
input_type=InputOutputType.SPLIT,
|
|
942
|
+
output_type=-5,
|
|
943
|
+
n_points=10,
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def test_om_compute_partials_joined_joined(make_data_mimo):
|
|
948
|
+
# Get the data.
|
|
949
|
+
si_model, input_test, _ = make_data_mimo
|
|
950
|
+
# Make the OpenMDAO problem.
|
|
951
|
+
problem = om.Problem()
|
|
952
|
+
component = SurrogateModelComp(
|
|
953
|
+
model=si_model,
|
|
954
|
+
input_type=InputOutputType.JOINED,
|
|
955
|
+
output_type=InputOutputType.JOINED,
|
|
956
|
+
n_points=input_test.shape[0],
|
|
957
|
+
)
|
|
958
|
+
problem.model.add_subsystem(
|
|
959
|
+
"surrogate", component, promotes_inputs=["x"], promotes_outputs=["y"]
|
|
960
|
+
)
|
|
961
|
+
problem.setup()
|
|
962
|
+
# Set the input.
|
|
963
|
+
problem.set_val("x", input_test)
|
|
964
|
+
# Check partial derivatives.
|
|
965
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
966
|
+
# Jacobian computed by finite differences.
|
|
967
|
+
jacobian_fd = om_partials["surrogate"][("y", "x")]["J_fd"]
|
|
968
|
+
# Exact Jacobian.
|
|
969
|
+
jacobian_exact = om_partials["surrogate"][("y", "x")]["J_fwd"]
|
|
970
|
+
# Test.
|
|
971
|
+
npt.assert_allclose(jacobian_fd, jacobian_exact, rtol=5e-1, atol=2e-1)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def test_om_compute_partials_joined_split(make_data_mimo):
|
|
975
|
+
# Get the data.
|
|
976
|
+
si_model, input_test, _ = make_data_mimo
|
|
977
|
+
# Make the OpenMDAO problem.
|
|
978
|
+
problem = om.Problem()
|
|
979
|
+
component = SurrogateModelComp(
|
|
980
|
+
model=si_model,
|
|
981
|
+
input_type=InputOutputType.JOINED,
|
|
982
|
+
output_type=InputOutputType.SPLIT,
|
|
983
|
+
n_points=input_test.shape[0],
|
|
984
|
+
)
|
|
985
|
+
problem.model.add_subsystem(
|
|
986
|
+
"surrogate", component, promotes_inputs=["x"], promotes_outputs=["*"]
|
|
987
|
+
)
|
|
988
|
+
problem.setup()
|
|
989
|
+
# Set the input.
|
|
990
|
+
problem.set_val("x", input_test)
|
|
991
|
+
# Check partial derivatives.
|
|
992
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
993
|
+
# Loop over the output channels.
|
|
994
|
+
for name in si_model.output_names:
|
|
995
|
+
# Jacobian computed by finite differences.
|
|
996
|
+
jacobian_fd = om_partials["surrogate"][(name, "x")]["J_fd"]
|
|
997
|
+
# Exact Jacobian.
|
|
998
|
+
jacobian_exact = om_partials["surrogate"][(name, "x")]["J_fwd"]
|
|
999
|
+
# Test.
|
|
1000
|
+
npt.assert_allclose(jacobian_fd, jacobian_exact, rtol=5e-1, atol=2e-1)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def test_om_compute_partials_split_joined(make_data_mimo):
|
|
1004
|
+
# Get the data.
|
|
1005
|
+
si_model, input_test, _ = make_data_mimo
|
|
1006
|
+
# Make the OpenMDAO problem.
|
|
1007
|
+
problem = om.Problem()
|
|
1008
|
+
component = SurrogateModelComp(
|
|
1009
|
+
model=si_model,
|
|
1010
|
+
input_type=InputOutputType.SPLIT,
|
|
1011
|
+
output_type=InputOutputType.JOINED,
|
|
1012
|
+
n_points=input_test.shape[0],
|
|
1013
|
+
)
|
|
1014
|
+
problem.model.add_subsystem(
|
|
1015
|
+
"surrogate", component, promotes_inputs=["*"], promotes_outputs=["y"]
|
|
1016
|
+
)
|
|
1017
|
+
problem.setup()
|
|
1018
|
+
# Set the input.
|
|
1019
|
+
for i in range(si_model.n_inputs):
|
|
1020
|
+
problem.set_val(si_model.input_names[i], input_test[:, i])
|
|
1021
|
+
# Check partial derivatives.
|
|
1022
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1023
|
+
# Loop over the input channels.
|
|
1024
|
+
for name in si_model.input_names:
|
|
1025
|
+
# Jacobian computed by finite differences.
|
|
1026
|
+
jacobian_fd = om_partials["surrogate"][("y", name)]["J_fd"]
|
|
1027
|
+
# Exact Jacobian.
|
|
1028
|
+
jacobian_exact = om_partials["surrogate"][("y", name)]["J_fwd"]
|
|
1029
|
+
# Test.
|
|
1030
|
+
npt.assert_allclose(jacobian_fd, jacobian_exact, rtol=5e-1, atol=2e-1)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def test_om_compute_partials_split_split(make_data_mimo):
|
|
1034
|
+
# Get the data.
|
|
1035
|
+
si_model, input_test, _ = make_data_mimo
|
|
1036
|
+
# Make the OpenMDAO problem.
|
|
1037
|
+
problem = om.Problem()
|
|
1038
|
+
component = SurrogateModelComp(
|
|
1039
|
+
model=si_model,
|
|
1040
|
+
input_type=InputOutputType.SPLIT,
|
|
1041
|
+
output_type=InputOutputType.SPLIT,
|
|
1042
|
+
n_points=input_test.shape[0],
|
|
1043
|
+
)
|
|
1044
|
+
problem.model.add_subsystem(
|
|
1045
|
+
"surrogate", component, promotes_inputs=["*"], promotes_outputs=["*"]
|
|
1046
|
+
)
|
|
1047
|
+
problem.setup()
|
|
1048
|
+
# Set the input.
|
|
1049
|
+
for i in range(si_model.n_inputs):
|
|
1050
|
+
problem.set_val(si_model.input_names[i], input_test[:, i])
|
|
1051
|
+
# Check partial derivatives.
|
|
1052
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1053
|
+
# Loop over the input channels.
|
|
1054
|
+
for name_out in si_model.output_names:
|
|
1055
|
+
for name_in in si_model.input_names:
|
|
1056
|
+
# Jacobian computed by finite differences.
|
|
1057
|
+
jacobian_fd = om_partials["surrogate"][(name_out, name_in)]["J_fd"]
|
|
1058
|
+
# Exact Jacobian.
|
|
1059
|
+
jacobian_exact = om_partials["surrogate"][(name_out, name_in)]["J_fwd"]
|
|
1060
|
+
# Test.
|
|
1061
|
+
npt.assert_allclose(jacobian_fd, jacobian_exact, rtol=5e-1, atol=2e-1)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def test_om_matrix_free_joined_joined(make_data_mimo):
|
|
1065
|
+
# Get the data.
|
|
1066
|
+
si_model, input_test, _ = make_data_mimo
|
|
1067
|
+
input_test = input_test[:10, :]
|
|
1068
|
+
# Make the OpenMDAO problem.
|
|
1069
|
+
problem = om.Problem()
|
|
1070
|
+
component = SurrogateModelCompMatrixFree(
|
|
1071
|
+
model=si_model,
|
|
1072
|
+
input_type=InputOutputType.JOINED,
|
|
1073
|
+
output_type=InputOutputType.JOINED,
|
|
1074
|
+
n_points=input_test.shape[0],
|
|
1075
|
+
)
|
|
1076
|
+
problem.model.add_subsystem(
|
|
1077
|
+
"surrogate", component, promotes_inputs=["x"], promotes_outputs=["y"]
|
|
1078
|
+
)
|
|
1079
|
+
problem.setup()
|
|
1080
|
+
# Set the input.
|
|
1081
|
+
problem.set_val("x", input_test)
|
|
1082
|
+
# Check partial derivatives.
|
|
1083
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1084
|
+
# Jacobian computed by finite differences.
|
|
1085
|
+
jacobian_fd = om_partials["surrogate"][("y", "x")]["J_fd"]
|
|
1086
|
+
# Exact Jacobian.
|
|
1087
|
+
jacobian_fwd = om_partials["surrogate"][("y", "x")]["J_fwd"]
|
|
1088
|
+
jacobian_rev = om_partials["surrogate"][("y", "x")]["J_rev"]
|
|
1089
|
+
# Test.
|
|
1090
|
+
npt.assert_allclose(jacobian_fd, jacobian_fwd, rtol=5e-1, atol=2e-1)
|
|
1091
|
+
npt.assert_allclose(jacobian_fd, jacobian_rev, rtol=5e-1, atol=2e-1)
|
|
1092
|
+
|
|
1093
|
+
|
|
1094
|
+
def test_om_matrix_free_joined_joined_outside_of_domain(make_data_miso):
|
|
1095
|
+
# Get the data.
|
|
1096
|
+
si_model, _, _ = make_data_miso
|
|
1097
|
+
input = np.column_stack(
|
|
1098
|
+
(
|
|
1099
|
+
np.linspace(-6.0, 20.0, 4, dtype=np.single),
|
|
1100
|
+
np.linspace(-2.0, 1.0, 4, dtype=np.single),
|
|
1101
|
+
)
|
|
1102
|
+
)
|
|
1103
|
+
# Make the OpenMDAO problem.
|
|
1104
|
+
problem = om.Problem()
|
|
1105
|
+
component = SurrogateModelCompMatrixFree(
|
|
1106
|
+
model=si_model,
|
|
1107
|
+
input_type=InputOutputType.JOINED,
|
|
1108
|
+
output_type=InputOutputType.JOINED,
|
|
1109
|
+
n_points=input.shape[0],
|
|
1110
|
+
)
|
|
1111
|
+
problem.model.add_subsystem(
|
|
1112
|
+
"surrogate", component, promotes_inputs=["x"], promotes_outputs=["y"]
|
|
1113
|
+
)
|
|
1114
|
+
problem.setup()
|
|
1115
|
+
# Set the input.
|
|
1116
|
+
problem.set_val("x", input)
|
|
1117
|
+
# Check partial derivatives.
|
|
1118
|
+
with pytest.warns(
|
|
1119
|
+
UserWarning, match="The hot point is outside of the input domain."
|
|
1120
|
+
):
|
|
1121
|
+
problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def test_om_matrix_free_joined_split(make_data_mimo):
|
|
1125
|
+
# Get the data.
|
|
1126
|
+
si_model, input_test, _ = make_data_mimo
|
|
1127
|
+
input_test = input_test[:10, :]
|
|
1128
|
+
# Make the OpenMDAO problem.
|
|
1129
|
+
problem = om.Problem()
|
|
1130
|
+
component = SurrogateModelCompMatrixFree(
|
|
1131
|
+
model=si_model,
|
|
1132
|
+
input_type=InputOutputType.JOINED,
|
|
1133
|
+
output_type=InputOutputType.SPLIT,
|
|
1134
|
+
n_points=input_test.shape[0],
|
|
1135
|
+
)
|
|
1136
|
+
problem.model.add_subsystem(
|
|
1137
|
+
"surrogate", component, promotes_inputs=["x"], promotes_outputs=["*"]
|
|
1138
|
+
)
|
|
1139
|
+
problem.setup()
|
|
1140
|
+
# Set the input.
|
|
1141
|
+
problem.set_val("x", input_test)
|
|
1142
|
+
# Check partial derivatives.
|
|
1143
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1144
|
+
# Loop over the output channels.
|
|
1145
|
+
for name in si_model.output_names:
|
|
1146
|
+
# Jacobian computed by finite differences.
|
|
1147
|
+
jacobian_fd = om_partials["surrogate"][(name, "x")]["J_fd"]
|
|
1148
|
+
# Exact Jacobian.
|
|
1149
|
+
jacobian_fwd = om_partials["surrogate"][(name, "x")]["J_fwd"]
|
|
1150
|
+
jacobian_rev = om_partials["surrogate"][(name, "x")]["J_rev"]
|
|
1151
|
+
# Test.
|
|
1152
|
+
npt.assert_allclose(jacobian_fd, jacobian_fwd, rtol=5e-1, atol=2e-1)
|
|
1153
|
+
npt.assert_allclose(jacobian_fd, jacobian_rev, rtol=5e-1, atol=2e-1)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
def test_om_matrix_free_split_joined(make_data_mimo):
|
|
1157
|
+
# Get the data.
|
|
1158
|
+
si_model, input_test, _ = make_data_mimo
|
|
1159
|
+
input_test = input_test[:10, :]
|
|
1160
|
+
# Make the OpenMDAO problem.
|
|
1161
|
+
problem = om.Problem()
|
|
1162
|
+
component = SurrogateModelCompMatrixFree(
|
|
1163
|
+
model=si_model,
|
|
1164
|
+
input_type=InputOutputType.SPLIT,
|
|
1165
|
+
output_type=InputOutputType.JOINED,
|
|
1166
|
+
n_points=input_test.shape[0],
|
|
1167
|
+
)
|
|
1168
|
+
problem.model.add_subsystem(
|
|
1169
|
+
"surrogate", component, promotes_inputs=["*"], promotes_outputs=["y"]
|
|
1170
|
+
)
|
|
1171
|
+
problem.setup()
|
|
1172
|
+
# Set the input.
|
|
1173
|
+
for i in range(si_model.n_inputs):
|
|
1174
|
+
problem.set_val(si_model.input_names[i], input_test[:, i])
|
|
1175
|
+
# Check partial derivatives.
|
|
1176
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1177
|
+
# Loop over the input channels.
|
|
1178
|
+
for name in si_model.input_names:
|
|
1179
|
+
# Jacobian computed by finite differences.
|
|
1180
|
+
jacobian_fd = om_partials["surrogate"][("y", name)]["J_fd"]
|
|
1181
|
+
# Exact Jacobian.
|
|
1182
|
+
jacobian_fwd = om_partials["surrogate"][("y", name)]["J_fwd"]
|
|
1183
|
+
jacobian_rev = om_partials["surrogate"][("y", name)]["J_rev"]
|
|
1184
|
+
# Test.
|
|
1185
|
+
npt.assert_allclose(jacobian_fd, jacobian_fwd, rtol=5e-1, atol=2e-1)
|
|
1186
|
+
npt.assert_allclose(jacobian_fd, jacobian_rev, rtol=5e-1, atol=2e-1)
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def test_om_matrix_free_split_split(make_data_mimo):
|
|
1190
|
+
# Get the data.
|
|
1191
|
+
si_model, input_test, _ = make_data_mimo
|
|
1192
|
+
input_test = input_test[:10, :]
|
|
1193
|
+
# Make the OpenMDAO problem.
|
|
1194
|
+
problem = om.Problem()
|
|
1195
|
+
component = SurrogateModelCompMatrixFree(
|
|
1196
|
+
model=si_model,
|
|
1197
|
+
input_type=InputOutputType.SPLIT,
|
|
1198
|
+
output_type=InputOutputType.SPLIT,
|
|
1199
|
+
n_points=input_test.shape[0],
|
|
1200
|
+
)
|
|
1201
|
+
problem.model.add_subsystem(
|
|
1202
|
+
"surrogate", component, promotes_inputs=["*"], promotes_outputs=["*"]
|
|
1203
|
+
)
|
|
1204
|
+
problem.setup()
|
|
1205
|
+
# Set the input.
|
|
1206
|
+
for i in range(si_model.n_inputs):
|
|
1207
|
+
problem.set_val(si_model.input_names[i], input_test[:, i])
|
|
1208
|
+
# Check partial derivatives.
|
|
1209
|
+
om_partials = problem.check_partials(out_stream=None, step=1e-3, step_calc="abs")
|
|
1210
|
+
# Loop over the input channels.
|
|
1211
|
+
for name_out in si_model.output_names:
|
|
1212
|
+
for name_in in si_model.input_names:
|
|
1213
|
+
# Jacobian computed by finite differences.
|
|
1214
|
+
jacobian_fd = om_partials["surrogate"][(name_out, name_in)]["J_fd"]
|
|
1215
|
+
# Exact Jacobian.
|
|
1216
|
+
jacobian_fwd = om_partials["surrogate"][(name_out, name_in)]["J_fwd"]
|
|
1217
|
+
jacobian_rev = om_partials["surrogate"][(name_out, name_in)]["J_rev"]
|
|
1218
|
+
# Test.
|
|
1219
|
+
npt.assert_allclose(jacobian_fd, jacobian_fwd, rtol=5e-1, atol=2e-1)
|
|
1220
|
+
npt.assert_allclose(jacobian_fd, jacobian_rev, rtol=5e-1, atol=2e-1)
|