physioblocks 1.0.0__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.
Files changed (93) hide show
  1. physioblocks/__init__.py +37 -0
  2. physioblocks/base/__init__.py +27 -0
  3. physioblocks/base/operators.py +176 -0
  4. physioblocks/base/registers.py +108 -0
  5. physioblocks/computing/__init__.py +47 -0
  6. physioblocks/computing/assembling.py +291 -0
  7. physioblocks/computing/models.py +811 -0
  8. physioblocks/computing/quantities.py +354 -0
  9. physioblocks/configuration/__init__.py +38 -0
  10. physioblocks/configuration/aliases.py +203 -0
  11. physioblocks/configuration/base.py +123 -0
  12. physioblocks/configuration/computing/__init__.py +27 -0
  13. physioblocks/configuration/computing/quantities.py +56 -0
  14. physioblocks/configuration/constants.py +121 -0
  15. physioblocks/configuration/description/__init__.py +33 -0
  16. physioblocks/configuration/description/blocks.py +239 -0
  17. physioblocks/configuration/description/nets.py +155 -0
  18. physioblocks/configuration/functions.py +695 -0
  19. physioblocks/configuration/simulation/__init__.py +32 -0
  20. physioblocks/configuration/simulation/simulations.py +280 -0
  21. physioblocks/description/__init__.py +34 -0
  22. physioblocks/description/blocks.py +418 -0
  23. physioblocks/description/flux.py +157 -0
  24. physioblocks/description/nets.py +746 -0
  25. physioblocks/io/__init__.py +29 -0
  26. physioblocks/io/aliases.py +73 -0
  27. physioblocks/io/configuration.py +125 -0
  28. physioblocks/launcher/__main__.py +285 -0
  29. physioblocks/launcher/configuration.py +231 -0
  30. physioblocks/launcher/configure/__main__.py +99 -0
  31. physioblocks/launcher/constants.py +105 -0
  32. physioblocks/launcher/files.py +150 -0
  33. physioblocks/launcher/series.py +165 -0
  34. physioblocks/library/__init__.py +27 -0
  35. physioblocks/library/aliases/blocks/c_block.json +5 -0
  36. physioblocks/library/aliases/blocks/rc_block.json +5 -0
  37. physioblocks/library/aliases/blocks/rcr_block.json +5 -0
  38. physioblocks/library/aliases/blocks/spherical_cavity_block.json +5 -0
  39. physioblocks/library/aliases/blocks/valve_rl_block.json +5 -0
  40. physioblocks/library/aliases/flux/heart_flux_dof_couples.jsonc +4 -0
  41. physioblocks/library/aliases/model_components/active_law_macro_huxley_two_moments.json +5 -0
  42. physioblocks/library/aliases/model_components/rheology_fiber_additive.json +5 -0
  43. physioblocks/library/aliases/model_components/spherical_dynamics.json +5 -0
  44. physioblocks/library/aliases/model_components/velocity_law_hht.json +5 -0
  45. physioblocks/library/aliases/nets/circulation_alone_net.json +31 -0
  46. physioblocks/library/aliases/nets/spherical_heart_net.json +93 -0
  47. physioblocks/library/aliases/simulations/circulation_alone_forward_simulation.jsonc +55 -0
  48. physioblocks/library/aliases/simulations/default_forward_simulation.jsonc +7 -0
  49. physioblocks/library/aliases/simulations/default_time.jsonc +8 -0
  50. physioblocks/library/aliases/simulations/newton_method_solver.json +5 -0
  51. physioblocks/library/aliases/simulations/spherical_heart_forward_simulation.jsonc +157 -0
  52. physioblocks/library/aliases/simulations/spherical_heart_with_respiration_forward_simulation.jsonc +45 -0
  53. physioblocks/library/blocks/__init__.py +27 -0
  54. physioblocks/library/blocks/capacitances.py +516 -0
  55. physioblocks/library/blocks/cavity.py +192 -0
  56. physioblocks/library/blocks/valves.py +281 -0
  57. physioblocks/library/functions/__init__.py +27 -0
  58. physioblocks/library/functions/base_operations.py +129 -0
  59. physioblocks/library/functions/first_order.py +113 -0
  60. physioblocks/library/functions/piecewise.py +271 -0
  61. physioblocks/library/functions/trigonometric.py +78 -0
  62. physioblocks/library/functions/watchers.py +113 -0
  63. physioblocks/library/model_components/__init__.py +27 -0
  64. physioblocks/library/model_components/active_law.py +345 -0
  65. physioblocks/library/model_components/dynamics.py +986 -0
  66. physioblocks/library/model_components/rheology.py +160 -0
  67. physioblocks/library/model_components/velocity_law.py +169 -0
  68. physioblocks/references/circulation_alone_sim.jsonc +24 -0
  69. physioblocks/references/spherical_heart_respiration_sim.jsonc +33 -0
  70. physioblocks/references/spherical_heart_sim.jsonc +29 -0
  71. physioblocks/registers/__init__.py +32 -0
  72. physioblocks/registers/load_function_register.py +93 -0
  73. physioblocks/registers/save_function_register.py +106 -0
  74. physioblocks/registers/type_register.py +97 -0
  75. physioblocks/simulation/__init__.py +48 -0
  76. physioblocks/simulation/constants.py +30 -0
  77. physioblocks/simulation/functions.py +71 -0
  78. physioblocks/simulation/runtime.py +484 -0
  79. physioblocks/simulation/saved_quantities.py +129 -0
  80. physioblocks/simulation/setup.py +576 -0
  81. physioblocks/simulation/solvers.py +235 -0
  82. physioblocks/simulation/state.py +340 -0
  83. physioblocks/simulation/time_manager.py +354 -0
  84. physioblocks/utils/__init__.py +27 -0
  85. physioblocks/utils/dynamic_import_utils.py +150 -0
  86. physioblocks/utils/exceptions_utils.py +115 -0
  87. physioblocks/utils/gradient_test_utils.py +337 -0
  88. physioblocks/utils/math_utils.py +109 -0
  89. physioblocks-1.0.0.dist-info/METADATA +127 -0
  90. physioblocks-1.0.0.dist-info/RECORD +93 -0
  91. physioblocks-1.0.0.dist-info/WHEEL +4 -0
  92. physioblocks-1.0.0.dist-info/licenses/licenses/GPL-3.0-only.txt +674 -0
  93. physioblocks-1.0.0.dist-info/licenses/licenses/LGPL-3.0-only.txt +165 -0
@@ -0,0 +1,695 @@
1
+ # SPDX-FileCopyrightText: Copyright INRIA
2
+ #
3
+ # SPDX-License-Identifier: LGPL-3.0-only
4
+ #
5
+ # Copyright INRIA
6
+ #
7
+ # This file is part of PhysioBlocks, a library mostly developed by the
8
+ # [Ananke project-team](https://team.inria.fr/ananke) at INRIA.
9
+ #
10
+ # Authors:
11
+ # - Colin Drieu
12
+ # - Dominique Chapelle
13
+ # - François Kimmig
14
+ # - Philippe Moireau
15
+ #
16
+ # PhysioBlocks is free software: you can redistribute it and/or modify it under the
17
+ # terms of the GNU Lesser General Public License as published by the Free Software
18
+ # Foundation, version 3 of the License.
19
+ #
20
+ # PhysioBlocks is distributed in the hope that it will be useful, but WITHOUT ANY
21
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
22
+ # PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
23
+ #
24
+ # You should have received a copy of the GNU Lesser General Public License along with
25
+ # PhysioBlocks. If not, see <https://www.gnu.org/licenses/>.
26
+
27
+ """Declares functions to load and save PhysioBlocks object to generic
28
+ :class:`~physioblocks.configuration.base.Configuration` objects.
29
+
30
+ Before using generic :func:`load` and :func:`save` functions on a PhysioBlocks object,
31
+ the object type must be registered with the
32
+ :func:`~physioblocks.registers.type_register.register_type` decorator.
33
+
34
+ To define a specific behavior when saving or loading an registered object with the
35
+ generic :func:`load` or :func:`save` functions, declare a function decorated with
36
+ :func:`~physioblocks.registers.load_function_register.loads` or
37
+ :func:`~physioblocks.registers.save_function_register.saves`.
38
+
39
+ .. note::
40
+
41
+ If you want to create a **Configurable Item** for a dataclass type,
42
+ you will not have to register a specific save or load function.
43
+
44
+ Registering the type the :func:`~physioblocks.registers.type_register.register_type`
45
+ decorator will suffice to create a configuration item that needs the same parameters
46
+ as the dataclass.
47
+
48
+ See :doc:`register module <./registers>` to for decorators documentation to
49
+ :func:`~physioblocks.registers.type_register.register_type` as well as
50
+ :func:`~physioblocks.registers.load_function_register.loads` and
51
+ :func:`~physioblocks.registers.save_function_register.saves`.
52
+ """
53
+
54
+ import functools
55
+ from collections.abc import Iterable, Mapping, Sequence
56
+ from inspect import signature
57
+ from typing import Any, TypeAlias, TypeVar
58
+
59
+ import numpy as np
60
+ from numpy.typing import NDArray
61
+
62
+ from physioblocks.configuration.base import Configuration, ConfigurationError
63
+ from physioblocks.registers.load_function_register import get_load_function, loads
64
+ from physioblocks.registers.save_function_register import (
65
+ get_save_function,
66
+ has_save_function,
67
+ )
68
+ from physioblocks.registers.type_register import (
69
+ get_registered_type,
70
+ get_registered_type_id,
71
+ is_registered,
72
+ )
73
+
74
+ BaseTypes: TypeAlias = float | int | bool | str
75
+ """Type alias for basic types usable in a Configuration File"""
76
+
77
+
78
+ def load(
79
+ configuration: Any,
80
+ configuration_key: str | None = None,
81
+ configuration_object: Any | None = None,
82
+ configuration_type: type[Any] | None = None,
83
+ configuration_references: dict[str, Any] | None = None,
84
+ configuration_sort: bool = False,
85
+ ) -> Any:
86
+ """
87
+ Generic load an object from the given configuration item.
88
+
89
+ The method can load:
90
+ - :class:`~physioblocks.configuration.base.Configuration`: Use the matching
91
+ registered load function.
92
+ - `dict` and `list`: recursivly load values in the collection
93
+
94
+ :param configuration: the configuration to load
95
+ :type configuration: Any
96
+
97
+ :param configuration_key: (optional) key of the configuration in the parent
98
+ configuration item.
99
+ :type configuration_key: str
100
+
101
+ :param configuration_object: (optional) The object to configure.
102
+ If empty, a the object is first instanciated then configured.
103
+ :type configuration_object: Any
104
+
105
+ :param configuration_type: (optional) the type of the object to configure.
106
+ If empty, it is determined from the configuration object.
107
+ :type configuration_type: Any
108
+
109
+ :param configuration_references: (optional) mapping of configuration item keys
110
+ with already configured objects to use in the current configured object.
111
+ :type configuration_references: dict[str, Any]
112
+
113
+ :param configuration_sort: (optional) flag to signal that configuration items
114
+ should be sorted be sorted before they are loaded. Default is False.
115
+ :type configuration_sort: dict[str, Any]
116
+
117
+ :return: the configured object
118
+ :rtype: Any
119
+ """
120
+ if (
121
+ isinstance(configuration, str)
122
+ and configuration_references is not None
123
+ and configuration in configuration_references
124
+ ):
125
+ # the value is already in the references:
126
+ return (
127
+ configuration_references[configuration]
128
+ if configuration_type is None
129
+ else configuration_type(configuration_references[configuration])
130
+ )
131
+
132
+ elif configuration_type is not None:
133
+ load_func = get_load_function(configuration_type)
134
+ return load_func(
135
+ configuration,
136
+ configuration_key=configuration_key,
137
+ configuration_object=configuration_object,
138
+ configuration_type=configuration_type,
139
+ configuration_references=configuration_references,
140
+ configuration_sort=configuration_sort,
141
+ )
142
+ elif isinstance(configuration, BaseTypes):
143
+ # No load function required
144
+ return configuration
145
+
146
+ elif isinstance(configuration, Configuration):
147
+ return load_configuration(
148
+ configuration,
149
+ configuration_key=configuration_key,
150
+ configuration_object=configuration_object,
151
+ configuration_type=configuration_type,
152
+ configuration_references=configuration_references,
153
+ configuration_sort=configuration_sort,
154
+ )
155
+ elif isinstance(configuration, Mapping):
156
+ return load_dict(
157
+ configuration,
158
+ configuration_key=configuration_key,
159
+ configuration_object=configuration_object,
160
+ configuration_type=configuration_type,
161
+ configuration_references=configuration_references,
162
+ configuration_sort=configuration_sort,
163
+ )
164
+ elif isinstance(configuration, Sequence):
165
+ return load_list(
166
+ configuration,
167
+ configuration_key=configuration_key,
168
+ configuration_object=configuration_object,
169
+ configuration_type=configuration_type,
170
+ configuration_references=configuration_references,
171
+ configuration_sort=configuration_sort,
172
+ )
173
+
174
+ raise TypeError(
175
+ str.format(
176
+ "Type {0} can not be loaded as a configuration.",
177
+ type(configuration).__name__,
178
+ )
179
+ )
180
+
181
+
182
+ def load_configuration(
183
+ configuration: Configuration,
184
+ configuration_key: str | None = None,
185
+ configuration_object: Any | None = None,
186
+ configuration_references: dict[str, Any] | None = None,
187
+ configuration_sort: bool = False,
188
+ *args: Any,
189
+ **kwargs: Any,
190
+ ) -> Any:
191
+ """
192
+ Specific load function for a
193
+ :class:`~physioblocks.configuration.base.Configuration`: configuration item.
194
+
195
+ It recursivly loads any configuration item used in the ``configuration`` parameter.
196
+
197
+ :param configuration: the configuration item to load
198
+ :type configuration: Configuration
199
+
200
+ :param configuration_key: (optional) key of the configuration in the parent
201
+ configuration item.
202
+ :type configuration_key: str
203
+
204
+ :param configuration_object: (optional) The object to configure.
205
+ If empty, a the object is first instanciated then configured.
206
+ :type configuration_object: Any
207
+
208
+ :param configuration_type: (optional) the type of the object to configure.
209
+ If empty, it is determined from the configuration object.
210
+ :type configuration_type: Any
211
+
212
+ :param configuration_references: (optional) mapping of configuration item keys
213
+ with already configured objects to use in the current configured object.
214
+ :type configuration_references: dict[str, Any]
215
+
216
+ :param configuration_sort: (optional) flag to signal that configuration items
217
+ should be sorted be sorted before they are loaded. Default is False.
218
+ :type configuration_sort: dict[str, Any]
219
+
220
+ :return: the configured object
221
+ :rtype: Any
222
+ """
223
+ configuration = (
224
+ configuration
225
+ if configuration_sort is False
226
+ else __sort_configuration(configuration)
227
+ )
228
+
229
+ new_configuration_type = get_registered_type(configuration.label)
230
+ load_func = get_load_function(new_configuration_type)
231
+
232
+ return load_func(
233
+ configuration,
234
+ configuration_key=configuration_key,
235
+ configuration_object=configuration_object,
236
+ configuration_type=new_configuration_type,
237
+ configuration_references=configuration_references,
238
+ configuration_sort=configuration_sort,
239
+ )
240
+
241
+
242
+ def load_dict(
243
+ configuration: Mapping[str, Any],
244
+ configuration_object: dict[str, Any] | None = None,
245
+ configuration_references: dict[str, Any] | None = None,
246
+ configuration_sort: bool = False,
247
+ *args: Any,
248
+ **kwargs: Any,
249
+ ) -> dict[str, Any]:
250
+ """
251
+ Specific load function for a `Mapping` configuration item.
252
+
253
+ It recursivly loads any configuration item used in the mapping values.
254
+
255
+ :param configuration: the configuration item to load
256
+ :type configuration: Configuration
257
+
258
+ :param configuration_key: (optional) key of the configuration in the parent
259
+ configuration item.
260
+ :type configuration_key: str
261
+
262
+ :param configuration_object: (optional) The object to configure.
263
+ If empty, a the object is first instanciated then configured.
264
+ :type configuration_object: Any
265
+
266
+ :param configuration_type: (optional) the type of the object to configure.
267
+ If empty, it is determined from the configuration object.
268
+ :type configuration_type: Any
269
+
270
+ :param configuration_references: (optional) mapping of configuration item keys
271
+ with already configured objects to use in the current configured object.
272
+ :type configuration_references: dict[str, Any]
273
+
274
+ :param configuration_sort: (optional) flag to signal that configuration items
275
+ should be sorted be sorted before they are loaded. Default is False.
276
+ :type configuration_sort: dict[str, Any]
277
+
278
+ :return: the configured object
279
+ :rtype: Any
280
+ """
281
+ configuration = (
282
+ configuration
283
+ if configuration_sort is False
284
+ else __sort_configuration(configuration)
285
+ )
286
+
287
+ if configuration_object is None:
288
+ configuration_object = {}
289
+
290
+ updated_references = (
291
+ configuration_references.copy() if configuration_references is not None else {}
292
+ )
293
+ configuration_values = {}
294
+
295
+ for key, value in configuration.items():
296
+ loaded_obj = load(
297
+ value,
298
+ configuration_key=key,
299
+ configuration_object=configuration_object.get(key),
300
+ configuration_type=type(configuration_object.get(key))
301
+ if configuration_object.get(key) is not None
302
+ and not isinstance(configuration_object.get(key), BaseTypes)
303
+ else None,
304
+ configuration_references=updated_references,
305
+ configuration_sort=configuration_sort,
306
+ )
307
+ configuration_values[key] = loaded_obj
308
+ updated_references[key] = loaded_obj
309
+ if isinstance(loaded_obj, Mapping):
310
+ updated_references.update(loaded_obj)
311
+
312
+ configuration_object.update(configuration_values)
313
+
314
+ return configuration_object
315
+
316
+
317
+ def load_list(
318
+ configuration: Sequence[Any],
319
+ configuration_object: list[Any] | None = None,
320
+ configuration_references: dict[str, Any] | None = None,
321
+ configuration_sort: bool = False,
322
+ *args: Any,
323
+ **kwargs: Any,
324
+ ) -> Sequence[Any]:
325
+ """
326
+ Specific load function for a `Sequence` configuration item.
327
+
328
+ It recursivly loads any configuration item used in the sequence values.
329
+
330
+ :param configuration: the configuration item to load
331
+ :type configuration: Configuration
332
+
333
+ :param configuration_key: (optional) key of the configuration in the parent
334
+ configuration item.
335
+ :type configuration_key: str
336
+
337
+ :param configuration_object: (optional) The object to configure.
338
+ If empty, a the object is first instanciated then configured.
339
+ :type configuration_object: Any
340
+
341
+ :param configuration_type: (optional) the type of the object to configure.
342
+ If empty, it is determined from the configuration object.
343
+ :type configuration_type: Any
344
+
345
+ :param configuration_references: (optional) mapping of configuration item keys
346
+ with already configured objects to use in the current configured object.
347
+ :type configuration_references: dict[str, Any]
348
+
349
+ :param configuration_sort: (optional) flag to signal that configuration items
350
+ should be sorted be sorted before they are loaded. Default is False.
351
+ :type configuration_sort: dict[str, Any]
352
+
353
+ :return: the configured object
354
+ :rtype: Any
355
+ """
356
+
357
+ if configuration_object is None:
358
+ configuration_object = []
359
+
360
+ configuration_values = [
361
+ load(
362
+ configuration[index],
363
+ configuration_object=configuration_object[index]
364
+ if index < len(configuration_object)
365
+ else None,
366
+ configuration_type=type(configuration_object[index])
367
+ if index < len(configuration_object)
368
+ and not isinstance(configuration[index], BaseTypes)
369
+ else None,
370
+ configuration_references=configuration_references,
371
+ configuration_sort=configuration_sort,
372
+ )
373
+ for index in range(0, len(configuration))
374
+ ]
375
+
376
+ return configuration_values
377
+
378
+
379
+ @loads(bool)
380
+ def _bool_load(
381
+ configuration: Any,
382
+ *args: Any,
383
+ **kwargs: Any,
384
+ ) -> bool:
385
+ if isinstance(configuration, str):
386
+ return configuration.lower() == str(True)
387
+ return bool(configuration)
388
+
389
+
390
+ T = TypeVar("T")
391
+
392
+
393
+ @loads(object)
394
+ def _base_load(
395
+ configuration: Any,
396
+ configuration_key: str | None = None,
397
+ configuration_object: T | None = None,
398
+ configuration_type: type[T] | None = None,
399
+ configuration_references: dict[str, Any] | None = None,
400
+ configuration_sort: bool = False,
401
+ *args: Any,
402
+ **kwargs: Any,
403
+ ) -> T:
404
+ """
405
+ Load function called when no other load function is defined.
406
+
407
+ :param configuration: the configuration to load
408
+ :type configuration: Configuration
409
+
410
+ :param configuration_object: The object to configure.
411
+ If empty, a new object is created and configured.
412
+ :type configuration_object: Any
413
+
414
+ :return: the configured object
415
+ :rtype: Any
416
+ """
417
+ if configuration_type is None and configuration_object is not None:
418
+ configuration_type = type(configuration_object)
419
+ elif configuration_type is None:
420
+ raise ConfigurationError(
421
+ str.format("Missing configuration type for {0}:{1}.", configuration_key)
422
+ )
423
+
424
+ config_args: list[Any] = []
425
+ config_kwargs: dict[str, Any] = {}
426
+
427
+ if isinstance(configuration, Configuration | Mapping):
428
+ config_kwargs.update(configuration)
429
+ elif isinstance(configuration, str | float | int | bool):
430
+ config_args.append(configuration)
431
+ elif isinstance(configuration, Sequence):
432
+ config_args.extend(configuration)
433
+
434
+ # load the values in the provided arguments
435
+ config_args = load(
436
+ config_args,
437
+ configuration_key=configuration_key,
438
+ configuration_references=configuration_references,
439
+ configuration_sort=configuration_sort,
440
+ )
441
+ config_kwargs = load(
442
+ config_kwargs,
443
+ configuration_key=configuration_key,
444
+ configuration_references=configuration_references,
445
+ configuration_sort=configuration_sort,
446
+ )
447
+
448
+ if configuration_object is None:
449
+ try:
450
+ configuration_object = configuration_type(*config_args, **config_kwargs)
451
+ except Exception as exception:
452
+ raise ConfigurationError(
453
+ str.format("Error while initialising key {0}", configuration_key)
454
+ ) from exception
455
+ else:
456
+ if len(config_args) == 0:
457
+ for key, value in config_kwargs.items():
458
+ setattr(configuration_object, key, value)
459
+ else:
460
+ raise ConfigurationError(
461
+ str.format(
462
+ "Can not set arguments {0} to existing object {1}. "
463
+ "Missing attribute keys.",
464
+ config_args,
465
+ configuration_object,
466
+ )
467
+ )
468
+
469
+ return configuration_object
470
+
471
+
472
+ @functools.singledispatch
473
+ def save(
474
+ obj: Any,
475
+ configuration_references: dict[str, Any] | None = None,
476
+ *args: Any,
477
+ **kwargs: Any,
478
+ ) -> Any:
479
+ """
480
+ Save an object to a configuration.
481
+
482
+ It first check if the object has a ``save`` function registered and use it.
483
+
484
+ When no specific ``save`` function is registered for a object type,
485
+ it saves recursivly every annotated parameters of the object class.
486
+
487
+ :param obj: the object to save
488
+ :type obj: Any
489
+
490
+ :return: the object configuration
491
+ :rtype: Any
492
+
493
+ :raise ConfigurationError: raise a Configuration Error when the
494
+ object can not be saved to a configuration.
495
+ """
496
+
497
+ # try to replace the object with a reference when possible.
498
+ if configuration_references is not None:
499
+ obj_reference = next(
500
+ (key for key, item in configuration_references.items() if item is obj), None
501
+ )
502
+ if obj_reference is not None:
503
+ return obj_reference
504
+
505
+ obj_type = type(obj)
506
+
507
+ if has_save_function(obj_type) is True:
508
+ save_func = get_save_function(obj_type)
509
+ return save_func(
510
+ obj, *args, configuration_references=configuration_references, **kwargs
511
+ )
512
+ elif is_registered(obj_type):
513
+ return _base_save_obj(
514
+ obj, *args, configuration_references=configuration_references, **kwargs
515
+ )
516
+ raise ConfigurationError(str.format("Can not configure object {0}.", obj))
517
+
518
+
519
+ def _base_save_obj(obj: Any, *args: Any, **kwargs: Any) -> Configuration:
520
+ obj_type = type(obj)
521
+ type_id = get_registered_type_id(obj_type)
522
+
523
+ # get parameters of the constructor
524
+ parameters_ids = _get_init_parameters(obj_type)
525
+ config_parameters = {
526
+ key: save(getattr(obj, key), *args, **kwargs) for key in parameters_ids
527
+ }
528
+ return Configuration(type_id, config_parameters)
529
+
530
+
531
+ @save.register
532
+ def _save_dict(obj: Mapping, *args: Any, **kwargs: Any) -> dict[str, Any]: # type: ignore
533
+ """Save specific function for mappings."""
534
+ return {
535
+ key: save(
536
+ value,
537
+ *args,
538
+ **kwargs,
539
+ )
540
+ for key, value in obj.items()
541
+ }
542
+
543
+
544
+ @save.register
545
+ def _save_list(obj: Sequence, *args: Any, **kwargs: Any) -> list[Any]: # type: ignore
546
+ return [
547
+ save(
548
+ value,
549
+ *args,
550
+ **kwargs,
551
+ )
552
+ for value in obj
553
+ ]
554
+
555
+
556
+ @save.register(float)
557
+ @save.register(str)
558
+ @save.register(int)
559
+ def _save_base_types(
560
+ obj: Any,
561
+ configuration_references: dict[str, Any] | None = None,
562
+ *args: Any,
563
+ **kwargs: Any,
564
+ ) -> Any:
565
+ if configuration_references is not None:
566
+ obj_reference = next(
567
+ (
568
+ key
569
+ for key, item in configuration_references.items()
570
+ if isinstance(item, type(obj)) and item == obj
571
+ ),
572
+ None,
573
+ )
574
+ if obj_reference is not None:
575
+ return obj_reference
576
+ return obj
577
+
578
+
579
+ @save.register
580
+ def _save_bool(obj: bool, *args: Any, **kwargs: Any) -> str:
581
+ return str(obj)
582
+
583
+
584
+ @save.register(np.ndarray)
585
+ def _save_array(obj: NDArray[Any], *args: Any, **kwargs: Any) -> Any:
586
+ return float(obj) if obj.size == 1 else obj.tolist()
587
+
588
+
589
+ def _get_init_parameters(obj_type: type[T]) -> list[str]:
590
+ # get parameters of the constructor
591
+ obj_cstr_sig = signature(obj_type.__init__)
592
+ # remove first argument (self)
593
+ parameters_ids = list(obj_cstr_sig.parameters.keys())[1:]
594
+ return parameters_ids
595
+
596
+
597
+ def __sort_configuration(configuration: Mapping[str, Any]) -> Any:
598
+ # Sort dict entries based on on their dependencies to initialize most
599
+ # required arguments first
600
+ dependencies_score = __build_dependencies_sorting_score(configuration)
601
+ sorted_values = dict(
602
+ sorted(
603
+ configuration.items(),
604
+ key=lambda item: dependencies_score[item[0]],
605
+ )
606
+ )
607
+ if isinstance(configuration, Configuration):
608
+ return Configuration(configuration.label, sorted_values)
609
+ elif isinstance(configuration, Mapping):
610
+ return sorted_values
611
+
612
+
613
+ def __build_dependencies_sorting_score(
614
+ configuration: Mapping[str, Any],
615
+ ) -> dict[str, int]:
616
+ dependencies = __build_dependencies(configuration)
617
+ for key, item in dependencies.items():
618
+ __check_dependencies(key, item, dependencies)
619
+ return {key: __get_score(key, dependencies) for key in configuration}
620
+
621
+
622
+ def __check_dependencies(
623
+ key: str,
624
+ dependencies: set[str],
625
+ all_dependencies: Mapping[str, set[str]],
626
+ dependency_chain: list[str] | None = None,
627
+ ) -> None:
628
+ dependency_chain_copy = (
629
+ dependency_chain.copy() if dependency_chain is not None else []
630
+ )
631
+ if key in dependency_chain_copy:
632
+ raise ConfigurationError(
633
+ str.format("Item {0} is referencing itself: {1}", key, dependency_chain)
634
+ )
635
+ dependency_chain_copy.append(key)
636
+ for dependency_item in dependencies:
637
+ __check_dependencies(
638
+ dependency_item,
639
+ all_dependencies[dependency_item],
640
+ all_dependencies,
641
+ dependency_chain_copy,
642
+ )
643
+
644
+
645
+ def __build_dependencies(configuration: Mapping[str, Any]) -> Mapping[str, set[str]]:
646
+ dependencies: dict[str, set[str]] = {}
647
+ for key, item in configuration.items():
648
+ all_values = __get_all_strings(item)
649
+ dependencies[key] = set.intersection(all_values, configuration.keys())
650
+ for new_key, new_item in configuration.items():
651
+ recursive_keys = __get_recursives_keys(new_item)
652
+ if new_key != key and any(
653
+ [
654
+ value in recursive_keys
655
+ for value in all_values
656
+ if value not in dependencies[key]
657
+ ]
658
+ ):
659
+ dependencies[key].add(new_key)
660
+
661
+ return dependencies
662
+
663
+
664
+ def __get_score(item_key: str, dependencies: Mapping[str, set[str]]) -> int:
665
+ score = 1
666
+ for dependencies_key in dependencies[item_key]:
667
+ score += __get_score(dependencies_key, dependencies)
668
+ return score
669
+
670
+
671
+ def __get_recursives_keys(entry: Any) -> set[str]:
672
+ result: set[str] = set()
673
+ if isinstance(entry, str):
674
+ return result
675
+ elif isinstance(entry, Mapping):
676
+ result = result.union(entry.keys())
677
+ result = result.union(__get_recursives_keys(entry.values()))
678
+ elif isinstance(entry, Iterable):
679
+ for value in entry:
680
+ result = result.union(__get_recursives_keys(value))
681
+ return result
682
+
683
+
684
+ def __get_all_strings(entry: Any) -> set[str]:
685
+ result = set()
686
+ if isinstance(entry, str):
687
+ result.add(entry)
688
+ elif isinstance(entry, Mapping):
689
+ result = result.union(__get_all_strings(entry.values()))
690
+ elif isinstance(entry, Iterable):
691
+ for sub_entry in entry:
692
+ entry_set = __get_all_strings(sub_entry)
693
+ result = result.union(entry_set)
694
+
695
+ return result