surrogates_interface 4.0.3__tar.gz → 4.1.1__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.3
3
+ Version: 4.1.1
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.3"
3
+ version = "4.1.1"
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,187 @@ class SMTModel(SurrogateModel):
1025
1027
 
1026
1028
  class SurrogateModelFamily:
1027
1029
  pass
1030
+
1031
+
1032
+ class PyTorchModel(SurrogateModel):
1033
+ """
1034
+ Class that stores and evaluates an existing PyTorch model.
1035
+ """
1036
+
1037
+ def __init__(self, *args, dtype=None, max_batch=50_000, **kwargs):
1038
+ import torch
1039
+ import torch._dynamo
1040
+
1041
+ torch._dynamo.config.suppress_errors = True
1042
+ super().__init__(*args, **kwargs)
1043
+ self.model.eval()
1044
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
1045
+ self.model.to(self.device)
1046
+ self.dtype = torch.float16 if dtype is None else dtype
1047
+ self.model.to(self.dtype)
1048
+ self.max_batch = max_batch
1049
+
1050
+ def __to_numpy(self, _x):
1051
+ import torch
1052
+
1053
+ try:
1054
+ import cupy as cp
1055
+ except ImportError: # pragma: no cover
1056
+ cp = np
1057
+ lalib = cp if self.device == torch.device("cuda") else np # pragma: no cover
1058
+ return lalib.asarray(_x).astype(lalib.float32)
1059
+
1060
+ def __to_tensor(self, _x):
1061
+ import torch
1062
+
1063
+ if len(_x.shape) < 2:
1064
+ _x = _x.reshape(1, -1)
1065
+ return torch.as_tensor(_x, device=self.device, dtype=self.dtype)
1066
+
1067
+ def __record_in_dtype(self, _x): # pragma: no cover
1068
+ # TODO: could be used to retain the input/output dtype the same
1069
+ self.in_dtype = _x.dtype
1070
+
1071
+ def _predict_model_output(self, x):
1072
+ import torch
1073
+
1074
+ x = self.__to_tensor(x)
1075
+
1076
+ with torch.no_grad():
1077
+ output = torch.zeros(
1078
+ (x.shape[0], self.n_outputs),
1079
+ device=self.device,
1080
+ dtype=self.dtype,
1081
+ )
1082
+ for i in range(0, x.shape[0], self.max_batch):
1083
+ inter_out = self.model(x[i : i + self.max_batch])
1084
+ output[i : i + self.max_batch].copy_(
1085
+ inter_out[: x[i : i + self.max_batch].shape[0]], non_blocking=True
1086
+ )
1087
+
1088
+ return self.__to_numpy(output)
1089
+
1090
+ def _predict_model_jacobian(self, x, ad_mode=None):
1091
+ if ad_mode is None:
1092
+ ad_mode = ADMode.REV if (self.n_outputs <= self.n_inputs) else ADMode.FWD
1093
+
1094
+ if ad_mode is ADMode.REV:
1095
+ y, jacobian = self._predict_model_output_and_jacobian_rev(x)
1096
+ elif ad_mode is ADMode.FWD:
1097
+ y, jacobian = self._predict_model_output_and_jacobian_fwd(x)
1098
+
1099
+ return y, jacobian
1100
+
1101
+ def predict_output(self, x):
1102
+ y = super().predict_output(x)
1103
+ return self.__to_numpy(y)
1104
+
1105
+ def _save_model(self, filepath):
1106
+ import torch
1107
+
1108
+ torch.save(self.model.state_dict(), filepath)
1109
+
1110
+ @staticmethod
1111
+ def _load_model(filepath, model_cls):
1112
+ import torch
1113
+
1114
+ model_cls.load_state_dict(torch.load(filepath, weights_only=True))
1115
+ return model_cls
1116
+
1117
+ def _predict_model_jvp(self, x, dx):
1118
+ import torch.func as torch_func
1119
+
1120
+ if not hasattr(self, "batched_jvp_func"):
1121
+ def g(_x): # fmt: skip
1122
+ return self.model(_x)
1123
+
1124
+ def single_jvp(_x, _v):
1125
+ return torch_func.jvp(g, (_x,), (_v,))
1126
+
1127
+ self.batched_jvp_func = torch_func.vmap(single_jvp)
1128
+
1129
+ x = self.__to_tensor(x)
1130
+ dx = self.__to_tensor(dx)
1131
+ output, jvp_res = self.batched_jvp_func(x, dx)
1132
+ return self.__to_numpy(output.detach()), self.__to_numpy(jvp_res.detach())
1133
+
1134
+ def _predict_model_vjp(self, x, dy):
1135
+ import torch.func as torch_func
1136
+
1137
+ if not hasattr(self, "batched_vjp_func"):
1138
+ def g(_x): # fmt: skip
1139
+ return self.model(_x)
1140
+
1141
+ def single_vjp(_x, _v):
1142
+ out, f = torch_func.vjp(g, _x)
1143
+ return out, f(_v)
1144
+
1145
+ self.batched_vjp_func = torch_func.vmap(single_vjp)
1146
+
1147
+ x = self.__to_tensor(x)
1148
+ dy = self.__to_tensor(dy)
1149
+ output, vjp_res = self.batched_vjp_func(x, dy)
1150
+ return self.__to_numpy(output.detach()), self.__to_numpy(vjp_res[0].detach())
1151
+
1152
+ def _predict_model_output_and_jacobian_fwd(self, x):
1153
+ import torch.func as torch_func
1154
+
1155
+ if not hasattr(self, "y_jac_fwd_func"):
1156
+ def g(_x): # fmt: skip
1157
+ result = self.model(_x)
1158
+ return (result, result)
1159
+ self.y_jac_fwd_func = torch_func.vmap(torch_func.jacfwd(g, has_aux=True))
1160
+
1161
+ x = self.__to_tensor(x)
1162
+ jac, output = self.y_jac_fwd_func(x)
1163
+ return self.__to_numpy(output.detach()), self.__to_numpy(jac.detach())
1164
+
1165
+ def _predict_model_output_and_jacobian_rev(self, x):
1166
+ import torch.func as torch_func
1167
+
1168
+ if not hasattr(self, "y_jac_rev_func"):
1169
+ def g(_x): # fmt: skip
1170
+ result = self.model(_x)
1171
+ return (result, result)
1172
+ self.y_jac_rev_func = torch_func.vmap(torch_func.jacrev(g, has_aux=True))
1173
+
1174
+ x = self.__to_tensor(x)
1175
+ jac, output = self.y_jac_rev_func(x)
1176
+ return self.__to_numpy(output.detach()), self.__to_numpy(jac.detach())
1177
+
1178
+ def _predict_model_jacobian_sbatch(self, x): # pragma: no cover
1179
+ # Not used for now, but left as a reference for future if GPU memory issues arise.
1180
+ import torch
1181
+ import torch.func as torch_func
1182
+
1183
+ if not hasattr(self, "y_jac_func"):
1184
+ def g(_x): # fmt: skip
1185
+ result = self.model(_x)
1186
+ return (result, result)
1187
+ self.y_jac_func = torch_func.vmap(torch_func.jacrev(g, has_aux=True))
1188
+
1189
+ x = self.__to_tensor(x)
1190
+ batch_size = x.shape[0]
1191
+ y = torch.zeros(
1192
+ (batch_size, self.n_outputs),
1193
+ device=self.device,
1194
+ dtype=self.dtype,
1195
+ )
1196
+ jac = torch.zeros(
1197
+ (batch_size, self.n_outputs, self.n_inputs),
1198
+ device=self.device,
1199
+ dtype=self.dtype,
1200
+ )
1201
+ for i in range(0, batch_size, self.max_batch):
1202
+ x_mb = x[i : i + self.max_batch]
1203
+ inter_jac, inter_y = self.y_jac_func(x_mb)
1204
+ y[i : i + self.max_batch].copy_(
1205
+ inter_y[: x_mb.shape[0]],
1206
+ non_blocking=True,
1207
+ )
1208
+ jac[i : i + self.max_batch].copy_(
1209
+ inter_jac[: x_mb.shape[0]],
1210
+ non_blocking=True,
1211
+ )
1212
+
1213
+ 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