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,186 @@
|
|
|
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
|
+
|
|
24
|
+
from pySimBlocks.core.block import Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DeadZone(Block):
|
|
28
|
+
"""
|
|
29
|
+
Discrete-time dead zone operator.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Suppresses the input signal within a specified interval around zero
|
|
33
|
+
and shifts the signal outside this interval.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
lower_bound : scalar or array-like
|
|
37
|
+
Lower bound of the dead zone (must be <= 0 component-wise).
|
|
38
|
+
upper_bound : scalar or array-like
|
|
39
|
+
Upper bound of the dead zone (must be >= 0 component-wise).
|
|
40
|
+
sample_time : float, optional
|
|
41
|
+
Block execution period.
|
|
42
|
+
|
|
43
|
+
Inputs:
|
|
44
|
+
in : array (m,n)
|
|
45
|
+
Input signal (must be 2D).
|
|
46
|
+
|
|
47
|
+
Outputs:
|
|
48
|
+
out : array (m,n)
|
|
49
|
+
Output signal after dead-zone transformation.
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
- Stateless block.
|
|
53
|
+
- Direct feedthrough.
|
|
54
|
+
- Bounds are applied component-wise.
|
|
55
|
+
- Broadcasting rules (to match input shape (m,n)):
|
|
56
|
+
* scalar (1,1) broadcasts to (m,n)
|
|
57
|
+
* vector (m,1) broadcasts across columns to (m,n)
|
|
58
|
+
* matrix (m,n) must match exactly
|
|
59
|
+
- The dead zone must include zero component-wise:
|
|
60
|
+
lower_bound <= 0 <= upper_bound.
|
|
61
|
+
- Once resolved, input shape must remain constant.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
direct_feedthrough = True
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
name: str,
|
|
69
|
+
lower_bound: ArrayLike = 0.0,
|
|
70
|
+
upper_bound: ArrayLike = 0.0,
|
|
71
|
+
sample_time: float | None = None,
|
|
72
|
+
):
|
|
73
|
+
super().__init__(name, sample_time)
|
|
74
|
+
|
|
75
|
+
self.inputs["in"] = None
|
|
76
|
+
self.outputs["out"] = None
|
|
77
|
+
|
|
78
|
+
self.lower_raw = self._to_2d_array("lower_bound", lower_bound)
|
|
79
|
+
self.upper_raw = self._to_2d_array("upper_bound", upper_bound)
|
|
80
|
+
|
|
81
|
+
self.lower_bound = None
|
|
82
|
+
self.upper_bound = None
|
|
83
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# --------------------------------------------------------------------------
|
|
87
|
+
# Private methods
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
def initialize(self, t0: float) -> None:
|
|
90
|
+
u = self.inputs["in"]
|
|
91
|
+
if u is None:
|
|
92
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None at initialization.")
|
|
93
|
+
|
|
94
|
+
u = np.asarray(u, dtype=float)
|
|
95
|
+
if u.ndim != 2:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
self._resolve_for_input(u)
|
|
101
|
+
self.outputs["out"] = self._apply_dead_zone(u)
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
105
|
+
u = self.inputs["in"]
|
|
106
|
+
if u is None:
|
|
107
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None.")
|
|
108
|
+
|
|
109
|
+
u = np.asarray(u, dtype=float)
|
|
110
|
+
if u.ndim != 2:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self._resolve_for_input(u)
|
|
116
|
+
self.outputs["out"] = self._apply_dead_zone(u)
|
|
117
|
+
|
|
118
|
+
# ------------------------------------------------------------------
|
|
119
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
120
|
+
return # stateless
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# --------------------------------------------------------------------------
|
|
124
|
+
# Private methods
|
|
125
|
+
# --------------------------------------------------------------------------
|
|
126
|
+
def _broadcast_bound(self, b: np.ndarray, target_shape: tuple[int, int], name: str) -> np.ndarray:
|
|
127
|
+
m, n = target_shape
|
|
128
|
+
|
|
129
|
+
# scalar -> full matrix
|
|
130
|
+
if self._is_scalar_2d(b):
|
|
131
|
+
return np.full(target_shape, float(b[0, 0]), dtype=float)
|
|
132
|
+
|
|
133
|
+
# vector (m,1) -> broadcast across columns
|
|
134
|
+
if b.ndim == 2 and b.shape[1] == 1 and b.shape[0] == m:
|
|
135
|
+
if n == 1:
|
|
136
|
+
return b.astype(float, copy=False)
|
|
137
|
+
return np.repeat(b.astype(float, copy=False), n, axis=1)
|
|
138
|
+
|
|
139
|
+
# matrix -> must match exactly
|
|
140
|
+
if b.shape == target_shape:
|
|
141
|
+
return b.astype(float, copy=False)
|
|
142
|
+
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"[{self.name}] {name} has incompatible shape {b.shape} for input shape {target_shape}. "
|
|
145
|
+
f"Allowed: scalar (1,1), vector (m,1), or matrix (m,n)."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
def _resolve_for_input(self, u: np.ndarray) -> None:
|
|
150
|
+
if u.ndim != 2:
|
|
151
|
+
raise ValueError(
|
|
152
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if self._resolved_shape is None:
|
|
156
|
+
self._resolved_shape = u.shape
|
|
157
|
+
self.lower_bound = self._broadcast_bound(self.lower_raw, u.shape, "lower_bound")
|
|
158
|
+
self.upper_bound = self._broadcast_bound(self.upper_raw, u.shape, "upper_bound")
|
|
159
|
+
|
|
160
|
+
# Component-wise validity checks (after broadcasting)
|
|
161
|
+
if np.any(self.lower_bound > self.upper_bound):
|
|
162
|
+
raise ValueError(f"[{self.name}] lower_bound must be <= upper_bound (component-wise).")
|
|
163
|
+
if np.any(self.lower_bound > 0):
|
|
164
|
+
raise ValueError(f"[{self.name}] lower_bound must be <= 0 (component-wise).")
|
|
165
|
+
if np.any(self.upper_bound < 0):
|
|
166
|
+
raise ValueError(f"[{self.name}] upper_bound must be >= 0 (component-wise).")
|
|
167
|
+
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if u.shape != self._resolved_shape:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"[{self.name}] Input 'in' shape changed after initialization: "
|
|
173
|
+
f"expected {self._resolved_shape}, got {u.shape}."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
def _apply_dead_zone(self, u: np.ndarray) -> np.ndarray:
|
|
178
|
+
y = np.zeros_like(u)
|
|
179
|
+
|
|
180
|
+
above = u > self.upper_bound
|
|
181
|
+
below = u < self.lower_bound
|
|
182
|
+
|
|
183
|
+
y[above] = u[above] - self.upper_bound[above]
|
|
184
|
+
y[below] = u[below] - self.lower_bound[below]
|
|
185
|
+
|
|
186
|
+
return y
|
|
@@ -0,0 +1,254 @@
|
|
|
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
|
+
|
|
24
|
+
from pySimBlocks.core.block import Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Delay(Block):
|
|
28
|
+
"""
|
|
29
|
+
N-step discrete delay block.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Outputs a delayed version of the input signal by a fixed number of
|
|
33
|
+
discrete time steps.
|
|
34
|
+
|
|
35
|
+
Parameters (overview):
|
|
36
|
+
num_delays : int
|
|
37
|
+
Number of discrete delays N (N >= 1).
|
|
38
|
+
initial_output : scalar or array-like, optional
|
|
39
|
+
Initial value used to fill the delay buffer.
|
|
40
|
+
Accepted: scalar -> (1,1), 1D -> (k,1), 2D -> (m,n).
|
|
41
|
+
Scalar (1,1) can be broadcast to match the input shape when the
|
|
42
|
+
first input becomes available.
|
|
43
|
+
sample_time : float, optional
|
|
44
|
+
Block execution period.
|
|
45
|
+
|
|
46
|
+
I/O:
|
|
47
|
+
Inputs:
|
|
48
|
+
in : Input signal (must be 2D).
|
|
49
|
+
reset: Optional reset signal.
|
|
50
|
+
Outputs:
|
|
51
|
+
out : Delayed output signal (2D).
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- Stateful block.
|
|
55
|
+
- No direct feedthrough.
|
|
56
|
+
- Output at time k is the input at time k − N.
|
|
57
|
+
- Buffer shape is inferred from the first available input if not
|
|
58
|
+
explicitly initialized.
|
|
59
|
+
- Policy:
|
|
60
|
+
+ Signals are 2D arrays.
|
|
61
|
+
+ Buffer always exists (never None).
|
|
62
|
+
+ Shape is fixed either:
|
|
63
|
+
* immediately if initial_output is non-scalar 2D (shape != (1,1))
|
|
64
|
+
* otherwise at the first non-None input seen by the block
|
|
65
|
+
+ If buffer is still "unfixed" and currently scalar (1,1), it can be
|
|
66
|
+
broadcast ONCE to match the first input shape.
|
|
67
|
+
+ After shape is fixed, any shape mismatch raises.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
direct_feedthrough = False
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
name: str,
|
|
75
|
+
num_delays: int = 1,
|
|
76
|
+
initial_output: ArrayLike | None = None,
|
|
77
|
+
sample_time: float | None = None,
|
|
78
|
+
):
|
|
79
|
+
super().__init__(name, sample_time)
|
|
80
|
+
|
|
81
|
+
if not isinstance(num_delays, int) or num_delays < 1:
|
|
82
|
+
raise ValueError(f"[{self.name}] num_delays must be >= 1.")
|
|
83
|
+
self.num_delays = num_delays
|
|
84
|
+
|
|
85
|
+
self.inputs["in"] = None
|
|
86
|
+
self.inputs["reset"] = None
|
|
87
|
+
self.outputs["out"] = None
|
|
88
|
+
|
|
89
|
+
self.state["buffer"] = None
|
|
90
|
+
self.next_state["buffer"] = None
|
|
91
|
+
|
|
92
|
+
# Shape management
|
|
93
|
+
self._shape_fixed: bool = False
|
|
94
|
+
self._buffer_shape: tuple[int, int] | None = None
|
|
95
|
+
|
|
96
|
+
# Initialize buffer as (1,1) by default, but NOT fixed yet.
|
|
97
|
+
self._initial_output = initial_output
|
|
98
|
+
init = np.zeros((1, 1), dtype=float)
|
|
99
|
+
|
|
100
|
+
if initial_output is not None:
|
|
101
|
+
arr = self._to_2d_array("initial_output", initial_output)
|
|
102
|
+
init = arr.astype(float, copy=False)
|
|
103
|
+
|
|
104
|
+
# If user provides a non-scalar 2D initial_output, shape is fixed now.
|
|
105
|
+
if not self._is_scalar_2d(init):
|
|
106
|
+
self._shape_fixed = True
|
|
107
|
+
self._buffer_shape = init.shape
|
|
108
|
+
|
|
109
|
+
# Buffer always exists (never None)
|
|
110
|
+
self.state["buffer"] = [init.copy() for _ in range(self.num_delays)]
|
|
111
|
+
self.next_state["buffer"] = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --------------------------------------------------------------------------
|
|
115
|
+
# Public methods
|
|
116
|
+
# --------------------------------------------------------------------------
|
|
117
|
+
def initialize(self, t0: float) -> None:
|
|
118
|
+
out = self.state["buffer"][0]
|
|
119
|
+
|
|
120
|
+
u = self.inputs["in"]
|
|
121
|
+
if u is not None:
|
|
122
|
+
u_arr = np.asarray(u, dtype=float)
|
|
123
|
+
self._ensure_shape_and_buffer(u_arr)
|
|
124
|
+
|
|
125
|
+
if self._is_scalar_2d(out) and self._buffer_shape != (1, 1):
|
|
126
|
+
out = np.full(self._buffer_shape, float(out[0, 0]), dtype=float)
|
|
127
|
+
else:
|
|
128
|
+
if out.shape != self._buffer_shape:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"[{self.name}] Initial output shape mismatch: expected {self._buffer_shape}, got {out.shape}."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
self.outputs["out"] = out
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
137
|
+
if not self._shape_fixed:
|
|
138
|
+
u = self.inputs["in"]
|
|
139
|
+
if u is not None:
|
|
140
|
+
u_arr = np.asarray(u, dtype=float)
|
|
141
|
+
self._ensure_shape_and_buffer(u_arr)
|
|
142
|
+
|
|
143
|
+
self.outputs["out"] = self.state["buffer"][0].copy()
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
147
|
+
if self._is_reset_active():
|
|
148
|
+
self._apply_reset()
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
u = self.inputs["in"]
|
|
152
|
+
if u is None:
|
|
153
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is not connected or not set.")
|
|
154
|
+
|
|
155
|
+
u_arr = np.asarray(u, dtype=float)
|
|
156
|
+
|
|
157
|
+
self._ensure_shape_and_buffer(u_arr)
|
|
158
|
+
|
|
159
|
+
buffer = self.state["buffer"]
|
|
160
|
+
|
|
161
|
+
# Shift left and append u
|
|
162
|
+
new_buffer = []
|
|
163
|
+
for i in range(self.num_delays - 1):
|
|
164
|
+
new_buffer.append(buffer[i + 1].copy())
|
|
165
|
+
new_buffer.append(u_arr.copy())
|
|
166
|
+
|
|
167
|
+
self.next_state["buffer"] = new_buffer
|
|
168
|
+
|
|
169
|
+
# --------------------------------------------------------------------------
|
|
170
|
+
# Private methods
|
|
171
|
+
# --------------------------------------------------------------------------
|
|
172
|
+
def _ensure_shape_and_buffer(self, u: np.ndarray) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Ensure:
|
|
175
|
+
- u is 2D
|
|
176
|
+
- buffer exists
|
|
177
|
+
- shape is fixed at the right time
|
|
178
|
+
- after shape is fixed, input must match buffer shape
|
|
179
|
+
"""
|
|
180
|
+
if u.ndim != 2:
|
|
181
|
+
raise ValueError(
|
|
182
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
buf0 = self.state["buffer"][0]
|
|
186
|
+
assert buf0 is not None
|
|
187
|
+
|
|
188
|
+
# If already fixed, enforce strict match
|
|
189
|
+
if self._shape_fixed:
|
|
190
|
+
expected = buf0.shape
|
|
191
|
+
if u.shape != expected:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"[{self.name}] Input 'in' shape mismatch: expected {expected}, got {u.shape}."
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Not fixed yet: decide whether we can/should fix now
|
|
198
|
+
# We fix the shape the first time we see a non-None input (whatever its shape is).
|
|
199
|
+
target_shape = u.shape
|
|
200
|
+
|
|
201
|
+
# If buffer is scalar placeholder, broadcast it to target shape (one-time)
|
|
202
|
+
if self._is_scalar_2d(buf0) and target_shape != (1, 1):
|
|
203
|
+
scalar = float(buf0[0, 0])
|
|
204
|
+
self.state["buffer"] = [
|
|
205
|
+
np.full(target_shape, scalar, dtype=float) for _ in range(self.num_delays)
|
|
206
|
+
]
|
|
207
|
+
buf0 = self.state["buffer"][0]
|
|
208
|
+
|
|
209
|
+
# If buffer is not scalar but we are not fixed yet, it must already match target shape
|
|
210
|
+
# (This can happen if you later decide to relax some init logic; keep strict.)
|
|
211
|
+
if buf0.shape != target_shape:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"[{self.name}] Cannot infer a consistent delay shape: "
|
|
214
|
+
f"buffer currently {buf0.shape} but first input is {target_shape}."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Now we can fix shape (including (1,1))
|
|
218
|
+
self._shape_fixed = True
|
|
219
|
+
self._buffer_shape = target_shape
|
|
220
|
+
|
|
221
|
+
# ------------------------------------------------------------------
|
|
222
|
+
def _is_reset_active(self) -> bool:
|
|
223
|
+
reset_signal = self.inputs.get("reset", None)
|
|
224
|
+
if reset_signal is None:
|
|
225
|
+
return False
|
|
226
|
+
reset_arr = np.asarray(reset_signal)
|
|
227
|
+
if reset_arr.ndim == 0:
|
|
228
|
+
return bool(reset_arr)
|
|
229
|
+
elif reset_arr.ndim == 1 and reset_arr.size == 1:
|
|
230
|
+
return bool(reset_arr[0])
|
|
231
|
+
elif reset_arr.ndim == 2 and reset_arr.shape == (1, 1):
|
|
232
|
+
return bool(reset_arr[0, 0])
|
|
233
|
+
else:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"[{self.name}] Reset signal must be a scalar or single-element array. Got shape {reset_arr.shape}."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def _apply_reset(self) -> None:
|
|
239
|
+
if self._initial_output is not None:
|
|
240
|
+
arr = self._to_2d_array("initial_output", self._initial_output)
|
|
241
|
+
init = arr.astype(float, copy=False)
|
|
242
|
+
if self._shape_fixed and self._buffer_shape is not None:
|
|
243
|
+
if self._is_scalar_2d(init) and self._buffer_shape != (1, 1):
|
|
244
|
+
scalar = float(init[0, 0])
|
|
245
|
+
init = np.full(self._buffer_shape, scalar, dtype=float)
|
|
246
|
+
|
|
247
|
+
elif self._shape_fixed and self._buffer_shape is not None:
|
|
248
|
+
init = np.zeros(self._buffer_shape, dtype=float)
|
|
249
|
+
|
|
250
|
+
else:
|
|
251
|
+
init = np.zeros((1, 1), dtype=float)
|
|
252
|
+
|
|
253
|
+
self.state["buffer"] = [init.copy() for _ in range(self.num_delays)]
|
|
254
|
+
self.next_state["buffer"] = [init.copy() for _ in range(self.num_delays)]
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
|
|
24
|
+
from pySimBlocks.core.block import Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Demux(Block):
|
|
28
|
+
"""
|
|
29
|
+
Vector split block (inverse of Mux).
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Splits one input column vector into multiple output segments.
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
num_outputs : int
|
|
36
|
+
Number of scalar outputs to produce.
|
|
37
|
+
sample_time : float, optional
|
|
38
|
+
Block execution period.
|
|
39
|
+
|
|
40
|
+
Inputs:
|
|
41
|
+
in : vector (n,1)
|
|
42
|
+
Input must be a column vector.
|
|
43
|
+
|
|
44
|
+
Outputs:
|
|
45
|
+
out1, out2, ..., outP : array (k,1)
|
|
46
|
+
Output segment sizes follow:
|
|
47
|
+
- q = n // p
|
|
48
|
+
- m = n % p
|
|
49
|
+
- first m outputs have size (q+1,1)
|
|
50
|
+
- remaining (p-m) outputs have size (q,1)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
direct_feedthrough = True
|
|
54
|
+
|
|
55
|
+
def __init__(self, name: str, num_outputs: int = 2, sample_time: float | None = None):
|
|
56
|
+
super().__init__(name, sample_time)
|
|
57
|
+
|
|
58
|
+
if not isinstance(num_outputs, int) or num_outputs < 1:
|
|
59
|
+
raise ValueError(f"[{self.name}] num_outputs must be a positive integer.")
|
|
60
|
+
self.num_outputs = num_outputs
|
|
61
|
+
|
|
62
|
+
self.inputs["in"] = None
|
|
63
|
+
for i in range(num_outputs):
|
|
64
|
+
self.outputs[f"out{i+1}"] = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------
|
|
68
|
+
# Public methods
|
|
69
|
+
# --------------------------------------------------------------------------
|
|
70
|
+
def initialize(self, t0: float) -> None:
|
|
71
|
+
if self.inputs["in"] is None:
|
|
72
|
+
for i in range(self.num_outputs):
|
|
73
|
+
self.outputs[f"out{i+1}"] = np.zeros((1, 1), dtype=float)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
self._compute_outputs()
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------
|
|
79
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
80
|
+
self._compute_outputs()
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------
|
|
83
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
84
|
+
return # stateless
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------------------------------
|
|
88
|
+
# Private methods
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
def _to_vector(self, value: ArrayLike) -> np.ndarray:
|
|
91
|
+
arr = np.asarray(value, dtype=float)
|
|
92
|
+
|
|
93
|
+
if arr.ndim != 2 or arr.shape[1] != 1:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"[{self.name}] Input 'in' must be a column vector (n,1). "
|
|
96
|
+
f"Got shape {arr.shape}."
|
|
97
|
+
)
|
|
98
|
+
return arr
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------
|
|
101
|
+
def _compute_outputs(self) -> None:
|
|
102
|
+
u = self.inputs["in"]
|
|
103
|
+
if u is None:
|
|
104
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is not connected or not set.")
|
|
105
|
+
|
|
106
|
+
vec = self._to_vector(u)
|
|
107
|
+
n = vec.shape[0]
|
|
108
|
+
p = self.num_outputs
|
|
109
|
+
|
|
110
|
+
if p > n:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"[{self.name}] num_outputs ({p}) must be <= input vector length ({n})."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
q = n // p
|
|
116
|
+
m = n % p
|
|
117
|
+
|
|
118
|
+
start = 0
|
|
119
|
+
for i in range(p):
|
|
120
|
+
seg_len = q + 1 if i < m else q
|
|
121
|
+
end = start + seg_len
|
|
122
|
+
self.outputs[f"out{i+1}"] = vec[start:end].copy()
|
|
123
|
+
start = end
|