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.
Files changed (262) hide show
  1. examples/advanced/hardware/block_diagram.py +162 -0
  2. examples/advanced/hardware/camera.py +93 -0
  3. examples/advanced/hardware/gui_emio.py +231 -0
  4. examples/advanced/hardware/lmi.py +69 -0
  5. examples/advanced/hardware/main.py +57 -0
  6. examples/advanced/hardware/parameters.py +54 -0
  7. examples/advanced/qp_problem/params.py +10 -0
  8. examples/advanced/qp_problem/run.py +14 -0
  9. examples/advanced/sofa/gui/finger/Finger.py +108 -0
  10. examples/advanced/sofa/gui/finger/FingerController.py +53 -0
  11. examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
  12. examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
  13. examples/advanced/sofa/python/Finger.py +107 -0
  14. examples/advanced/sofa/python/FingerController.py +48 -0
  15. examples/advanced/sofa/python/closedloop.py +64 -0
  16. examples/advanced/sofa/python/openloop.py +58 -0
  17. examples/basics/algebraic_function/g.py +7 -0
  18. examples/basics/algebraic_function/run.py +14 -0
  19. examples/basics/dc_motor/gui/run.py +14 -0
  20. examples/basics/dc_motor/multiple_order.py +91 -0
  21. examples/basics/dc_motor/openloop.py +62 -0
  22. examples/basics/dc_motor/pi.py +99 -0
  23. examples/basics/dc_motor/pid.py +118 -0
  24. examples/basics/dc_motor/pid_block.py +82 -0
  25. examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
  26. examples/basics/external/parameters.py +4 -0
  27. examples/basics/external/run.py +14 -0
  28. examples/basics/file_sources/temp.py +16 -0
  29. examples/basics/observer.py +76 -0
  30. examples/basics/state_feedback/gui/parameters.py +9 -0
  31. examples/basics/state_feedback/gui/run.py +14 -0
  32. examples/basics/state_feedback/state_feedback.py +102 -0
  33. examples/basics/state_space/linear/gui/run.py +14 -0
  34. examples/basics/state_space/linear/ss_block.py +89 -0
  35. examples/basics/state_space/linear/ss_manual.py +105 -0
  36. examples/basics/state_space/non_linear/nlss.py +47 -0
  37. examples/basics/state_space/non_linear/run.py +14 -0
  38. examples/basics/state_space/polytope/params.py +39 -0
  39. examples/basics/state_space/polytope/weights.py +12 -0
  40. examples/comparison/simulink/convert.py +29 -0
  41. examples/comparison/simulink/openloop.py +127 -0
  42. examples/comparison/simulink/pi.py +148 -0
  43. examples/comparison/simulink/pid.py +154 -0
  44. examples/comparison/simulink/pid_backward.py +155 -0
  45. examples/quick_start/filter.py +35 -0
  46. examples/quick_start/gui/run.py +14 -0
  47. examples/tutorials/tutorial_1_python/main.py +99 -0
  48. examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
  49. examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
  50. pySimBlocks/__init__.py +35 -0
  51. pySimBlocks/blocks/__init__.py +68 -0
  52. pySimBlocks/blocks/controllers/__init__.py +27 -0
  53. pySimBlocks/blocks/controllers/pid.py +250 -0
  54. pySimBlocks/blocks/controllers/state_feedback.py +141 -0
  55. pySimBlocks/blocks/interfaces/__init__.py +27 -0
  56. pySimBlocks/blocks/interfaces/external_input.py +107 -0
  57. pySimBlocks/blocks/interfaces/external_output.py +113 -0
  58. pySimBlocks/blocks/observers/__init__.py +23 -0
  59. pySimBlocks/blocks/observers/luenberger.py +175 -0
  60. pySimBlocks/blocks/operators/__init__.py +47 -0
  61. pySimBlocks/blocks/operators/algebraic_function.py +258 -0
  62. pySimBlocks/blocks/operators/dead_zone.py +186 -0
  63. pySimBlocks/blocks/operators/delay.py +254 -0
  64. pySimBlocks/blocks/operators/demux.py +123 -0
  65. pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
  66. pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
  67. pySimBlocks/blocks/operators/gain.py +294 -0
  68. pySimBlocks/blocks/operators/mux.py +131 -0
  69. pySimBlocks/blocks/operators/product.py +194 -0
  70. pySimBlocks/blocks/operators/rate_limiter.py +214 -0
  71. pySimBlocks/blocks/operators/saturation.py +178 -0
  72. pySimBlocks/blocks/operators/sum.py +188 -0
  73. pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
  74. pySimBlocks/blocks/optimizers/__init__.py +23 -0
  75. pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
  76. pySimBlocks/blocks/sources/__init__.py +37 -0
  77. pySimBlocks/blocks/sources/chirp.py +168 -0
  78. pySimBlocks/blocks/sources/constant.py +82 -0
  79. pySimBlocks/blocks/sources/file_source.py +294 -0
  80. pySimBlocks/blocks/sources/function_source.py +197 -0
  81. pySimBlocks/blocks/sources/ramp.py +99 -0
  82. pySimBlocks/blocks/sources/sinusoidal.py +113 -0
  83. pySimBlocks/blocks/sources/step.py +138 -0
  84. pySimBlocks/blocks/sources/white_noise.py +107 -0
  85. pySimBlocks/blocks/systems/__init__.py +27 -0
  86. pySimBlocks/blocks/systems/linear_state_space.py +180 -0
  87. pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
  88. pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
  89. pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
  90. pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
  91. pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
  92. pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
  93. pySimBlocks/cli.py +127 -0
  94. pySimBlocks/core/__init__.py +32 -0
  95. pySimBlocks/core/block.py +167 -0
  96. pySimBlocks/core/block_source.py +94 -0
  97. pySimBlocks/core/config.py +90 -0
  98. pySimBlocks/core/fixed_time_manager.py +46 -0
  99. pySimBlocks/core/model.py +250 -0
  100. pySimBlocks/core/scheduler.py +30 -0
  101. pySimBlocks/core/simulator.py +329 -0
  102. pySimBlocks/core/task.py +73 -0
  103. pySimBlocks/docs/blocks/controllers/pid.md +78 -0
  104. pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
  105. pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
  106. pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
  107. pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
  108. pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
  109. pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
  110. pySimBlocks/docs/blocks/operators/delay.md +76 -0
  111. pySimBlocks/docs/blocks/operators/demux.md +64 -0
  112. pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
  113. pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
  114. pySimBlocks/docs/blocks/operators/gain.md +97 -0
  115. pySimBlocks/docs/blocks/operators/mux.md +64 -0
  116. pySimBlocks/docs/blocks/operators/product.md +51 -0
  117. pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
  118. pySimBlocks/docs/blocks/operators/saturation.md +62 -0
  119. pySimBlocks/docs/blocks/operators/sum.md +49 -0
  120. pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
  121. pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
  122. pySimBlocks/docs/blocks/sources/chirp.md +102 -0
  123. pySimBlocks/docs/blocks/sources/constant.md +53 -0
  124. pySimBlocks/docs/blocks/sources/file_source.md +51 -0
  125. pySimBlocks/docs/blocks/sources/function_source.md +52 -0
  126. pySimBlocks/docs/blocks/sources/ramp.md +58 -0
  127. pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
  128. pySimBlocks/docs/blocks/sources/step.md +63 -0
  129. pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
  130. pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
  131. pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
  132. pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
  133. pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
  134. pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
  135. pySimBlocks/gui/__init__.py +20 -0
  136. pySimBlocks/gui/addons/__init__.py +19 -0
  137. pySimBlocks/gui/addons/sofa/__init__.py +19 -0
  138. pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
  139. pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
  140. pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
  141. pySimBlocks/gui/blocks/block_meta.py +408 -0
  142. pySimBlocks/gui/blocks/controllers/pid.py +116 -0
  143. pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
  144. pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
  145. pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
  146. pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
  147. pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
  148. pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
  149. pySimBlocks/gui/blocks/operators/delay.py +79 -0
  150. pySimBlocks/gui/blocks/operators/demux.py +98 -0
  151. pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
  152. pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
  153. pySimBlocks/gui/blocks/operators/gain.py +79 -0
  154. pySimBlocks/gui/blocks/operators/mux.py +100 -0
  155. pySimBlocks/gui/blocks/operators/product.py +109 -0
  156. pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
  157. pySimBlocks/gui/blocks/operators/saturation.py +72 -0
  158. pySimBlocks/gui/blocks/operators/sum.py +91 -0
  159. pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
  160. pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
  161. pySimBlocks/gui/blocks/parameter_meta.py +34 -0
  162. pySimBlocks/gui/blocks/port_meta.py +31 -0
  163. pySimBlocks/gui/blocks/sources/chirp.py +146 -0
  164. pySimBlocks/gui/blocks/sources/constant.py +63 -0
  165. pySimBlocks/gui/blocks/sources/file_source.py +196 -0
  166. pySimBlocks/gui/blocks/sources/function_source.py +202 -0
  167. pySimBlocks/gui/blocks/sources/ramp.py +73 -0
  168. pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
  169. pySimBlocks/gui/blocks/sources/step.py +83 -0
  170. pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
  171. pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
  172. pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
  173. pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
  174. pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
  175. pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
  176. pySimBlocks/gui/dialogs/__init__.py +19 -0
  177. pySimBlocks/gui/dialogs/block_dialog.py +125 -0
  178. pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
  179. pySimBlocks/gui/dialogs/help_dialog.py +39 -0
  180. pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
  181. pySimBlocks/gui/dialogs/settings/plots.py +187 -0
  182. pySimBlocks/gui/dialogs/settings/project.py +146 -0
  183. pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
  184. pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
  185. pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
  186. pySimBlocks/gui/editor.py +49 -0
  187. pySimBlocks/gui/graphics/__init__.py +29 -0
  188. pySimBlocks/gui/graphics/block_item.py +289 -0
  189. pySimBlocks/gui/graphics/connection_item.py +266 -0
  190. pySimBlocks/gui/graphics/port_item.py +153 -0
  191. pySimBlocks/gui/graphics/theme.py +119 -0
  192. pySimBlocks/gui/main_window.py +162 -0
  193. pySimBlocks/gui/models/__init__.py +31 -0
  194. pySimBlocks/gui/models/block_instance.py +114 -0
  195. pySimBlocks/gui/models/connection_instance.py +43 -0
  196. pySimBlocks/gui/models/port_instance.py +52 -0
  197. pySimBlocks/gui/models/project_simulation_params.py +50 -0
  198. pySimBlocks/gui/models/project_state.py +114 -0
  199. pySimBlocks/gui/project_controller.py +302 -0
  200. pySimBlocks/gui/services/__init__.py +20 -0
  201. pySimBlocks/gui/services/project_loader.py +381 -0
  202. pySimBlocks/gui/services/project_saver.py +68 -0
  203. pySimBlocks/gui/services/simulation_runner.py +66 -0
  204. pySimBlocks/gui/services/yaml_tools.py +261 -0
  205. pySimBlocks/gui/widgets/__init__.py +19 -0
  206. pySimBlocks/gui/widgets/block_list.py +131 -0
  207. pySimBlocks/gui/widgets/diagram_view.py +312 -0
  208. pySimBlocks/gui/widgets/toolbar_view.py +161 -0
  209. pySimBlocks/project/__init__.py +35 -0
  210. pySimBlocks/project/build_model.py +92 -0
  211. pySimBlocks/project/generate_run_script.py +109 -0
  212. pySimBlocks/project/generate_sofa_controller.py +215 -0
  213. pySimBlocks/project/load_project_config.py +197 -0
  214. pySimBlocks/project/load_simulation_config.py +166 -0
  215. pySimBlocks/project/load_simulator.py +45 -0
  216. pySimBlocks/project/plot_from_config.py +174 -0
  217. pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
  218. pySimBlocks/real_time/__init__.py +23 -0
  219. pySimBlocks/real_time/real_time_runner.py +148 -0
  220. pySimBlocks/tools/__init__.py +19 -0
  221. pySimBlocks/tools/blocks_registry.py +141 -0
  222. pySimBlocks/tools/generate_blocks_index.py +124 -0
  223. pysimblocks-0.1.0.dist-info/METADATA +180 -0
  224. pysimblocks-0.1.0.dist-info/RECORD +262 -0
  225. pysimblocks-0.1.0.dist-info/WHEEL +5 -0
  226. pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
  227. pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
  228. pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
  229. tests/blocks/controllers/test_pid.py +143 -0
  230. tests/blocks/controllers/test_state_feedback.py +193 -0
  231. tests/blocks/observers/test_luenberger.py +225 -0
  232. tests/blocks/operators/test_algebraic_function.py +230 -0
  233. tests/blocks/operators/test_dead_zone.py +106 -0
  234. tests/blocks/operators/test_delay.py +167 -0
  235. tests/blocks/operators/test_demux.py +111 -0
  236. tests/blocks/operators/test_discrete_derivator.py +160 -0
  237. tests/blocks/operators/test_discrete_integrator.py +149 -0
  238. tests/blocks/operators/test_gain.py +182 -0
  239. tests/blocks/operators/test_mux.py +101 -0
  240. tests/blocks/operators/test_product.py +171 -0
  241. tests/blocks/operators/test_rate_limiter.py +166 -0
  242. tests/blocks/operators/test_saturation.py +157 -0
  243. tests/blocks/operators/test_sum.py +183 -0
  244. tests/blocks/operators/test_zero_order_hold.py +106 -0
  245. tests/blocks/sources/test_chirp.py +157 -0
  246. tests/blocks/sources/test_constant.py +55 -0
  247. tests/blocks/sources/test_file_source.py +185 -0
  248. tests/blocks/sources/test_function_source.py +135 -0
  249. tests/blocks/sources/test_ramp.py +110 -0
  250. tests/blocks/sources/test_sinusoidal.py +113 -0
  251. tests/blocks/sources/test_step.py +76 -0
  252. tests/blocks/sources/test_white_noise.py +108 -0
  253. tests/blocks/systems/test_linear_state_space.py +135 -0
  254. tests/blocks/systems/test_polytopic_state_space.py +133 -0
  255. tests/core/test_core_errors_and_determinism.py +82 -0
  256. tests/core/test_core_multirate_tasks.py +88 -0
  257. tests/core/test_core_two_phase_and_propagation.py +132 -0
  258. tests/gui/main_window_open_test.py +162 -0
  259. tests/gui/test_block_meta_param_cache.py +16 -0
  260. tests/gui/test_file_source_meta.py +17 -0
  261. tests/gui/test_function_source_meta.py +28 -0
  262. 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