surrogates_interface 4.0.2__tar.gz → 4.1.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: surrogates_interface
3
- Version: 4.0.2
3
+ Version: 4.1.0
4
4
  Summary: Interface for surrogate models.
5
5
  Home-page: https://gitlab.windenergy.dtu.dk/surrogate-models/surrogate-models-interface
6
6
  License: MIT
@@ -19,6 +19,7 @@ Provides-Extra: om
19
19
  Provides-Extra: smt
20
20
  Provides-Extra: test
21
21
  Provides-Extra: tf
22
+ Provides-Extra: torch
22
23
  Requires-Dist: black ; extra == "code-style"
23
24
  Requires-Dist: codespell ; extra == "code-style"
24
25
  Requires-Dist: flake8 ; extra == "code-style"
@@ -43,6 +44,7 @@ Requires-Dist: tensorflow-intel (>=2.10.0,<3.0.0) ; (sys_platform == "win32") an
43
44
  Requires-Dist: tensorflow-io-gcs-filesystem (<0.32.0) ; (python_version >= "3.9" and python_version < "3.12" and platform_system == "Windows") and (extra == "test" or extra == "tf")
44
45
  Requires-Dist: tensorflow-io-gcs-filesystem (>=0.23.1) ; (platform_machine != "arm64" and python_version >= "3.9" and python_version < "3.12" or platform_system != "Darwin" and python_version >= "3.9" and python_version < "3.12") and (extra == "test" or extra == "tf")
45
46
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; (python_version < "3.11") and (extra == "test")
47
+ Requires-Dist: torch (>=2.0.0,<3.0.0) ; extra == "test" or extra == "torch"
46
48
  Project-URL: Repository, https://gitlab.windenergy.dtu.dk/surrogate-models/surrogate-models-interface
47
49
  Description-Content-Type: text/markdown
48
50
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "surrogates_interface"
3
- version = "4.0.2"
3
+ version = "4.1.0"
4
4
  description = "Interface for surrogate models."
5
5
  authors = ["DTU Wind and Energy Systems"]
6
6
  maintainers = [
@@ -45,6 +45,7 @@ tensorflow-io-gcs-filesystem = [
45
45
  tensorflow-intel = {version="^2.10.0", platform = "win32", optional=true}
46
46
  smt = {version="^2.0.1", optional=true}
47
47
  openmdao = {version="^3.25", optional=true}
48
+ torch = { version = "^2.0.0", optional=true }
48
49
 
49
50
  [tool.poetry.extras]
50
51
 
@@ -74,6 +75,7 @@ test = ["matplotlib", # Needed by SMT.
74
75
  "tensorflow-io-gcs-filesystem",
75
76
  "tensorflow-intel",
76
77
  "openmdao",
78
+ "torch",
77
79
  ]
78
80
 
79
81
  smt = ["matplotlib", # Needed by SMT.
@@ -89,6 +91,9 @@ tf = ["scikit-learn",
89
91
 
90
92
  om = ["openmdao"]
91
93
 
94
+ torch = ["torch"]
95
+
96
+
92
97
 
93
98
  [build-system]
94
99
  build-backend = "poetry.core.masonry.api"
@@ -452,6 +452,8 @@ class SurrogateModel:
452
452
  x_transformed = x.copy()
453
453
  # The input transformation Jacobian starts as an identity matrix,
454
454
  # repeated over all points.
455
+ if x.ndim == 1:
456
+ x = x.reshape(1, -1)
455
457
  input_transformation_jacobian = np.tile(np.eye(x.shape[1]), (x.shape[0], 1, 1))
456
458
  for transformer in self.input_transformers:
457
459
  # Get Jacobian of current input transformation.
@@ -736,9 +738,9 @@ class TensorFlowModel(SurrogateModel):
736
738
  IOError
737
739
  In case of an invalid savefile.
738
740
  """
739
- from tensorflow.keras.models import load_model
741
+ import tensorflow as tf
740
742
 
741
- return load_model(filepath, custom_objects, compile, options)
743
+ return tf.keras.models.load_model(filepath, custom_objects, compile, options)
742
744
 
743
745
  def _save_model(self, filepath, overwrite=True, save_format="h5"):
744
746
  """
@@ -1025,3 +1027,172 @@ class SMTModel(SurrogateModel):
1025
1027
 
1026
1028
  class SurrogateModelFamily:
1027
1029
  pass
1030
+
1031
+
1032
+ class PyTorchModel(SurrogateModel):
1033
+ import torch
1034
+ import torch._dynamo # fmt: skip
1035
+ torch._dynamo.config.suppress_errors = True
1036
+ import torch.func as torch_func # fmt: skip
1037
+
1038
+ # from torch.func import jacfwd, jacrev, jvp, vjp, vmap # fmt: skip
1039
+
1040
+ def __init__(self, *args, dtype=torch.float16, max_batch=50_000, **kwargs):
1041
+ super().__init__(*args, **kwargs)
1042
+ self.model.eval()
1043
+ self.device = self.torch.device(
1044
+ "cuda" if self.torch.cuda.is_available() else "cpu"
1045
+ )
1046
+ self.model.to(self.device)
1047
+ self.dtype = dtype
1048
+ self.model.to(dtype)
1049
+ self.max_batch = max_batch
1050
+
1051
+ def __to_numpy(self, _x):
1052
+ try:
1053
+ import cupy as cp
1054
+ except ImportError: # pragma: no cover
1055
+ cp = np
1056
+ lalib = (
1057
+ cp if self.device == self.torch.device("cuda") else np
1058
+ ) # pragma: no cover
1059
+ return lalib.asarray(_x).astype(lalib.float32)
1060
+
1061
+ def __to_tensor(self, _x):
1062
+ if len(_x.shape) < 2:
1063
+ _x = _x.reshape(1, -1)
1064
+ return self.torch.as_tensor(_x, device=self.device, dtype=self.dtype)
1065
+
1066
+ def __record_in_dtype(self, _x): # pragma: no cover
1067
+ # TODO: could be used to retain the input/output dtype the same
1068
+ self.in_dtype = _x.dtype
1069
+
1070
+ def _predict_model_output(self, x):
1071
+ x = self.__to_tensor(x)
1072
+
1073
+ with self.torch.no_grad():
1074
+ output = self.torch.zeros(
1075
+ (x.shape[0], self.n_outputs),
1076
+ device=self.device,
1077
+ dtype=self.dtype,
1078
+ )
1079
+ for i in range(0, x.shape[0], self.max_batch):
1080
+ inter_out = self.model(x[i : i + self.max_batch])
1081
+ output[i : i + self.max_batch].copy_(
1082
+ inter_out[: x[i : i + self.max_batch].shape[0]], non_blocking=True
1083
+ )
1084
+
1085
+ return self.__to_numpy(output)
1086
+
1087
+ def _predict_model_jacobian(self, x, ad_mode=None):
1088
+ if ad_mode is None:
1089
+ ad_mode = ADMode.REV if (self.n_outputs <= self.n_inputs) else ADMode.FWD
1090
+
1091
+ if ad_mode is ADMode.REV:
1092
+ y, jacobian = self._predict_model_output_and_jacobian_rev(x)
1093
+ elif ad_mode is ADMode.FWD:
1094
+ y, jacobian = self._predict_model_output_and_jacobian_fwd(x)
1095
+
1096
+ return y, jacobian
1097
+
1098
+ def predict_output(self, x):
1099
+ y = super().predict_output(x)
1100
+ return self.__to_numpy(y)
1101
+
1102
+ def _save_model(self, filepath):
1103
+ self.torch.save(self.model.state_dict(), filepath)
1104
+
1105
+ @staticmethod
1106
+ def _load_model(filepath, model_cls):
1107
+ import torch # fmt: skip
1108
+ model_cls.load_state_dict(torch.load(filepath, weights_only=True))
1109
+ return model_cls
1110
+
1111
+ def _predict_model_jvp(self, x, dx):
1112
+ if not hasattr(self, "batched_jvp_func"):
1113
+ def g(_x): # fmt: skip
1114
+ return self.model(_x)
1115
+ def single_jvp(_x, _v): # fmt: skip
1116
+ return self.torch_func.jvp(g, (_x,), (_v,))
1117
+ self.batched_jvp_func = self.torch_func.vmap(single_jvp)
1118
+
1119
+ x = self.__to_tensor(x)
1120
+ dx = self.__to_tensor(dx)
1121
+ output, jvp_res = self.batched_jvp_func(x, dx)
1122
+ return self.__to_numpy(output.detach()), self.__to_numpy(jvp_res.detach())
1123
+
1124
+ def _predict_model_vjp(self, x, dy):
1125
+ if not hasattr(self, "batched_vjp_func"):
1126
+ def g(_x): # fmt: skip
1127
+ return self.model(_x)
1128
+ def single_vjp(_x, _v): # fmt: skip
1129
+ out, f = self.torch_func.vjp(g, _x)
1130
+ return out, f(_v)
1131
+ self.batched_vjp_func = self.torch_func.vmap(single_vjp)
1132
+
1133
+ x = self.__to_tensor(x)
1134
+ dy = self.__to_tensor(dy)
1135
+ output, vjp_res = self.batched_vjp_func(x, dy)
1136
+ return self.__to_numpy(output.detach()), self.__to_numpy(vjp_res[0].detach())
1137
+
1138
+ def _predict_model_output_and_jacobian_fwd(self, x):
1139
+ if not hasattr(self, "y_jac_fwd_func"):
1140
+ def g(_x): # fmt: skip
1141
+ result = self.model(_x)
1142
+ return (result, result)
1143
+ self.y_jac_fwd_func = self.torch_func.vmap(
1144
+ self.torch_func.jacfwd(g, has_aux=True)
1145
+ )
1146
+
1147
+ x = self.__to_tensor(x)
1148
+ jac, output = self.y_jac_fwd_func(x)
1149
+ return self.__to_numpy(output.detach()), self.__to_numpy(jac.detach())
1150
+
1151
+ def _predict_model_output_and_jacobian_rev(self, x):
1152
+ if not hasattr(self, "y_jac_rev_func"):
1153
+ def g(_x): # fmt: skip
1154
+ result = self.model(_x)
1155
+ return (result, result)
1156
+ self.y_jac_rev_func = self.torch_func.vmap(
1157
+ self.torch_func.jacrev(g, has_aux=True)
1158
+ )
1159
+
1160
+ x = self.__to_tensor(x)
1161
+ jac, output = self.y_jac_rev_func(x)
1162
+ return self.__to_numpy(output.detach()), self.__to_numpy(jac.detach())
1163
+
1164
+ def _predict_model_jacobian_sbatch(self, x): # pragma: no cover
1165
+ """Not used for now, but left as a reference for future if GPU memory issues arise."""
1166
+ if not hasattr(self, "y_jac_func"):
1167
+ def g(_x): # fmt: skip
1168
+ result = self.model(_x)
1169
+ return (result, result)
1170
+ self.y_jac_func = self.torch_func.vmap(
1171
+ self.torch_func.jacrev(g, has_aux=True)
1172
+ )
1173
+
1174
+ x = self.__to_tensor(x)
1175
+ batch_size = x.shape[0]
1176
+ y = self.torch.zeros(
1177
+ (batch_size, self.n_outputs),
1178
+ device=self.device,
1179
+ dtype=self.dtype,
1180
+ )
1181
+ jac = self.torch.zeros(
1182
+ (batch_size, self.n_outputs, self.n_inputs),
1183
+ device=self.device,
1184
+ dtype=self.dtype,
1185
+ )
1186
+ for i in range(0, batch_size, self.max_batch):
1187
+ x_mb = x[i : i + self.max_batch]
1188
+ inter_jac, inter_y = self.y_jac_func(x_mb)
1189
+ y[i : i + self.max_batch].copy_(
1190
+ inter_y[: x_mb.shape[0]],
1191
+ non_blocking=True,
1192
+ )
1193
+ jac[i : i + self.max_batch].copy_(
1194
+ inter_jac[: x_mb.shape[0]],
1195
+ non_blocking=True,
1196
+ )
1197
+
1198
+ return self.__to_numpy(y.detach()), self.__to_numpy(jac.detach())
@@ -814,3 +814,42 @@ class PowerTransformerYeoJohnson(Transformer):
814
814
  if isinstance(other, self.__class__):
815
815
  return np.array_equal(self.lambdas_, other.lambdas_)
816
816
  return False
817
+
818
+
819
+ class StandardScalerTorch(StandardScaler):
820
+ import torch
821
+
822
+ def __init__(self, scale, mean):
823
+ super().__init__(scale, mean)
824
+ device = self.torch.device("cuda" if self.torch.cuda.is_available() else "cpu")
825
+ self.device = device
826
+ def __np2torch(v): # fmt: skip
827
+ return self.torch.as_tensor(v).to(device)
828
+ self.gain_ = __np2torch(self.gain_)
829
+ self.inv_gain_ = __np2torch(self.inv_gain_)
830
+ self.offset_ = __np2torch(self.offset_)
831
+
832
+ def transform(self, x, inplace=True):
833
+ if inplace:
834
+ raise ValueError("No inplace...")
835
+ x = self.torch.as_tensor(x, device=self.device, dtype=self.torch.float32)
836
+ return super().transform(x, inplace)
837
+
838
+ def inverse_transform(self, x, inplace=True):
839
+ if inplace:
840
+ raise ValueError("No inplace...")
841
+ x = self.torch.as_tensor(x, device=self.device, dtype=self.torch.float32)
842
+ return super().inverse_transform(x, inplace)
843
+
844
+ def transform_gradient(self, x):
845
+ return self.torch.broadcast_to(self.gain_, x.shape).numpy()
846
+
847
+ def inverse_transform_gradient(self, x):
848
+ return self.torch.broadcast_to(self.inv_gain_, x.shape).numpy()
849
+
850
+ def __eq__(self, other):
851
+ if isinstance(other, self.__class__):
852
+ test_gain = self.torch.equal(self.gain_, other.gain_)
853
+ test_offset = self.torch.equal(self.offset_, other.offset_)
854
+ return test_gain and test_offset
855
+ return False
@@ -1,44 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import numpy as np
3
- import numpy.testing as npt
4
- import pytest
5
-
6
- from surrogates_interface.domains import BoxDomain, GenericDomain
7
-
8
-
9
- class TestBoxDomain:
10
- @staticmethod
11
- def test_in_domain():
12
- min = np.array([-1.0, +2.0, -15.0])
13
- max = np.array([+1.0, +7.0, +8.0])
14
- x = np.array(
15
- [[0.0, 0.0, 0.0], [0.0, 4.0, 9.0], [0.0, 4.0, 0.0], [3.0, 4.0, 6.0]]
16
- )
17
- box = BoxDomain(min, max)
18
- out = box.in_domain(x)
19
- npt.assert_array_equal(out, np.array([False, False, True, False]))
20
-
21
- @staticmethod
22
- def test_bad_box():
23
- min = np.array([-1.0, +7.0])
24
- max = np.array([+1.0, -15.0])
25
- with pytest.raises(AssertionError):
26
- BoxDomain(min, max)
27
-
28
- @staticmethod
29
- def test_eq():
30
- box1 = BoxDomain([-1.0, +2.0, -15.0], [+1.0, +7.0, +8.0])
31
- box2 = BoxDomain([-1.0, -5.0, -15.0], [+1.0, +7.0, +8.0])
32
- assert not box1 == box2
33
-
34
- @staticmethod
35
- def test_eq_different_type():
36
- class DummyDomain(GenericDomain):
37
- fields = []
38
-
39
- def in_domain():
40
- pass
41
-
42
- box1 = BoxDomain([-1.0, +2.0, -15.0], [+1.0, +7.0, +8.0])
43
- box2 = DummyDomain()
44
- assert not box1 == box2
@@ -1,12 +0,0 @@
1
- try:
2
- import tomllib # From Python 3.11.
3
- except ModuleNotFoundError:
4
- import tomli as tomllib # Before Python 3.11.
5
-
6
- import surrogates_interface
7
-
8
-
9
- def test_version():
10
- with open("pyproject.toml", "rb") as fid:
11
- data = tomllib.load(fid)
12
- assert surrogates_interface.__version__ == data["tool"]["poetry"]["version"]
@@ -1,137 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import numpy as np
4
- import numpy.testing as npt
5
- import openmdao.api as om
6
- import pytest
7
- from sklearn.decomposition import PCA
8
- from sklearn.preprocessing import MinMaxScaler, PowerTransformer
9
-
10
- from surrogates_interface.openmdao import InputOutputType, SurrogateModelComp
11
- from surrogates_interface.surrogates import SurrogateModel
12
- from surrogates_interface.transformers import Transformer
13
-
14
-
15
- class Paraboloid(SurrogateModel):
16
- """Define a paraboloid."""
17
-
18
- coef_1 = np.array([0.2, 0.5, 0.3])
19
- coef_2 = np.array([1.2, 1.5, 1.3])
20
-
21
- def _predict_model_output(self, x):
22
- y = np.column_stack(
23
- (np.sum(self.coef_1 * x**2, axis=1), np.sum(self.coef_2 * x**2, axis=1))
24
- )
25
- return y
26
-
27
- def _predict_model_jacobian(self, x):
28
- y = self._predict_model_output(x)
29
- jac = np.stack((2.0 * self.coef_1 * x, 2.0 * self.coef_2 * x), axis=1)
30
- return y, jac
31
-
32
-
33
- @pytest.fixture
34
- def make_data():
35
- rng = np.random.default_rng(seed=123)
36
- n_points = 100
37
- n_inputs_original = 5
38
- n_outputs_original = 4
39
- input = rng.uniform(-1.0, +1.0, size=(n_points, n_inputs_original))
40
- # Introduce linear correlation between some input features.
41
- input[:, 1] = 2.0 * input[:, 0]
42
- input[:, 2] = 3.0 * input[:, 0] - 5.0
43
- # Build input transformers.
44
- sklearn_input_transformers = [
45
- PowerTransformer(standardize=False),
46
- PCA(n_components=3, whiten=False),
47
- ]
48
- input_transformed = input.copy()
49
- for transformer in sklearn_input_transformers:
50
- input_transformed = transformer.fit_transform(input_transformed)
51
- si_input_transformers = [
52
- Transformer.from_sklearn(t) for t in sklearn_input_transformers
53
- ]
54
- # Make a dummy output. We only need it to create the transformers.
55
- output = np.zeros((n_points, n_outputs_original))
56
- output[:, 0] = np.sin(input[:, 0]) * np.cos(input[:, 4])
57
- output[:, 1] = np.sum(input**2, axis=1)
58
- # Introduce linear correlation between some output features.
59
- output[:, 2] = 1.1 * output[:, 0]
60
- output[:, 3] = 0.5 * output[:, 1]
61
- # Build output transformers.
62
- sklearn_output_transformers = [
63
- MinMaxScaler(feature_range=(-1.0, +1.0), clip=False),
64
- PCA(n_components=2, whiten=False),
65
- ]
66
- output_transformed = output.copy()
67
- for transformer in sklearn_output_transformers:
68
- output_transformed = transformer.fit_transform(output_transformed)
69
- si_output_transformers = [
70
- Transformer.from_sklearn(t) for t in sklearn_output_transformers
71
- ]
72
- # Build the surrogate model.
73
- si_model = Paraboloid(
74
- [], # model is dummy.
75
- input_transformers=si_input_transformers,
76
- output_transformers=si_output_transformers,
77
- n_inputs=n_inputs_original,
78
- n_outputs=n_outputs_original,
79
- )
80
- # Evaluate it on the input, and overwrite the transformed output.
81
- output_transformed = si_model._predict_model_output(input_transformed)
82
- # Apply inverse output transformations and overwrite the original output.
83
- output = output_transformed.copy()
84
- for transformer in reversed(si_output_transformers):
85
- output = transformer.inverse_transform(output, inplace=False)
86
- return (
87
- si_model,
88
- input,
89
- output,
90
- )
91
-
92
-
93
- def test_jacobian(make_data):
94
- si_model, input, _ = make_data
95
- # Compute exact Jacobian.
96
- jacobian_si = si_model.predict_jacobian(input)
97
- # Compute Jacobian using central finite differences.
98
- jacobian_fd = np.zeros_like(jacobian_si)
99
- h = 1e-6
100
- for i in range(input.shape[1]):
101
- xm = input.copy()
102
- xp = input.copy()
103
- xm[:, i] -= h
104
- xp[:, i] += h
105
- jacobian_fd[:, :, i] = si_model.predict_output(xp) - si_model.predict_output(xm)
106
- jacobian_fd /= 2.0 * h
107
- # Test.
108
- npt.assert_allclose(jacobian_si, jacobian_fd, rtol=2e-7)
109
-
110
-
111
- def test_om_compute_partials_joined_joined(make_data):
112
- # Get the data.
113
- si_model, input, _ = make_data
114
- # Make the OpenMDAO problem.
115
- problem = om.Problem()
116
- component = SurrogateModelComp(
117
- model=si_model,
118
- input_type=InputOutputType.JOINED,
119
- output_type=InputOutputType.JOINED,
120
- n_points=input.shape[0],
121
- )
122
- problem.model.add_subsystem(
123
- "surrogate", component, promotes_inputs=["x"], promotes_outputs=["y"]
124
- )
125
- problem.setup()
126
- # Set the input.
127
- problem.set_val("x", input)
128
- # Check partial derivatives.
129
- om_partials = problem.check_partials(
130
- out_stream=None, step=1e-6, step_calc="abs", form="central"
131
- )
132
- # Jacobian computed by finite differences.
133
- jacobian_fd = om_partials["surrogate"][("y", "x")]["J_fd"]
134
- # Exact Jacobian.
135
- jacobian_exact = om_partials["surrogate"][("y", "x")]["J_fwd"]
136
- # Test.
137
- npt.assert_allclose(jacobian_fd, jacobian_exact, rtol=2e-7)