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,141 @@
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 ZeroOrderHold(Block):
27
+ """
28
+ Zero-Order Hold (ZOH) block.
29
+
30
+ Summary:
31
+ Samples the input signal at discrete instants and holds the sampled
32
+ value constant between sampling instants.
33
+
34
+ Parameters:
35
+ sample_time : float
36
+ Sampling period Ts (> 0).
37
+
38
+ Inputs:
39
+ in : array (m,n)
40
+ Input signal (must be 2D).
41
+
42
+ Outputs:
43
+ out : array (m,n)
44
+ Held output signal.
45
+
46
+ Notes:
47
+ - Stateful block.
48
+ - Direct feedthrough (output depends on current u only at sampling instants).
49
+ - Shape is frozen after first resolution.
50
+ """
51
+
52
+ direct_feedthrough = True
53
+
54
+ def __init__(self, name: str, sample_time: float):
55
+ super().__init__(name, sample_time)
56
+
57
+ if not isinstance(sample_time, (float, int)) or float(sample_time) <= 0.0:
58
+ raise ValueError(f"[{self.name}] sample_time must be > 0.")
59
+
60
+ self.sample_time = float(sample_time)
61
+ self.EPS = 1e-12
62
+
63
+ self.inputs["in"] = None
64
+ self.outputs["out"] = None
65
+
66
+ self.state["y"] = None
67
+ self.next_state["y"] = None
68
+ self.state["t_last"] = None
69
+ self.next_state["t_last"] = None
70
+
71
+ self._resolved_shape: tuple[int, int] | None = None
72
+
73
+
74
+ # --------------------------------------------------------------------------
75
+ # Public methods
76
+ # --------------------------------------------------------------------------
77
+ def initialize(self, t0: float):
78
+ u = self.inputs["in"]
79
+ if u is None:
80
+ raise RuntimeError(f"[{self.name}] Input 'in' is None at initialization.")
81
+
82
+ u = self._to_2d_array("input", u)
83
+ self._ensure_shape(u)
84
+
85
+ y0 = u.copy()
86
+ self.state["y"] = y0
87
+ self.state["t_last"] = float(t0)
88
+
89
+ self.next_state["y"] = y0.copy()
90
+ self.next_state["t_last"] = float(t0)
91
+
92
+ self.outputs["out"] = y0.copy()
93
+
94
+ # ------------------------------------------------------------------
95
+ def output_update(self, t: float, dt: float):
96
+ u = self.inputs["in"]
97
+ if u is None:
98
+ raise RuntimeError(f"[{self.name}] Input 'in' is None.")
99
+
100
+ u = self._to_2d_array("input", u)
101
+ self._ensure_shape(u)
102
+
103
+ t_last = self.state["t_last"]
104
+ if t_last is None:
105
+ raise RuntimeError(f"[{self.name}] ZOH not initialized (t_last is None).")
106
+
107
+ # Sample if enough time elapsed
108
+ if (t - t_last) >= self.sample_time - self.EPS:
109
+ self.outputs["out"] = u.copy()
110
+ else:
111
+ self.outputs["out"] = self.state["y"].copy()
112
+
113
+ # ------------------------------------------------------------------
114
+ def state_update(self, t: float, dt: float):
115
+ t_last = self.state["t_last"]
116
+ if t_last is None:
117
+ raise RuntimeError(f"[{self.name}] ZOH not initialized (t_last is None).")
118
+
119
+ if (t - t_last) >= self.sample_time - self.EPS:
120
+ self.next_state["y"] = self.outputs["out"].copy()
121
+ self.next_state["t_last"] = float(t)
122
+ else:
123
+ self.next_state["y"] = self.state["y"].copy()
124
+ self.next_state["t_last"] = float(t_last)
125
+
126
+
127
+ # --------------------------------------------------------------------------
128
+ # Private methods
129
+ # --------------------------------------------------------------------------
130
+ def _ensure_shape(self, u: np.ndarray) -> None:
131
+ if u.ndim != 2:
132
+ raise ValueError(
133
+ f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
134
+ )
135
+ if self._resolved_shape is None:
136
+ self._resolved_shape = u.shape
137
+ return
138
+ if u.shape != self._resolved_shape:
139
+ raise ValueError(
140
+ f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {u.shape}."
141
+ )
@@ -0,0 +1,23 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from pySimBlocks.blocks.optimizers.quadratic_program import QuadraticProgram
22
+
23
+ __all__ = ["QuadraticProgram"]
@@ -0,0 +1,326 @@
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 qpsolvers import Problem, solve_problem, available_solvers
23
+
24
+ from pySimBlocks.core.block import Block
25
+
26
+
27
+ class QuadraticProgram(Block):
28
+ """
29
+ General time-varying quadratic program solver.
30
+
31
+ Summary:
32
+ Solves at each time step the quadratic program:
33
+
34
+ minimize 1/2 x^T P x + q^T x
35
+ subject to G x <= h
36
+ A x = b
37
+ lb <= x <= ub
38
+
39
+ All problem data are provided as inputs and may vary with time.
40
+ Constraints may be omitted by providing None.
41
+
42
+ Parameters:
43
+ name: str
44
+ Block name.
45
+ solver: str
46
+ QP solver name (default: "clarabel").
47
+
48
+ I/O:
49
+ Inputs:
50
+ P: array (n,n)
51
+ Quadratic cost matrix.
52
+ q: array (n,) or (n,1)
53
+ Linear cost vector.
54
+ G: array (m,n) or None
55
+ Inequality constraint matrix.
56
+ h: array (m,) or (m,1) or None
57
+ Inequality constraint vector.
58
+ A: array (p,n) or None
59
+ Equality constraint matrix.
60
+ b: array (p,) or (p,1) or None
61
+ Equality constraint vector.
62
+ lb: array (n,) or (n,1) or None
63
+ Lower bound on x. (No scalar broadcast.)
64
+ ub: array (n,) or (n,1) or None
65
+ Upper bound on x. (No scalar broadcast.)
66
+
67
+ Outputs:
68
+ x: array (n,1)
69
+ Optimal solution (or zeros on failure).
70
+ status: array (1,1)
71
+ Solver status:
72
+ 0 = optimal
73
+ 1 = infeasible / no solution
74
+ 2 = solver error
75
+ 3 = input error
76
+ cost: array (1,1)
77
+ Optimal cost value (NaN on failure).
78
+ """
79
+
80
+ def __init__(self, name: str, solver: str = "clarabel"):
81
+ super().__init__(name)
82
+
83
+ self._size: int | None = None
84
+
85
+ if solver not in available_solvers:
86
+ raise ValueError(
87
+ f"Solver '{solver}' is not available. Available solvers: {available_solvers}"
88
+ )
89
+ self.solver = solver
90
+
91
+ self.inputs = {
92
+ "P": None,
93
+ "q": None,
94
+ "G": None,
95
+ "h": None,
96
+ "A": None,
97
+ "b": None,
98
+ "lb": None,
99
+ "ub": None,
100
+ }
101
+
102
+ self.outputs = {
103
+ "x": None,
104
+ "status": None,
105
+ "cost": None,
106
+ }
107
+
108
+ self.state = {}
109
+ self.next_state = {}
110
+
111
+
112
+ # --------------------------------------------------------------------------
113
+ # Public methods
114
+ # --------------------------------------------------------------------------
115
+ def initialize(self, t0: float):
116
+ self.outputs["x"] = np.zeros((1, 1))
117
+ self.outputs["status"] = np.array([[2]])
118
+ self.outputs["cost"] = np.array([[np.nan]])
119
+
120
+ # ------------------------------------------------------------------
121
+ def output_update(self, t: float, dt: float):
122
+ # --- fetch raw
123
+ P_raw = self.inputs.get("P", None)
124
+ q_raw = self.inputs.get("q", None)
125
+ G_raw = self.inputs.get("G", None)
126
+ h_raw = self.inputs.get("h", None)
127
+ A_raw = self.inputs.get("A", None)
128
+ b_raw = self.inputs.get("b", None)
129
+ lb_raw = self.inputs.get("lb", None)
130
+ ub_raw = self.inputs.get("ub", None)
131
+
132
+ # --- normalize types/shapes (numpy)
133
+ try:
134
+ P = self._as_matrix(P_raw) if P_raw is not None else None
135
+ q = self._as_vector(q_raw) if q_raw is not None else None
136
+
137
+ G = self._as_matrix(G_raw) if G_raw is not None else None
138
+ h = self._as_vector(h_raw) if h_raw is not None else None
139
+
140
+ A = self._as_matrix(A_raw) if A_raw is not None else None
141
+ b = self._as_vector(b_raw) if b_raw is not None else None
142
+
143
+ lb = self._as_vector(lb_raw) if lb_raw is not None else None
144
+ ub = self._as_vector(ub_raw) if ub_raw is not None else None
145
+
146
+ self._check_needed_input(P, q, G, h, A, b)
147
+ self._check_size_compatibility(P, q, G, h, A, b, lb, ub)
148
+
149
+ except (ValueError, TypeError):
150
+ self._set_failure(status=3)
151
+ return
152
+
153
+ # Ensure output placeholder x matches resolved size (even before solving)
154
+ self._ensure_output_x_size()
155
+
156
+ # --- build & solve
157
+ try:
158
+ problem = Problem(P, q, G, h, A, b, lb, ub)
159
+ sol = solve_problem(problem, solver=self.solver)
160
+
161
+ if sol is None or getattr(sol, "x", None) is None:
162
+ self._set_failure(status=1)
163
+ return
164
+
165
+ x = np.asarray(sol.x, dtype=float).reshape(-1, 1)
166
+ if x.shape != (self._size, 1):
167
+ raise RuntimeError(
168
+ f"[{self.name}] Solver returned x with shape {x.shape}, expected ({self._size},1)."
169
+ )
170
+
171
+ # cost = 1/2 x^T P x + q^T x
172
+ cost = 0.5 * float(x.T @ P @ x) + float(q.reshape(1, -1) @ x)
173
+
174
+ self.outputs["x"] = x
175
+ self.outputs["status"] = np.array([[0]])
176
+ self.outputs["cost"] = np.array([[cost]])
177
+
178
+ except Exception:
179
+ self._set_failure(status=2)
180
+
181
+ # ------------------------------------------------------------------
182
+ def state_update(self, t: float, dt: float):
183
+ pass
184
+
185
+
186
+ # --------------------------------------------------------------------------
187
+ # Private methods
188
+ # --------------------------------------------------------------------------
189
+ @staticmethod
190
+ def _check_needed_input(P, q, G, h, A, b):
191
+ if P is None:
192
+ raise ValueError("Missing required QP input 'P'.")
193
+ if q is None:
194
+ raise ValueError("Missing required QP input 'q'.")
195
+
196
+ # paired constraints
197
+ if (G is None) != (h is None):
198
+ raise ValueError("Inequality constraints G and h must both be provided or both be None.")
199
+ if (A is None) != (b is None):
200
+ raise ValueError("Equality constraints A and b must both be provided or both be None.")
201
+
202
+ # ------------------------------------------------------------------
203
+ @staticmethod
204
+ def _as_matrix(value) -> np.ndarray:
205
+ arr = np.asarray(value, dtype=float)
206
+ if arr.ndim != 2:
207
+ raise ValueError(f"Matrix input must be 2D. Got shape {arr.shape}.")
208
+ return arr
209
+
210
+ # ------------------------------------------------------------------
211
+ @staticmethod
212
+ def _as_vector(value) -> np.ndarray:
213
+ """
214
+ Convert input to a strict 1D vector (n,).
215
+ Accepts:
216
+ - (n,) -> ok
217
+ - (n,1) -> flatten
218
+ Rejects:
219
+ - scalar
220
+ - (1,1)
221
+ - any other shape
222
+ """
223
+ arr = np.asarray(value, dtype=float)
224
+
225
+ if arr.ndim == 1:
226
+ return arr
227
+
228
+ if arr.ndim == 2 and arr.shape[1] == 1:
229
+ return arr[:, 0]
230
+
231
+ raise ValueError(
232
+ f"Vector input must be shape (n,) or (n,1). Got shape {arr.shape}."
233
+ )
234
+
235
+ # ------------------------------------------------------------------
236
+ def _ensure_output_x_size(self) -> None:
237
+ """
238
+ Keep outputs['x'] consistent with resolved size if known.
239
+ This prevents dimension-propagation issues when the solver fails.
240
+ """
241
+ size = self._size if self._size is not None else 1
242
+
243
+ x = self.outputs.get("x", None)
244
+ if x is None:
245
+ self.outputs["x"] = np.zeros((size, 1))
246
+ return
247
+
248
+ x_arr = np.asarray(x)
249
+ if x_arr.shape != (size, 1):
250
+ self.outputs["x"] = np.zeros((size, 1))
251
+
252
+ # ------------------------------------------------------------------
253
+ def _set_failure(self, status: int):
254
+ self.outputs["status"] = np.array([[status]])
255
+ self.outputs["cost"] = np.array([[np.nan]])
256
+ self._ensure_output_x_size()
257
+
258
+ # ------------------------------------------------------------------
259
+ def _check_size_compatibility(self, P, q, G, h, A, b, lb, ub):
260
+ # P defines the size n
261
+ n = P.shape[0]
262
+
263
+ # Freeze size once known
264
+ if self._size is None:
265
+ self._size = n
266
+ elif self._size != n:
267
+ raise ValueError(
268
+ f"[{self.name}] Inconsistent QP size across time steps. "
269
+ f"Previous size: {self._size}, current size: {n}."
270
+ )
271
+
272
+ # P must be square
273
+ if P.ndim != 2 or P.shape[1] != n:
274
+ raise ValueError(f"[{self.name}] Input 'P' must be square, got shape {P.shape}.")
275
+
276
+ # q must be (n,)
277
+ if q.ndim != 1 or q.shape[0] != n:
278
+ raise ValueError(
279
+ f"[{self.name}] Input 'q' has shape {q.shape}. Must be (n,) with n={n}."
280
+ )
281
+
282
+ # Inequality constraints
283
+ if G is not None:
284
+ if h is None:
285
+ raise ValueError(f"[{self.name}] Inequality constraints require both G and h.")
286
+ if G.ndim != 2 or G.shape[1] != n:
287
+ raise ValueError(
288
+ f"[{self.name}] Input 'G' has shape {G.shape}. Must be (m,n) with n={n}."
289
+ )
290
+ m = G.shape[0]
291
+ if h.ndim != 1 or h.shape[0] != m:
292
+ raise ValueError(
293
+ f"[{self.name}] Input 'h' has shape {h.shape}. Must be (m,) with m={m}."
294
+ )
295
+ else:
296
+ if h is not None:
297
+ raise ValueError(f"[{self.name}] Inequality constraints G and h must both be provided or both be None.")
298
+
299
+ # Equality constraints
300
+ if A is not None:
301
+ if b is None:
302
+ raise ValueError(f"[{self.name}] Equality constraints require both A and b.")
303
+ if A.ndim != 2 or A.shape[1] != n:
304
+ raise ValueError(
305
+ f"[{self.name}] Input 'A' has shape {A.shape}. Must be (p,n) with n={n}."
306
+ )
307
+ p = A.shape[0]
308
+ if b.ndim != 1 or b.shape[0] != p:
309
+ raise ValueError(
310
+ f"[{self.name}] Input 'b' has shape {b.shape}. Must be (p,) with p={p}."
311
+ )
312
+ else:
313
+ if b is not None:
314
+ raise ValueError(f"[{self.name}] Equality constraints A and b must both be provided or both be None.")
315
+
316
+ # Bounds (no scalar broadcast)
317
+ if lb is not None:
318
+ if lb.ndim != 1 or lb.shape[0] != n:
319
+ raise ValueError(
320
+ f"[{self.name}] Input 'lb' has shape {lb.shape}. Must be (n,) with n={n}."
321
+ )
322
+ if ub is not None:
323
+ if ub.ndim != 1 or ub.shape[0] != n:
324
+ raise ValueError(
325
+ f"[{self.name}] Input 'ub' has shape {ub.shape}. Must be (n,) with n={n}."
326
+ )
@@ -0,0 +1,37 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ from pySimBlocks.blocks.sources.constant import Constant
22
+ from pySimBlocks.blocks.sources.file_source import FileSource
23
+ from pySimBlocks.blocks.sources.function_source import FunctionSource
24
+ from pySimBlocks.blocks.sources.ramp import Ramp
25
+ from pySimBlocks.blocks.sources.step import Step
26
+ from pySimBlocks.blocks.sources.sinusoidal import Sinusoidal
27
+ from pySimBlocks.blocks.sources.white_noise import WhiteNoise
28
+
29
+ __all__ = [
30
+ "Constant",
31
+ "FileSource",
32
+ "FunctionSource",
33
+ "Ramp",
34
+ "Step",
35
+ "Sinusoidal",
36
+ "WhiteNoise",
37
+ ]
@@ -0,0 +1,168 @@
1
+ # ******************************************************************************
2
+ # pySimBlocks
3
+ # Copyright (c) 2026 Université de Lille & INRIA
4
+ # ******************************************************************************
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or (at your
8
+ # option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
13
+ # for more details.
14
+ #
15
+ # You should have received a copy of the GNU Lesser General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+ # ******************************************************************************
18
+ # Authors: see Authors.txt
19
+ # ******************************************************************************
20
+
21
+ import numpy as np
22
+ from numpy.typing import ArrayLike
23
+ from pySimBlocks.core.block_source import BlockSource
24
+
25
+
26
+ class Chirp(BlockSource):
27
+ """
28
+ Multi-dimensional chirp signal source (linear or logarithmic).
29
+
30
+ mode:
31
+ "linear" -> linear frequency sweep
32
+ "log" -> logarithmic (exponential) sweep
33
+ """
34
+
35
+ VALID_MODES = {"linear", "log"}
36
+
37
+ def __init__(
38
+ self,
39
+ name: str,
40
+ amplitude: ArrayLike,
41
+ f0: ArrayLike,
42
+ f1: ArrayLike,
43
+ duration: ArrayLike,
44
+ start_time: ArrayLike = 0.0,
45
+ offset: ArrayLike = 0.0,
46
+ phase: ArrayLike = 0.0,
47
+ mode: str = "linear",
48
+ sample_time: float | None = None,
49
+ ):
50
+ super().__init__(name, sample_time)
51
+
52
+ if mode not in self.VALID_MODES:
53
+ raise ValueError(
54
+ f"[{name}] mode must be one of {self.VALID_MODES}"
55
+ )
56
+
57
+ self.mode = mode
58
+
59
+ A = self._to_2d_array("amplitude", amplitude, dtype=float)
60
+ F0 = self._to_2d_array("f0", f0, dtype=float)
61
+ F1 = self._to_2d_array("f1", f1, dtype=float)
62
+ D = self._to_2d_array("duration", duration, dtype=float)
63
+ T0 = self._to_2d_array("start_time", start_time, dtype=float)
64
+ O = self._to_2d_array("offset", offset, dtype=float)
65
+ P = self._to_2d_array("phase", phase, dtype=float)
66
+
67
+ target_shape = self._resolve_common_shape({
68
+ "amplitude": A,
69
+ "f0": F0,
70
+ "f1": F1,
71
+ "duration": D,
72
+ "start_time": T0,
73
+ "offset": O,
74
+ "phase": P,
75
+ })
76
+
77
+ self.amplitude = self._broadcast_scalar_only("amplitude", A, target_shape)
78
+ self.f0 = self._broadcast_scalar_only("f0", F0, target_shape)
79
+ self.f1 = self._broadcast_scalar_only("f1", F1, target_shape)
80
+ self.duration = self._broadcast_scalar_only("duration", D, target_shape)
81
+ self.start_time = self._broadcast_scalar_only("start_time", T0, target_shape)
82
+ self.offset = self._broadcast_scalar_only("offset", O, target_shape)
83
+ self.phase = self._broadcast_scalar_only("phase", P, target_shape)
84
+
85
+ if np.any(self.duration <= 0.0):
86
+ raise ValueError(f"[{self.name}] duration must be > 0.")
87
+
88
+ if self.mode == "log":
89
+ if np.any(self.f0 <= 0.0) or np.any(self.f1 <= 0.0):
90
+ raise ValueError(
91
+ f"[{self.name}] f0 and f1 must be > 0 for log mode."
92
+ )
93
+ if np.any(self.f0 == self.f1):
94
+ raise ValueError(
95
+ f"[{self.name}] f0 must differ from f1 in log mode."
96
+ )
97
+
98
+ self.outputs["out"] = np.zeros(target_shape, dtype=float)
99
+
100
+ # ------------------------------------------------------------------
101
+
102
+ def initialize(self, t0: float) -> None:
103
+ self._compute_output(t0)
104
+
105
+ # ------------------------------------------------------------------
106
+
107
+ def output_update(self, t: float, dt: float) -> None:
108
+ self._compute_output(t)
109
+
110
+ # ------------------------------------------------------------------
111
+
112
+ def _compute_output(self, t: float) -> None:
113
+
114
+ tau = np.maximum(0.0, t - self.start_time)
115
+ tau_clip = np.minimum(tau, self.duration)
116
+
117
+ if self.mode == "linear":
118
+ phi = self._linear_phase(tau, tau_clip)
119
+
120
+ else: # log
121
+ phi = self._log_phase(tau, tau_clip)
122
+
123
+ self.outputs["out"] = (
124
+ self.amplitude * np.sin(phi) + self.offset
125
+ )
126
+
127
+ # ------------------------------------------------------------------
128
+
129
+ def _linear_phase(self, tau, tau_clip):
130
+
131
+ k = (self.f1 - self.f0) / self.duration
132
+
133
+ phi_sweep = (
134
+ 2.0 * np.pi *
135
+ (self.f0 * tau_clip + 0.5 * k * tau_clip * tau_clip)
136
+ )
137
+
138
+ extra = (
139
+ 2.0 * np.pi *
140
+ self.f1 *
141
+ np.maximum(0.0, tau - self.duration)
142
+ )
143
+
144
+ return phi_sweep + extra + self.phase
145
+
146
+ # ------------------------------------------------------------------
147
+
148
+ def _log_phase(self, tau, tau_clip):
149
+
150
+ ratio = self.f1 / self.f0
151
+ log_ratio = np.log(ratio)
152
+
153
+ coeff = 2.0 * np.pi * self.f0 * self.duration / log_ratio
154
+
155
+ phi_sweep = coeff * (
156
+ np.power(ratio, tau_clip / self.duration) - 1.0
157
+ )
158
+
159
+ # phase continuity after duration
160
+ phi_end = coeff * (ratio - 1.0)
161
+
162
+ extra = (
163
+ 2.0 * np.pi *
164
+ self.f1 *
165
+ np.maximum(0.0, tau - self.duration)
166
+ )
167
+
168
+ return phi_sweep + extra + self.phase