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,94 @@
|
|
|
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
|
+
|
|
23
|
+
from pySimBlocks.core.block import Block
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BlockSource(Block):
|
|
27
|
+
"""
|
|
28
|
+
Base class for all source blocks (Constant, Step, Ramp, Sinusoidal, ...).
|
|
29
|
+
|
|
30
|
+
Provides:
|
|
31
|
+
- normalization utilities for source parameters to produce 2D signals
|
|
32
|
+
- strict scalar-only broadcasting to a common 2D shape
|
|
33
|
+
- no state update by default
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
direct_feedthrough = False
|
|
37
|
+
is_source = True
|
|
38
|
+
|
|
39
|
+
def __init__(self, name: str, sample_time: float | None = None):
|
|
40
|
+
super().__init__(name, sample_time)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------
|
|
44
|
+
# Public methods
|
|
45
|
+
# --------------------------------------------------------------------------
|
|
46
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
47
|
+
pass # all sources are stateless
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# --------------------------------------------------------------------------
|
|
51
|
+
# Private methods
|
|
52
|
+
# --------------------------------------------------------------------------
|
|
53
|
+
def _resolve_common_shape(self, params: dict[str, np.ndarray]) -> tuple[int, int]:
|
|
54
|
+
"""
|
|
55
|
+
Determine the common target shape among parameters.
|
|
56
|
+
|
|
57
|
+
Policy:
|
|
58
|
+
- scalars (1,1) are broadcastable
|
|
59
|
+
- any non-scalar (2D not (1,1)) fixes the target shape
|
|
60
|
+
- if multiple non-scalars exist, all must have exactly the same shape
|
|
61
|
+
- if all are scalars -> target shape is (1,1)
|
|
62
|
+
"""
|
|
63
|
+
non_scalar_shapes = {a.shape for a in params.values() if not self._is_scalar_2d(a)}
|
|
64
|
+
|
|
65
|
+
if len(non_scalar_shapes) == 0:
|
|
66
|
+
return (1, 1)
|
|
67
|
+
|
|
68
|
+
if len(non_scalar_shapes) == 1:
|
|
69
|
+
return next(iter(non_scalar_shapes))
|
|
70
|
+
|
|
71
|
+
details = ", ".join(f"{k}={v.shape}" for k, v in params.items())
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"[{self.name}] Inconsistent parameter shapes. "
|
|
74
|
+
f"All non-scalar parameters must have the same (m,n) shape. Got: {details}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
def _broadcast_scalar_only(self, param_name: str, arr: np.ndarray, target_shape: tuple[int, int]) -> np.ndarray:
|
|
79
|
+
"""
|
|
80
|
+
Broadcast only scalar (1,1) to target_shape.
|
|
81
|
+
Any non-scalar must already match target_shape exactly.
|
|
82
|
+
"""
|
|
83
|
+
if self._is_scalar_2d(arr):
|
|
84
|
+
if target_shape == (1, 1):
|
|
85
|
+
return arr.astype(float, copy=False)
|
|
86
|
+
return np.full(target_shape, float(arr[0, 0]), dtype=float)
|
|
87
|
+
|
|
88
|
+
if arr.shape != target_shape:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"[{self.name}] '{param_name}' shape {arr.shape} is incompatible with "
|
|
91
|
+
f"target shape {target_shape}. Only scalar-to-shape broadcasting is allowed."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return arr.astype(float, copy=False)
|
|
@@ -0,0 +1,90 @@
|
|
|
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 dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Dict, List
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------
|
|
26
|
+
# Classes
|
|
27
|
+
# ---------------------------------------------------------------------
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SimulationConfig:
|
|
30
|
+
"""
|
|
31
|
+
Simulation execution configuration.
|
|
32
|
+
|
|
33
|
+
This object contains ONLY execution-related parameters.
|
|
34
|
+
It must not contain any model or block-specific information.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
dt: float
|
|
38
|
+
T: float
|
|
39
|
+
t0: float = 0.0
|
|
40
|
+
solver: str = "fixed"
|
|
41
|
+
logging: List[str] = field(default_factory=list)
|
|
42
|
+
clock: str = "internal" # "internal" or "external"
|
|
43
|
+
|
|
44
|
+
def validate(self) -> None:
|
|
45
|
+
"""Verify that the configuration is valid.
|
|
46
|
+
(ie: dt > 0, T > t0, solver is known)
|
|
47
|
+
"""
|
|
48
|
+
if self.dt <= 0.0:
|
|
49
|
+
raise ValueError("SimulationConfig.dt must be > 0")
|
|
50
|
+
|
|
51
|
+
if self.T <= self.t0:
|
|
52
|
+
raise ValueError("SimulationConfig.T must be > t0")
|
|
53
|
+
|
|
54
|
+
if self.solver not in {"fixed", "variable"}:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"Unknown solver '{self.solver}'. "
|
|
57
|
+
"Allowed values: {'fixed', 'variable'}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if self.clock not in {"internal", "external"}:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Unknown clock '{self.clock}'. "
|
|
63
|
+
"Allowed values: {'internal', 'external'}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class PlotConfig:
|
|
69
|
+
"""
|
|
70
|
+
Plot configuration.
|
|
71
|
+
|
|
72
|
+
Describes how logged signals should be visualized.
|
|
73
|
+
This object contains NO plotting logic.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
plots: List[Dict[str, Any]]
|
|
77
|
+
|
|
78
|
+
def validate(self) -> None:
|
|
79
|
+
"""Verify that the configuration is valid.
|
|
80
|
+
(ie: each plot has required fields)
|
|
81
|
+
"""
|
|
82
|
+
for i, plot in enumerate(self.plots):
|
|
83
|
+
if "signals" not in plot:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Plot #{i} is missing required field 'signals'"
|
|
86
|
+
)
|
|
87
|
+
if not isinstance(plot["signals"], list):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"'signals' in plot #{i} must be a list"
|
|
90
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
class FixedStepTimeManager:
|
|
22
|
+
"""A time manager for fixed-step simulations.
|
|
23
|
+
Handle multiple sample times by ensuring they are multiples of the base time step.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, dt_base: float, sample_times: list[float]):
|
|
26
|
+
|
|
27
|
+
if dt_base <= 0:
|
|
28
|
+
raise ValueError("Base time step must be strictly positive.")
|
|
29
|
+
|
|
30
|
+
self.dt = dt_base
|
|
31
|
+
self._check_sample_times(sample_times)
|
|
32
|
+
|
|
33
|
+
def _check_sample_times(self, sample_times):
|
|
34
|
+
"""Ensure all sample times are multiples of the base time step."""
|
|
35
|
+
eps = 1e-12
|
|
36
|
+
for st in sample_times:
|
|
37
|
+
ratio = st / self.dt
|
|
38
|
+
if abs(ratio - round(ratio)) > eps:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"In fixed-step mode, sample_time={st} "
|
|
41
|
+
f"is not a multiple of base dt={self.dt}."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def next_dt(self, t):
|
|
45
|
+
"""Get the next time step (always fixed)."""
|
|
46
|
+
return self.dt
|
|
@@ -0,0 +1,250 @@
|
|
|
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 collections import deque
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Tuple
|
|
24
|
+
|
|
25
|
+
from pySimBlocks.core.block import Block
|
|
26
|
+
|
|
27
|
+
# A connection is:
|
|
28
|
+
# ( (src_block, src_port), (dst_block, dst_port) )
|
|
29
|
+
Connection = Tuple[Tuple[str, str], Tuple[str, str]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Model:
|
|
33
|
+
"""
|
|
34
|
+
Discrete-time block-diagram model (Simulink-like).
|
|
35
|
+
|
|
36
|
+
Responsibilities:
|
|
37
|
+
- Store blocks.
|
|
38
|
+
- Store signal connections.
|
|
39
|
+
- Build execution order (topological sort).
|
|
40
|
+
- Provide fast access to downstream connections.
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
* Topological sorting is applied only to the combinational graph.
|
|
44
|
+
* Blocks with state (i.e., blocks where next_state is non-empty)
|
|
45
|
+
are treated as "cycle breakers" (delay elements),
|
|
46
|
+
exactly like Simulink does for algebraic loops.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
name: str = "model",
|
|
52
|
+
model_data: Dict[str, Any] | None = None,
|
|
53
|
+
params_dir: Path | None = None,
|
|
54
|
+
verbose: bool = False,
|
|
55
|
+
):
|
|
56
|
+
self.name = name
|
|
57
|
+
self.verbose = verbose
|
|
58
|
+
|
|
59
|
+
self.blocks: Dict[str, Block] = {}
|
|
60
|
+
self.connections: List[Connection] = []
|
|
61
|
+
|
|
62
|
+
self._output_execution_order: List[Block] = []
|
|
63
|
+
self._state_execution_order: List[Block] = []
|
|
64
|
+
|
|
65
|
+
self._downstream_map: Dict[str, List[Connection]] = {}
|
|
66
|
+
self._connections_dirty: bool = True
|
|
67
|
+
|
|
68
|
+
if model_data is not None:
|
|
69
|
+
from pySimBlocks.project.build_model import build_model_from_dict
|
|
70
|
+
build_model_from_dict(self, model_data, params_dir=params_dir)
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------------------------------
|
|
73
|
+
# Public methods
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
def add_block(self, block: Block) -> Block:
|
|
76
|
+
"""Add a block to the model."""
|
|
77
|
+
if block.name in self.blocks:
|
|
78
|
+
raise ValueError(f"Block name '{block.name}' already exists.")
|
|
79
|
+
|
|
80
|
+
self.blocks[block.name] = block
|
|
81
|
+
return block
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
def get_block_by_name(self, name: str) -> Block:
|
|
85
|
+
"""Get a block by its name."""
|
|
86
|
+
if name not in self.blocks:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Block name '{name}' not found. Known blocks: {list(self.blocks.keys())}"
|
|
89
|
+
)
|
|
90
|
+
return self.blocks[name]
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
def connect(self, src_block: str, src_port: str,
|
|
94
|
+
dst_block: str, dst_port: str) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Connect:
|
|
97
|
+
blocks[src_block].outputs[src_port]
|
|
98
|
+
to:
|
|
99
|
+
blocks[dst_block].inputs[dst_port]
|
|
100
|
+
"""
|
|
101
|
+
if src_block not in self.blocks:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Unknown source block '{src_block}'. "
|
|
104
|
+
f"Known blocks: {list(self.blocks.keys())}"
|
|
105
|
+
)
|
|
106
|
+
if dst_block not in self.blocks:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Unknown destination block '{dst_block}'. "
|
|
109
|
+
f"Known blocks: {list(self.blocks.keys())}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.connections.append(
|
|
113
|
+
((src_block, src_port), (dst_block, dst_port))
|
|
114
|
+
)
|
|
115
|
+
self._connections_dirty = True
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
def build_execution_order(self):
|
|
119
|
+
"""
|
|
120
|
+
Build Simulink-like execution order based solely on direct-feedthrough
|
|
121
|
+
causal dependencies (Simulink PDF p.7).
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
blocks = self.blocks
|
|
125
|
+
names = list(blocks.keys())
|
|
126
|
+
|
|
127
|
+
vprint = print if self.verbose else (lambda *a, **k: None)
|
|
128
|
+
|
|
129
|
+
vprint("\n================= BUILD EXECUTION ORDER =================")
|
|
130
|
+
vprint(f"Blocks in model: {names}")
|
|
131
|
+
|
|
132
|
+
# STEP 1 — Build dependency graph
|
|
133
|
+
vprint("\n--- STEP 1: CONNECTION ANALYSIS (direct-feedthrough rules) ---")
|
|
134
|
+
|
|
135
|
+
graph = {name: [] for name in names}
|
|
136
|
+
indegree = {name: 0 for name in names}
|
|
137
|
+
|
|
138
|
+
for (src, dst) in self.connections:
|
|
139
|
+
src_block, src_port = src
|
|
140
|
+
dst_block, dst_port = dst
|
|
141
|
+
|
|
142
|
+
block_dist = blocks[dst_block]
|
|
143
|
+
|
|
144
|
+
if block_dist.direct_feedthrough:
|
|
145
|
+
graph[src_block].append(dst_block)
|
|
146
|
+
indegree[dst_block] += 1
|
|
147
|
+
vprint(f" DEPENDENCY: {src_block}.{src_port} -> {dst_block}.{dst_port} "
|
|
148
|
+
f"(direct-feedthrough)")
|
|
149
|
+
else:
|
|
150
|
+
vprint(f" NO DEPENDENCY: {src_block}.{src_port} -> {dst_block}.{dst_port} "
|
|
151
|
+
f"(destination NOT direct-feedthrough)")
|
|
152
|
+
|
|
153
|
+
# Show resulting graph
|
|
154
|
+
vprint("\nGraph adjacency list:")
|
|
155
|
+
for k, v in graph.items():
|
|
156
|
+
vprint(f" {k}: {v}")
|
|
157
|
+
|
|
158
|
+
vprint("\nInitial indegree:")
|
|
159
|
+
for k, v in indegree.items():
|
|
160
|
+
vprint(f" {k}: {v}")
|
|
161
|
+
|
|
162
|
+
# STEP 2 — Kahn topological sort
|
|
163
|
+
vprint("\n--- STEP 2: TOPOLOGICAL SORT ---")
|
|
164
|
+
|
|
165
|
+
ready = deque([b for b in names if indegree[b] == 0])
|
|
166
|
+
|
|
167
|
+
vprint(f"Initial READY queue: {list(ready)}")
|
|
168
|
+
|
|
169
|
+
execution_order = []
|
|
170
|
+
|
|
171
|
+
while ready:
|
|
172
|
+
current = ready.popleft()
|
|
173
|
+
execution_order.append(current)
|
|
174
|
+
|
|
175
|
+
vprint(f"\n==> EXECUTE: '{current}'")
|
|
176
|
+
|
|
177
|
+
# Decrease indegree for successors
|
|
178
|
+
for succ in graph[current]:
|
|
179
|
+
indegree[succ] -= 1
|
|
180
|
+
vprint(f" indegree[{succ}] -> {indegree[succ]}")
|
|
181
|
+
if indegree[succ] == 0:
|
|
182
|
+
ready.append(succ)
|
|
183
|
+
vprint(f" '{succ}' added to READY")
|
|
184
|
+
|
|
185
|
+
# STEP 3 — Detect algebraic loops
|
|
186
|
+
if len(execution_order) != len(names):
|
|
187
|
+
vprint("\n!!! ALGEBRAIC LOOP DETECTED !!!")
|
|
188
|
+
raise RuntimeError(
|
|
189
|
+
"Algebraic loop detected: direct-feedthrough cycle exists."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# STEP 4 — Final result
|
|
193
|
+
vprint("\n--- FINAL SIMULINK-LIKE EXECUTION ORDER ---")
|
|
194
|
+
for i, b in enumerate(execution_order, 1):
|
|
195
|
+
vprint(f" {i}. {b}")
|
|
196
|
+
vprint("========================================================\n")
|
|
197
|
+
|
|
198
|
+
# Final storage
|
|
199
|
+
self._output_execution_order = [blocks[n] for n in execution_order]
|
|
200
|
+
|
|
201
|
+
return self._output_execution_order
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
def downstream_of(self, block_name: str):
|
|
205
|
+
"""
|
|
206
|
+
Returns all connections where block_name is the source.
|
|
207
|
+
"""
|
|
208
|
+
if self._connections_dirty or not self._downstream_map:
|
|
209
|
+
self._rebuild_downstream_map()
|
|
210
|
+
return self._downstream_map.get(block_name, [])
|
|
211
|
+
|
|
212
|
+
# ------------------------------------------------------------------
|
|
213
|
+
def execution_order(self):
|
|
214
|
+
"""Get execution order, building it if necessary."""
|
|
215
|
+
if not self._output_execution_order:
|
|
216
|
+
return self.build_execution_order()
|
|
217
|
+
return self._output_execution_order
|
|
218
|
+
|
|
219
|
+
# ------------------------------------------------------------------
|
|
220
|
+
def predecessors_of(self, block_name):
|
|
221
|
+
"""Get all blocks that feed into block_name."""
|
|
222
|
+
for (src, dst) in self.connections:
|
|
223
|
+
if dst[0] == block_name:
|
|
224
|
+
yield src[0]
|
|
225
|
+
|
|
226
|
+
# ------------------------------------------------------------------
|
|
227
|
+
def resolve_sample_times(self, dt):
|
|
228
|
+
"""
|
|
229
|
+
Resolve effective sample times for all blocks.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
has_explicit_rate (bool): True if at least one block defines a sample_time.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
for b in self.blocks.values():
|
|
236
|
+
if b.sample_time is None:
|
|
237
|
+
b._effective_sample_time = dt
|
|
238
|
+
else:
|
|
239
|
+
b._effective_sample_time = b.sample_time
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# --------------------------------------------------------------------------
|
|
243
|
+
# Private methods
|
|
244
|
+
# --------------------------------------------------------------------------
|
|
245
|
+
def _rebuild_downstream_map(self) -> None:
|
|
246
|
+
downstream = {name: [] for name in self.blocks.keys()}
|
|
247
|
+
for (src, dst) in self.connections:
|
|
248
|
+
downstream[src[0]].append((src, dst))
|
|
249
|
+
self._downstream_map = downstream
|
|
250
|
+
self._connections_dirty = False
|
|
@@ -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.core.task import Task
|
|
22
|
+
|
|
23
|
+
class Scheduler:
|
|
24
|
+
"""A simple scheduler for managing tasks based on their start times."""
|
|
25
|
+
def __init__(self, tasks: list[Task]):
|
|
26
|
+
self.tasks = sorted(tasks, key=lambda t: t.sample_time)
|
|
27
|
+
|
|
28
|
+
def active_tasks(self, t):
|
|
29
|
+
"""Return the list of tasks that should run at time t."""
|
|
30
|
+
return [task for task in self.tasks if task.should_run(t)]
|