lume-model 1.7.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.
- lume_model/__init__.py +4 -0
- lume_model/_version.py +16 -0
- lume_model/base.py +379 -0
- lume_model/models/__init__.py +46 -0
- lume_model/models/torch_model.py +399 -0
- lume_model/models/torch_module.py +179 -0
- lume_model/utils.py +224 -0
- lume_model/variables.py +460 -0
- lume_model-1.7.1.dist-info/LICENSE +42 -0
- lume_model-1.7.1.dist-info/METADATA +320 -0
- lume_model-1.7.1.dist-info/RECORD +13 -0
- lume_model-1.7.1.dist-info/WHEEL +5 -0
- lume_model-1.7.1.dist-info/top_level.txt +1 -0
lume_model/__init__.py
ADDED
lume_model/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '1.7.1'
|
|
16
|
+
__version_tuple__ = version_tuple = (1, 7, 1)
|
lume_model/base.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, Callable, Union
|
|
6
|
+
from types import FunctionType, MethodType
|
|
7
|
+
from io import TextIOWrapper
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
import numpy as np
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, field_validator, SerializeAsAny
|
|
12
|
+
|
|
13
|
+
from lume_model.variables import (
|
|
14
|
+
InputVariable,
|
|
15
|
+
OutputVariable, ScalarInputVariable, ScalarOutputVariable,
|
|
16
|
+
)
|
|
17
|
+
from lume_model.utils import (
|
|
18
|
+
try_import_module,
|
|
19
|
+
verify_unique_variable_names,
|
|
20
|
+
serialize_variables,
|
|
21
|
+
deserialize_variables,
|
|
22
|
+
variables_from_dict,
|
|
23
|
+
replace_relative_paths,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
JSON_ENCODERS = {
|
|
29
|
+
# function/method type distinguished for class members and not recognized as callables
|
|
30
|
+
FunctionType: lambda x: f"{x.__module__}.{x.__qualname__}",
|
|
31
|
+
MethodType: lambda x: f"{x.__module__}.{x.__qualname__}",
|
|
32
|
+
Callable: lambda x: f"{x.__module__}.{x.__qualname__}",
|
|
33
|
+
type: lambda x: f"{x.__module__}.{x.__name__}",
|
|
34
|
+
np.ndarray: lambda x: x.tolist(),
|
|
35
|
+
np.int64: lambda x: int(x),
|
|
36
|
+
np.float64: lambda x: float(x),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def process_torch_module(
|
|
41
|
+
module,
|
|
42
|
+
base_key: str = "",
|
|
43
|
+
key: str = "",
|
|
44
|
+
file_prefix: Union[str, os.PathLike] = "",
|
|
45
|
+
save_modules: bool = True,
|
|
46
|
+
):
|
|
47
|
+
"""Optionally saves the given torch module to file and returns the filename.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_key: Base key at this stage of serialization.
|
|
51
|
+
key: Key corresponding to the torch module.
|
|
52
|
+
module: The torch module to process.
|
|
53
|
+
file_prefix: Prefix for generated filenames.
|
|
54
|
+
save_modules: Determines whether torch modules are saved to file.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Filename under which the torch module is (or would be) saved.
|
|
58
|
+
"""
|
|
59
|
+
torch = try_import_module("torch")
|
|
60
|
+
filepath_prefix, filename_prefix = os.path.split(file_prefix)
|
|
61
|
+
prefixes = [ele for ele in [filename_prefix, base_key] if not ele == ""]
|
|
62
|
+
filename = "{}.pt".format(key)
|
|
63
|
+
if prefixes:
|
|
64
|
+
filename = "_".join((*prefixes, filename))
|
|
65
|
+
filepath = os.path.join(filepath_prefix, filename)
|
|
66
|
+
if save_modules:
|
|
67
|
+
torch.save(module, filepath)
|
|
68
|
+
return filename
|
|
69
|
+
|
|
70
|
+
def recursive_serialize(
|
|
71
|
+
v: dict[str, Any],
|
|
72
|
+
base_key: str = "",
|
|
73
|
+
file_prefix: Union[str, os.PathLike] = "",
|
|
74
|
+
save_models: bool = True,
|
|
75
|
+
):
|
|
76
|
+
"""Recursively performs custom serialization for the given object.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
v: Object to serialize.
|
|
80
|
+
base_key: Base key at this stage of serialization.
|
|
81
|
+
file_prefix: Prefix for generated filenames.
|
|
82
|
+
save_models: Determines whether models are saved to file.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Serialized object.
|
|
86
|
+
"""
|
|
87
|
+
# try to import modules for LUMEBaseModel child classes
|
|
88
|
+
torch = try_import_module("torch")
|
|
89
|
+
# serialize
|
|
90
|
+
v = serialize_variables(v)
|
|
91
|
+
for key, value in v.items():
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
v[key] = recursive_serialize(value, key)
|
|
94
|
+
elif torch is not None and isinstance(value, torch.nn.Module):
|
|
95
|
+
v[key] = process_torch_module(value, base_key, key, file_prefix,
|
|
96
|
+
save_models)
|
|
97
|
+
elif isinstance(value, list) and torch is not None and any(
|
|
98
|
+
isinstance(ele, torch.nn.Module) for ele in value):
|
|
99
|
+
v[key] = [
|
|
100
|
+
process_torch_module(value[i], base_key, f"{key}_{i}", file_prefix,
|
|
101
|
+
save_models)
|
|
102
|
+
for i in range(len(value))
|
|
103
|
+
]
|
|
104
|
+
else:
|
|
105
|
+
for _type, func in JSON_ENCODERS.items():
|
|
106
|
+
if isinstance(value, _type):
|
|
107
|
+
v[key] = func(value)
|
|
108
|
+
# check to make sure object has been serialized, if not use a generic serializer
|
|
109
|
+
try:
|
|
110
|
+
json.dumps(v[key])
|
|
111
|
+
except (TypeError, OverflowError):
|
|
112
|
+
v[key] = f"{v[key].__module__}.{v[key].__class__.__qualname__}"
|
|
113
|
+
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def recursive_deserialize(v):
|
|
118
|
+
"""Recursively performs custom deserialization for the given object.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
v: Object to deserialize.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Deserialized object.
|
|
125
|
+
"""
|
|
126
|
+
# deserialize
|
|
127
|
+
v = deserialize_variables(v)
|
|
128
|
+
for key, value in v.items():
|
|
129
|
+
if isinstance(value, dict):
|
|
130
|
+
v[key] = recursive_deserialize(value)
|
|
131
|
+
return v
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def json_dumps(
|
|
135
|
+
v,
|
|
136
|
+
*,
|
|
137
|
+
base_key="",
|
|
138
|
+
file_prefix: Union[str, os.PathLike] = "",
|
|
139
|
+
save_models: bool = True,
|
|
140
|
+
):
|
|
141
|
+
"""Serializes variables before dumping with json.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
v: Object to dump.
|
|
145
|
+
base_key: Base key for serialization.
|
|
146
|
+
file_prefix: Prefix for generated filenames.
|
|
147
|
+
save_models: Determines whether models are saved to file.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
JSON formatted string.
|
|
151
|
+
"""
|
|
152
|
+
v = recursive_serialize(v.model_dump(), base_key, file_prefix, save_models)
|
|
153
|
+
v = json.dumps(v)
|
|
154
|
+
return v
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def json_loads(v):
|
|
158
|
+
"""Loads JSON formatted string and recursively deserializes the result.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
v: JSON formatted string to load.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Deserialized object.
|
|
165
|
+
"""
|
|
166
|
+
v = json.loads(v)
|
|
167
|
+
v = recursive_deserialize(v)
|
|
168
|
+
return v
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_config(
|
|
172
|
+
config: Union[dict, str, TextIOWrapper, os.PathLike],
|
|
173
|
+
model_fields: dict = None,
|
|
174
|
+
) -> dict:
|
|
175
|
+
"""Parses model configuration and returns keyword arguments for model constructor.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
config: Model configuration as dictionary, YAML or JSON formatted string, file or file path.
|
|
179
|
+
model_fields: Fields expected by the model (required for replacing relative paths).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Configuration as keyword arguments for model constructor.
|
|
183
|
+
"""
|
|
184
|
+
config_file = None
|
|
185
|
+
if isinstance(config, dict):
|
|
186
|
+
d = config
|
|
187
|
+
else:
|
|
188
|
+
if isinstance(config, TextIOWrapper):
|
|
189
|
+
yaml_str = config.read()
|
|
190
|
+
config_file = os.path.abspath(config.name)
|
|
191
|
+
elif isinstance(config, (str, os.PathLike)) and os.path.exists(config):
|
|
192
|
+
with open(config) as f:
|
|
193
|
+
yaml_str = f.read()
|
|
194
|
+
config_file = os.path.abspath(config)
|
|
195
|
+
else:
|
|
196
|
+
yaml_str = config
|
|
197
|
+
d = recursive_deserialize(yaml.safe_load(yaml_str))
|
|
198
|
+
if config_file is not None:
|
|
199
|
+
config_dir = os.path.dirname(os.path.realpath(config_file))
|
|
200
|
+
d = replace_relative_paths(d, model_fields, config_dir)
|
|
201
|
+
return model_kwargs_from_dict(d)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def model_kwargs_from_dict(config: dict) -> dict:
|
|
205
|
+
"""Processes model configuration and returns the corresponding keyword arguments for model constructor.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
config: Model configuration.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Configuration as keyword arguments for model constructor.
|
|
212
|
+
"""
|
|
213
|
+
config = deserialize_variables(config)
|
|
214
|
+
if all(key in config.keys() for key in ["input_variables", "output_variables"]):
|
|
215
|
+
config["input_variables"], config["output_variables"] = variables_from_dict(
|
|
216
|
+
config)
|
|
217
|
+
config.pop("model_class", None)
|
|
218
|
+
return config
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class LUMEBaseModel(BaseModel, ABC):
|
|
222
|
+
"""Abstract base class for models using lume-model variables.
|
|
223
|
+
|
|
224
|
+
Inheriting classes must define the evaluate method and variable names must be unique (respectively).
|
|
225
|
+
Models build using this framework will be compatible with the lume-epics EPICS server and associated tools.
|
|
226
|
+
|
|
227
|
+
Attributes:
|
|
228
|
+
input_variables: List defining the input variables and their order.
|
|
229
|
+
output_variables: List defining the output variables and their order.
|
|
230
|
+
"""
|
|
231
|
+
input_variables: list[SerializeAsAny[InputVariable]]
|
|
232
|
+
output_variables: list[SerializeAsAny[OutputVariable]]
|
|
233
|
+
|
|
234
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True)
|
|
235
|
+
|
|
236
|
+
@field_validator("input_variables", mode="before")
|
|
237
|
+
def validate_input_variables(cls, value):
|
|
238
|
+
new_value = []
|
|
239
|
+
if isinstance(value, dict):
|
|
240
|
+
for name, val in value.items():
|
|
241
|
+
if isinstance(val, dict):
|
|
242
|
+
if val["variable_type"] == "scalar":
|
|
243
|
+
new_value.append(ScalarInputVariable(name=name, **val))
|
|
244
|
+
elif isinstance(val, InputVariable):
|
|
245
|
+
new_value.append(val)
|
|
246
|
+
else:
|
|
247
|
+
raise TypeError(f"type {type(val)} not supported")
|
|
248
|
+
elif isinstance(value, list):
|
|
249
|
+
new_value = value
|
|
250
|
+
|
|
251
|
+
return new_value
|
|
252
|
+
|
|
253
|
+
@field_validator("output_variables", mode="before")
|
|
254
|
+
def validate_output_variables(cls, value):
|
|
255
|
+
new_value = []
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
for name, val in value.items():
|
|
258
|
+
if isinstance(val, dict):
|
|
259
|
+
if val["variable_type"] == "scalar":
|
|
260
|
+
new_value.append(ScalarOutputVariable(name=name, **val))
|
|
261
|
+
elif isinstance(val, OutputVariable):
|
|
262
|
+
new_value.append(val)
|
|
263
|
+
else:
|
|
264
|
+
raise TypeError(f"type {type(val)} not supported")
|
|
265
|
+
elif isinstance(value, list):
|
|
266
|
+
new_value = value
|
|
267
|
+
|
|
268
|
+
return new_value
|
|
269
|
+
|
|
270
|
+
def __init__(self, *args, **kwargs):
|
|
271
|
+
"""Initializes LUMEBaseModel.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
*args: Accepts a single argument which is the model configuration as dictionary, YAML or JSON
|
|
275
|
+
formatted string or file path.
|
|
276
|
+
**kwargs: See class attributes.
|
|
277
|
+
"""
|
|
278
|
+
if len(args) == 1:
|
|
279
|
+
if len(kwargs) > 0:
|
|
280
|
+
raise ValueError("Cannot specify YAML string and keyword arguments for LUMEBaseModel init.")
|
|
281
|
+
super().__init__(**parse_config(args[0], self.model_fields))
|
|
282
|
+
elif len(args) > 1:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
"Arguments to LUMEBaseModel must be either a single YAML string "
|
|
285
|
+
"or keyword arguments passed directly to pydantic."
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
super().__init__(**kwargs)
|
|
289
|
+
|
|
290
|
+
@field_validator("input_variables", "output_variables")
|
|
291
|
+
def unique_variable_names(cls, value):
|
|
292
|
+
verify_unique_variable_names(value)
|
|
293
|
+
return value
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def input_names(self) -> list[str]:
|
|
297
|
+
return [var.name for var in self.input_variables]
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def output_names(self) -> list[str]:
|
|
301
|
+
return [var.name for var in self.output_variables]
|
|
302
|
+
|
|
303
|
+
@abstractmethod
|
|
304
|
+
def evaluate(self, input_dict: dict[str, Any]) -> dict[str, Any]:
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
def to_json(self, **kwargs) -> str:
|
|
308
|
+
return json_dumps(self, **kwargs)
|
|
309
|
+
|
|
310
|
+
def dict(self, **kwargs) -> dict[str, Any]:
|
|
311
|
+
config = super().model_dump(**kwargs)
|
|
312
|
+
return {"model_class": self.__class__.__name__} | config
|
|
313
|
+
|
|
314
|
+
def json(self, **kwargs) -> str:
|
|
315
|
+
result = self.to_json(**kwargs)
|
|
316
|
+
config = json.loads(result)
|
|
317
|
+
config = {"model_class": self.__class__.__name__} | config
|
|
318
|
+
return json.dumps(config)
|
|
319
|
+
|
|
320
|
+
def yaml(
|
|
321
|
+
self,
|
|
322
|
+
base_key: str = "",
|
|
323
|
+
file_prefix: str = "",
|
|
324
|
+
save_models: bool = False,
|
|
325
|
+
) -> str:
|
|
326
|
+
"""Serializes the object and returns a YAML formatted string defining the model.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
base_key: Base key for serialization.
|
|
330
|
+
file_prefix: Prefix for generated filenames.
|
|
331
|
+
save_models: Determines whether models are saved to file.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
YAML formatted string defining the model.
|
|
335
|
+
"""
|
|
336
|
+
output = json.loads(
|
|
337
|
+
self.to_json(
|
|
338
|
+
base_key=base_key,
|
|
339
|
+
file_prefix=file_prefix,
|
|
340
|
+
save_models=save_models,
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
s = yaml.dump({"model_class": self.__class__.__name__} | output,
|
|
344
|
+
default_flow_style=None, sort_keys=False)
|
|
345
|
+
return s
|
|
346
|
+
|
|
347
|
+
def dump(
|
|
348
|
+
self,
|
|
349
|
+
file: Union[str, os.PathLike],
|
|
350
|
+
base_key: str = "",
|
|
351
|
+
save_models: bool = True,
|
|
352
|
+
):
|
|
353
|
+
"""Returns and optionally saves YAML formatted string defining the model.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
file: File path to which the YAML formatted string and corresponding files are saved.
|
|
357
|
+
base_key: Base key for serialization.
|
|
358
|
+
save_models: Determines whether models are saved to file.
|
|
359
|
+
"""
|
|
360
|
+
file_prefix = os.path.splitext(os.path.abspath(file))[0]
|
|
361
|
+
with open(file, "w") as f:
|
|
362
|
+
f.write(
|
|
363
|
+
self.yaml(
|
|
364
|
+
base_key=base_key,
|
|
365
|
+
file_prefix=file_prefix,
|
|
366
|
+
save_models=save_models,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@classmethod
|
|
371
|
+
def from_file(cls, filename: str):
|
|
372
|
+
if not os.path.exists(filename):
|
|
373
|
+
raise OSError(f"File {filename} is not found.")
|
|
374
|
+
with open(filename, "r") as file:
|
|
375
|
+
return cls.from_yaml(file)
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def from_yaml(cls, yaml_obj: [str, TextIOWrapper]):
|
|
379
|
+
return cls.model_validate(parse_config(yaml_obj, cls.model_fields))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
registered_models = []
|
|
6
|
+
|
|
7
|
+
# models requiring torch
|
|
8
|
+
try:
|
|
9
|
+
from lume_model.models.torch_model import TorchModel
|
|
10
|
+
from lume_model.models.torch_module import TorchModule
|
|
11
|
+
registered_models += [TorchModel, TorchModule]
|
|
12
|
+
except ModuleNotFoundError:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_model(name: str):
|
|
17
|
+
"""Returns the LUME model class for the given name.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
name: Name of LUME model class.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
LUME model class for the given name.
|
|
24
|
+
"""
|
|
25
|
+
model_lookup = {m.__name__: m for m in registered_models}
|
|
26
|
+
if name not in model_lookup.keys():
|
|
27
|
+
raise KeyError(f"No model named {name}, available models are {list(model_lookup.keys())}")
|
|
28
|
+
return model_lookup[name]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def model_from_yaml(yaml_str: Union[str, os.PathLike]):
|
|
32
|
+
"""Creates LUME model from the given YAML formatted string or file path.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
yaml_str: YAML formatted string or file path.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Created LUME model.
|
|
39
|
+
"""
|
|
40
|
+
if os.path.exists(yaml_str):
|
|
41
|
+
with open(yaml_str) as f:
|
|
42
|
+
config = yaml.safe_load(f.read())
|
|
43
|
+
else:
|
|
44
|
+
config = yaml.safe_load(yaml_str)
|
|
45
|
+
model_class = get_model(config["model_class"])
|
|
46
|
+
return model_class(yaml_str)
|