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,294 @@
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 re
22
+ import unicodedata
23
+
24
+ import numpy as np
25
+ from numpy.typing import ArrayLike
26
+
27
+ from pySimBlocks.core.block import Block
28
+
29
+
30
+ class Gain(Block):
31
+ """
32
+ Static gain block.
33
+
34
+ Summary:
35
+ Applies a gain to the input signal according to the selected multiplication mode.
36
+
37
+ Parameters:
38
+ gain: scalar, vector (m,), or matrix (m,n)
39
+ Gain coefficient(s).
40
+ multiplication: str
41
+ One of:
42
+ - "Element wise (K * u)"
43
+ - "Matrix (K @ u)"
44
+ - "Matrix (u @ K)"
45
+ sample_time: float, optional
46
+ Block execution period.
47
+
48
+ Inputs:
49
+ in: array (r,c)
50
+ Input signal (must be 2D).
51
+
52
+ Outputs:
53
+ out: array
54
+ Output signal (2D), depending on multiplication mode.
55
+ """
56
+
57
+ direct_feedthrough = True
58
+
59
+ MULT_ELEMENTWISE = "Element wise (K * u)"
60
+ MULT_LEFT = "Matrix (K @ u)"
61
+ MULT_RIGHT = "Matrix (u @ K)"
62
+ ALLOWED_MULTIPLICATIONS = {MULT_ELEMENTWISE, MULT_LEFT, MULT_RIGHT}
63
+
64
+ def __init__(
65
+ self,
66
+ name: str,
67
+ gain: ArrayLike = 1.0,
68
+ multiplication: str = MULT_ELEMENTWISE,
69
+ sample_time: float | None = None,
70
+ ):
71
+ super().__init__(name, sample_time)
72
+
73
+ self.multiplication = self._parse_multiplication(multiplication)
74
+
75
+ # Normalize gain
76
+ if np.isscalar(gain):
77
+ self.gain = float(gain)
78
+ self._gain_kind = "scalar"
79
+ else:
80
+ g = np.asarray(gain, dtype=float)
81
+ if g.ndim not in (1, 2):
82
+ raise ValueError(
83
+ f"[{self.name}] 'gain' must be a scalar, 1D vector, or 2D matrix. "
84
+ f"Got ndim={g.ndim} with shape {g.shape}."
85
+ )
86
+ self.gain = g
87
+ self._gain_kind = "vector" if g.ndim == 1 else "matrix"
88
+
89
+ self.inputs["in"] = None
90
+ self.outputs["out"] = None
91
+
92
+ # --------------------------------------------------------------------------
93
+ # Class methods
94
+ # --------------------------------------------------------------------------
95
+ @classmethod
96
+ def _parse_multiplication(cls, multiplication: str) -> str:
97
+ if not isinstance(multiplication, str):
98
+ raise TypeError(f"[{cls.__name__}] 'multiplication' must be a str.")
99
+
100
+ m = cls._normalize_user_string(multiplication)
101
+
102
+ # --- Element-wise
103
+ if m in {
104
+ "elementwise(k*u)", "elementwise", "elem", "k*u", "*", "k×u", "kxu"
105
+ }:
106
+ return cls.MULT_ELEMENTWISE
107
+
108
+ # --- Left: K @ u
109
+ if m in {
110
+ "matrix(k@u)", "k@u", "left", "matleft", "@left"
111
+ }:
112
+ return cls.MULT_LEFT
113
+
114
+ # --- Right: u @ K
115
+ if m in {
116
+ "matrix(u@k)", "u@k", "right", "matright", "@right"
117
+ }:
118
+ return cls.MULT_RIGHT
119
+
120
+ # fallback pattern-based (tolère "matrix(...)" etc.)
121
+ if "k@u" in m:
122
+ return cls.MULT_LEFT
123
+ if "u@k" in m:
124
+ return cls.MULT_RIGHT
125
+
126
+ raise ValueError(
127
+ f"[{cls.__name__}] Invalid 'multiplication'='{multiplication}'. "
128
+ f"Examples: '{cls.MULT_ELEMENTWISE}', '{cls.MULT_RIGHT}', '{cls.MULT_LEFT}'."
129
+ )
130
+
131
+ # --------------------------------------------------------------------------
132
+ # Public methods
133
+ # --------------------------------------------------------------------------
134
+ def initialize(self, t0: float) -> None:
135
+ u = self.inputs["in"]
136
+ if u is None:
137
+ self.outputs["out"] = None
138
+ return
139
+
140
+ if not self._gain_kind == "scalar":
141
+ if u.ndim == 1 and u.shape[0] == 1:
142
+ u = self._resolve_initialize(u)
143
+ if u.ndim == 2 and u.shape == (1, 1):
144
+ u = self._resolve_initialize(u)
145
+
146
+ self.outputs["out"] = self._compute(u)
147
+
148
+ # ------------------------------------------------------------------
149
+ def output_update(self, t: float, dt: float) -> None:
150
+ u = self.inputs["in"]
151
+ if u is None:
152
+ raise RuntimeError(f"[{self.name}] Input 'in' is not connected or not set.")
153
+ self.outputs["out"] = self._compute(u)
154
+
155
+ # ------------------------------------------------------------------
156
+ def state_update(self, t: float, dt: float) -> None:
157
+ return # stateless
158
+
159
+
160
+ # --------------------------------------------------------------------------
161
+ # Private methods
162
+ # --------------------------------------------------------------------------
163
+ @staticmethod
164
+ def _normalize_user_string(s: str) -> str:
165
+ s = unicodedata.normalize("NFKC", s)
166
+ s = s.strip().lower()
167
+ s = s.replace("\u00A0", " ")
168
+ s = re.sub(r"\s+", "", s, flags=re.UNICODE)
169
+ return s
170
+
171
+ # ------------------------------------------------------------------
172
+ def _resolve_initialize(self, u) -> np.ndarray:
173
+ u = u.flatten()
174
+ if self.multiplication != self.MULT_ELEMENTWISE:
175
+ u = np.full((self.gain.shape[1], 1), u[0], dtype=float)
176
+ elif self.multiplication == self.MULT_ELEMENTWISE:
177
+ if self._gain_kind == "vector":
178
+ u = np.full((self.gain.shape[0], 1), u[0], dtype=float)
179
+ elif self._gain_kind == "matrix":
180
+ u = np.full(self.gain.shape, u[0], dtype=float)
181
+ return u
182
+
183
+ # ------------------------------------------------------------------
184
+ def _compute(self, u) -> np.ndarray:
185
+ u = np.asarray(u, dtype=float)
186
+ if u.ndim != 2:
187
+ raise ValueError(
188
+ f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
189
+ )
190
+
191
+ if self.multiplication == self.MULT_ELEMENTWISE:
192
+ return self._elementwise(u)
193
+
194
+ if self.multiplication == self.MULT_LEFT:
195
+ return self._left_multiply(u)
196
+
197
+ if self.multiplication == self.MULT_RIGHT:
198
+ return self._right_multiply(u)
199
+
200
+ # Should be impossible due to validation in __init__
201
+ raise RuntimeError(f"[{self.name}] Unhandled multiplication mode: {self.multiplication}")
202
+
203
+ # ------------------------------------------------------------------
204
+ def _elementwise(self, u: np.ndarray) -> np.ndarray:
205
+ """
206
+ Element-wise multiplication: K * u
207
+
208
+ Rules:
209
+ - scalar K: y = K * u
210
+ - vector K (m,): y = K[:,None] * u, requires u.shape[0] == m
211
+ - matrix K (m,n): y = K * u, requires u.shape == (m,n)
212
+ """
213
+ if self._gain_kind == "scalar":
214
+ return self.gain * u
215
+
216
+ if self._gain_kind == "vector":
217
+ g = self.gain # shape (m,)
218
+ if g.shape[0] != 1 and u.shape[0] != g.shape[0]:
219
+ raise ValueError(
220
+ f"[{self.name}] Element-wise mode requires u.shape[0] == len(gain). "
221
+ f"Got u.shape={u.shape}, gain.shape={g.shape}."
222
+ )
223
+ return g.reshape(-1, 1) * u
224
+
225
+ # matrix gain
226
+ g = self.gain
227
+ if not self._is_scalar_2d(g) and u.shape != g.shape:
228
+ raise ValueError(
229
+ f"[{self.name}] Element-wise mode with matrix gain requires u.shape == gain.shape. "
230
+ f"Got u.shape={u.shape}, gain.shape={g.shape}."
231
+ )
232
+ return g * u
233
+
234
+ # ------------------------------------------------------------------
235
+ def _left_multiply(self, u: np.ndarray) -> np.ndarray:
236
+ """
237
+ Matrix multiplication: K @ u
238
+
239
+ Rules:
240
+ - K must be 2D matrix (p,m)
241
+ - u must be 2D (m,ncols)
242
+ - output is (p,ncols)
243
+ """
244
+ if self._gain_kind != "matrix":
245
+ raise ValueError(
246
+ f"[{self.name}] Multiplication mode '{self.MULT_LEFT}' requires a 2D matrix gain. "
247
+ f"Got gain kind '{self._gain_kind}'."
248
+ )
249
+
250
+ K = self.gain
251
+ p, m = K.shape
252
+ if u.shape[0] != m:
253
+ raise ValueError(
254
+ f"[{self.name}] Left matrix product requires u.shape[0] == gain.shape[1]. "
255
+ f"Got u.shape={u.shape}, gain.shape={K.shape}."
256
+ )
257
+ return K @ u
258
+
259
+ # ------------------------------------------------------------------
260
+ def _right_multiply(self, u: np.ndarray) -> np.ndarray:
261
+ """
262
+ Matrix multiplication: u @ K
263
+
264
+ Rules:
265
+ - K must be 2D matrix (m,q)
266
+ - u must be 2D (nrows,m)
267
+ - output is (nrows,q)
268
+ """
269
+
270
+ if self._gain_kind != "matrix":
271
+ raise ValueError(
272
+ f"[{self.name}] Multiplication mode '{self.MULT_RIGHT}' requires a 2D matrix gain. "
273
+ f"Got gain kind '{self._gain_kind}'."
274
+ )
275
+
276
+ K = self.gain
277
+ m, q = K.shape
278
+
279
+ # --- Special case: u is a vector (n,1)
280
+ if u.shape[1] == 1:
281
+ if u.shape[0] != m:
282
+ raise ValueError(
283
+ f"[{self.name}] Right matrix product with vector requires u.shape[0] == gain.shape[0]."
284
+ f"Got u.shape={u.shape}, gain.shape={K.shape}."
285
+ )
286
+ return (u.T @ K).T
287
+
288
+ # --- General case: u is a matrix (nrows,m)
289
+ if u.shape[1] != m:
290
+ raise ValueError(
291
+ f"[{self.name}] Right matrix product requires u.shape[1] == gain.shape[0]. "
292
+ f"Got u.shape={u.shape}, gain.shape={K.shape}."
293
+ )
294
+ return u @ K
@@ -0,0 +1,131 @@
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 Mux(Block):
28
+ """
29
+ Vertical signal concatenation block.
30
+
31
+ Summary:
32
+ Concatenates multiple scalar or column-vector inputs vertically into a
33
+ single column vector.
34
+
35
+ Parameters:
36
+ num_inputs : int
37
+ Number of input ports to concatenate.
38
+ sample_time : float, optional
39
+ Block execution period.
40
+
41
+ Inputs:
42
+ in1, in2, ..., inN : array
43
+ Each input must be either:
44
+ - scalar (will be converted to (1,1))
45
+ - 1D array (k,) (will be converted to (k,1))
46
+ - column vector (k,1)
47
+
48
+ Any 2D non-column input (k,n) with n != 1 is rejected.
49
+
50
+ Outputs:
51
+ out : array (sum_k, 1)
52
+ Concatenated column vector.
53
+
54
+ Notes:
55
+ - Stateless.
56
+ - Direct feedthrough.
57
+ - This block intentionally enforces vector signals (Simulink-like Mux).
58
+ """
59
+
60
+ direct_feedthrough = True
61
+
62
+ def __init__(self, name: str, num_inputs: int = 2, sample_time: float | None = None):
63
+ super().__init__(name, sample_time)
64
+
65
+ if not isinstance(num_inputs, int) or num_inputs < 1:
66
+ raise ValueError(f"[{self.name}] num_inputs must be a positive integer.")
67
+ self.num_inputs = num_inputs
68
+
69
+ for i in range(num_inputs):
70
+ self.inputs[f"in{i+1}"] = None
71
+
72
+ self.outputs["out"] = None
73
+
74
+
75
+ # --------------------------------------------------------------------------
76
+ # Public methods
77
+ # --------------------------------------------------------------------------
78
+ def initialize(self, t0: float) -> None:
79
+ # If not all inputs available, defer
80
+ for i in range(self.num_inputs):
81
+ if self.inputs[f"in{i+1}"] is None:
82
+ self.outputs["out"] = None
83
+ return
84
+
85
+ self.outputs["out"] = self._compute_output()
86
+
87
+ # ---------------------------------------------------------
88
+ def output_update(self, t: float, dt: float) -> None:
89
+ self.outputs["out"] = self._compute_output()
90
+
91
+ # ---------------------------------------------------------
92
+ def state_update(self, t: float, dt: float) -> None:
93
+ return # stateless
94
+
95
+
96
+ # --------------------------------------------------------------------------
97
+ # Private methods
98
+ # --------------------------------------------------------------------------
99
+ def _to_column_vector(self, input_name: str, value: ArrayLike) -> np.ndarray:
100
+ arr = np.asarray(value, dtype=float)
101
+
102
+ if arr.ndim == 0:
103
+ return arr.reshape(1, 1)
104
+ if arr.ndim == 1:
105
+ return arr.reshape(-1, 1)
106
+ if arr.ndim == 2:
107
+ if arr.shape[1] != 1:
108
+ raise ValueError(
109
+ f"[{self.name}] Input '{input_name}' must be a column vector (n,1). "
110
+ f"Got shape {arr.shape}."
111
+ )
112
+ return arr
113
+
114
+ raise ValueError(
115
+ f"[{self.name}] Input '{input_name}' must be scalar, 1D, or a column vector (n,1). "
116
+ f"Got ndim={arr.ndim} with shape {arr.shape}."
117
+ )
118
+
119
+ # ---------------------------------------------------------
120
+ def _compute_output(self) -> np.ndarray:
121
+ vectors = []
122
+
123
+ for i in range(self.num_inputs):
124
+ key = f"in{i+1}"
125
+ u = self.inputs[key]
126
+ if u is None:
127
+ raise RuntimeError(f"[{self.name}] Input '{key}' is not connected or not set.")
128
+
129
+ vectors.append(self._to_column_vector(key, u))
130
+
131
+ return np.vstack(vectors)
@@ -0,0 +1,194 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ import numpy as np
22
+
23
+ from pySimBlocks.core.block import Block
24
+
25
+
26
+ class Product(Block):
27
+ """
28
+ Multi-input product block.
29
+
30
+ Summary:
31
+ Computes a product/division of multiple input signals.
32
+
33
+ Parameters:
34
+ operations : str
35
+ String of operators between inputs, using '*' and '/'.
36
+ If length is L, number of inputs is L+1.
37
+ multiplication : str
38
+ "Element-wise (*)" or "Matrix (@)".
39
+ sample_time : float, optional
40
+ Block execution period.
41
+
42
+ Inputs:
43
+ in1, in2, ..., inN : array (m,n)
44
+ Input signals (must be 2D arrays).
45
+
46
+ Outputs:
47
+ out : array (p,q)
48
+ Product output.
49
+
50
+ Notes:
51
+ - Stateless block.
52
+ - Direct feedthrough.
53
+ - Shapes are frozen per input port on first use.
54
+ - Element-wise mode supports scalar (1,1) broadcasting only.
55
+ - Matrix mode supports '*' only; '/' is rejected.
56
+ """
57
+
58
+ direct_feedthrough = True
59
+
60
+ def __init__(
61
+ self,
62
+ name: str,
63
+ operations: str | None = None,
64
+ multiplication: str = "Element-wise (*)",
65
+ sample_time: float | None = None,
66
+ ):
67
+ super().__init__(name, sample_time)
68
+
69
+ if operations is None:
70
+ operations = "*"
71
+
72
+ if not isinstance(operations, str):
73
+ raise TypeError(f"[{self.name}] 'operations' must be a str.")
74
+ if any(op not in ("*", "/") for op in operations):
75
+ raise ValueError(f"[{self.name}] 'operations' must contain only '*' or '/'.")
76
+
77
+ if not isinstance(multiplication, str):
78
+ raise TypeError(f"[{self.name}] 'multiplication' must be a str.")
79
+ if multiplication not in ("Element-wise (*)", "Matrix (@)"):
80
+ raise ValueError(
81
+ f"[{self.name}] 'multiplication' must be 'Element-wise (*)' or 'Matrix (@)'."
82
+ )
83
+
84
+ if multiplication == "Matrix (@)" and "/" in operations:
85
+ raise ValueError(f"[{self.name}] Division '/' is not supported in 'Matrix (@)' mode.")
86
+
87
+ self.operations = operations
88
+ self.multiplication = multiplication
89
+ self.num_inputs = len(self.operations) + 1
90
+
91
+ for i in range(self.num_inputs):
92
+ self.inputs[f"in{i+1}"] = None
93
+
94
+ self.outputs["out"] = None
95
+
96
+ # Shape freezing per input port
97
+ self._input_shapes: dict[str, tuple[int, int]] = {}
98
+
99
+
100
+ # --------------------------------------------------------------------------
101
+ # Public methods
102
+ # --------------------------------------------------------------------------
103
+ def initialize(self, t0: float):
104
+ # No "fallback" values: missing inputs should be detected normally
105
+ # but for init, if any input missing, output stays None
106
+ for i in range(self.num_inputs):
107
+ if self.inputs[f"in{i+1}"] is None:
108
+ self.outputs["out"] = None
109
+ return
110
+ self.outputs["out"] = self._compute_output()
111
+
112
+ # ------------------------------------------------------------------
113
+ def output_update(self, t: float, dt: float):
114
+ self.outputs["out"] = self._compute_output()
115
+
116
+ # ------------------------------------------------------------------
117
+ def state_update(self, t: float, dt: float):
118
+ pass
119
+
120
+
121
+ # --------------------------------------------------------------------------
122
+ # Private methods
123
+ # --------------------------------------------------------------------------
124
+ def _get_input_2d(self, port: str) -> np.ndarray:
125
+ u = self.inputs[port]
126
+ if u is None:
127
+ raise RuntimeError(f"[{self.name}] Input '{port}' is not connected or not set.")
128
+ u_arr = self._to_2d_array(port, u) # uses Block helper
129
+ # freeze shape per port
130
+ if port not in self._input_shapes:
131
+ self._input_shapes[port] = u_arr.shape
132
+ elif u_arr.shape != self._input_shapes[port]:
133
+ raise ValueError(
134
+ f"[{self.name}] Input '{port}' shape changed: expected {self._input_shapes[port]}, got {u_arr.shape}."
135
+ )
136
+ return u_arr
137
+
138
+ # ------------------------------------------------------------------
139
+ def _compute_output(self) -> np.ndarray:
140
+ arrays = [self._get_input_2d(f"in{i+1}") for i in range(self.num_inputs)]
141
+
142
+ if self.multiplication == "Element-wise (*)":
143
+ # Only scalar (1,1) broadcasting allowed
144
+ non_scalar_shapes = {a.shape for a in arrays if not self._is_scalar_2d(a)}
145
+ if len(non_scalar_shapes) > 1:
146
+ raise ValueError(
147
+ f"[{self.name}] Incompatible input shapes for element-wise product: {sorted(non_scalar_shapes)}."
148
+ )
149
+
150
+ # target shape = the unique non-scalar shape if any, else (1,1)
151
+ target_shape = (1, 1) if len(non_scalar_shapes) == 0 else next(iter(non_scalar_shapes))
152
+
153
+ def expand(a: np.ndarray) -> np.ndarray:
154
+ if self._is_scalar_2d(a) and target_shape != (1, 1):
155
+ return np.full(target_shape, float(a[0, 0]), dtype=float)
156
+ return a.astype(float)
157
+
158
+ arrays = [expand(a) for a in arrays]
159
+
160
+ result = arrays[0].copy()
161
+ for op, a in zip(self.operations, arrays[1:]):
162
+ if op == "*":
163
+ result = result * a
164
+ else: # "/"
165
+ result = result / a
166
+ return result
167
+
168
+ # -------------------- Matrix (@) mode
169
+ # Only '*' allowed by __init__ guard
170
+ result = arrays[0].astype(float)
171
+
172
+ for a in arrays[1:]:
173
+ a = a.astype(float)
174
+
175
+ # scalar scaling cases
176
+ if self._is_scalar_2d(result) and not self._is_scalar_2d(a):
177
+ result = float(result[0, 0]) * a
178
+ continue
179
+ if not self._is_scalar_2d(result) and self._is_scalar_2d(a):
180
+ result = result * float(a[0, 0])
181
+ continue
182
+ if self._is_scalar_2d(result) and self._is_scalar_2d(a):
183
+ result = np.array([[float(result[0, 0]) * float(a[0, 0])]], dtype=float)
184
+ continue
185
+
186
+ # true matrix multiplication
187
+ if result.shape[1] != a.shape[0]:
188
+ raise ValueError(
189
+ f"[{self.name}] Incompatible dimensions for matrix product: "
190
+ f"{result.shape} @ {a.shape}."
191
+ )
192
+ result = result @ a
193
+
194
+ return result