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,225 @@
1
+ # tests/blocks/observers/test_luenberger.py
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from pySimBlocks.core import Model, Simulator, SimulationConfig
7
+ from pySimBlocks.blocks.sources.constant import Constant
8
+ from pySimBlocks.blocks.sources.step import Step
9
+ from pySimBlocks.blocks.observers.luenberger import Luenberger
10
+
11
+
12
+ # ------------------------------------------------------------
13
+ # Helpers
14
+ # ------------------------------------------------------------
15
+ def run_sim(obs: Luenberger, u_src, y_src, dt=0.1, T=0.3, logging=None):
16
+ m = Model()
17
+ m.add_block(u_src)
18
+ m.add_block(y_src)
19
+ m.add_block(obs)
20
+
21
+ m.connect(u_src.name, "out", obs.name, "u")
22
+ m.connect(y_src.name, "out", obs.name, "y")
23
+
24
+ if logging is None:
25
+ logging = [
26
+ f"{obs.name}.outputs.y_hat",
27
+ f"{obs.name}.outputs.x_hat",
28
+ f"{obs.name}.state.x_hat",
29
+ ]
30
+
31
+ sim_cfg = SimulationConfig(dt, T, logging=logging)
32
+ sim = Simulator(m, sim_cfg)
33
+ logs = sim.run()
34
+ return logs
35
+
36
+
37
+ def manual_observer_rollout(A, B, C, L, x0, u_seq, y_seq):
38
+ """
39
+ Returns:
40
+ y_hat_seq: list of y_hat[k] = C x[k]
41
+ x_next_seq: list of x[k+1]
42
+ """
43
+ x = x0.copy()
44
+ y_hat_seq = []
45
+ x_next_seq = []
46
+
47
+ for (u, y) in zip(u_seq, y_seq):
48
+ y_hat = C @ x
49
+ x_next = A @ x + B @ u + L @ (y - y_hat)
50
+
51
+ y_hat_seq.append(y_hat.copy())
52
+ x_next_seq.append(x_next.copy())
53
+ x = x_next
54
+
55
+ return y_hat_seq, x_next_seq
56
+
57
+
58
+ # ------------------------------------------------------------
59
+ # 1) Basic correctness (state + output timing)
60
+ # ------------------------------------------------------------
61
+ def test_luenberger_basic_update_and_logging_timing():
62
+ # Dimensions: n=2, m=1, p=1
63
+ A = np.array([[0.9, 0.1],
64
+ [0.0, 0.95]], dtype=float)
65
+ B = np.array([[0.1],
66
+ [0.05]], dtype=float)
67
+ C = np.array([[1.0, 0.0]], dtype=float)
68
+ L = np.array([[0.5],
69
+ [0.1]], dtype=float)
70
+
71
+ obs = Luenberger("obs", A=A, B=B, C=C, L=L, x0=None)
72
+
73
+ # Use constants so u[k] and y[k] are stable and easy to predict
74
+ u_src = Constant("u_src", [[1.0]]) # u = 1
75
+ y_src = Constant("y_src", [[0.0]]) # y = 0
76
+
77
+ dt = 0.1
78
+ T = 0.3 # 3 steps -> logs at k=0,1,2
79
+
80
+ logs = run_sim(
81
+ obs,
82
+ u_src=u_src,
83
+ y_src=y_src,
84
+ dt=dt,
85
+ T=T,
86
+ logging=[
87
+ "obs.outputs.y_hat",
88
+ "obs.state.x_hat",
89
+ ],
90
+ )
91
+
92
+ # Manual reference:
93
+ # log index k corresponds to:
94
+ # - y_hat[k] computed from x[k]
95
+ # - state.x_hat already committed to x[k+1]
96
+ x0 = np.zeros((2, 1), dtype=float)
97
+ u_seq = [np.array([[1.0]]) for _ in range(3)]
98
+ y_seq = [np.array([[0.0]]) for _ in range(3)]
99
+ y_hat_ref, x_next_ref = manual_observer_rollout(A, B, C, L, x0, u_seq, y_seq)
100
+
101
+ # outputs.y_hat
102
+ assert np.allclose(logs["obs.outputs.y_hat"][0], y_hat_ref[0])
103
+ assert np.allclose(logs["obs.outputs.y_hat"][1], y_hat_ref[1])
104
+ assert np.allclose(logs["obs.outputs.y_hat"][2], y_hat_ref[2])
105
+
106
+ # state.x_hat is x[k+1]
107
+ assert np.allclose(logs["obs.state.x_hat"][0], x_next_ref[0])
108
+ assert np.allclose(logs["obs.state.x_hat"][1], x_next_ref[1])
109
+ assert np.allclose(logs["obs.state.x_hat"][2], x_next_ref[2])
110
+
111
+
112
+ # ------------------------------------------------------------
113
+ # 2) Provided x0 should be respected
114
+ # ------------------------------------------------------------
115
+ def test_luenberger_respects_x0():
116
+ A = np.eye(2)
117
+ B = np.zeros((2, 1))
118
+ C = np.array([[1.0, 0.0]])
119
+ L = np.zeros((2, 1))
120
+
121
+ x0 = np.array([[2.0], [3.0]])
122
+ obs = Luenberger("obs", A=A, B=B, C=C, L=L, x0=x0)
123
+
124
+ u_src = Constant("u_src", [[0.0]])
125
+ y_src = Constant("y_src", [[0.0]])
126
+
127
+ logs = run_sim(
128
+ obs,
129
+ u_src=u_src,
130
+ y_src=y_src,
131
+ dt=0.1,
132
+ T=0.1,
133
+ logging=["obs.outputs.y_hat", "obs.state.x_hat"],
134
+ )
135
+
136
+ # At k=0: y_hat[0] = C x0 = 2
137
+ assert np.allclose(logs["obs.outputs.y_hat"][0], [[2.0]])
138
+ # state after first step is x1 = A x0 = x0 (since everything else is zero)
139
+ assert np.allclose(logs["obs.state.x_hat"][0], x0)
140
+
141
+
142
+ # ------------------------------------------------------------
143
+ # 3) Missing inputs should raise (at runtime state_update)
144
+ # ------------------------------------------------------------
145
+ def test_luenberger_missing_u_raises():
146
+ A = np.eye(1)
147
+ B = np.ones((1, 1))
148
+ C = np.ones((1, 1))
149
+ L = np.ones((1, 1))
150
+
151
+ obs = Luenberger("obs", A=A, B=B, C=C, L=L)
152
+ y_src = Constant("y_src", [[0.0]])
153
+
154
+ m = Model()
155
+ m.add_block(y_src)
156
+ m.add_block(obs)
157
+ # only connect y
158
+ m.connect("y_src", "out", "obs", "y")
159
+
160
+ sim = Simulator(m, SimulationConfig(0.1, 0.1, logging=["obs.outputs.y_hat"]))
161
+ sim.initialize()
162
+ with pytest.raises(RuntimeError, match="Input 'u'"):
163
+ sim.step()
164
+
165
+
166
+ def test_luenberger_missing_y_raises():
167
+ A = np.eye(1)
168
+ B = np.ones((1, 1))
169
+ C = np.ones((1, 1))
170
+ L = np.ones((1, 1))
171
+
172
+ obs = Luenberger("obs", A=A, B=B, C=C, L=L)
173
+ u_src = Constant("u_src", [[0.0]])
174
+
175
+ m = Model()
176
+ m.add_block(u_src)
177
+ m.add_block(obs)
178
+ # only connect u
179
+ m.connect("u_src", "out", "obs", "u")
180
+
181
+ sim = Simulator(m, SimulationConfig(0.1, 0.1, logging=["obs.outputs.y_hat"]))
182
+ sim.initialize()
183
+ with pytest.raises(RuntimeError, match="Input 'y'"):
184
+ sim.step()
185
+
186
+
187
+ # ------------------------------------------------------------
188
+ # 4) Invalid matrix dimensions should raise at construction
189
+ # ------------------------------------------------------------
190
+ def test_luenberger_invalid_dimensions():
191
+ A = np.eye(2)
192
+ B = np.ones((3, 1)) # wrong rows
193
+ C = np.ones((1, 2))
194
+ L = np.ones((2, 1))
195
+
196
+ with pytest.raises(ValueError):
197
+ Luenberger("obs", A=A, B=B, C=C, L=L)
198
+
199
+
200
+ # ------------------------------------------------------------
201
+ # 5) Bad input shape (wrong number of elements) triggers reshape error
202
+ # ------------------------------------------------------------
203
+ def test_luenberger_bad_input_size_raises_value_error():
204
+ A = np.eye(2)
205
+ B = np.ones((2, 1))
206
+ C = np.ones((1, 2))
207
+ L = np.ones((2, 1))
208
+ obs = Luenberger("obs", A=A, B=B, C=C, L=L)
209
+
210
+ # u must be (1,1) -> OK, y must be (1,1) -> but we provide 2 elements
211
+ u_src = Constant("u_src", [[0.0]])
212
+ y_src = Constant("y_src", [[1.0], [2.0]]) # wrong total size for (1,1)
213
+
214
+ m = Model()
215
+ m.add_block(u_src)
216
+ m.add_block(y_src)
217
+ m.add_block(obs)
218
+ m.connect("u_src", "out", "obs", "u")
219
+ m.connect("y_src", "out", "obs", "y")
220
+
221
+ sim = Simulator(m, SimulationConfig(0.1, 0.1, logging=["obs.outputs.y_hat"]))
222
+ sim.initialize()
223
+ with pytest.raises(ValueError):
224
+ sim.step()
225
+
@@ -0,0 +1,230 @@
1
+
2
+ import numpy as np
3
+ import pytest
4
+
5
+ from pySimBlocks.blocks.operators.algebraic_function import AlgebraicFunction
6
+
7
+
8
+ # ---------------------------------------------------------------------
9
+ # Helpers
10
+ # ---------------------------------------------------------------------
11
+ def make_block(func, input_keys=("u",), output_keys=("y",)):
12
+ return AlgebraicFunction(
13
+ name="F",
14
+ function=func,
15
+ input_keys=list(input_keys),
16
+ output_keys=list(output_keys),
17
+ )
18
+
19
+
20
+ # ---------------------------------------------------------------------
21
+ # 1) Basic pass-through (scalar as (1,1))
22
+ # ---------------------------------------------------------------------
23
+ def test_algebraic_function_pass_through_scalar():
24
+ def f(t, dt, u):
25
+ if u is None:
26
+ return {"y": np.zeros((1, 1))}
27
+ return {"y": u}
28
+
29
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
30
+ blk.initialize(0.0)
31
+
32
+ blk.inputs["u"] = np.array([[2.0]])
33
+ blk.output_update(0.0, 0.1)
34
+
35
+ assert np.allclose(blk.outputs["y"], [[2.0]])
36
+
37
+
38
+ # ---------------------------------------------------------------------
39
+ # 2) Matrix support
40
+ # ---------------------------------------------------------------------
41
+ def test_algebraic_function_matrix_support():
42
+ def f(t, dt, A, B):
43
+ if A is None or B is None:
44
+ return {"Y": np.zeros((1, 1))}
45
+ return {"Y": A @ B}
46
+
47
+ blk = make_block(f, input_keys=("A", "B"), output_keys=("Y",))
48
+ blk.initialize(0.0)
49
+
50
+ A = np.array([[1.0, 2.0],
51
+ [3.0, 4.0]])
52
+ B = np.array([[5.0],
53
+ [6.0]])
54
+
55
+ blk.inputs["A"] = A
56
+ blk.inputs["B"] = B
57
+ blk.output_update(0.0, 0.1)
58
+
59
+ assert np.allclose(blk.outputs["Y"], A @ B)
60
+ assert blk.outputs["Y"].shape == (2, 1)
61
+
62
+
63
+ # ---------------------------------------------------------------------
64
+ # 3) Multiple outputs
65
+ # ---------------------------------------------------------------------
66
+ def test_algebraic_function_multiple_outputs():
67
+ def f(t, dt, u):
68
+ if u is None:
69
+ return {"y1": np.zeros((1, 1)), "y2": np.zeros((1, 1))}
70
+ return {"y1": u, "y2": 2.0 * u}
71
+
72
+ blk = make_block(f, input_keys=("u",), output_keys=("y1", "y2"))
73
+ blk.initialize(0.0)
74
+
75
+ u = np.array([[3.0], [4.0]])
76
+ blk.inputs["u"] = u
77
+ blk.output_update(0.0, 0.1)
78
+
79
+ assert np.allclose(blk.outputs["y1"], u)
80
+ assert np.allclose(blk.outputs["y2"], 2.0 * u)
81
+
82
+
83
+ # ---------------------------------------------------------------------
84
+ # 4) Missing input -> RuntimeError
85
+ # ---------------------------------------------------------------------
86
+ def test_algebraic_function_missing_input_raises():
87
+ def f(t, dt, u):
88
+ if u is None:
89
+ return {"y": np.zeros((1, 1))}
90
+ return {"y": u}
91
+
92
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
93
+ blk.initialize(0.0)
94
+
95
+ blk.inputs["u"] = None
96
+ with pytest.raises(RuntimeError) as err:
97
+ blk.output_update(0.0, 0.1)
98
+
99
+ assert "not set" in str(err.value).lower()
100
+
101
+
102
+ # ---------------------------------------------------------------------
103
+ # 5) Signature mismatch (input_keys != function args) -> ValueError in initialize
104
+ # ---------------------------------------------------------------------
105
+ def test_algebraic_function_signature_mismatch_raises():
106
+ # function declares 'u', but block expects 'x'
107
+ def f(t, dt, u):
108
+ if u is None:
109
+ return {"y": np.zeros((1, 1))}
110
+ return {"y": u}
111
+
112
+ blk = make_block(f, input_keys=("x",), output_keys=("y",))
113
+ with pytest.raises(ValueError) as err:
114
+ blk.initialize(0.0)
115
+
116
+ assert "arguments mismatch" in str(err.value).lower()
117
+
118
+
119
+ # ---------------------------------------------------------------------
120
+ # 6) Function must return a dict
121
+ # ---------------------------------------------------------------------
122
+ def test_algebraic_function_return_not_dict_raises():
123
+ def f(t, dt, u):
124
+ if u is None:
125
+ return {"y": np.zeros((1, 1))}
126
+ return u # invalid
127
+
128
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
129
+ blk.initialize(0.0)
130
+
131
+ blk.inputs["u"] = np.array([[1.0]])
132
+ with pytest.raises(RuntimeError) as err:
133
+ blk.output_update(0.0, 0.1)
134
+
135
+ assert "must return a dict" in str(err.value).lower()
136
+
137
+
138
+ # ---------------------------------------------------------------------
139
+ # 7) Output keys mismatch
140
+ # ---------------------------------------------------------------------
141
+ def test_algebraic_function_output_keys_mismatch_raises():
142
+ def f(t, dt, u):
143
+ if u is None:
144
+ return {"y": np.zeros((1, 1))}
145
+ return {"z": u} # wrong key
146
+
147
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
148
+ blk.initialize(0.0)
149
+
150
+ blk.inputs["u"] = np.array([[1.0]])
151
+ with pytest.raises(RuntimeError) as err:
152
+ blk.output_update(0.0, 0.1)
153
+
154
+ assert "missing output keys" in str(err.value).lower()
155
+
156
+
157
+ # ---------------------------------------------------------------------
158
+ # 8) Output must be 2D
159
+ # ---------------------------------------------------------------------
160
+ def test_algebraic_function_output_must_be_2d():
161
+ def f(t, dt, u):
162
+ return {"y": np.array([1.0, 2.0])} # 1D invalid
163
+
164
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
165
+ blk.initialize(0.0)
166
+
167
+ blk.inputs["u"] = np.array([[1.0]])
168
+ with pytest.raises(ValueError) as err:
169
+ blk.output_update(0.0, 0.1)
170
+
171
+ assert "must be a 2d" in str(err.value).lower()
172
+
173
+
174
+ # ---------------------------------------------------------------------
175
+ # 9) Input shape freeze: once seen, shape cannot change
176
+ # ---------------------------------------------------------------------
177
+ def test_algebraic_function_input_shape_change_raises():
178
+ def f(t, dt, u):
179
+ if u is None:
180
+ return {"y": np.zeros((1, 1))}
181
+ return {"y": u}
182
+
183
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
184
+ blk.initialize(0.0)
185
+
186
+ blk.inputs["u"] = np.array([[1.0]]) # (1,1)
187
+ blk.output_update(0.0, 0.1)
188
+
189
+ blk.inputs["u"] = np.array([[1.0, 2.0],
190
+ [3.0, 4.0]]) # (2,2) -> shape change
191
+ with pytest.raises(ValueError) as err:
192
+ blk.output_update(0.1, 0.1)
193
+
194
+ assert "shape changed" in str(err.value).lower()
195
+
196
+
197
+ # ---------------------------------------------------------------------
198
+ # 10) Output shape freeze: once produced, shape cannot change
199
+ # ---------------------------------------------------------------------
200
+ def test_algebraic_function_output_shape_change_raises():
201
+ # function changes output shape depending on time -> should raise at second call
202
+ def f(t, dt, u):
203
+ if t < 0.1:
204
+ return {"y": np.array([[1.0]])} # (1,1)
205
+ return {"y": np.array([[1.0, 2.0]])} # (1,2) -> shape change
206
+
207
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
208
+ blk.initialize(0.0)
209
+
210
+ blk.inputs["u"] = np.array([[0.0]]) # dummy input (shape fixed too)
211
+ blk.output_update(0.0, 0.1)
212
+
213
+ with pytest.raises(ValueError) as err:
214
+ blk.output_update(0.1, 0.1)
215
+
216
+ assert "shape changed" in str(err.value).lower()
217
+
218
+
219
+ def test_algebraic_function_func_error():
220
+ def f(t, dt, u):
221
+ return 2 * u # invalid
222
+
223
+ blk = make_block(f, input_keys=("u",), output_keys=("y",))
224
+ with pytest.raises(RuntimeError) as err:
225
+ blk.initialize(0.0)
226
+
227
+ assert "function call error" in str(err.value).lower()
228
+
229
+ if __name__ == "__main__":
230
+ test_algebraic_function_func_error()
@@ -0,0 +1,106 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.core import Model, Simulator, SimulationConfig
5
+ from pySimBlocks.blocks.sources.constant import Constant
6
+ from pySimBlocks.blocks.operators.dead_zone import DeadZone
7
+
8
+
9
+ def run_sim(src, dz, dt=0.1, T=0.1):
10
+ m = Model()
11
+ m.add_block(src)
12
+ m.add_block(dz)
13
+ m.connect(src.name, "out", dz.name, "in")
14
+
15
+ sim_cfg = SimulationConfig(dt, T, logging=[f"{dz.name}.outputs.out"])
16
+ sim = Simulator(m, sim_cfg)
17
+ logs = sim.run()
18
+ return logs[f"{dz.name}.outputs.out"]
19
+
20
+
21
+ def test_dead_zone_identity_default():
22
+ src = Constant("src", 2.0)
23
+ dz = DeadZone("dz")
24
+ logs = run_sim(src, dz)
25
+ assert np.allclose(logs[0], [[2.0]])
26
+
27
+
28
+ def test_dead_zone_inside_zone():
29
+ src = Constant("src", 0.2)
30
+ dz = DeadZone("dz", lower_bound=-0.5, upper_bound=0.5)
31
+ logs = run_sim(src, dz)
32
+ assert np.allclose(logs[0], [[0.0]])
33
+
34
+
35
+ def test_dead_zone_above():
36
+ src = Constant("src", 2.0)
37
+ dz = DeadZone("dz", lower_bound=-0.5, upper_bound=0.5)
38
+ logs = run_sim(src, dz)
39
+ assert np.allclose(logs[0], [[1.5]])
40
+
41
+
42
+ def test_dead_zone_below():
43
+ src = Constant("src", -2.0)
44
+ dz = DeadZone("dz", lower_bound=-0.5, upper_bound=0.5)
45
+ logs = run_sim(src, dz)
46
+ assert np.allclose(logs[0], [[-1.5]])
47
+
48
+
49
+ def test_dead_zone_vector():
50
+ src = Constant("src", [[0.2], [1.0], [-1.0]])
51
+ dz = DeadZone("dz", lower_bound=-0.3, upper_bound=0.3)
52
+ logs = run_sim(src, dz)
53
+ assert np.allclose(logs[0], [[0.0], [0.7], [-0.7]])
54
+
55
+
56
+ def test_dead_zone_matrix_scalar_bounds():
57
+ src = Constant("src", [[0.2, 1.0],
58
+ [-1.0, 0.0]])
59
+ dz = DeadZone("dz", lower_bound=-0.3, upper_bound=0.3)
60
+ logs = run_sim(src, dz)
61
+
62
+ # Apply component-wise:
63
+ # 0.2 -> 0
64
+ # 1.0 -> 1.0 - 0.3 = 0.7
65
+ # -1.0 -> -1.0 - (-0.3) = -0.7
66
+ # 0.0 -> 0
67
+ expected = np.array([[0.0, 0.7],
68
+ [-0.7, 0.0]])
69
+ assert np.allclose(logs[0], expected)
70
+
71
+
72
+ def test_dead_zone_matrix_vector_bounds_broadcast_columns():
73
+ src = Constant("src", [[1.0, -1.0, 0.0],
74
+ [2.0, -2.0, 0.1]]) # (2,3)
75
+
76
+ # (2,1) bounds via 1D -> broadcast on 3 cols
77
+ dz = DeadZone("dz", lower_bound=[-0.5, -1.0], upper_bound=[0.5, 1.0])
78
+ logs = run_sim(src, dz)
79
+
80
+ # Row1 bounds [-0.5,0.5]:
81
+ # [1.0 -> 0.5], [-1.0 -> -0.5], [0.0 -> 0]
82
+ # Row2 bounds [-1,1]:
83
+ # [2 -> 1], [-2 -> -1], [0.1 -> 0]
84
+ expected = np.array([[0.5, -0.5, 0.0],
85
+ [1.0, -1.0, 0.0]])
86
+ assert np.allclose(logs[0], expected)
87
+
88
+
89
+ def test_dead_zone_invalid_bounds():
90
+ # Must include zero: lower <= 0 <= upper, and lower <= upper
91
+ with pytest.raises(ValueError):
92
+ dz = DeadZone("dz", lower_bound=1.0, upper_bound=0.0)
93
+ dz.inputs["in"] = np.array([[0.0]])
94
+ dz.initialize(0.0)
95
+
96
+
97
+ def test_dead_zone_missing_input():
98
+ dz = DeadZone("dz")
99
+ m = Model()
100
+ m.add_block(dz)
101
+
102
+ sim_cfg = SimulationConfig(0.1, 1.0)
103
+ sim = Simulator(m, sim_cfg)
104
+
105
+ with pytest.raises(RuntimeError):
106
+ sim.initialize()