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 ADDED
@@ -0,0 +1,4 @@
1
+ try:
2
+ from ._version import __version__
3
+ except ImportError:
4
+ __version__ = "0.0.0"
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)