pySimBlocks 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/advanced/hardware/block_diagram.py +162 -0
- examples/advanced/hardware/camera.py +93 -0
- examples/advanced/hardware/gui_emio.py +231 -0
- examples/advanced/hardware/lmi.py +69 -0
- examples/advanced/hardware/main.py +57 -0
- examples/advanced/hardware/parameters.py +54 -0
- examples/advanced/qp_problem/params.py +10 -0
- examples/advanced/qp_problem/run.py +14 -0
- examples/advanced/sofa/gui/finger/Finger.py +108 -0
- examples/advanced/sofa/gui/finger/FingerController.py +53 -0
- examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
- examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
- examples/advanced/sofa/python/Finger.py +107 -0
- examples/advanced/sofa/python/FingerController.py +48 -0
- examples/advanced/sofa/python/closedloop.py +64 -0
- examples/advanced/sofa/python/openloop.py +58 -0
- examples/basics/algebraic_function/g.py +7 -0
- examples/basics/algebraic_function/run.py +14 -0
- examples/basics/dc_motor/gui/run.py +14 -0
- examples/basics/dc_motor/multiple_order.py +91 -0
- examples/basics/dc_motor/openloop.py +62 -0
- examples/basics/dc_motor/pi.py +99 -0
- examples/basics/dc_motor/pid.py +118 -0
- examples/basics/dc_motor/pid_block.py +82 -0
- examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
- examples/basics/external/parameters.py +4 -0
- examples/basics/external/run.py +14 -0
- examples/basics/file_sources/temp.py +16 -0
- examples/basics/observer.py +76 -0
- examples/basics/state_feedback/gui/parameters.py +9 -0
- examples/basics/state_feedback/gui/run.py +14 -0
- examples/basics/state_feedback/state_feedback.py +102 -0
- examples/basics/state_space/linear/gui/run.py +14 -0
- examples/basics/state_space/linear/ss_block.py +89 -0
- examples/basics/state_space/linear/ss_manual.py +105 -0
- examples/basics/state_space/non_linear/nlss.py +47 -0
- examples/basics/state_space/non_linear/run.py +14 -0
- examples/basics/state_space/polytope/params.py +39 -0
- examples/basics/state_space/polytope/weights.py +12 -0
- examples/comparison/simulink/convert.py +29 -0
- examples/comparison/simulink/openloop.py +127 -0
- examples/comparison/simulink/pi.py +148 -0
- examples/comparison/simulink/pid.py +154 -0
- examples/comparison/simulink/pid_backward.py +155 -0
- examples/quick_start/filter.py +35 -0
- examples/quick_start/gui/run.py +14 -0
- examples/tutorials/tutorial_1_python/main.py +99 -0
- examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
- examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
- pySimBlocks/__init__.py +35 -0
- pySimBlocks/blocks/__init__.py +68 -0
- pySimBlocks/blocks/controllers/__init__.py +27 -0
- pySimBlocks/blocks/controllers/pid.py +250 -0
- pySimBlocks/blocks/controllers/state_feedback.py +141 -0
- pySimBlocks/blocks/interfaces/__init__.py +27 -0
- pySimBlocks/blocks/interfaces/external_input.py +107 -0
- pySimBlocks/blocks/interfaces/external_output.py +113 -0
- pySimBlocks/blocks/observers/__init__.py +23 -0
- pySimBlocks/blocks/observers/luenberger.py +175 -0
- pySimBlocks/blocks/operators/__init__.py +47 -0
- pySimBlocks/blocks/operators/algebraic_function.py +258 -0
- pySimBlocks/blocks/operators/dead_zone.py +186 -0
- pySimBlocks/blocks/operators/delay.py +254 -0
- pySimBlocks/blocks/operators/demux.py +123 -0
- pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
- pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
- pySimBlocks/blocks/operators/gain.py +294 -0
- pySimBlocks/blocks/operators/mux.py +131 -0
- pySimBlocks/blocks/operators/product.py +194 -0
- pySimBlocks/blocks/operators/rate_limiter.py +214 -0
- pySimBlocks/blocks/operators/saturation.py +178 -0
- pySimBlocks/blocks/operators/sum.py +188 -0
- pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
- pySimBlocks/blocks/optimizers/__init__.py +23 -0
- pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
- pySimBlocks/blocks/sources/__init__.py +37 -0
- pySimBlocks/blocks/sources/chirp.py +168 -0
- pySimBlocks/blocks/sources/constant.py +82 -0
- pySimBlocks/blocks/sources/file_source.py +294 -0
- pySimBlocks/blocks/sources/function_source.py +197 -0
- pySimBlocks/blocks/sources/ramp.py +99 -0
- pySimBlocks/blocks/sources/sinusoidal.py +113 -0
- pySimBlocks/blocks/sources/step.py +138 -0
- pySimBlocks/blocks/sources/white_noise.py +107 -0
- pySimBlocks/blocks/systems/__init__.py +27 -0
- pySimBlocks/blocks/systems/linear_state_space.py +180 -0
- pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
- pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
- pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
- pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
- pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
- pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
- pySimBlocks/cli.py +127 -0
- pySimBlocks/core/__init__.py +32 -0
- pySimBlocks/core/block.py +167 -0
- pySimBlocks/core/block_source.py +94 -0
- pySimBlocks/core/config.py +90 -0
- pySimBlocks/core/fixed_time_manager.py +46 -0
- pySimBlocks/core/model.py +250 -0
- pySimBlocks/core/scheduler.py +30 -0
- pySimBlocks/core/simulator.py +329 -0
- pySimBlocks/core/task.py +73 -0
- pySimBlocks/docs/blocks/controllers/pid.md +78 -0
- pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
- pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
- pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
- pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
- pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
- pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
- pySimBlocks/docs/blocks/operators/delay.md +76 -0
- pySimBlocks/docs/blocks/operators/demux.md +64 -0
- pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
- pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
- pySimBlocks/docs/blocks/operators/gain.md +97 -0
- pySimBlocks/docs/blocks/operators/mux.md +64 -0
- pySimBlocks/docs/blocks/operators/product.md +51 -0
- pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
- pySimBlocks/docs/blocks/operators/saturation.md +62 -0
- pySimBlocks/docs/blocks/operators/sum.md +49 -0
- pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
- pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
- pySimBlocks/docs/blocks/sources/chirp.md +102 -0
- pySimBlocks/docs/blocks/sources/constant.md +53 -0
- pySimBlocks/docs/blocks/sources/file_source.md +51 -0
- pySimBlocks/docs/blocks/sources/function_source.md +52 -0
- pySimBlocks/docs/blocks/sources/ramp.md +58 -0
- pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
- pySimBlocks/docs/blocks/sources/step.md +63 -0
- pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
- pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
- pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
- pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
- pySimBlocks/gui/__init__.py +20 -0
- pySimBlocks/gui/addons/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
- pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
- pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
- pySimBlocks/gui/blocks/block_meta.py +408 -0
- pySimBlocks/gui/blocks/controllers/pid.py +116 -0
- pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
- pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
- pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
- pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
- pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
- pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
- pySimBlocks/gui/blocks/operators/delay.py +79 -0
- pySimBlocks/gui/blocks/operators/demux.py +98 -0
- pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
- pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
- pySimBlocks/gui/blocks/operators/gain.py +79 -0
- pySimBlocks/gui/blocks/operators/mux.py +100 -0
- pySimBlocks/gui/blocks/operators/product.py +109 -0
- pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
- pySimBlocks/gui/blocks/operators/saturation.py +72 -0
- pySimBlocks/gui/blocks/operators/sum.py +91 -0
- pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
- pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
- pySimBlocks/gui/blocks/parameter_meta.py +34 -0
- pySimBlocks/gui/blocks/port_meta.py +31 -0
- pySimBlocks/gui/blocks/sources/chirp.py +146 -0
- pySimBlocks/gui/blocks/sources/constant.py +63 -0
- pySimBlocks/gui/blocks/sources/file_source.py +196 -0
- pySimBlocks/gui/blocks/sources/function_source.py +202 -0
- pySimBlocks/gui/blocks/sources/ramp.py +73 -0
- pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
- pySimBlocks/gui/blocks/sources/step.py +83 -0
- pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
- pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
- pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
- pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
- pySimBlocks/gui/dialogs/__init__.py +19 -0
- pySimBlocks/gui/dialogs/block_dialog.py +125 -0
- pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
- pySimBlocks/gui/dialogs/help_dialog.py +39 -0
- pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
- pySimBlocks/gui/dialogs/settings/plots.py +187 -0
- pySimBlocks/gui/dialogs/settings/project.py +146 -0
- pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
- pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
- pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
- pySimBlocks/gui/editor.py +49 -0
- pySimBlocks/gui/graphics/__init__.py +29 -0
- pySimBlocks/gui/graphics/block_item.py +289 -0
- pySimBlocks/gui/graphics/connection_item.py +266 -0
- pySimBlocks/gui/graphics/port_item.py +153 -0
- pySimBlocks/gui/graphics/theme.py +119 -0
- pySimBlocks/gui/main_window.py +162 -0
- pySimBlocks/gui/models/__init__.py +31 -0
- pySimBlocks/gui/models/block_instance.py +114 -0
- pySimBlocks/gui/models/connection_instance.py +43 -0
- pySimBlocks/gui/models/port_instance.py +52 -0
- pySimBlocks/gui/models/project_simulation_params.py +50 -0
- pySimBlocks/gui/models/project_state.py +114 -0
- pySimBlocks/gui/project_controller.py +302 -0
- pySimBlocks/gui/services/__init__.py +20 -0
- pySimBlocks/gui/services/project_loader.py +381 -0
- pySimBlocks/gui/services/project_saver.py +68 -0
- pySimBlocks/gui/services/simulation_runner.py +66 -0
- pySimBlocks/gui/services/yaml_tools.py +261 -0
- pySimBlocks/gui/widgets/__init__.py +19 -0
- pySimBlocks/gui/widgets/block_list.py +131 -0
- pySimBlocks/gui/widgets/diagram_view.py +312 -0
- pySimBlocks/gui/widgets/toolbar_view.py +161 -0
- pySimBlocks/project/__init__.py +35 -0
- pySimBlocks/project/build_model.py +92 -0
- pySimBlocks/project/generate_run_script.py +109 -0
- pySimBlocks/project/generate_sofa_controller.py +215 -0
- pySimBlocks/project/load_project_config.py +197 -0
- pySimBlocks/project/load_simulation_config.py +166 -0
- pySimBlocks/project/load_simulator.py +45 -0
- pySimBlocks/project/plot_from_config.py +174 -0
- pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
- pySimBlocks/real_time/__init__.py +23 -0
- pySimBlocks/real_time/real_time_runner.py +148 -0
- pySimBlocks/tools/__init__.py +19 -0
- pySimBlocks/tools/blocks_registry.py +141 -0
- pySimBlocks/tools/generate_blocks_index.py +124 -0
- pysimblocks-0.1.0.dist-info/METADATA +180 -0
- pysimblocks-0.1.0.dist-info/RECORD +262 -0
- pysimblocks-0.1.0.dist-info/WHEEL +5 -0
- pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
- pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
- pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
- tests/blocks/controllers/test_pid.py +143 -0
- tests/blocks/controllers/test_state_feedback.py +193 -0
- tests/blocks/observers/test_luenberger.py +225 -0
- tests/blocks/operators/test_algebraic_function.py +230 -0
- tests/blocks/operators/test_dead_zone.py +106 -0
- tests/blocks/operators/test_delay.py +167 -0
- tests/blocks/operators/test_demux.py +111 -0
- tests/blocks/operators/test_discrete_derivator.py +160 -0
- tests/blocks/operators/test_discrete_integrator.py +149 -0
- tests/blocks/operators/test_gain.py +182 -0
- tests/blocks/operators/test_mux.py +101 -0
- tests/blocks/operators/test_product.py +171 -0
- tests/blocks/operators/test_rate_limiter.py +166 -0
- tests/blocks/operators/test_saturation.py +157 -0
- tests/blocks/operators/test_sum.py +183 -0
- tests/blocks/operators/test_zero_order_hold.py +106 -0
- tests/blocks/sources/test_chirp.py +157 -0
- tests/blocks/sources/test_constant.py +55 -0
- tests/blocks/sources/test_file_source.py +185 -0
- tests/blocks/sources/test_function_source.py +135 -0
- tests/blocks/sources/test_ramp.py +110 -0
- tests/blocks/sources/test_sinusoidal.py +113 -0
- tests/blocks/sources/test_step.py +76 -0
- tests/blocks/sources/test_white_noise.py +108 -0
- tests/blocks/systems/test_linear_state_space.py +135 -0
- tests/blocks/systems/test_polytopic_state_space.py +133 -0
- tests/core/test_core_errors_and_determinism.py +82 -0
- tests/core/test_core_multirate_tasks.py +88 -0
- tests/core/test_core_two_phase_and_propagation.py +132 -0
- tests/gui/main_window_open_test.py +162 -0
- tests/gui/test_block_meta_param_cache.py +16 -0
- tests/gui/test_file_source_meta.py +17 -0
- tests/gui/test_function_source_meta.py +28 -0
- tests/regression/test_simulink_dc_motor.py +217 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
23
|
+
from pySimBlocks.core.block_source import BlockSource
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Sinusoidal(BlockSource):
|
|
27
|
+
"""
|
|
28
|
+
Multi-dimensional sinusoidal signal source block (Option B).
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Generates sinusoidal signals element-wise on a 2D output array:
|
|
32
|
+
y(t) = amplitude * sin(2*pi*frequency*t + phase) + offset
|
|
33
|
+
|
|
34
|
+
Parameters may be scalars, vectors, or matrices. Only scalar-to-shape
|
|
35
|
+
broadcasting is allowed; all non-scalar parameters must share the same
|
|
36
|
+
shape.
|
|
37
|
+
|
|
38
|
+
Parameters (overview):
|
|
39
|
+
amplitude : float or array-like
|
|
40
|
+
Sinusoidal amplitude.
|
|
41
|
+
frequency : float or array-like
|
|
42
|
+
Sinusoidal frequency in Hertz.
|
|
43
|
+
phase : float or array-like, optional
|
|
44
|
+
Phase shift in radians.
|
|
45
|
+
offset : float or array-like, optional
|
|
46
|
+
Constant offset added to the signal.
|
|
47
|
+
sample_time : float, optional
|
|
48
|
+
Block execution period.
|
|
49
|
+
|
|
50
|
+
Outputs:
|
|
51
|
+
out : sinusoidal output signal (2D ndarray)
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- Stateless.
|
|
55
|
+
- Normalization:
|
|
56
|
+
scalar -> (1,1), 1D -> (n,1), 2D -> (m,n)
|
|
57
|
+
- Broadcasting:
|
|
58
|
+
Only (1,1) scalars are broadcast to the common shape.
|
|
59
|
+
No NumPy broadcasting beyond that.
|
|
60
|
+
- No implicit flattening is performed.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
name: str,
|
|
66
|
+
amplitude: ArrayLike,
|
|
67
|
+
frequency: ArrayLike,
|
|
68
|
+
offset: ArrayLike = 0.0,
|
|
69
|
+
phase: ArrayLike = 0.0,
|
|
70
|
+
sample_time: float | None = None,
|
|
71
|
+
):
|
|
72
|
+
super().__init__(name, sample_time)
|
|
73
|
+
|
|
74
|
+
A = self._to_2d_array("amplitude", amplitude, dtype=float)
|
|
75
|
+
F = self._to_2d_array("frequency", frequency, dtype=float)
|
|
76
|
+
O = self._to_2d_array("offset", offset, dtype=float)
|
|
77
|
+
P = self._to_2d_array("phase", phase, dtype=float)
|
|
78
|
+
|
|
79
|
+
target_shape = self._resolve_common_shape({
|
|
80
|
+
"amplitude": A,
|
|
81
|
+
"frequency": F,
|
|
82
|
+
"offset": O,
|
|
83
|
+
"phase": P,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
self.amplitude = self._broadcast_scalar_only("amplitude", A, target_shape)
|
|
87
|
+
self.frequency = self._broadcast_scalar_only("frequency", F, target_shape)
|
|
88
|
+
self.offset = self._broadcast_scalar_only("offset", O, target_shape)
|
|
89
|
+
self.phase = self._broadcast_scalar_only("phase", P, target_shape)
|
|
90
|
+
|
|
91
|
+
self.outputs["out"] = np.zeros(target_shape, dtype=float)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# --------------------------------------------------------------------------
|
|
95
|
+
# Public methods
|
|
96
|
+
# --------------------------------------------------------------------------
|
|
97
|
+
def initialize(self, t0: float) -> None:
|
|
98
|
+
self._compute_output(t0)
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
102
|
+
self._compute_output(t)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --------------------------------------------------------------------------
|
|
106
|
+
# Private methods
|
|
107
|
+
# --------------------------------------------------------------------------
|
|
108
|
+
def _compute_output(self, t: float) -> None:
|
|
109
|
+
self.outputs["out"] = (
|
|
110
|
+
self.amplitude
|
|
111
|
+
* np.sin(2.0 * np.pi * self.frequency * t + self.phase)
|
|
112
|
+
+ self.offset
|
|
113
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
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_source import BlockSource
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Step(BlockSource):
|
|
27
|
+
"""
|
|
28
|
+
Step signal source block.
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Generates a step signal that switches from an initial value to a final
|
|
32
|
+
value at a specified time.
|
|
33
|
+
|
|
34
|
+
Parameters (overview):
|
|
35
|
+
value_before : float or array-like
|
|
36
|
+
Output value before the step time. Scalar, vector, or matrix.
|
|
37
|
+
value_after : float or array-like
|
|
38
|
+
Output value after the step time. Scalar, vector, or matrix.
|
|
39
|
+
start_time : float
|
|
40
|
+
Time at which the step occurs.
|
|
41
|
+
sample_time : float, optional
|
|
42
|
+
Block execution period.
|
|
43
|
+
|
|
44
|
+
I/O:
|
|
45
|
+
Outputs:
|
|
46
|
+
out : step output signal.
|
|
47
|
+
|
|
48
|
+
Notes:
|
|
49
|
+
- The block has no internal state.
|
|
50
|
+
- Signals are normalized to 2D arrays:
|
|
51
|
+
scalar -> (1,1), 1D -> (n,1), 2D -> (m,n).
|
|
52
|
+
- If one value is scalar (1,1) and the other is not, scalar is broadcast
|
|
53
|
+
to match the other shape.
|
|
54
|
+
- EPS compensates floating-point rounding to ensure consistent behavior
|
|
55
|
+
on discrete time grids.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
name: str,
|
|
61
|
+
value_before: ArrayLike = 0.0,
|
|
62
|
+
value_after: ArrayLike = 1.0,
|
|
63
|
+
start_time: float = 1.0,
|
|
64
|
+
sample_time: float | None = None,
|
|
65
|
+
eps: float = 1e-12,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(name, sample_time)
|
|
68
|
+
|
|
69
|
+
vb = self._to_2d_array("value_before", value_before, dtype=float)
|
|
70
|
+
va = self._to_2d_array("value_after", value_after, dtype=float)
|
|
71
|
+
|
|
72
|
+
vb, va = self._match_shapes_with_scalar_broadcast(vb, va)
|
|
73
|
+
|
|
74
|
+
# --- Validate start_time ---
|
|
75
|
+
if not isinstance(start_time, (float, int)):
|
|
76
|
+
raise TypeError(f"[{self.name}] start_time must be a float or int.")
|
|
77
|
+
self.start_time = float(start_time)
|
|
78
|
+
|
|
79
|
+
self.value_before = vb
|
|
80
|
+
self.value_after = va
|
|
81
|
+
|
|
82
|
+
self.outputs["out"] = None
|
|
83
|
+
self.EPS = float(eps)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------------------------
|
|
87
|
+
# Public methods
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
def initialize(self, t0: float) -> None:
|
|
90
|
+
self.outputs["out"] = (
|
|
91
|
+
self.value_before.copy()
|
|
92
|
+
if t0 < self.start_time - self.EPS
|
|
93
|
+
else self.value_after.copy()
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
98
|
+
self.outputs["out"] = (
|
|
99
|
+
self.value_before.copy()
|
|
100
|
+
if t < self.start_time - self.EPS
|
|
101
|
+
else self.value_after.copy()
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --------------------------------------------------------------------------
|
|
106
|
+
# Private methods
|
|
107
|
+
# --------------------------------------------------------------------------
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _is_scalar_2d(arr: np.ndarray) -> bool:
|
|
110
|
+
return arr.shape == (1, 1)
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
def _match_shapes_with_scalar_broadcast(
|
|
114
|
+
self,
|
|
115
|
+
a: np.ndarray,
|
|
116
|
+
b: np.ndarray,
|
|
117
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
118
|
+
"""
|
|
119
|
+
If shapes differ:
|
|
120
|
+
- allow scalar (1,1) to broadcast to the other's shape
|
|
121
|
+
- otherwise raise ValueError
|
|
122
|
+
"""
|
|
123
|
+
if a.shape == b.shape:
|
|
124
|
+
return a, b
|
|
125
|
+
|
|
126
|
+
a_is_scalar = self._is_scalar_2d(a)
|
|
127
|
+
b_is_scalar = self._is_scalar_2d(b)
|
|
128
|
+
|
|
129
|
+
if a_is_scalar and not b_is_scalar:
|
|
130
|
+
return np.full(b.shape, float(a[0, 0])), b
|
|
131
|
+
|
|
132
|
+
if b_is_scalar and not a_is_scalar:
|
|
133
|
+
return a, np.full(a.shape, float(b[0, 0]))
|
|
134
|
+
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"[{self.name}] 'value_before' and 'value_after' must have compatible shapes. "
|
|
137
|
+
f"Got {a.shape} vs {b.shape}. Only scalar-to-shape broadcasting is allowed."
|
|
138
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
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_source import BlockSource
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class WhiteNoise(BlockSource):
|
|
27
|
+
"""
|
|
28
|
+
Multi-dimensional Gaussian white noise source block (Option B).
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Generates independent Gaussian noise samples at each simulation step,
|
|
32
|
+
element-wise on a 2D output array:
|
|
33
|
+
y = mean + std * N(0,1)
|
|
34
|
+
|
|
35
|
+
Parameters may be scalars, vectors, or matrices. Only scalar-to-shape
|
|
36
|
+
broadcasting is allowed; all non-scalar parameters must share the same
|
|
37
|
+
shape.
|
|
38
|
+
|
|
39
|
+
Parameters (overview):
|
|
40
|
+
mean : float or array-like, optional
|
|
41
|
+
Mean value of the noise.
|
|
42
|
+
std : float or array-like, optional
|
|
43
|
+
Standard deviation of the noise (must be >= 0 element-wise).
|
|
44
|
+
seed : int, optional
|
|
45
|
+
Random seed for reproducibility.
|
|
46
|
+
sample_time : float, optional
|
|
47
|
+
Block execution period.
|
|
48
|
+
|
|
49
|
+
Outputs:
|
|
50
|
+
out : noise output signal (2D ndarray)
|
|
51
|
+
|
|
52
|
+
Notes:
|
|
53
|
+
- Stateless (but uses an internal RNG).
|
|
54
|
+
- Normalization:
|
|
55
|
+
scalar -> (1,1), 1D -> (n,1), 2D -> (m,n)
|
|
56
|
+
- Broadcasting:
|
|
57
|
+
Only (1,1) scalars are broadcast to the common shape.
|
|
58
|
+
No NumPy broadcasting beyond that.
|
|
59
|
+
- No implicit flattening is performed.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
name: str,
|
|
65
|
+
mean: ArrayLike = 0.0,
|
|
66
|
+
std: ArrayLike = 1.0,
|
|
67
|
+
seed: int | None = None,
|
|
68
|
+
sample_time: float | None = None,
|
|
69
|
+
):
|
|
70
|
+
super().__init__(name, sample_time)
|
|
71
|
+
|
|
72
|
+
M = self._to_2d_array("mean", mean, dtype=float)
|
|
73
|
+
S = self._to_2d_array("std", std, dtype=float)
|
|
74
|
+
|
|
75
|
+
target_shape = self._resolve_common_shape({
|
|
76
|
+
"mean": M,
|
|
77
|
+
"std": S,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
self.mean = self._broadcast_scalar_only("mean", M, target_shape)
|
|
81
|
+
self.std = self._broadcast_scalar_only("std", S, target_shape)
|
|
82
|
+
|
|
83
|
+
# Validate std >= 0 element-wise
|
|
84
|
+
if np.any(self.std < 0.0):
|
|
85
|
+
raise ValueError(f"[{self.name}] std must be >= 0 (element-wise).")
|
|
86
|
+
|
|
87
|
+
self.rng = np.random.default_rng(seed)
|
|
88
|
+
|
|
89
|
+
self.outputs["out"] = np.zeros(target_shape, dtype=float)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --------------------------------------------------------------------------
|
|
93
|
+
# Public methods
|
|
94
|
+
# --------------------------------------------------------------------------
|
|
95
|
+
def initialize(self, t0: float) -> None:
|
|
96
|
+
self.outputs["out"] = self._draw()
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
100
|
+
self.outputs["out"] = self._draw()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
# Private methods
|
|
105
|
+
# --------------------------------------------------------------------------
|
|
106
|
+
def _draw(self) -> np.ndarray:
|
|
107
|
+
return self.mean + self.std * self.rng.standard_normal(self.mean.shape)
|
|
@@ -0,0 +1,27 @@
|
|
|
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.linear_state_space import LinearStateSpace
|
|
22
|
+
from pySimBlocks.blocks.systems.polytopic_state_space import PolytopicStateSpace
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"LinearStateSpace",
|
|
26
|
+
"PolytopicStateSpace",
|
|
27
|
+
]
|
|
@@ -0,0 +1,180 @@
|
|
|
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 LinearStateSpace(Block):
|
|
27
|
+
"""
|
|
28
|
+
Discrete-time linear state-space system block.
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Implements a discrete-time linear state-space system without direct
|
|
32
|
+
feedthrough, defined by state and output equations.
|
|
33
|
+
|
|
34
|
+
Parameters (overview):
|
|
35
|
+
A : array-like
|
|
36
|
+
State transition matrix.
|
|
37
|
+
B : array-like
|
|
38
|
+
Input matrix.
|
|
39
|
+
C : array-like
|
|
40
|
+
Output matrix.
|
|
41
|
+
x0 : array-like, optional
|
|
42
|
+
Initial state vector.
|
|
43
|
+
sample_time : float, optional
|
|
44
|
+
Block execution period.
|
|
45
|
+
|
|
46
|
+
I/O:
|
|
47
|
+
Inputs:
|
|
48
|
+
u : input vector.
|
|
49
|
+
Outputs:
|
|
50
|
+
y : output vector.
|
|
51
|
+
x : state vector.
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- The system is strictly proper (no direct feedthrough).
|
|
55
|
+
- The block has internal state.
|
|
56
|
+
- Matrix D is intentionally not supported to avoid algebraic loops.
|
|
57
|
+
- The state is updated once per simulation step.
|
|
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
|
+
# --- basic matrix checks
|
|
78
|
+
if self.A.ndim != 2:
|
|
79
|
+
raise ValueError(f"[{self.name}] A must be 2D. Got shape {self.A.shape}.")
|
|
80
|
+
if self.B.ndim != 2:
|
|
81
|
+
raise ValueError(f"[{self.name}] B must be 2D. Got shape {self.B.shape}.")
|
|
82
|
+
if self.C.ndim != 2:
|
|
83
|
+
raise ValueError(f"[{self.name}] C must be 2D. Got shape {self.C.shape}.")
|
|
84
|
+
|
|
85
|
+
n = self.A.shape[0]
|
|
86
|
+
if self.A.shape != (n, n):
|
|
87
|
+
raise ValueError(f"[{self.name}] A must be square (n,n). Got {self.A.shape}.")
|
|
88
|
+
|
|
89
|
+
if self.B.shape[0] != n:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"[{self.name}] B must have n rows. A is {self.A.shape}, B is {self.B.shape}."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if self.C.shape[1] != n:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"[{self.name}] C must have n columns. A is {self.A.shape}, C is {self.C.shape}."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self._n = n
|
|
100
|
+
self._m = self.B.shape[1]
|
|
101
|
+
self._p = self.C.shape[0]
|
|
102
|
+
|
|
103
|
+
# --- initial state x0
|
|
104
|
+
if x0 is None:
|
|
105
|
+
x0_arr = np.zeros((n, 1), dtype=float)
|
|
106
|
+
else:
|
|
107
|
+
x0_arr = np.asarray(x0, dtype=float)
|
|
108
|
+
if x0_arr.ndim == 0:
|
|
109
|
+
# scalar x0 is not acceptable for n>1
|
|
110
|
+
x0_arr = x0_arr.reshape(1, 1)
|
|
111
|
+
elif x0_arr.ndim == 1:
|
|
112
|
+
x0_arr = x0_arr.reshape(-1, 1)
|
|
113
|
+
elif x0_arr.ndim == 2:
|
|
114
|
+
pass
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(f"[{self.name}] x0 must be 1D or 2D. Got shape {x0_arr.shape}.")
|
|
117
|
+
|
|
118
|
+
if x0_arr.shape != (n, 1):
|
|
119
|
+
raise ValueError(f"[{self.name}] x0 must have shape ({n}, 1). Got {x0_arr.shape}.")
|
|
120
|
+
|
|
121
|
+
self.state["x"] = x0_arr.copy()
|
|
122
|
+
self.next_state["x"] = x0_arr.copy()
|
|
123
|
+
|
|
124
|
+
# ports
|
|
125
|
+
self.inputs["u"] = None
|
|
126
|
+
self.outputs["y"] = None
|
|
127
|
+
self.outputs["x"] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# --------------------------------------------------------------------------
|
|
131
|
+
# Public methods
|
|
132
|
+
# --------------------------------------------------------------------------
|
|
133
|
+
def initialize(self, t0: float) -> None:
|
|
134
|
+
x = self.state["x"]
|
|
135
|
+
self.outputs["y"] = self.C @ x
|
|
136
|
+
self.outputs["x"] = x.copy()
|
|
137
|
+
self.next_state["x"] = x.copy()
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
141
|
+
x = self.state["x"]
|
|
142
|
+
self.outputs["y"] = self.C @ x
|
|
143
|
+
self.outputs["x"] = x.copy()
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
147
|
+
u = self.inputs["u"]
|
|
148
|
+
if u is None:
|
|
149
|
+
raise RuntimeError(f"[{self.name}] Input 'u' is not connected or not set.")
|
|
150
|
+
|
|
151
|
+
u_vec = self._to_col_vec("u", u, self._m)
|
|
152
|
+
x = self.state["x"]
|
|
153
|
+
|
|
154
|
+
self.next_state["x"] = self.A @ x + self.B @ u_vec
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --------------------------------------------------------------------------
|
|
158
|
+
# Private methods
|
|
159
|
+
# --------------------------------------------------------------------------
|
|
160
|
+
def _to_col_vec(self, name: str, value: ArrayLike, expected_rows: int) -> np.ndarray:
|
|
161
|
+
arr = np.asarray(value, dtype=float)
|
|
162
|
+
|
|
163
|
+
if arr.ndim == 0:
|
|
164
|
+
arr = arr.reshape(1, 1)
|
|
165
|
+
elif arr.ndim == 1:
|
|
166
|
+
arr = arr.reshape(-1, 1)
|
|
167
|
+
elif arr.ndim == 2:
|
|
168
|
+
pass
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"[{self.name}] {name} must be 1D or 2D. Got shape {arr.shape}.")
|
|
171
|
+
|
|
172
|
+
if arr.shape[1] != 1:
|
|
173
|
+
raise ValueError(f"[{self.name}] {name} must be a column vector (k,1). Got {arr.shape}.")
|
|
174
|
+
|
|
175
|
+
if arr.shape[0] != expected_rows:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"[{self.name}] {name} must have shape ({expected_rows},1). Got {arr.shape}."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return arr
|