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,309 @@
|
|
|
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 NonLinearStateSpace(Block):
|
|
32
|
+
"""
|
|
33
|
+
User-defined algebraic function block.
|
|
34
|
+
|
|
35
|
+
Summary:
|
|
36
|
+
Stateless block defined by a user-provided Python function:
|
|
37
|
+
x+ = f(t, dt, x, u1, u2, ...)
|
|
38
|
+
y = g(t, dt, x)
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
state_function : callable
|
|
42
|
+
Function to compute next state.
|
|
43
|
+
output_function_name : callable
|
|
44
|
+
Function to compute outputs.
|
|
45
|
+
input_keys : list[str]
|
|
46
|
+
Names of input ports.
|
|
47
|
+
output_keys : list[str]
|
|
48
|
+
Names of output ports.
|
|
49
|
+
sample_time : float, optional
|
|
50
|
+
Block execution period.
|
|
51
|
+
|
|
52
|
+
I/O:
|
|
53
|
+
Inputs:
|
|
54
|
+
Defined dynamically by input_keys.
|
|
55
|
+
Outputs:
|
|
56
|
+
Defined dynamically by output_keys.
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
- This block is stateless.
|
|
60
|
+
- Ports are fully declarative (V1).
|
|
61
|
+
- The function must return a dict with exactly output_keys.
|
|
62
|
+
- All inputs and outputs must be numpy arrays of shape (n,1).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
direct_feedthrough = False
|
|
66
|
+
is_source = False
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
name: str,
|
|
71
|
+
state_function: Callable,
|
|
72
|
+
output_function: Callable,
|
|
73
|
+
input_keys: List[str],
|
|
74
|
+
output_keys: List[str],
|
|
75
|
+
x0: np.ndarray,
|
|
76
|
+
sample_time: float | None = None,
|
|
77
|
+
):
|
|
78
|
+
super().__init__(name=name, sample_time=sample_time)
|
|
79
|
+
|
|
80
|
+
# ---- parameters
|
|
81
|
+
self._state_func = state_function
|
|
82
|
+
self._output_func = output_function
|
|
83
|
+
self.input_keys = list(input_keys)
|
|
84
|
+
self.output_keys = list(output_keys)
|
|
85
|
+
|
|
86
|
+
# ---- initial state
|
|
87
|
+
if not isinstance(x0, np.ndarray):
|
|
88
|
+
raise TypeError(
|
|
89
|
+
f"{self.name}: x0 must be a numpy array"
|
|
90
|
+
)
|
|
91
|
+
if x0.ndim == 1:
|
|
92
|
+
x0 = x0.reshape(-1, 1)
|
|
93
|
+
elif x0.ndim != 2 or x0.shape[1] != 1:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"{self.name}: x0 must have shape (n,1) or (n,)"
|
|
96
|
+
)
|
|
97
|
+
self.state["x"] = x0.copy()
|
|
98
|
+
self.next_state["x"] = x0.copy()
|
|
99
|
+
|
|
100
|
+
# --------------------------------------------------------------------------
|
|
101
|
+
# Class Methods
|
|
102
|
+
# --------------------------------------------------------------------------
|
|
103
|
+
@classmethod
|
|
104
|
+
def adapt_params(cls,
|
|
105
|
+
params: Dict[str, Any],
|
|
106
|
+
params_dir: Path | None = None) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Adapt parameters from yaml format to class constructor format.
|
|
109
|
+
Adapt function file and name in a yaml format into callable.
|
|
110
|
+
"""
|
|
111
|
+
# --- 1. Validate required fields
|
|
112
|
+
if params_dir is None:
|
|
113
|
+
raise ValueError("parameters_dir must be provided for AlgebraicFunction adapter.")
|
|
114
|
+
try:
|
|
115
|
+
file_path = params["file_path"]
|
|
116
|
+
state_func_name = params["state_function_name"]
|
|
117
|
+
output_func_name = params["output_function_name"]
|
|
118
|
+
except KeyError as e:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"NonLinearStateSpace adapter missing parameter: {e}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# --- 2. Resolve file path (relative to project.yaml directory)
|
|
124
|
+
path = Path(file_path).expanduser()
|
|
125
|
+
if not path.is_absolute():
|
|
126
|
+
path = (params_dir / path).resolve()
|
|
127
|
+
|
|
128
|
+
if not path.exists():
|
|
129
|
+
raise FileNotFoundError(
|
|
130
|
+
f"NonLinearStateSpace function file not found: {path}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# --- 3. Load module
|
|
134
|
+
spec = importlib.util.spec_from_file_location(path.stem, path)
|
|
135
|
+
module = importlib.util.module_from_spec(spec)
|
|
136
|
+
assert spec.loader is not None
|
|
137
|
+
spec.loader.exec_module(module)
|
|
138
|
+
|
|
139
|
+
# --- 4. Extract functions
|
|
140
|
+
try:
|
|
141
|
+
state_func: Callable = getattr(module, state_func_name)
|
|
142
|
+
except AttributeError:
|
|
143
|
+
raise AttributeError(
|
|
144
|
+
f"State function '{state_func_name}' not found in {path}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
output_func: Callable = getattr(module, output_func_name)
|
|
149
|
+
except AttributeError:
|
|
150
|
+
raise AttributeError(
|
|
151
|
+
f"Output function '{output_func_name}' not found in {path}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if not callable(state_func):
|
|
155
|
+
raise TypeError(
|
|
156
|
+
f"'{state_func_name}' in {path} is not callable"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if not callable(output_func):
|
|
160
|
+
raise TypeError(
|
|
161
|
+
f"'{output_func_name}' in {path} is not callable"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# --- 5. Build adapted parameter dict
|
|
165
|
+
adapted = dict(params)
|
|
166
|
+
|
|
167
|
+
adapted.pop("file_path", None)
|
|
168
|
+
adapted.pop("state_function_name", None)
|
|
169
|
+
adapted.pop("output_function_name", None)
|
|
170
|
+
adapted["state_function"] = state_func
|
|
171
|
+
adapted["output_function"] = output_func
|
|
172
|
+
|
|
173
|
+
return adapted
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# --------------------------------------------------------------------------
|
|
177
|
+
# Public Methods
|
|
178
|
+
# --------------------------------------------------------------------------
|
|
179
|
+
def initialize(self, t0: float):
|
|
180
|
+
"""
|
|
181
|
+
Load the user function and validate its signature.
|
|
182
|
+
"""
|
|
183
|
+
self._validate_signature()
|
|
184
|
+
|
|
185
|
+
# ---- declare ports
|
|
186
|
+
for k in self.input_keys:
|
|
187
|
+
self.inputs[k] = None
|
|
188
|
+
|
|
189
|
+
for k in self.output_keys:
|
|
190
|
+
self.outputs[k] = None
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------------------
|
|
193
|
+
def output_update(self, t: float, dt: float):
|
|
194
|
+
"""
|
|
195
|
+
Compute outputs from current inputs.
|
|
196
|
+
"""
|
|
197
|
+
assert self._output_func is not None
|
|
198
|
+
|
|
199
|
+
# ---- call function
|
|
200
|
+
x = self.state["x"]
|
|
201
|
+
out = self._call_output_func(t, dt, x=x)
|
|
202
|
+
|
|
203
|
+
# ---- assign outputs
|
|
204
|
+
for k in self.output_keys:
|
|
205
|
+
self.outputs[k] = out[k]
|
|
206
|
+
|
|
207
|
+
# ------------------------------------------------------------------
|
|
208
|
+
def state_update(self, t: float, dt: float):
|
|
209
|
+
"""
|
|
210
|
+
Compute next state from current inputs.
|
|
211
|
+
"""
|
|
212
|
+
assert self._output_func is not None
|
|
213
|
+
|
|
214
|
+
# ---- collect inputs
|
|
215
|
+
kwargs: Dict[str, np.ndarray] = {}
|
|
216
|
+
for k in self.input_keys:
|
|
217
|
+
u = self.inputs[k]
|
|
218
|
+
if not isinstance(u, np.ndarray):
|
|
219
|
+
raise TypeError(
|
|
220
|
+
f"{self.name}: input '{k}' is not a numpy array"
|
|
221
|
+
)
|
|
222
|
+
if u.ndim != 2 or u.shape[1] != 1:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"{self.name}: input '{k}' must have shape (n,1)"
|
|
225
|
+
)
|
|
226
|
+
kwargs[k] = u
|
|
227
|
+
|
|
228
|
+
# ---- call function
|
|
229
|
+
x = self.state["x"]
|
|
230
|
+
out = self._state_func(t, dt, x=x, **kwargs)
|
|
231
|
+
self.next_state["x"] = out
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# --------------------------------------------------------------------------
|
|
235
|
+
# Private Methods
|
|
236
|
+
# --------------------------------------------------------------------------
|
|
237
|
+
def _call_state_func(self, t, dt, x, **kwargs):
|
|
238
|
+
try:
|
|
239
|
+
out = self._state_func(t, dt, x, **kwargs)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise RuntimeError(f"{self.name}: state function call error: {e}\n"
|
|
242
|
+
f"Must always return the next state as a column vector array.")
|
|
243
|
+
|
|
244
|
+
if not isinstance(out, np.ndarray):
|
|
245
|
+
raise RuntimeError(f"{self.name}: state function must return a numpy array")
|
|
246
|
+
if out.ndim != 2 or out.shape[1] != 1:
|
|
247
|
+
raise RuntimeError(f"{self.name}: state function must return an array of shape (n,1)")
|
|
248
|
+
|
|
249
|
+
return out
|
|
250
|
+
|
|
251
|
+
# ------------------------------------------------------------------
|
|
252
|
+
def _call_output_func(self, t, dt, x):
|
|
253
|
+
try:
|
|
254
|
+
out = self._output_func(t, dt, x)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
raise RuntimeError(f"{self.name}: output function call error: {e}\n"
|
|
257
|
+
f"Must always return a dict with output keys: {self.output_keys}")
|
|
258
|
+
|
|
259
|
+
if not isinstance(out, dict):
|
|
260
|
+
raise RuntimeError(f"{self.name}: output function must return a dict")
|
|
261
|
+
if set(out.keys()) != set(self.output_keys):
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
f"{self.name}: output keys mismatch "
|
|
264
|
+
f"(expected {self.output_keys}, got {list(out.keys())})"
|
|
265
|
+
)
|
|
266
|
+
for k in self.output_keys:
|
|
267
|
+
y = out[k]
|
|
268
|
+
if not isinstance(y, np.ndarray):
|
|
269
|
+
raise RuntimeError(f"{self.name}: output '{k}' is not a numpy array")
|
|
270
|
+
if y.ndim != 2 or y.shape[1] != 1:
|
|
271
|
+
raise RuntimeError(f"{self.name}: output '{k}' must have shape (n,1)")
|
|
272
|
+
|
|
273
|
+
return out
|
|
274
|
+
|
|
275
|
+
# ------------------------------------------------------------------
|
|
276
|
+
def _validate_signature(self):
|
|
277
|
+
"""
|
|
278
|
+
Validate function signature against input_keys.
|
|
279
|
+
"""
|
|
280
|
+
assert self._state_func is not None
|
|
281
|
+
assert self._output_func is not None
|
|
282
|
+
|
|
283
|
+
for f in [self._state_func, self._output_func]:
|
|
284
|
+
sig = inspect.signature(f)
|
|
285
|
+
params = list(sig.parameters.values())
|
|
286
|
+
|
|
287
|
+
# ---- minimum signature: (t, dt, ...)
|
|
288
|
+
if len(params) < 3:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
f"{self.name}: function must have at least arguments (t, dt, x)"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if params[0].name != "t" or params[1].name != "dt" or params[2].name != "x":
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"{self.name}: first arguments must be (t, dt, x)"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# ---- no *args / **kwargs / defaults
|
|
299
|
+
for p in params:
|
|
300
|
+
if p.kind not in (
|
|
301
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
302
|
+
):
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"{self.name}: *args and **kwargs are not allowed"
|
|
305
|
+
)
|
|
306
|
+
if p.default is not inspect.Parameter.empty:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
f"{self.name}: default arguments are not allowed"
|
|
309
|
+
)
|
|
@@ -0,0 +1,193 @@
|
|
|
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 numpy.typing import ArrayLike
|
|
23
|
+
from pySimBlocks.core.block import Block
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PolytopicStateSpace(Block):
|
|
27
|
+
"""
|
|
28
|
+
Discrete-time polytopic state-space block.
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Implements:
|
|
32
|
+
x[k+1] = sum_{i=1}^r w_i[k] (A_i x[k] + B_i u[k])
|
|
33
|
+
y[k] = C x[k]
|
|
34
|
+
|
|
35
|
+
Parameters (overview):
|
|
36
|
+
A : array-like
|
|
37
|
+
concatenation of A_i matrices [A_1, A_2, ..., A_r], shape (nx, r*nx).
|
|
38
|
+
B : array-like
|
|
39
|
+
concatenation of B_i matrices [B_1, B_2, ..., B_r], shape (nx, r*nu).
|
|
40
|
+
C : array-like
|
|
41
|
+
Output matrix.
|
|
42
|
+
x0 : array-like, optional
|
|
43
|
+
Initial state vector.
|
|
44
|
+
sample_time : float, optional
|
|
45
|
+
Block execution period.
|
|
46
|
+
|
|
47
|
+
I/O:
|
|
48
|
+
Inputs:
|
|
49
|
+
u : input vector.
|
|
50
|
+
w : vertex weight vector (must sum to 1).
|
|
51
|
+
Outputs:
|
|
52
|
+
y : output vector.
|
|
53
|
+
x : state vector.
|
|
54
|
+
|
|
55
|
+
Notes:
|
|
56
|
+
- The system is strictly proper (no direct feedthrough).
|
|
57
|
+
- The block has internal state.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
direct_feedthrough = False
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
A: ArrayLike,
|
|
66
|
+
B: ArrayLike,
|
|
67
|
+
C: ArrayLike,
|
|
68
|
+
x0: ArrayLike | None = None,
|
|
69
|
+
sample_time: float | None = None,
|
|
70
|
+
):
|
|
71
|
+
super().__init__(name, sample_time)
|
|
72
|
+
|
|
73
|
+
self.A = np.asarray(A, dtype=float)
|
|
74
|
+
self.B = np.asarray(B, dtype=float)
|
|
75
|
+
self.C = np.asarray(C, dtype=float)
|
|
76
|
+
|
|
77
|
+
if self.A.ndim != 2:
|
|
78
|
+
raise ValueError(f"[{self.name}] A must be 2D. Got shape {self.A.shape}.")
|
|
79
|
+
if self.B.ndim != 2:
|
|
80
|
+
raise ValueError(f"[{self.name}] B must be 2D. Got shape {self.B.shape}.")
|
|
81
|
+
if self.C.ndim != 2:
|
|
82
|
+
raise ValueError(f"[{self.name}] C must be 2D. Got shape {self.C.shape}.")
|
|
83
|
+
|
|
84
|
+
nx = self.A.shape[0]
|
|
85
|
+
if nx <= 0:
|
|
86
|
+
raise ValueError(f"[{self.name}] A must have at least one row.")
|
|
87
|
+
if self.A.shape[1] % nx != 0:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"[{self.name}] A must have shape (nx, r*nx). Got {self.A.shape}."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
r = self.A.shape[1] // nx
|
|
93
|
+
if r <= 0:
|
|
94
|
+
raise ValueError(f"[{self.name}] Number of vertices r must be >= 1.")
|
|
95
|
+
|
|
96
|
+
if self.B.shape[0] != nx:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"[{self.name}] B must have nx rows. A is {self.A.shape}, B is {self.B.shape}."
|
|
99
|
+
)
|
|
100
|
+
if self.B.shape[1] % r != 0:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"[{self.name}] B must have shape (nx, r*nu). A gives r={r}, B is {self.B.shape}."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
nu = self.B.shape[1] // r
|
|
106
|
+
if nu <= 0:
|
|
107
|
+
raise ValueError(f"[{self.name}] Input size nu must be >= 1.")
|
|
108
|
+
|
|
109
|
+
if self.C.shape[1] != nx:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"[{self.name}] C must have nx columns. A is {self.A.shape}, C is {self.C.shape}."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
ny = self.C.shape[0]
|
|
115
|
+
|
|
116
|
+
self._nx = nx
|
|
117
|
+
self._nu = nu
|
|
118
|
+
self._ny = ny
|
|
119
|
+
self._r = r
|
|
120
|
+
|
|
121
|
+
if x0 is None:
|
|
122
|
+
x0_arr = np.zeros((nx, 1), dtype=float)
|
|
123
|
+
else:
|
|
124
|
+
x0_arr = self._to_col_vec("x0", x0, nx)
|
|
125
|
+
|
|
126
|
+
self.state["x"] = x0_arr.copy()
|
|
127
|
+
self.next_state["x"] = x0_arr.copy()
|
|
128
|
+
|
|
129
|
+
self.inputs["w"] = None
|
|
130
|
+
self.inputs["u"] = None
|
|
131
|
+
self.outputs["x"] = None
|
|
132
|
+
self.outputs["y"] = None
|
|
133
|
+
|
|
134
|
+
# --------------------------------------------------------------------------
|
|
135
|
+
# Public methods
|
|
136
|
+
# --------------------------------------------------------------------------
|
|
137
|
+
def initialize(self, t0: float) -> None:
|
|
138
|
+
x = self.state["x"]
|
|
139
|
+
self.outputs["x"] = x.copy()
|
|
140
|
+
self.outputs["y"] = self.C @ x
|
|
141
|
+
self.next_state["x"] = x.copy()
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
145
|
+
x = self.state["x"]
|
|
146
|
+
self.outputs["x"] = x.copy()
|
|
147
|
+
self.outputs["y"] = self.C @ x
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
151
|
+
w = self.inputs["w"]
|
|
152
|
+
u = self.inputs["u"]
|
|
153
|
+
if w is None:
|
|
154
|
+
raise RuntimeError(f"[{self.name}] Input 'w' is not connected or not set.")
|
|
155
|
+
if u is None:
|
|
156
|
+
raise RuntimeError(f"[{self.name}] Input 'u' is not connected or not set.")
|
|
157
|
+
|
|
158
|
+
w_vec = self._to_col_vec("w", w, self._r)
|
|
159
|
+
if not np.isclose(np.sum(w_vec), 1.0):
|
|
160
|
+
raise ValueError(f"[{self.name}] Vertex weights w must sum to 1. Got sum {np.sum(w_vec)}.")
|
|
161
|
+
if np.any(w_vec < 0):
|
|
162
|
+
raise ValueError(f"[{self.name}] Vertex weights w must be non-negative. Got {w_vec.flatten()}.")
|
|
163
|
+
|
|
164
|
+
u_vec = self._to_col_vec("u", u, self._nu)
|
|
165
|
+
x = self.state["x"]
|
|
166
|
+
|
|
167
|
+
x_next = self.A @ np.kron(w_vec, x) + self.B @ np.kron(w_vec, u_vec)
|
|
168
|
+
self.next_state["x"] = x_next
|
|
169
|
+
|
|
170
|
+
# --------------------------------------------------------------------------
|
|
171
|
+
# Private methods
|
|
172
|
+
# --------------------------------------------------------------------------
|
|
173
|
+
def _to_col_vec(self, name: str, value: ArrayLike, expected_rows: int) -> np.ndarray:
|
|
174
|
+
arr = np.asarray(value, dtype=float)
|
|
175
|
+
|
|
176
|
+
if arr.ndim == 0:
|
|
177
|
+
arr = arr.reshape(1, 1)
|
|
178
|
+
elif arr.ndim == 1:
|
|
179
|
+
arr = arr.reshape(-1, 1)
|
|
180
|
+
elif arr.ndim == 2:
|
|
181
|
+
pass
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError(f"[{self.name}] {name} must be 1D or 2D. Got shape {arr.shape}.")
|
|
184
|
+
|
|
185
|
+
if arr.shape[1] != 1:
|
|
186
|
+
raise ValueError(f"[{self.name}] {name} must be a column vector (k,1). Got {arr.shape}.")
|
|
187
|
+
|
|
188
|
+
if arr.shape[0] != expected_rows:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"[{self.name}] {name} must have shape ({expected_rows},1). Got {arr.shape}."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return arr
|
|
@@ -0,0 +1,30 @@
|
|
|
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.systems.sofa.sofa_controller import SofaPysimBlocksController
|
|
22
|
+
from pySimBlocks.blocks.systems.sofa.sofa_exchange_i_o import SofaExchangeIO
|
|
23
|
+
from pySimBlocks.blocks.systems.sofa.sofa_plant import SofaPlant
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ =[
|
|
27
|
+
"SofaPysimBlocksController",
|
|
28
|
+
"SofaExchangeIO",
|
|
29
|
+
"SofaPlant",
|
|
30
|
+
]
|