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,354 @@
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
+ """
28
+ Defines a Time object, and a manager to hold the simulation time.
29
+ """
30
+
31
+ from typing import Any
32
+
33
+ import numpy as np
34
+
35
+ from physioblocks.computing.quantities import Quantity
36
+ from physioblocks.registers.type_register import register_type
37
+
38
+ # Constant for the time quantity id in the simulation
39
+ TIME_QUANTITY_ID = "time"
40
+
41
+ _DEFAULT_STEP = 0.001
42
+ _DEFAULT_MIN_STEP = _DEFAULT_STEP / 16.0
43
+
44
+
45
+ class Time(Quantity[np.float64]):
46
+ """
47
+ Extend :class:`~physioblocks.computing.quantities.Quantity` class to define
48
+ simulation time.
49
+
50
+ Whenever the time is updated, it recomputes the difference between the new
51
+ and the current value and its inverse (accessibles trough the ``dt`` and ``inv_dt``
52
+ properties)
53
+ """
54
+
55
+ _dt: float
56
+ """Difference between ``new`` and ``current`` values"""
57
+
58
+ _inv_dt: float
59
+ """Inverse of ``dt``"""
60
+
61
+ def __init__(self, value: float):
62
+ """
63
+ Initialize ``dt`` and ``inv_dt`` to 0.
64
+
65
+ :param value: the initial time value
66
+ :type value: float
67
+ """
68
+ super().__init__(value)
69
+ self._dt = 0.0
70
+ self._inv_dt = 0.0
71
+
72
+ @property
73
+ def dt(self) -> float:
74
+ """
75
+ Get the difference between the ``new`` and the ``current`` value of the time.
76
+
77
+ :return: the delta time value
78
+ :rtype: float
79
+ """
80
+ return self._dt
81
+
82
+ @property
83
+ def inv_dt(self) -> float:
84
+ """
85
+ Get the inverse of ``dt``.
86
+
87
+ :return: the delta time inverse
88
+ :rtype: float
89
+ """
90
+ return self._inv_dt
91
+
92
+ def update(self, new: Any) -> None:
93
+ """
94
+ Update the ``new`` value of the time and recomputes ``dt`` and ``inv_dt``.
95
+
96
+ .. note::
97
+
98
+ If ``dt`` is 0.0, ``inv_dt`` is set to 0.0.
99
+
100
+ :param new: the new value to set
101
+ :type new: float
102
+ """
103
+ super().update(new)
104
+
105
+ self._dt = self._new - self._current
106
+ if self._dt != 0.0:
107
+ self._inv_dt = 1.0 / self._dt
108
+ else:
109
+ self._inv_dt = 0.0
110
+
111
+ def initialize(self, value: Any) -> None:
112
+ """
113
+ Initialize the ``new`` and ``current`` value.
114
+
115
+ .. note::
116
+
117
+ It set both ``dt`` and ``inv_dt`` to 0.
118
+
119
+ :param value: the value to set
120
+ :type new: Any
121
+ """
122
+ super().initialize(value)
123
+ self._dt = 0.0
124
+ self._inv_dt = 0.0
125
+
126
+
127
+ # Constant for time manager type
128
+ TIME_MANAGER_ID = "time"
129
+
130
+
131
+ @register_type(TIME_MANAGER_ID)
132
+ class TimeManager:
133
+ """
134
+ Updates the time value during the simulation.
135
+
136
+ :param start: start value for the simulation time.
137
+ :type start: float
138
+
139
+ :param end: end value for the simulation time.
140
+ The time ``new`` and ``current`` value can not exceed it.
141
+ :type end: float
142
+
143
+ :param time_step: time increment when update time is called.
144
+ :type time_step: float
145
+
146
+ :param min_step: minimum allowed value of the time increment.
147
+ :type time_step: float
148
+
149
+ :raise ValueError: ValueError is raised when :
150
+ * the start parameter is superior to the end parameter.
151
+ * the time_step parameter is negative.
152
+
153
+ """
154
+
155
+ _time: Time
156
+ """Store the current time value"""
157
+
158
+ start: float
159
+ """The starting time of the time manager."""
160
+
161
+ def __init__(
162
+ self,
163
+ start: float = 0.0,
164
+ duration: float = _DEFAULT_STEP,
165
+ step_size: float = _DEFAULT_STEP,
166
+ min_step: float = _DEFAULT_MIN_STEP,
167
+ ):
168
+ if min_step <= 0.0:
169
+ raise ValueError(
170
+ str.format(
171
+ "Time Manager minimum time step value can not be 0 or negative",
172
+ )
173
+ )
174
+ self._min_step = min_step
175
+ self._time = Time(start)
176
+ self.start = start
177
+ self.duration = duration
178
+ self.step_size = step_size
179
+ self.current_step_size = step_size
180
+
181
+ @property
182
+ def min_step(self) -> float:
183
+ """
184
+ Get the minimum time step size allowed.
185
+
186
+ :return: the minimum time step size.
187
+ :rtype: float
188
+ """
189
+ return self._min_step
190
+
191
+ @min_step.setter
192
+ def min_step(self, value: float) -> None:
193
+ """
194
+ Set the start time.
195
+
196
+ :param value: the start time to set
197
+ :type value: float
198
+
199
+ :raise ValueError: Raises a ValueError if the given value is greater than the
200
+ end time.
201
+ """
202
+
203
+ if value <= 0.0:
204
+ raise ValueError(
205
+ str.format(
206
+ "Time Manager minimum time step value can not be 0 or negative",
207
+ )
208
+ )
209
+ elif value > self.step_size:
210
+ raise ValueError(
211
+ str.format(
212
+ "Time Manager minimum step value can not be superior to time_step",
213
+ )
214
+ )
215
+ self._min_step = value
216
+
217
+ @property
218
+ def end(self) -> float:
219
+ """
220
+ Get the current end time.
221
+
222
+ :return: the end time
223
+ :rtype: float
224
+ """
225
+ return self.start + self._duration
226
+
227
+ @property
228
+ def duration(self) -> float:
229
+ """
230
+ Get the duration.
231
+
232
+ :return: the duration
233
+ :rtype: float
234
+ """
235
+ return self._duration
236
+
237
+ @duration.setter
238
+ def duration(self, value: float) -> None:
239
+ """
240
+ Set the duration
241
+
242
+ :return: the duration
243
+ :rtype: float
244
+ """
245
+ if value <= 0.0:
246
+ raise ValueError(
247
+ str.format(
248
+ "Time Manager duration value can not be 0 or negative",
249
+ )
250
+ )
251
+ self._duration = value
252
+
253
+ @property
254
+ def step_size(self) -> float:
255
+ """
256
+ Get the standard step size
257
+
258
+ :return: the standard step size
259
+ :rtype: float
260
+ """
261
+ return self._max_step
262
+
263
+ @step_size.setter
264
+ def step_size(self, value: float) -> None:
265
+ """
266
+ Get the standard step size
267
+
268
+ :return: the standard step size
269
+ :rtype: float
270
+ """
271
+ if value <= 0.0:
272
+ raise ValueError(
273
+ str.format(
274
+ "Time Manager time step value can not be 0 or negative",
275
+ )
276
+ )
277
+ self._max_step = value
278
+ self.current_step_size = value
279
+
280
+ @property
281
+ def current_step_size(self) -> float:
282
+ """
283
+ Get the current time step size.
284
+
285
+ :return: the time step
286
+ :rtype: float
287
+ """
288
+ return self._time_step
289
+
290
+ @current_step_size.setter
291
+ def current_step_size(self, value: float) -> None:
292
+ """
293
+ Set the current time step size.
294
+
295
+ :param value: the time step to set
296
+ :type value: float
297
+
298
+ :raise ValueError: Raises a ValueError if the given value is
299
+ less than min_step or greater than max_step.
300
+ """
301
+
302
+ if value < self.min_step:
303
+ raise ValueError(
304
+ "Time Manager time step value can not be inferior to minimum step",
305
+ )
306
+ elif value > self._max_step:
307
+ raise ValueError(
308
+ "Time Manager time step value can not be superior to default step",
309
+ )
310
+
311
+ self._time_step = value
312
+
313
+ @property
314
+ def time(self) -> Time:
315
+ """
316
+ Get the current :class:`~.Time` quantity
317
+
318
+ :return: the time quantity
319
+ :rtype: Time
320
+ """
321
+ return self._time
322
+
323
+ @property
324
+ def ended(self) -> bool:
325
+ """
326
+ Get if the time has reached the end time.
327
+
328
+ :return: True if the new time value has reached the end time, False otherwise.
329
+ :rtype: bool
330
+ """
331
+ return bool(self._time.new >= self.end)
332
+
333
+ def update_time(self) -> None:
334
+ """
335
+ Set the ``current`` time value to the ``new`` time value and update the ``new``
336
+ time value with the defined time step increment.
337
+
338
+ If the ``end`` value is reached, the ``new`` time value is set
339
+ to the ``end`` value.
340
+ """
341
+ updated_time = self._time.current + self._time_step
342
+ if updated_time < self.end:
343
+ self._time.initialize(self._time.new)
344
+ self._time.update(self._time.current + self._time_step)
345
+ else:
346
+ # time manager reached end time
347
+ self._time.initialize(self._time.new)
348
+ self._time.update(self.end)
349
+
350
+ def initialize(self) -> None:
351
+ """
352
+ Initialize the time with the ``start`` value.
353
+ """
354
+ self._time.initialize(self.start)
@@ -0,0 +1,27 @@
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
+ """Defines utils modules for Physioblocks"""
@@ -0,0 +1,150 @@
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
+ """
28
+ Defines methods to dynamically import modules while in a script or python
29
+ application.
30
+ """
31
+
32
+ import importlib
33
+ import logging
34
+ import pkgutil
35
+ import sys
36
+ from pathlib import Path
37
+
38
+ from physioblocks.utils.exceptions_utils import log_exception
39
+
40
+ _logger = logging.getLogger(__name__)
41
+
42
+
43
+ def import_libraries(libraries_folder_paths: list[Path]) -> None:
44
+ """
45
+ Dynamically import all the modules at the given paths.
46
+
47
+ .. note:: The libraries folders must be in a site to be able to
48
+ load theirs modules.
49
+
50
+ :param libraries_folder_path: the paths
51
+ :type libraries_folder_path: list[Path]
52
+
53
+ Example
54
+ ^^^^^^^
55
+
56
+ .. code:: python
57
+
58
+ # Add site for the library to load
59
+ site.addsitedir(ABSOLUTE_PATH_TO_LIBRARY)
60
+
61
+ lib_path = Path(ABSOLUTE_PATH_TO_LIBRARY)
62
+ import_libraries([lib_path]) # dynamically import the library
63
+
64
+ """
65
+ packages_absolute_paths: list[tuple[Path, str]] = []
66
+
67
+ for library_path in libraries_folder_paths:
68
+ if library_path.exists() is True:
69
+ absolute_path = library_path.absolute()
70
+ full_package_name = _get_full_package_name(absolute_path)
71
+ packages_absolute_paths.append((absolute_path, full_package_name))
72
+ else:
73
+ _logger.error(
74
+ str.format(
75
+ "There is no library folder at path {0}. Path skipped.",
76
+ str(library_path),
77
+ )
78
+ )
79
+
80
+ for package_path, full_package_name in packages_absolute_paths:
81
+ _import_modules_recursivly_at_path(package_path, full_package_name)
82
+
83
+
84
+ def _get_full_package_name(package_path: Path) -> str:
85
+ if _is_package(package_path) is False:
86
+ raise ImportError(str.format("{0} is not a package", package_path.name))
87
+
88
+ full_package_name = package_path.name
89
+ parent = package_path.parent
90
+ check_parent = True
91
+ while check_parent is True:
92
+ check_parent = False
93
+ if _is_package(parent) is True:
94
+ full_package_name = ".".join([parent.name, full_package_name])
95
+ parent = parent.parent
96
+ check_parent = True
97
+
98
+ return full_package_name
99
+
100
+
101
+ def _is_package(dir_path: Path) -> bool:
102
+ return (
103
+ any(
104
+ path.is_file() and path.name == "__init__.py" for path in dir_path.iterdir()
105
+ )
106
+ is True
107
+ )
108
+
109
+
110
+ def _import_modules_recursivly_at_path(
111
+ package_path: Path, full_package_name: str
112
+ ) -> None:
113
+ for module_info in pkgutil.walk_packages([str(package_path)]):
114
+ full_module_name = module_info.name
115
+ if full_package_name is not None:
116
+ full_module_name = ".".join([full_package_name, module_info.name])
117
+ if full_module_name in sys.modules:
118
+ # already loaded module
119
+ _logger.warning(
120
+ str.format(
121
+ "Module {0} at {1} already loaded. Module skipped ",
122
+ full_module_name,
123
+ str(package_path),
124
+ )
125
+ )
126
+ else:
127
+ try:
128
+ importlib.import_module(full_module_name)
129
+ except ImportError as import_exception:
130
+ # Error while loading the module, log and skip the module
131
+ log_exception(
132
+ _logger,
133
+ ImportError,
134
+ import_exception,
135
+ import_exception.__traceback__,
136
+ logging.WARNING,
137
+ )
138
+ _logger.warning(
139
+ str.format(
140
+ "Import Error while loading {0} from {1}. Module skipped ",
141
+ full_module_name,
142
+ str(package_path),
143
+ )
144
+ )
145
+
146
+ if module_info.ispkg is True:
147
+ # recursivle load packa submodules
148
+ _import_modules_recursivly_at_path(
149
+ package_path / module_info.name, full_module_name
150
+ )
@@ -0,0 +1,115 @@
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 a functions to log exceptions and to caught unhandled exceptions"""
28
+
29
+ import logging
30
+ import traceback
31
+ from collections.abc import Callable
32
+ from logging import Logger
33
+ from types import TracebackType
34
+ from typing import TypeAlias
35
+
36
+ ExceptionHandlerFunction: TypeAlias = Callable[
37
+ [type[BaseException], BaseException, TracebackType | None], None
38
+ ]
39
+ """Type alias for exception handler signature"""
40
+
41
+
42
+ def create_uncaught_exception_logger_handler(
43
+ logger: Logger,
44
+ ) -> ExceptionHandlerFunction:
45
+ """create_uncaught_exception_logger_handler(logger: Logger) -> ExceptionHandlerFunction
46
+
47
+ Create an handler that log an uncaught exception.
48
+
49
+ :param logger: the logger to use to log the exception
50
+ :type logger: Logger
51
+
52
+ :return: the exception handler
53
+ :rtype: Callable
54
+
55
+ Example
56
+ ^^^^^^^
57
+
58
+ .. code:: python
59
+
60
+ # get a logger
61
+ SIMULATION_LOG_FORMATER = logging.Formatter(logging.BASIC_FORMAT)
62
+ _root_logger = logging.getLogger()
63
+ _root_logger.setLevel(logging.DEBUG)
64
+
65
+ # register a hook to log uncaught exceptions to the logger
66
+ sys.excepthook = create_uncaught_exception_logger_handler(_root_logger)
67
+
68
+ """ # noqa: E501
69
+
70
+ def log_handler(
71
+ exception_type: type[BaseException],
72
+ exception_value: BaseException,
73
+ tb: TracebackType | None = None,
74
+ ) -> None:
75
+ log_exception(logger, exception_type, exception_value, tb, logger.level)
76
+
77
+ return log_handler
78
+
79
+
80
+ def log_exception(
81
+ logger: Logger,
82
+ exc_type: type[BaseException],
83
+ exception: BaseException,
84
+ tb: TracebackType | None,
85
+ loglevel: int = logging.ERROR,
86
+ ) -> None:
87
+ """
88
+ Log the provided exception.
89
+
90
+ .. note:: Type and message of the exception are logged with the provided
91
+ ``loglevel``, while the traceback informations are logged as ``DEBUG``.
92
+
93
+ :param logger: the logger used to log
94
+ :type logger: Logger
95
+
96
+ :param exc_type: the exception type
97
+ :type exception: type[BaseException]
98
+
99
+ :param exception: the exception to log
100
+ :type exception: Exception
101
+
102
+ :param tb: the traceback
103
+ :type exception: TracebackType
104
+
105
+ :param loglevel: The loglevel, default is error
106
+ :type loglevel: int
107
+ """
108
+
109
+ logger.log(loglevel, str.format("{0}: {1}", exc_type.__name__, exception))
110
+
111
+ if tb is not None:
112
+ exception_tb = str.join(
113
+ "", traceback.format_exception(exc_type, value=exception, tb=tb)
114
+ )
115
+ logger.debug(exception_tb)