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,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)