pySimBlocks 0.1.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.
- examples/advanced/hardware/block_diagram.py +162 -0
- examples/advanced/hardware/camera.py +93 -0
- examples/advanced/hardware/gui_emio.py +231 -0
- examples/advanced/hardware/lmi.py +69 -0
- examples/advanced/hardware/main.py +57 -0
- examples/advanced/hardware/parameters.py +54 -0
- examples/advanced/qp_problem/params.py +10 -0
- examples/advanced/qp_problem/run.py +14 -0
- examples/advanced/sofa/gui/finger/Finger.py +108 -0
- examples/advanced/sofa/gui/finger/FingerController.py +53 -0
- examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
- examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
- examples/advanced/sofa/python/Finger.py +107 -0
- examples/advanced/sofa/python/FingerController.py +48 -0
- examples/advanced/sofa/python/closedloop.py +64 -0
- examples/advanced/sofa/python/openloop.py +58 -0
- examples/basics/algebraic_function/g.py +7 -0
- examples/basics/algebraic_function/run.py +14 -0
- examples/basics/dc_motor/gui/run.py +14 -0
- examples/basics/dc_motor/multiple_order.py +91 -0
- examples/basics/dc_motor/openloop.py +62 -0
- examples/basics/dc_motor/pi.py +99 -0
- examples/basics/dc_motor/pid.py +118 -0
- examples/basics/dc_motor/pid_block.py +82 -0
- examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
- examples/basics/external/parameters.py +4 -0
- examples/basics/external/run.py +14 -0
- examples/basics/file_sources/temp.py +16 -0
- examples/basics/observer.py +76 -0
- examples/basics/state_feedback/gui/parameters.py +9 -0
- examples/basics/state_feedback/gui/run.py +14 -0
- examples/basics/state_feedback/state_feedback.py +102 -0
- examples/basics/state_space/linear/gui/run.py +14 -0
- examples/basics/state_space/linear/ss_block.py +89 -0
- examples/basics/state_space/linear/ss_manual.py +105 -0
- examples/basics/state_space/non_linear/nlss.py +47 -0
- examples/basics/state_space/non_linear/run.py +14 -0
- examples/basics/state_space/polytope/params.py +39 -0
- examples/basics/state_space/polytope/weights.py +12 -0
- examples/comparison/simulink/convert.py +29 -0
- examples/comparison/simulink/openloop.py +127 -0
- examples/comparison/simulink/pi.py +148 -0
- examples/comparison/simulink/pid.py +154 -0
- examples/comparison/simulink/pid_backward.py +155 -0
- examples/quick_start/filter.py +35 -0
- examples/quick_start/gui/run.py +14 -0
- examples/tutorials/tutorial_1_python/main.py +99 -0
- examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
- examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
- pySimBlocks/__init__.py +35 -0
- pySimBlocks/blocks/__init__.py +68 -0
- pySimBlocks/blocks/controllers/__init__.py +27 -0
- pySimBlocks/blocks/controllers/pid.py +250 -0
- pySimBlocks/blocks/controllers/state_feedback.py +141 -0
- pySimBlocks/blocks/interfaces/__init__.py +27 -0
- pySimBlocks/blocks/interfaces/external_input.py +107 -0
- pySimBlocks/blocks/interfaces/external_output.py +113 -0
- pySimBlocks/blocks/observers/__init__.py +23 -0
- pySimBlocks/blocks/observers/luenberger.py +175 -0
- pySimBlocks/blocks/operators/__init__.py +47 -0
- pySimBlocks/blocks/operators/algebraic_function.py +258 -0
- pySimBlocks/blocks/operators/dead_zone.py +186 -0
- pySimBlocks/blocks/operators/delay.py +254 -0
- pySimBlocks/blocks/operators/demux.py +123 -0
- pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
- pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
- pySimBlocks/blocks/operators/gain.py +294 -0
- pySimBlocks/blocks/operators/mux.py +131 -0
- pySimBlocks/blocks/operators/product.py +194 -0
- pySimBlocks/blocks/operators/rate_limiter.py +214 -0
- pySimBlocks/blocks/operators/saturation.py +178 -0
- pySimBlocks/blocks/operators/sum.py +188 -0
- pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
- pySimBlocks/blocks/optimizers/__init__.py +23 -0
- pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
- pySimBlocks/blocks/sources/__init__.py +37 -0
- pySimBlocks/blocks/sources/chirp.py +168 -0
- pySimBlocks/blocks/sources/constant.py +82 -0
- pySimBlocks/blocks/sources/file_source.py +294 -0
- pySimBlocks/blocks/sources/function_source.py +197 -0
- pySimBlocks/blocks/sources/ramp.py +99 -0
- pySimBlocks/blocks/sources/sinusoidal.py +113 -0
- pySimBlocks/blocks/sources/step.py +138 -0
- pySimBlocks/blocks/sources/white_noise.py +107 -0
- pySimBlocks/blocks/systems/__init__.py +27 -0
- pySimBlocks/blocks/systems/linear_state_space.py +180 -0
- pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
- pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
- pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
- pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
- pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
- pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
- pySimBlocks/cli.py +127 -0
- pySimBlocks/core/__init__.py +32 -0
- pySimBlocks/core/block.py +167 -0
- pySimBlocks/core/block_source.py +94 -0
- pySimBlocks/core/config.py +90 -0
- pySimBlocks/core/fixed_time_manager.py +46 -0
- pySimBlocks/core/model.py +250 -0
- pySimBlocks/core/scheduler.py +30 -0
- pySimBlocks/core/simulator.py +329 -0
- pySimBlocks/core/task.py +73 -0
- pySimBlocks/docs/blocks/controllers/pid.md +78 -0
- pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
- pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
- pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
- pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
- pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
- pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
- pySimBlocks/docs/blocks/operators/delay.md +76 -0
- pySimBlocks/docs/blocks/operators/demux.md +64 -0
- pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
- pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
- pySimBlocks/docs/blocks/operators/gain.md +97 -0
- pySimBlocks/docs/blocks/operators/mux.md +64 -0
- pySimBlocks/docs/blocks/operators/product.md +51 -0
- pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
- pySimBlocks/docs/blocks/operators/saturation.md +62 -0
- pySimBlocks/docs/blocks/operators/sum.md +49 -0
- pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
- pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
- pySimBlocks/docs/blocks/sources/chirp.md +102 -0
- pySimBlocks/docs/blocks/sources/constant.md +53 -0
- pySimBlocks/docs/blocks/sources/file_source.md +51 -0
- pySimBlocks/docs/blocks/sources/function_source.md +52 -0
- pySimBlocks/docs/blocks/sources/ramp.md +58 -0
- pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
- pySimBlocks/docs/blocks/sources/step.md +63 -0
- pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
- pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
- pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
- pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
- pySimBlocks/gui/__init__.py +20 -0
- pySimBlocks/gui/addons/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
- pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
- pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
- pySimBlocks/gui/blocks/block_meta.py +408 -0
- pySimBlocks/gui/blocks/controllers/pid.py +116 -0
- pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
- pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
- pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
- pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
- pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
- pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
- pySimBlocks/gui/blocks/operators/delay.py +79 -0
- pySimBlocks/gui/blocks/operators/demux.py +98 -0
- pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
- pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
- pySimBlocks/gui/blocks/operators/gain.py +79 -0
- pySimBlocks/gui/blocks/operators/mux.py +100 -0
- pySimBlocks/gui/blocks/operators/product.py +109 -0
- pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
- pySimBlocks/gui/blocks/operators/saturation.py +72 -0
- pySimBlocks/gui/blocks/operators/sum.py +91 -0
- pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
- pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
- pySimBlocks/gui/blocks/parameter_meta.py +34 -0
- pySimBlocks/gui/blocks/port_meta.py +31 -0
- pySimBlocks/gui/blocks/sources/chirp.py +146 -0
- pySimBlocks/gui/blocks/sources/constant.py +63 -0
- pySimBlocks/gui/blocks/sources/file_source.py +196 -0
- pySimBlocks/gui/blocks/sources/function_source.py +202 -0
- pySimBlocks/gui/blocks/sources/ramp.py +73 -0
- pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
- pySimBlocks/gui/blocks/sources/step.py +83 -0
- pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
- pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
- pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
- pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
- pySimBlocks/gui/dialogs/__init__.py +19 -0
- pySimBlocks/gui/dialogs/block_dialog.py +125 -0
- pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
- pySimBlocks/gui/dialogs/help_dialog.py +39 -0
- pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
- pySimBlocks/gui/dialogs/settings/plots.py +187 -0
- pySimBlocks/gui/dialogs/settings/project.py +146 -0
- pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
- pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
- pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
- pySimBlocks/gui/editor.py +49 -0
- pySimBlocks/gui/graphics/__init__.py +29 -0
- pySimBlocks/gui/graphics/block_item.py +289 -0
- pySimBlocks/gui/graphics/connection_item.py +266 -0
- pySimBlocks/gui/graphics/port_item.py +153 -0
- pySimBlocks/gui/graphics/theme.py +119 -0
- pySimBlocks/gui/main_window.py +162 -0
- pySimBlocks/gui/models/__init__.py +31 -0
- pySimBlocks/gui/models/block_instance.py +114 -0
- pySimBlocks/gui/models/connection_instance.py +43 -0
- pySimBlocks/gui/models/port_instance.py +52 -0
- pySimBlocks/gui/models/project_simulation_params.py +50 -0
- pySimBlocks/gui/models/project_state.py +114 -0
- pySimBlocks/gui/project_controller.py +302 -0
- pySimBlocks/gui/services/__init__.py +20 -0
- pySimBlocks/gui/services/project_loader.py +381 -0
- pySimBlocks/gui/services/project_saver.py +68 -0
- pySimBlocks/gui/services/simulation_runner.py +66 -0
- pySimBlocks/gui/services/yaml_tools.py +261 -0
- pySimBlocks/gui/widgets/__init__.py +19 -0
- pySimBlocks/gui/widgets/block_list.py +131 -0
- pySimBlocks/gui/widgets/diagram_view.py +312 -0
- pySimBlocks/gui/widgets/toolbar_view.py +161 -0
- pySimBlocks/project/__init__.py +35 -0
- pySimBlocks/project/build_model.py +92 -0
- pySimBlocks/project/generate_run_script.py +109 -0
- pySimBlocks/project/generate_sofa_controller.py +215 -0
- pySimBlocks/project/load_project_config.py +197 -0
- pySimBlocks/project/load_simulation_config.py +166 -0
- pySimBlocks/project/load_simulator.py +45 -0
- pySimBlocks/project/plot_from_config.py +174 -0
- pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
- pySimBlocks/real_time/__init__.py +23 -0
- pySimBlocks/real_time/real_time_runner.py +148 -0
- pySimBlocks/tools/__init__.py +19 -0
- pySimBlocks/tools/blocks_registry.py +141 -0
- pySimBlocks/tools/generate_blocks_index.py +124 -0
- pysimblocks-0.1.0.dist-info/METADATA +180 -0
- pysimblocks-0.1.0.dist-info/RECORD +262 -0
- pysimblocks-0.1.0.dist-info/WHEEL +5 -0
- pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
- pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
- pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
- tests/blocks/controllers/test_pid.py +143 -0
- tests/blocks/controllers/test_state_feedback.py +193 -0
- tests/blocks/observers/test_luenberger.py +225 -0
- tests/blocks/operators/test_algebraic_function.py +230 -0
- tests/blocks/operators/test_dead_zone.py +106 -0
- tests/blocks/operators/test_delay.py +167 -0
- tests/blocks/operators/test_demux.py +111 -0
- tests/blocks/operators/test_discrete_derivator.py +160 -0
- tests/blocks/operators/test_discrete_integrator.py +149 -0
- tests/blocks/operators/test_gain.py +182 -0
- tests/blocks/operators/test_mux.py +101 -0
- tests/blocks/operators/test_product.py +171 -0
- tests/blocks/operators/test_rate_limiter.py +166 -0
- tests/blocks/operators/test_saturation.py +157 -0
- tests/blocks/operators/test_sum.py +183 -0
- tests/blocks/operators/test_zero_order_hold.py +106 -0
- tests/blocks/sources/test_chirp.py +157 -0
- tests/blocks/sources/test_constant.py +55 -0
- tests/blocks/sources/test_file_source.py +185 -0
- tests/blocks/sources/test_function_source.py +135 -0
- tests/blocks/sources/test_ramp.py +110 -0
- tests/blocks/sources/test_sinusoidal.py +113 -0
- tests/blocks/sources/test_step.py +76 -0
- tests/blocks/sources/test_white_noise.py +108 -0
- tests/blocks/systems/test_linear_state_space.py +135 -0
- tests/blocks/systems/test_polytopic_state_space.py +133 -0
- tests/core/test_core_errors_and_determinism.py +82 -0
- tests/core/test_core_multirate_tasks.py +88 -0
- tests/core/test_core_two_phase_and_propagation.py +132 -0
- tests/gui/main_window_open_test.py +162 -0
- tests/gui/test_block_meta_param_cache.py +16 -0
- tests/gui/test_file_source_meta.py +17 -0
- tests/gui/test_function_source_meta.py +28 -0
- tests/regression/test_simulink_dc_motor.py +217 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from pySimBlocks.core.block import Block
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExternalOutput(Block):
|
|
26
|
+
"""
|
|
27
|
+
External output interface block.
|
|
28
|
+
|
|
29
|
+
Summary:
|
|
30
|
+
Pass-through block for external real-time commands/values.
|
|
31
|
+
|
|
32
|
+
Parameters:
|
|
33
|
+
sample_time: float, optional
|
|
34
|
+
Execution period of the block. If not specified, the simulator time
|
|
35
|
+
step is used.
|
|
36
|
+
|
|
37
|
+
I/O:
|
|
38
|
+
Input:
|
|
39
|
+
in: array (n,1)
|
|
40
|
+
Value produced by the model. Scalar, (n,), (n,1) accepted.
|
|
41
|
+
Output:
|
|
42
|
+
out: array (n,1)
|
|
43
|
+
Value forwarded to the external side as a column vector (n,1).
|
|
44
|
+
|
|
45
|
+
Policy:
|
|
46
|
+
- Accepts scalar, (n,), (n,1)
|
|
47
|
+
- Outputs strict (n,1)
|
|
48
|
+
- Once shape is known, it is frozen (cannot change)
|
|
49
|
+
"""
|
|
50
|
+
direct_feedthrough = True
|
|
51
|
+
|
|
52
|
+
def __init__(self, name: str, sample_time: float | None = None):
|
|
53
|
+
super().__init__(name, sample_time)
|
|
54
|
+
self.inputs["in"] = None
|
|
55
|
+
self.outputs["out"] = None
|
|
56
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --------------------------------------------------------------------------
|
|
60
|
+
# Public methods
|
|
61
|
+
# --------------------------------------------------------------------------
|
|
62
|
+
def initialize(self, t0: float) -> None:
|
|
63
|
+
u = self.inputs["in"]
|
|
64
|
+
if u is None:
|
|
65
|
+
self.outputs["out"] = None
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self.outputs["out"] = self._to_col_vec(u)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
72
|
+
u = self.inputs["in"]
|
|
73
|
+
if u is None:
|
|
74
|
+
raise RuntimeError(f"[{self.name}] Missing input 'in'.")
|
|
75
|
+
self.outputs["out"] = self._to_col_vec(u)
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --------------------------------------------------------------------------
|
|
83
|
+
# Private methods
|
|
84
|
+
# --------------------------------------------------------------------------
|
|
85
|
+
def _to_col_vec(self, value) -> np.ndarray:
|
|
86
|
+
arr = np.asarray(value, dtype=float)
|
|
87
|
+
|
|
88
|
+
# scalar
|
|
89
|
+
if arr.ndim == 0:
|
|
90
|
+
arr = arr.reshape(1, 1)
|
|
91
|
+
|
|
92
|
+
# vector (n,)
|
|
93
|
+
elif arr.ndim == 1:
|
|
94
|
+
arr = arr.reshape(-1, 1)
|
|
95
|
+
|
|
96
|
+
# column (n,1)
|
|
97
|
+
elif arr.ndim == 2 and arr.shape[1] == 1:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"[{self.name}] Input 'in' must be scalar, (n,), or (n,1). Got shape {arr.shape}."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Freeze shape
|
|
106
|
+
if self._resolved_shape is None:
|
|
107
|
+
self._resolved_shape = arr.shape
|
|
108
|
+
elif arr.shape != self._resolved_shape:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {arr.shape}."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return arr
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from pySimBlocks.blocks.observers.luenberger import Luenberger
|
|
22
|
+
|
|
23
|
+
all = ["Luenberger"]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from pySimBlocks.core.block import Block
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Luenberger(Block):
|
|
26
|
+
"""
|
|
27
|
+
Discrete-time Luenberger state observer block.
|
|
28
|
+
|
|
29
|
+
Summary:
|
|
30
|
+
Estimates the state using:
|
|
31
|
+
y_hat[k] = C x_hat[k]
|
|
32
|
+
x_hat[k+1] = A x_hat[k] + B u[k] + L (y[k] - y_hat[k])
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
A : array-like, shape (n, n)
|
|
36
|
+
B : array-like, shape (n, m)
|
|
37
|
+
C : array-like, shape (p, n)
|
|
38
|
+
L : array-like, shape (n, p)
|
|
39
|
+
x0 : array-like, shape (n, 1), optional
|
|
40
|
+
sample_time : float, optional
|
|
41
|
+
|
|
42
|
+
Inputs:
|
|
43
|
+
u : array (m, 1)
|
|
44
|
+
y : array (p, 1)
|
|
45
|
+
|
|
46
|
+
Outputs:
|
|
47
|
+
x_hat : array (n, 1)
|
|
48
|
+
y_hat : array (p, 1)
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
- Stateful block.
|
|
52
|
+
- No direct feedthrough.
|
|
53
|
+
- D matrix intentionally not supported.
|
|
54
|
+
- Input shapes are frozen once first seen.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
direct_feedthrough = False
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
A,
|
|
63
|
+
B,
|
|
64
|
+
C,
|
|
65
|
+
L,
|
|
66
|
+
x0=None,
|
|
67
|
+
sample_time: float | None = None,
|
|
68
|
+
):
|
|
69
|
+
super().__init__(name, sample_time)
|
|
70
|
+
|
|
71
|
+
# --- Matrices: strict 2D
|
|
72
|
+
self.A = np.asarray(A, dtype=float)
|
|
73
|
+
self.B = np.asarray(B, dtype=float)
|
|
74
|
+
self.C = np.asarray(C, dtype=float)
|
|
75
|
+
self.L = np.asarray(L, dtype=float)
|
|
76
|
+
|
|
77
|
+
for M_name, M in (("A", self.A), ("B", self.B), ("C", self.C), ("L", self.L)):
|
|
78
|
+
if M.ndim != 2:
|
|
79
|
+
raise ValueError(f"[{self.name}] {M_name} must be a 2D array. Got shape {M.shape}.")
|
|
80
|
+
|
|
81
|
+
n = self.A.shape[0]
|
|
82
|
+
if self.A.shape[1] != n:
|
|
83
|
+
raise ValueError(f"[{self.name}] A must be square (n,n). Got {self.A.shape}.")
|
|
84
|
+
|
|
85
|
+
if self.B.shape[0] != n:
|
|
86
|
+
raise ValueError(f"[{self.name}] B must have n rows to match A. Got B={self.B.shape}, A={self.A.shape}.")
|
|
87
|
+
|
|
88
|
+
if self.C.shape[1] != n:
|
|
89
|
+
raise ValueError(f"[{self.name}] C must have n columns to match A. Got C={self.C.shape}, A={self.A.shape}.")
|
|
90
|
+
|
|
91
|
+
p = self.C.shape[0]
|
|
92
|
+
m = self.B.shape[1]
|
|
93
|
+
|
|
94
|
+
if self.L.shape != (n, p):
|
|
95
|
+
raise ValueError(f"[{self.name}] L must have shape (n,p) = ({n},{p}). Got {self.L.shape}.")
|
|
96
|
+
|
|
97
|
+
self._n = n
|
|
98
|
+
self._m = m
|
|
99
|
+
self._p = p
|
|
100
|
+
|
|
101
|
+
# --- Initial state x0: strict (n,1), no flatten
|
|
102
|
+
if x0 is None:
|
|
103
|
+
x0_arr = np.zeros((n, 1), dtype=float)
|
|
104
|
+
else:
|
|
105
|
+
x0_arr = np.asarray(x0, dtype=float)
|
|
106
|
+
if x0_arr.ndim != 2 or x0_arr.shape != (n, 1):
|
|
107
|
+
raise ValueError(f"[{self.name}] x0 must have shape ({n},1). Got {x0_arr.shape}.")
|
|
108
|
+
|
|
109
|
+
self.state["x_hat"] = x0_arr.copy()
|
|
110
|
+
self.next_state["x_hat"] = x0_arr.copy()
|
|
111
|
+
|
|
112
|
+
# --- Ports
|
|
113
|
+
self.inputs["u"] = None
|
|
114
|
+
self.inputs["y"] = None
|
|
115
|
+
self.outputs["y_hat"] = None
|
|
116
|
+
self.outputs["x_hat"] = None
|
|
117
|
+
|
|
118
|
+
# Freeze input shapes once first seen
|
|
119
|
+
self._input_shapes = {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------
|
|
123
|
+
# Public methods
|
|
124
|
+
# --------------------------------------------------------------------------
|
|
125
|
+
def initialize(self, t0: float) -> None:
|
|
126
|
+
x_hat = self.state["x_hat"]
|
|
127
|
+
self.outputs["x_hat"] = x_hat.copy()
|
|
128
|
+
self.outputs["y_hat"] = self.C @ x_hat
|
|
129
|
+
self.next_state["x_hat"] = x_hat.copy()
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
133
|
+
x_hat = self.state["x_hat"]
|
|
134
|
+
self.outputs["x_hat"] = x_hat.copy()
|
|
135
|
+
self.outputs["y_hat"] = self.C @ x_hat
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
139
|
+
u = self._require_col_vector("u", self._m)
|
|
140
|
+
y = self._require_col_vector("y", self._p)
|
|
141
|
+
|
|
142
|
+
x_hat = self.state["x_hat"]
|
|
143
|
+
y_hat = self.C @ x_hat
|
|
144
|
+
|
|
145
|
+
self.next_state["x_hat"] = self.A @ x_hat + self.B @ u + self.L @ (y - y_hat)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --------------------------------------------------------------------------
|
|
149
|
+
# Private methods
|
|
150
|
+
# --------------------------------------------------------------------------
|
|
151
|
+
def _require_col_vector(self, port: str, expected_rows: int) -> np.ndarray:
|
|
152
|
+
val = self.inputs[port]
|
|
153
|
+
if val is None:
|
|
154
|
+
raise RuntimeError(f"[{self.name}] Input '{port}' is not connected or not set.")
|
|
155
|
+
|
|
156
|
+
arr = np.asarray(val, dtype=float)
|
|
157
|
+
|
|
158
|
+
# Strict: column vector only (no implicit flatten)
|
|
159
|
+
if arr.ndim != 2 or arr.shape[1] != 1:
|
|
160
|
+
raise ValueError(f"[{self.name}] Input '{port}' must be a column vector (n,1). Got {arr.shape}.")
|
|
161
|
+
|
|
162
|
+
if arr.shape[0] != expected_rows:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"[{self.name}] Input '{port}' has wrong dimension: expected ({expected_rows},1), got {arr.shape}."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Freeze shape
|
|
168
|
+
if port not in self._input_shapes:
|
|
169
|
+
self._input_shapes[port] = arr.shape
|
|
170
|
+
elif arr.shape != self._input_shapes[port]:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"[{self.name}] Input '{port}' shape changed: expected {self._input_shapes[port]}, got {arr.shape}."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return arr
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
from pySimBlocks.blocks.operators.dead_zone import DeadZone
|
|
22
|
+
from pySimBlocks.blocks.operators.demux import Demux
|
|
23
|
+
from pySimBlocks.blocks.operators.delay import Delay
|
|
24
|
+
from pySimBlocks.blocks.operators.discrete_derivator import DiscreteDerivator
|
|
25
|
+
from pySimBlocks.blocks.operators.discrete_integrator import DiscreteIntegrator
|
|
26
|
+
from pySimBlocks.blocks.operators.gain import Gain
|
|
27
|
+
from pySimBlocks.blocks.operators.mux import Mux
|
|
28
|
+
from pySimBlocks.blocks.operators.product import Product
|
|
29
|
+
from pySimBlocks.blocks.operators.rate_limiter import RateLimiter
|
|
30
|
+
from pySimBlocks.blocks.operators.saturation import Saturation
|
|
31
|
+
from pySimBlocks.blocks.operators.sum import Sum
|
|
32
|
+
from pySimBlocks.blocks.operators.zero_order_hold import ZeroOrderHold
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"DeadZone",
|
|
36
|
+
"Demux",
|
|
37
|
+
"Delay",
|
|
38
|
+
"DiscreteDerivator",
|
|
39
|
+
"DiscreteIntegrator",
|
|
40
|
+
"Gain",
|
|
41
|
+
"Mux",
|
|
42
|
+
"Product",
|
|
43
|
+
"RateLimiter",
|
|
44
|
+
"Saturation",
|
|
45
|
+
"Sum",
|
|
46
|
+
"ZeroOrderHold"
|
|
47
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import importlib.util
|
|
22
|
+
import inspect
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Dict, List
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from pySimBlocks.core.block import Block
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AlgebraicFunction(Block):
|
|
32
|
+
"""
|
|
33
|
+
User-defined algebraic function block.
|
|
34
|
+
|
|
35
|
+
Summary:
|
|
36
|
+
Stateless block defined by a user-provided Python function:
|
|
37
|
+
y = g(t, dt, u1, u2, ...)
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
function : callable
|
|
41
|
+
User-defined function.
|
|
42
|
+
input_keys : list[str]
|
|
43
|
+
Names of input ports.
|
|
44
|
+
output_keys : list[str]
|
|
45
|
+
Names of output ports.
|
|
46
|
+
sample_time : float, optional
|
|
47
|
+
Block execution period.
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
- Stateless.
|
|
51
|
+
- Function must return a dict with exactly output_keys.
|
|
52
|
+
- Inputs/outputs must be 2D numpy arrays (matrices allowed).
|
|
53
|
+
- Input/output shapes are frozen per port after first resolution.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
direct_feedthrough = True
|
|
57
|
+
is_source = False
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
function: Callable,
|
|
63
|
+
input_keys: List[str],
|
|
64
|
+
output_keys: List[str],
|
|
65
|
+
sample_time: float | None = None,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(name=name, sample_time=sample_time)
|
|
68
|
+
|
|
69
|
+
if function is None or not callable(function):
|
|
70
|
+
raise TypeError(f"[{self.name}] 'function' must be callable.")
|
|
71
|
+
|
|
72
|
+
self._func = function
|
|
73
|
+
self.input_keys = list(input_keys)
|
|
74
|
+
self.output_keys = list(output_keys)
|
|
75
|
+
|
|
76
|
+
if len(self.input_keys) == 0:
|
|
77
|
+
raise ValueError(f"[{self.name}] input_keys cannot be empty.")
|
|
78
|
+
if len(self.output_keys) == 0:
|
|
79
|
+
raise ValueError(f"[{self.name}] output_keys cannot be empty.")
|
|
80
|
+
|
|
81
|
+
self.inputs: Dict[str, np.ndarray | None] = {k: None for k in self.input_keys}
|
|
82
|
+
self.outputs: Dict[str, np.ndarray | None] = {k: None for k in self.output_keys}
|
|
83
|
+
|
|
84
|
+
# Shape freeze per port
|
|
85
|
+
self._in_shapes: Dict[str, tuple[int, int] | None] = {k: None for k in self.input_keys}
|
|
86
|
+
self._out_shapes: Dict[str, tuple[int, int] | None] = {k: None for k in self.output_keys}
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
# Class Methods
|
|
90
|
+
# --------------------------------------------------------------------------
|
|
91
|
+
@classmethod
|
|
92
|
+
def adapt_params(cls,
|
|
93
|
+
params: Dict[str, Any],
|
|
94
|
+
params_dir: Path | None = None) -> Dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Adapt parameters from yaml format to class constructor format.
|
|
97
|
+
Adapt function file and name in a yaml format into callable.
|
|
98
|
+
"""
|
|
99
|
+
# --- 1. Extract function file and name
|
|
100
|
+
if params_dir is None:
|
|
101
|
+
raise ValueError("parameters_dir must be provided for AlgebraicFunction adapter.")
|
|
102
|
+
try:
|
|
103
|
+
file_path = params["file_path"]
|
|
104
|
+
func_name = params["function_name"]
|
|
105
|
+
except KeyError as e:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"AlgebraicFunction adapter missing parameter: {e}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# --- 2. Resolve file path (relative to project.yaml directory)
|
|
111
|
+
path = Path(file_path).expanduser()
|
|
112
|
+
if not path.is_absolute():
|
|
113
|
+
path = (params_dir / path).resolve()
|
|
114
|
+
|
|
115
|
+
if not path.exists():
|
|
116
|
+
raise FileNotFoundError(f"Function file not found: {path}")
|
|
117
|
+
|
|
118
|
+
# --- 3. Load module
|
|
119
|
+
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
120
|
+
module = importlib.util.module_from_spec(spec)
|
|
121
|
+
assert spec.loader is not None
|
|
122
|
+
spec.loader.exec_module(module)
|
|
123
|
+
|
|
124
|
+
# --- 4. Extract function
|
|
125
|
+
try:
|
|
126
|
+
func = getattr(module, func_name)
|
|
127
|
+
except AttributeError:
|
|
128
|
+
raise AttributeError(
|
|
129
|
+
f"Function '{func_name}' not found in {path}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not callable(func):
|
|
133
|
+
raise TypeError(
|
|
134
|
+
f"'{func_name}' in {path} is not callable"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# --- 5. Build adapted parameter dict
|
|
138
|
+
adapted = dict(params)
|
|
139
|
+
adapted.pop("file_path", None)
|
|
140
|
+
adapted.pop("function_name", None)
|
|
141
|
+
adapted["function"] = func
|
|
142
|
+
|
|
143
|
+
return adapted
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# --------------------------------------------------------------------------
|
|
147
|
+
# Public Methods
|
|
148
|
+
# --------------------------------------------------------------------------
|
|
149
|
+
def initialize(self, t0: float):
|
|
150
|
+
self._validate_signature()
|
|
151
|
+
|
|
152
|
+
out = self._call_func(t0, 0, **self.inputs)
|
|
153
|
+
if not isinstance(out, dict):
|
|
154
|
+
raise RuntimeError(f"[{self.name}] function must return a dict.")
|
|
155
|
+
|
|
156
|
+
if set(out.keys()) != set(self.output_keys):
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
f"[{self.name}] output keys mismatch "
|
|
159
|
+
f"(expected {self.output_keys}, got {list(out.keys())})."
|
|
160
|
+
)
|
|
161
|
+
for k in self.output_keys:
|
|
162
|
+
self.outputs[k] = out[k]
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
def output_update(self, t: float, dt: float):
|
|
166
|
+
# collect inputs
|
|
167
|
+
kwargs: Dict[str, np.ndarray] = {}
|
|
168
|
+
for k in self.input_keys:
|
|
169
|
+
u = self.inputs[k]
|
|
170
|
+
if u is None:
|
|
171
|
+
raise RuntimeError(f"[{self.name}] input '{k}' is not set.")
|
|
172
|
+
u = np.asarray(u) # allow array-like injection, but freeze as ndarray 2D
|
|
173
|
+
self._check_freeze_shape("input", k, u, self._in_shapes)
|
|
174
|
+
kwargs[k] = u
|
|
175
|
+
|
|
176
|
+
# call function
|
|
177
|
+
out = self._call_func(t, dt, **kwargs)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# assign outputs
|
|
182
|
+
for k in self.output_keys:
|
|
183
|
+
y = out[k]
|
|
184
|
+
y = np.asarray(y)
|
|
185
|
+
self._check_freeze_shape("output", k, y, self._out_shapes)
|
|
186
|
+
self.outputs[k] = y
|
|
187
|
+
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
def state_update(self, t: float, dt: float):
|
|
190
|
+
return # stateless
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --------------------------------------------------------------------------
|
|
194
|
+
# Private Methods
|
|
195
|
+
# --------------------------------------------------------------------------
|
|
196
|
+
def _call_func(self, t: float, dt: float, **kwargs) -> Dict[str, np.ndarray]:
|
|
197
|
+
try:
|
|
198
|
+
out = self._func(t, dt, **kwargs)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
raise RuntimeError(f"[{self.name}] function call error: {e}\n"
|
|
201
|
+
f"Must always return a dict with output keys: {self.output_keys}")
|
|
202
|
+
|
|
203
|
+
if not isinstance(out, dict):
|
|
204
|
+
raise RuntimeError(f"[{self.name}] function must return a dict.")
|
|
205
|
+
|
|
206
|
+
if not set(self.output_keys).issubset(out.keys()):
|
|
207
|
+
raise RuntimeError(
|
|
208
|
+
f"[{self.name}] missing output keys "
|
|
209
|
+
f"(expected {self.output_keys}, got {list(out.keys())})."
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for k in self.output_keys:
|
|
213
|
+
y = out[k]
|
|
214
|
+
if not isinstance(y, np.ndarray):
|
|
215
|
+
raise RuntimeError(f"{self.name}: output '{k}' is not a numpy array")
|
|
216
|
+
if y.ndim > 2:
|
|
217
|
+
raise RuntimeError(f"{self.name}: output '{k}' must be at most 2D (got shape {y.shape})")
|
|
218
|
+
return out
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
def _validate_signature(self) -> None:
|
|
222
|
+
sig = inspect.signature(self._func)
|
|
223
|
+
params = list(sig.parameters.values())
|
|
224
|
+
|
|
225
|
+
if len(params) < 2:
|
|
226
|
+
raise ValueError(f"[{self.name}] function must have at least arguments (t, dt).")
|
|
227
|
+
|
|
228
|
+
if params[0].name != "t" or params[1].name != "dt":
|
|
229
|
+
raise ValueError(f"[{self.name}] first arguments must be (t, dt).")
|
|
230
|
+
|
|
231
|
+
# no *args / **kwargs / defaults
|
|
232
|
+
for p in params:
|
|
233
|
+
if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD,):
|
|
234
|
+
raise ValueError(f"[{self.name}] *args and **kwargs are not allowed.")
|
|
235
|
+
|
|
236
|
+
declared = [p.name for p in params[2:]]
|
|
237
|
+
if set(declared) != set(self.input_keys):
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"[{self.name}] function arguments mismatch.\n"
|
|
240
|
+
f"Expected inputs: {self.input_keys}\n"
|
|
241
|
+
f"Function declares: {declared}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
def _check_freeze_shape(self, which: str, key: str, arr: np.ndarray, store: Dict[str, tuple[int, int] | None]) -> None:
|
|
246
|
+
if not isinstance(arr, np.ndarray):
|
|
247
|
+
raise TypeError(f"[{self.name}] {which} '{key}' is not a numpy array.")
|
|
248
|
+
if arr.ndim != 2:
|
|
249
|
+
raise ValueError(f"[{self.name}] {which} '{key}' must be a 2D array. Got shape {arr.shape}.")
|
|
250
|
+
|
|
251
|
+
if store[key] is None:
|
|
252
|
+
store[key] = arr.shape
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
if arr.shape != store[key]:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"[{self.name}] {which} '{key}' shape changed: expected {store[key]}, got {arr.shape}."
|
|
258
|
+
)
|