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,45 @@
|
|
|
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 pathlib import Path
|
|
22
|
+
from typing import Tuple
|
|
23
|
+
|
|
24
|
+
from pySimBlocks.core.config import PlotConfig
|
|
25
|
+
from pySimBlocks.core.model import Model
|
|
26
|
+
from pySimBlocks.core.simulator import Simulator
|
|
27
|
+
from pySimBlocks.project.build_model import build_model_from_dict
|
|
28
|
+
from pySimBlocks.project.load_project_config import load_project_config
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_simulator_from_project(
|
|
32
|
+
project_yaml: str | Path,
|
|
33
|
+
) -> Tuple[Simulator, PlotConfig | None]:
|
|
34
|
+
"""
|
|
35
|
+
Build and return a ready-to-run Simulator from a unified project.yaml.
|
|
36
|
+
"""
|
|
37
|
+
sim_cfg, model_dict, plot_cfg, project_name, params_dir = load_project_config(
|
|
38
|
+
project_yaml
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
model = Model(name=project_name)
|
|
42
|
+
build_model_from_dict(model, model_dict, params_dir=params_dir)
|
|
43
|
+
|
|
44
|
+
sim = Simulator(model, sim_cfg)
|
|
45
|
+
return sim, plot_cfg
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
|
|
24
|
+
from pySimBlocks.core.config import PlotConfig
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _stack_logged_signal(logs: dict, sig: str) -> np.ndarray:
|
|
28
|
+
"""
|
|
29
|
+
Stack a logged signal over time, preserving its 2D shape.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
data: np.ndarray of shape (T, m, n)
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: if the signal is not a list of 2D arrays with consistent shape.
|
|
36
|
+
"""
|
|
37
|
+
samples = logs[sig]
|
|
38
|
+
if not isinstance(samples, list) or len(samples) == 0:
|
|
39
|
+
raise ValueError(f"Signal '{sig}' has no samples in logs.")
|
|
40
|
+
|
|
41
|
+
# Find first non-None sample to define shape
|
|
42
|
+
first = None
|
|
43
|
+
for s in samples:
|
|
44
|
+
if s is not None:
|
|
45
|
+
first = np.asarray(s)
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
if first is None:
|
|
49
|
+
raise ValueError(f"Signal '{sig}' is always None; cannot plot.")
|
|
50
|
+
|
|
51
|
+
if first.ndim != 2:
|
|
52
|
+
raise ValueError(f"Signal '{sig}' must be 2D. Got ndim={first.ndim} with shape {first.shape}.")
|
|
53
|
+
|
|
54
|
+
shape0 = first.shape
|
|
55
|
+
|
|
56
|
+
stacked = []
|
|
57
|
+
for k, s in enumerate(samples):
|
|
58
|
+
if s is None:
|
|
59
|
+
raise ValueError(f"Signal '{sig}' contains None at index {k}; cannot plot.")
|
|
60
|
+
a = np.asarray(s)
|
|
61
|
+
if a.ndim != 2:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Signal '{sig}' sample {k} must be 2D. Got ndim={a.ndim} with shape {a.shape}."
|
|
64
|
+
)
|
|
65
|
+
if a.shape != shape0:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"Signal '{sig}' shape changed over time: expected {shape0}, got {a.shape} at sample {k}."
|
|
68
|
+
)
|
|
69
|
+
stacked.append(a)
|
|
70
|
+
|
|
71
|
+
data = np.stack(stacked, axis=0) # (T, m, n)
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def plot_from_config(
|
|
76
|
+
logs: dict,
|
|
77
|
+
plot_cfg: PlotConfig | None,
|
|
78
|
+
show: bool = True,
|
|
79
|
+
block: bool = True
|
|
80
|
+
):
|
|
81
|
+
"""
|
|
82
|
+
Plot logged simulation signals according to a PlotConfig.
|
|
83
|
+
|
|
84
|
+
Supports 2D signals:
|
|
85
|
+
- scalar (1,1)
|
|
86
|
+
- column vectors (n,1)
|
|
87
|
+
- matrices (m,n) with n>1
|
|
88
|
+
|
|
89
|
+
Plot semantics:
|
|
90
|
+
- each component is plotted as a separate curve
|
|
91
|
+
- labels:
|
|
92
|
+
scalar: sig
|
|
93
|
+
vector: sig[i]
|
|
94
|
+
matrix: sig[r,c]
|
|
95
|
+
"""
|
|
96
|
+
if plot_cfg is None:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------
|
|
100
|
+
# Global validation: all plotted signals must be logged
|
|
101
|
+
# ------------------------------------------------------------
|
|
102
|
+
requested_signals = set()
|
|
103
|
+
for plot in plot_cfg.plots:
|
|
104
|
+
requested_signals.update(plot["signals"])
|
|
105
|
+
|
|
106
|
+
available_signals = set(logs.keys())
|
|
107
|
+
available_signals.discard("time")
|
|
108
|
+
|
|
109
|
+
missing = sorted(requested_signals - available_signals)
|
|
110
|
+
if missing:
|
|
111
|
+
raise KeyError(
|
|
112
|
+
"The following signals are requested for plotting but were not logged:\n"
|
|
113
|
+
+ "\n".join(f" - {sig}" for sig in missing)
|
|
114
|
+
+ "\n\nAvailable logged signals:\n"
|
|
115
|
+
+ "\n".join(f" - {sig}" for sig in sorted(available_signals))
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if "time" not in logs:
|
|
119
|
+
raise KeyError("Logs must contain a 'time' entry.")
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------
|
|
122
|
+
# Time base
|
|
123
|
+
# Your simulator logs time as np.array([t_step]) each step,
|
|
124
|
+
# so flatten() is appropriate.
|
|
125
|
+
# ------------------------------------------------------------
|
|
126
|
+
time = np.asarray(logs["time"]).flatten()
|
|
127
|
+
T = len(time)
|
|
128
|
+
if T == 0:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------
|
|
132
|
+
# Plotting
|
|
133
|
+
# ------------------------------------------------------------
|
|
134
|
+
for plot in plot_cfg.plots:
|
|
135
|
+
title = plot.get("title", "")
|
|
136
|
+
signals = plot["signals"]
|
|
137
|
+
|
|
138
|
+
plt.figure()
|
|
139
|
+
|
|
140
|
+
for sig in signals:
|
|
141
|
+
data = _stack_logged_signal(logs, sig) # (T, m, n)
|
|
142
|
+
|
|
143
|
+
if data.shape[0] != T:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Time length mismatch for '{sig}': time has {T} samples but signal has {data.shape[0]}."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
m, n = data.shape[1], data.shape[2]
|
|
149
|
+
|
|
150
|
+
# scalar
|
|
151
|
+
if (m, n) == (1, 1):
|
|
152
|
+
plt.step(time, data[:, 0, 0], where="post", label=sig)
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# vector column (n,1) -> label sig[i]
|
|
156
|
+
if n == 1:
|
|
157
|
+
for i in range(m):
|
|
158
|
+
plt.step(time, data[:, i, 0], where="post", label=f"{sig}[{i}]")
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
# matrix (m,n) -> label sig[r,c]
|
|
162
|
+
for r in range(m):
|
|
163
|
+
for c in range(n):
|
|
164
|
+
plt.step(time, data[:, r, c], where="post", label=f"{sig}[{r},{c}]")
|
|
165
|
+
|
|
166
|
+
plt.xlabel("Time [s]")
|
|
167
|
+
plt.grid(True)
|
|
168
|
+
plt.legend()
|
|
169
|
+
|
|
170
|
+
if title:
|
|
171
|
+
plt.title(title)
|
|
172
|
+
|
|
173
|
+
if show:
|
|
174
|
+
plt.show(block=block)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
controllers:
|
|
2
|
+
pid:
|
|
3
|
+
class: Pid
|
|
4
|
+
module: pySimBlocks.blocks.controllers.pid
|
|
5
|
+
state_feedback:
|
|
6
|
+
class: StateFeedback
|
|
7
|
+
module: pySimBlocks.blocks.controllers.state_feedback
|
|
8
|
+
interfaces:
|
|
9
|
+
external_input:
|
|
10
|
+
class: ExternalInput
|
|
11
|
+
module: pySimBlocks.blocks.interfaces.external_input
|
|
12
|
+
external_output:
|
|
13
|
+
class: ExternalOutput
|
|
14
|
+
module: pySimBlocks.blocks.interfaces.external_output
|
|
15
|
+
observers:
|
|
16
|
+
luenberger:
|
|
17
|
+
class: Luenberger
|
|
18
|
+
module: pySimBlocks.blocks.observers.luenberger
|
|
19
|
+
operators:
|
|
20
|
+
algebraic_function:
|
|
21
|
+
class: AlgebraicFunction
|
|
22
|
+
module: pySimBlocks.blocks.operators.algebraic_function
|
|
23
|
+
dead_zone:
|
|
24
|
+
class: DeadZone
|
|
25
|
+
module: pySimBlocks.blocks.operators.dead_zone
|
|
26
|
+
delay:
|
|
27
|
+
class: Delay
|
|
28
|
+
module: pySimBlocks.blocks.operators.delay
|
|
29
|
+
demux:
|
|
30
|
+
class: Demux
|
|
31
|
+
module: pySimBlocks.blocks.operators.demux
|
|
32
|
+
discrete_derivator:
|
|
33
|
+
class: DiscreteDerivator
|
|
34
|
+
module: pySimBlocks.blocks.operators.discrete_derivator
|
|
35
|
+
discrete_integrator:
|
|
36
|
+
class: DiscreteIntegrator
|
|
37
|
+
module: pySimBlocks.blocks.operators.discrete_integrator
|
|
38
|
+
gain:
|
|
39
|
+
class: Gain
|
|
40
|
+
module: pySimBlocks.blocks.operators.gain
|
|
41
|
+
mux:
|
|
42
|
+
class: Mux
|
|
43
|
+
module: pySimBlocks.blocks.operators.mux
|
|
44
|
+
product:
|
|
45
|
+
class: Product
|
|
46
|
+
module: pySimBlocks.blocks.operators.product
|
|
47
|
+
rate_limiter:
|
|
48
|
+
class: RateLimiter
|
|
49
|
+
module: pySimBlocks.blocks.operators.rate_limiter
|
|
50
|
+
saturation:
|
|
51
|
+
class: Saturation
|
|
52
|
+
module: pySimBlocks.blocks.operators.saturation
|
|
53
|
+
sum:
|
|
54
|
+
class: Sum
|
|
55
|
+
module: pySimBlocks.blocks.operators.sum
|
|
56
|
+
zero_order_hold:
|
|
57
|
+
class: ZeroOrderHold
|
|
58
|
+
module: pySimBlocks.blocks.operators.zero_order_hold
|
|
59
|
+
optimizers:
|
|
60
|
+
quadratic_program:
|
|
61
|
+
class: QuadraticProgram
|
|
62
|
+
module: pySimBlocks.blocks.optimizers.quadratic_program
|
|
63
|
+
sources:
|
|
64
|
+
chirp:
|
|
65
|
+
class: Chirp
|
|
66
|
+
module: pySimBlocks.blocks.sources.chirp
|
|
67
|
+
constant:
|
|
68
|
+
class: Constant
|
|
69
|
+
module: pySimBlocks.blocks.sources.constant
|
|
70
|
+
file_source:
|
|
71
|
+
class: FileSource
|
|
72
|
+
module: pySimBlocks.blocks.sources.file_source
|
|
73
|
+
function_source:
|
|
74
|
+
class: FunctionSource
|
|
75
|
+
module: pySimBlocks.blocks.sources.function_source
|
|
76
|
+
ramp:
|
|
77
|
+
class: Ramp
|
|
78
|
+
module: pySimBlocks.blocks.sources.ramp
|
|
79
|
+
sinusoidal:
|
|
80
|
+
class: Sinusoidal
|
|
81
|
+
module: pySimBlocks.blocks.sources.sinusoidal
|
|
82
|
+
step:
|
|
83
|
+
class: Step
|
|
84
|
+
module: pySimBlocks.blocks.sources.step
|
|
85
|
+
white_noise:
|
|
86
|
+
class: WhiteNoise
|
|
87
|
+
module: pySimBlocks.blocks.sources.white_noise
|
|
88
|
+
systems:
|
|
89
|
+
linear_state_space:
|
|
90
|
+
class: LinearStateSpace
|
|
91
|
+
module: pySimBlocks.blocks.systems.linear_state_space
|
|
92
|
+
non_linear_state_space:
|
|
93
|
+
class: NonLinearStateSpace
|
|
94
|
+
module: pySimBlocks.blocks.systems.non_linear_state_space
|
|
95
|
+
polytopic_state_space:
|
|
96
|
+
class: PolytopicStateSpace
|
|
97
|
+
module: pySimBlocks.blocks.systems.polytopic_state_space
|
|
98
|
+
sofa_controller:
|
|
99
|
+
class: SofaPysimBlocksController
|
|
100
|
+
module: pySimBlocks.blocks.systems.sofa.sofa_controller
|
|
101
|
+
sofa_exchange_i_o:
|
|
102
|
+
class: SofaExchangeIO
|
|
103
|
+
module: pySimBlocks.blocks.systems.sofa.sofa_exchange_i_o
|
|
104
|
+
sofa_plant:
|
|
105
|
+
class: SofaPlant
|
|
106
|
+
module: pySimBlocks.blocks.systems.sofa.sofa_plant
|
|
@@ -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.real_time.real_time_runner import RealTimeRunner
|
|
22
|
+
|
|
23
|
+
__all__ = ["RealTimeRunner"]
|
|
@@ -0,0 +1,148 @@
|
|
|
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 time
|
|
22
|
+
import numpy as np
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
from pySimBlocks.core.simulator import Simulator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RealTimeRunner:
|
|
28
|
+
"""
|
|
29
|
+
Generic real-time runner for pySimBlocks.
|
|
30
|
+
|
|
31
|
+
Responsibilities:
|
|
32
|
+
- Provide an external clock (dt measured or provided).
|
|
33
|
+
- Push external inputs into ExternalInput blocks.
|
|
34
|
+
- Call Simulator.step(dt_override=...).
|
|
35
|
+
- Pull outputs from ExternalOutput blocks.
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
- No threading, no I/O drivers here. The application owns that.
|
|
39
|
+
- Designed to be embedded in user-specific real-time apps.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
sim: Simulator,
|
|
45
|
+
input_blocks: List[str],
|
|
46
|
+
output_blocks: List[str],
|
|
47
|
+
*,
|
|
48
|
+
target_dt: Optional[float] = None,
|
|
49
|
+
time_source: str = "perf_counter", # "perf_counter" | "time"
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
sim:
|
|
55
|
+
Initialized Simulator instance (model already compiled).
|
|
56
|
+
input_blocks:
|
|
57
|
+
Mapping external input name -> block name in model
|
|
58
|
+
Example: {"camera": "Camera", "ref": "Reference"}
|
|
59
|
+
output_blocks:
|
|
60
|
+
Mapping external output name -> block name in model
|
|
61
|
+
Example: {"motor": "Motor"}
|
|
62
|
+
target_dt:
|
|
63
|
+
If provided, runner will pace the loop to approximately this period
|
|
64
|
+
(best effort). If None, no pacing.
|
|
65
|
+
time_source:
|
|
66
|
+
Timer function selection.
|
|
67
|
+
"""
|
|
68
|
+
self.sim = sim
|
|
69
|
+
self.input_blocks = {block_name: sim.model.get_block_by_name(block_name) for block_name in input_blocks}
|
|
70
|
+
self.output_blocks = {block_name: sim.model.get_block_by_name(block_name) for block_name in output_blocks}
|
|
71
|
+
self.target_dt = target_dt
|
|
72
|
+
|
|
73
|
+
if time_source == "perf_counter":
|
|
74
|
+
self._now = time.perf_counter
|
|
75
|
+
elif time_source == "time":
|
|
76
|
+
self._now = time.time
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("time_source must be 'perf_counter' or 'time'")
|
|
79
|
+
|
|
80
|
+
self._t_prev: Optional[float] = None
|
|
81
|
+
|
|
82
|
+
def initialize(self, t0: float = 0.0) -> None:
|
|
83
|
+
self.sim.initialize(t0)
|
|
84
|
+
self._t_prev = self._now()
|
|
85
|
+
|
|
86
|
+
def tick(
|
|
87
|
+
self,
|
|
88
|
+
inputs: Dict[str, Any],
|
|
89
|
+
*,
|
|
90
|
+
dt: Optional[float] = None,
|
|
91
|
+
pace: bool = False,
|
|
92
|
+
) -> Dict[str, np.ndarray]:
|
|
93
|
+
"""
|
|
94
|
+
Execute one real-time tick.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
inputs:
|
|
99
|
+
External inputs keyed by input_blocks mapping key.
|
|
100
|
+
Example: {"camera": markers_pos, "ref": ref_value}
|
|
101
|
+
dt:
|
|
102
|
+
Optional explicit dt_override. If None, dt is measured from wall clock.
|
|
103
|
+
pace:
|
|
104
|
+
If True and target_dt is set, sleep to approximate target period.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
outputs:
|
|
109
|
+
Dict mapping output key -> np.ndarray (n,1)
|
|
110
|
+
"""
|
|
111
|
+
if self._t_prev is None:
|
|
112
|
+
self._t_prev = self._now()
|
|
113
|
+
|
|
114
|
+
t_now = self._now()
|
|
115
|
+
dt_meas = t_now - self._t_prev
|
|
116
|
+
dt_used = float(dt) if dt is not None else float(dt_meas)
|
|
117
|
+
|
|
118
|
+
# warning if dt is much larger than target_dt
|
|
119
|
+
if self.target_dt is not None and dt_used > 1.5 * self.target_dt:
|
|
120
|
+
print(f"[RealTimeRunner] Warning: dt={dt_used:.3f}s exceeds target_dt={self.target_dt:.3f}s")
|
|
121
|
+
|
|
122
|
+
# 1) push inputs
|
|
123
|
+
for block_name, block in self.input_blocks.items():
|
|
124
|
+
if block_name not in inputs:
|
|
125
|
+
raise KeyError(f"[RealTimeRunner] Missing input '{block_name}'")
|
|
126
|
+
block.inputs["in"] = inputs[block_name]
|
|
127
|
+
|
|
128
|
+
# 2) step with external dt
|
|
129
|
+
self.sim.step(dt_override=dt_used)
|
|
130
|
+
|
|
131
|
+
# 3) pull outputs
|
|
132
|
+
outputs: Dict[str, np.ndarray] = {}
|
|
133
|
+
for block_name, block in self.output_blocks.items():
|
|
134
|
+
y = block.outputs["out"]
|
|
135
|
+
if y is None:
|
|
136
|
+
raise RuntimeError(f"[RealTimeRunner] Output 'out' of block '{block_name}' is None")
|
|
137
|
+
outputs[block_name] = np.asarray(y, dtype=float).reshape(-1, 1)
|
|
138
|
+
|
|
139
|
+
# 4) bookkeeping + pacing
|
|
140
|
+
self._t_prev = t_now
|
|
141
|
+
|
|
142
|
+
if pace and self.target_dt is not None:
|
|
143
|
+
elapsed = self._now() - t_now
|
|
144
|
+
sleep_time = self.target_dt - elapsed
|
|
145
|
+
if sleep_time > 0:
|
|
146
|
+
time.sleep(sleep_time)
|
|
147
|
+
|
|
148
|
+
return outputs
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
# ******************************************************************************
|
|
@@ -0,0 +1,141 @@
|
|
|
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 __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import importlib
|
|
24
|
+
import inspect
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Dict, Optional
|
|
27
|
+
|
|
28
|
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta
|
|
29
|
+
|
|
30
|
+
BlockRegistry = Dict[str, Dict[str, BlockMeta]]
|
|
31
|
+
|
|
32
|
+
def load_block_registry(
|
|
33
|
+
metadata_root: Path | str | None = None,
|
|
34
|
+
) -> BlockRegistry:
|
|
35
|
+
|
|
36
|
+
if metadata_root is None:
|
|
37
|
+
metadata_root = Path(__file__).parents[1] / "gui" / "blocks"
|
|
38
|
+
else:
|
|
39
|
+
metadata_root = Path(metadata_root).resolve()
|
|
40
|
+
|
|
41
|
+
if not metadata_root.exists():
|
|
42
|
+
raise FileNotFoundError(f"blocks_metadata directory not found: {metadata_root}")
|
|
43
|
+
|
|
44
|
+
registry: BlockRegistry = {}
|
|
45
|
+
|
|
46
|
+
for py_path in metadata_root.rglob("*.py"):
|
|
47
|
+
_register_block_from_py(py_path, registry)
|
|
48
|
+
|
|
49
|
+
return registry
|
|
50
|
+
|
|
51
|
+
def _register_block_from_py(
|
|
52
|
+
py_path: Path,
|
|
53
|
+
registry: BlockRegistry,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Import a *.py file and register all BlockMeta subclasses inside.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
module_name = _path_to_module(py_path)
|
|
60
|
+
|
|
61
|
+
module = importlib.import_module(module_name)
|
|
62
|
+
|
|
63
|
+
doc_path = _resolve_doc_path(py_path)
|
|
64
|
+
|
|
65
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
66
|
+
if not issubclass(obj, BlockMeta):
|
|
67
|
+
continue
|
|
68
|
+
if obj is BlockMeta:
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
meta: BlockMeta = obj()
|
|
72
|
+
|
|
73
|
+
meta.doc_path = doc_path
|
|
74
|
+
|
|
75
|
+
category = meta.category
|
|
76
|
+
block_type = meta.type
|
|
77
|
+
|
|
78
|
+
registry.setdefault(category, {})
|
|
79
|
+
|
|
80
|
+
if block_type in registry[category]:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Duplicate block type '{block_type}' in category '{category}'.\n"
|
|
83
|
+
f"Conflict in module: {module_name}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
registry[category][block_type] = meta
|
|
87
|
+
|
|
88
|
+
def _path_to_module(py_path: Path) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Convert a file path to a Python module path.
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
pySimBlocks/blocks_metadata/operators/sum_meta.py
|
|
94
|
+
-> pySimBlocks.blocks_metadata.operators.sum_meta
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
py_path = py_path.with_suffix("")
|
|
98
|
+
|
|
99
|
+
package_root = Path(__file__).parents[1] # pySimBlocks/
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
rel_path = py_path.relative_to(package_root)
|
|
103
|
+
except ValueError:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"File {py_path} is not inside package root {package_root}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
module_name = (
|
|
109
|
+
package_root.name
|
|
110
|
+
+ "."
|
|
111
|
+
+ rel_path.as_posix().replace("/", ".")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return module_name
|
|
115
|
+
|
|
116
|
+
def _resolve_doc_path(py_path: Path) -> Optional[Path]:
|
|
117
|
+
"""
|
|
118
|
+
Resolve the documentation markdown file corresponding to a YAML metadata file.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
blocks_metadata/systems/sofa/sofa_plant.yaml
|
|
122
|
+
-> docs/blocks_metadata/systems/sofa/sofa_plant.md
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
parts = list(py_path.parts)
|
|
127
|
+
idx = parts.index("gui")
|
|
128
|
+
except ValueError:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
doc_root = Path(*parts[:idx]) / "docs"
|
|
132
|
+
|
|
133
|
+
rel = Path(*parts[idx + 1 :]).with_suffix(".md")
|
|
134
|
+
|
|
135
|
+
doc_path = doc_root / rel
|
|
136
|
+
|
|
137
|
+
return doc_path if doc_path.exists() else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
registry = load_block_registry()
|