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,182 @@
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.gain import Gain
7
+
8
+
9
+ # ------------------------------------------------------------
10
+ # Helper : run a tiny simulation and return last output sample
11
+ # ------------------------------------------------------------
12
+ def run_sim(const_value, gain_block, dt=0.1):
13
+ m = Model()
14
+
15
+ src = Constant("src", const_value)
16
+ m.add_block(src)
17
+ m.add_block(gain_block)
18
+
19
+ m.connect("src", "out", gain_block.name, "in")
20
+
21
+ sim_cfg = SimulationConfig(dt, dt, logging=[f"{gain_block.name}.outputs.out"])
22
+ sim = Simulator(m, sim_cfg)
23
+ logs = sim.run()
24
+ return logs[f"{gain_block.name}.outputs.out"][-1]
25
+
26
+
27
+ # ------------------------------------------------------------
28
+ # 1) Element-wise mode (default)
29
+ # ------------------------------------------------------------
30
+ def test_gain_elementwise_scalar():
31
+ out = run_sim([[2.0]], Gain("G", gain=3.0))
32
+ assert np.allclose(out, [[6.0]])
33
+
34
+
35
+ def test_gain_elementwise_vector_row_scaling():
36
+ # gain vector scales rows: y = [2,3]^T * u
37
+ # u must have shape (2, ncols)
38
+ out = run_sim([[1.0], [1.0]], Gain("G", gain=[2.0, 3.0]))
39
+ assert np.allclose(out, [[2.0], [3.0]])
40
+
41
+
42
+ def test_gain_elementwise_vector_bad_u_rows():
43
+ g = Gain("G", gain=[1.0, 2.0]) # len=2, expects u.shape[0]==2
44
+
45
+ m = Model()
46
+ src = Constant("src", [[1.0], [2.0], [3.0]]) # shape (3,1) incompatible
47
+ m.add_block(src)
48
+ m.add_block(g)
49
+ m.connect("src", "out", "G", "in")
50
+
51
+ sim_cfg = SimulationConfig(0.1, 0.1)
52
+ sim = Simulator(m, sim_cfg)
53
+ with pytest.raises(RuntimeError) as err:
54
+ sim.initialize()
55
+
56
+ # Simulator wraps the exception message during initialization
57
+ assert "Element-wise mode requires u.shape[0] == len(gain)" in str(err.value)
58
+
59
+
60
+ def test_gain_elementwise_matrix_same_shape():
61
+ K = np.array([[2.0, 0.0],
62
+ [1.0, -1.0]])
63
+ u = np.array([[3.0, 4.0],
64
+ [1.0, 2.0]])
65
+ out = run_sim(u, Gain("G", gain=K))
66
+ assert np.allclose(out, K * u)
67
+
68
+
69
+ def test_gain_elementwise_matrix_bad_shape():
70
+ K = np.zeros((2, 2))
71
+ g = Gain("G", gain=K)
72
+
73
+ m = Model()
74
+ src = Constant("src", np.zeros((2, 3))) # mismatch
75
+ m.add_block(src)
76
+ m.add_block(g)
77
+ m.connect("src", "out", "G", "in")
78
+
79
+ sim_cfg = SimulationConfig(0.1, 0.1)
80
+ sim = Simulator(m, sim_cfg)
81
+ with pytest.raises(RuntimeError) as err:
82
+ sim.initialize()
83
+
84
+ assert "Element-wise mode with matrix gain requires u.shape == gain.shape" in str(err.value)
85
+
86
+
87
+ # ------------------------------------------------------------
88
+ # 2) Matrix (K @ u)
89
+ # ------------------------------------------------------------
90
+ def test_gain_left_matrix_product():
91
+ K = [[2.0, 0.0],
92
+ [1.0, -1.0]]
93
+ G = Gain("G", gain=K, multiplication="Matrix (K @ u)")
94
+
95
+ out = run_sim([[3.0], [1.0]], G)
96
+ # Expected: [[2*3 + 0*1], [1*3 + -1*1]] = [[6], [2]]
97
+ assert np.allclose(out, [[6.0], [2.0]])
98
+
99
+
100
+ def test_gain_left_matrix_bad_dimensions():
101
+ K = [[1.0, 2.0]] # shape (1,2)
102
+ g = Gain("G", gain=K, multiplication="Matrix (K @ u)")
103
+
104
+ m = Model()
105
+ src = Constant("src", [[1.0], [2.0], [3.0]]) # shape (3,1), incompatible
106
+ m.add_block(src)
107
+ m.add_block(g)
108
+ m.connect("src", "out", "G", "in")
109
+
110
+ sim_cfg = SimulationConfig(0.1, 0.1)
111
+ sim = Simulator(m, sim_cfg)
112
+ with pytest.raises(RuntimeError) as err:
113
+ sim.initialize()
114
+
115
+ assert "Left matrix product requires u.shape[0] == gain.shape[1]" in str(err.value)
116
+
117
+
118
+ def test_gain_left_matrix_requires_matrix_gain():
119
+ g = Gain("G", gain=2.0, multiplication="Matrix (K @ u)") # scalar gain not allowed here
120
+ m = Model()
121
+ src = Constant("src", [[1.0]])
122
+ m.add_block(src)
123
+ m.add_block(g)
124
+ m.connect("src", "out", "G", "in")
125
+
126
+ sim_cfg = SimulationConfig(0.1, 0.1)
127
+ sim = Simulator(m, sim_cfg)
128
+ with pytest.raises(RuntimeError) as err:
129
+ sim.initialize()
130
+
131
+ assert "requires a 2D matrix gain" in str(err.value)
132
+
133
+
134
+ # ------------------------------------------------------------
135
+ # 3) Matrix (u @ K)
136
+ # ------------------------------------------------------------
137
+ def test_gain_right_matrix_product():
138
+ # u shape (1,2), K shape (2,3) => out shape (1,3)
139
+ K = np.array([[1.0, 2.0, 3.0],
140
+ [4.0, 5.0, 6.0]])
141
+ G = Gain("G", gain=K, multiplication="Matrix (u @ K)")
142
+
143
+ u = np.array([[10.0, 1.0]])
144
+ out = run_sim(u, G)
145
+ expected = (u @ K).T
146
+ assert np.allclose(out, expected)
147
+
148
+
149
+ def test_gain_right_matrix_bad_dimensions():
150
+ K = np.zeros((2, 3))
151
+ g = Gain("G", gain=K, multiplication="Matrix (u @ K)")
152
+
153
+ m = Model()
154
+ src = Constant("src", np.zeros((2, 4))) # u.shape[1]=4, expected 2
155
+ m.add_block(src)
156
+ m.add_block(g)
157
+ m.connect("src", "out", "G", "in")
158
+
159
+ sim_cfg = SimulationConfig(0.1, 0.1)
160
+ sim = Simulator(m, sim_cfg)
161
+ with pytest.raises(RuntimeError) as err:
162
+ sim.initialize()
163
+
164
+ assert "Right matrix product requires u.shape[1] == gain.shape[0]" in str(err.value)
165
+
166
+
167
+ # ------------------------------------------------------------
168
+ # 4) Initialization sanity
169
+ # ------------------------------------------------------------
170
+ def test_initialization_output_elementwise_default():
171
+ G = Gain("G", gain=2.0) # default: Element wise
172
+ m = Model()
173
+ src = Constant("src", [[1.5]])
174
+ m.add_block(src)
175
+ m.add_block(G)
176
+ m.connect("src", "out", "G", "in")
177
+
178
+ sim_cfg = SimulationConfig(0.1, 0.1)
179
+ sim = Simulator(m, sim_cfg)
180
+ sim.initialize(0.0)
181
+
182
+ assert np.allclose(G.outputs["out"], [[3.0]])
@@ -0,0 +1,101 @@
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.mux import Mux
7
+
8
+
9
+ # ------------------------------------------------------------
10
+ def run_sim(values, mux):
11
+ m = Model()
12
+
13
+ const_blocks = []
14
+ for i, val in enumerate(values):
15
+ c = Constant(f"c{i+1}", val)
16
+ m.add_block(c)
17
+ const_blocks.append(c)
18
+
19
+ m.add_block(mux)
20
+ for i, c in enumerate(const_blocks):
21
+ m.connect(c.name, "out", mux.name, f"in{i+1}")
22
+
23
+ sim_cfg = SimulationConfig(0.1, 0.1, logging=[f"{mux.name}.outputs.out"])
24
+ sim = Simulator(m, sim_cfg)
25
+ logs = sim.run()
26
+ return logs[f"{mux.name}.outputs.out"][-1]
27
+
28
+
29
+ # ------------------------------------------------------------
30
+ def test_mux_basic_concatenation():
31
+ mux = Mux("M", num_inputs=2)
32
+ out = run_sim([[[1.0]], [[2.0]]], mux)
33
+ assert np.allclose(out, [[1.0], [2.0]])
34
+
35
+
36
+ # ------------------------------------------------------------
37
+ def test_mux_multi_dimensional_inputs():
38
+ mux = Mux("M", num_inputs=3)
39
+ out = run_sim(
40
+ [
41
+ [[1.0], [2.0]], # (2,1)
42
+ [[3.0]], # (1,1)
43
+ [[4.0], [5.0], [6.0]], # (3,1)
44
+ ],
45
+ mux,
46
+ )
47
+
48
+ expected = np.array([[1.0], [2.0], [3.0], [4.0], [5.0], [6.0]])
49
+ assert np.allclose(out, expected)
50
+
51
+
52
+ # ------------------------------------------------------------
53
+ def test_mux_scalar_inputs_allowed():
54
+ mux = Mux("M", num_inputs=3)
55
+ out = run_sim([1.0, 2.0, 3.0], mux) # scalars -> (1,1) each
56
+ expected = np.array([[1.0], [2.0], [3.0]])
57
+ assert np.allclose(out, expected)
58
+
59
+
60
+ # ------------------------------------------------------------
61
+ def test_mux_missing_input_raises():
62
+ m = Model()
63
+ c1 = Constant("c1", [[1.0]])
64
+ mux = Mux("M", num_inputs=2)
65
+
66
+ m.add_block(c1)
67
+ m.add_block(mux)
68
+
69
+ m.connect("c1", "out", "M", "in1")
70
+ # in2 not connected
71
+
72
+ sim_cfg = SimulationConfig(0.1, 0.1)
73
+ sim = Simulator(m, sim_cfg)
74
+
75
+ with pytest.raises(RuntimeError) as err:
76
+ sim.run(T=0.1)
77
+
78
+ assert "not connected or not set" in str(err.value)
79
+
80
+
81
+ # ------------------------------------------------------------
82
+ def test_mux_invalid_shape_rejected():
83
+ # in2 is a matrix (2,2) -> must raise
84
+ m = Model()
85
+ c1 = Constant("c1", [[1.0]]) # OK
86
+ c2 = Constant("c2", [[1.0, 2.0], [3.0, 4]])
87
+
88
+ mux = Mux("M", num_inputs=2)
89
+ m.add_block(c1)
90
+ m.add_block(c2)
91
+ m.add_block(mux)
92
+
93
+ m.connect("c1", "out", "M", "in1")
94
+ m.connect("c2", "out", "M", "in2")
95
+
96
+ sim_cfg = SimulationConfig(0.1, 0.1)
97
+ sim = Simulator(m, sim_cfg)
98
+ with pytest.raises(RuntimeError) as err:
99
+ sim.run(T=0.1)
100
+
101
+ assert "must be a column vector" in str(err.value)
@@ -0,0 +1,171 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.core.model import Model
5
+ from pySimBlocks.core.simulator import Simulator
6
+ from pySimBlocks.core.config import SimulationConfig
7
+ from pySimBlocks.blocks.sources.constant import Constant
8
+ from pySimBlocks.blocks.operators.product import Product
9
+
10
+
11
+ # ------------------------------------------------------------
12
+ # Helper
13
+ # ------------------------------------------------------------
14
+ def run_two_inputs(v1, v2, operations="*", multiplication="Element-wise (*)", dt=0.1):
15
+ m = Model()
16
+
17
+ s1 = Constant("s1", v1)
18
+ s2 = Constant("s2", v2)
19
+
20
+ m.add_block(s1)
21
+ m.add_block(s2)
22
+
23
+ pr = Product("P", operations=operations, multiplication=multiplication)
24
+ m.add_block(pr)
25
+
26
+ m.connect("s1", "out", "P", "in1")
27
+ m.connect("s2", "out", "P", "in2")
28
+
29
+ sim_cfg = SimulationConfig(dt, dt, logging=["P.outputs.out"])
30
+ sim = Simulator(m, sim_cfg)
31
+ logs = sim.run()
32
+ return logs["P.outputs.out"][-1]
33
+
34
+
35
+ # ------------------------------------------------------------
36
+ # 1) Basic product: u1 * u2
37
+ # ------------------------------------------------------------
38
+ def test_product_basic():
39
+ out = run_two_inputs([[2.0]], [[3.0]])
40
+ assert np.allclose(out, [[6.0]])
41
+
42
+
43
+ # ------------------------------------------------------------
44
+ # 2) Division: u1 / u2
45
+ # ------------------------------------------------------------
46
+ def test_product_division():
47
+ out = run_two_inputs([[6.0]], [[3.0]], operations="/")
48
+ assert np.allclose(out, [[2.0]])
49
+
50
+
51
+ # ------------------------------------------------------------
52
+ # 3) Scalar × vector (broadcast scalar (1,1) only)
53
+ # ------------------------------------------------------------
54
+ def test_product_scalar_vector():
55
+ out = run_two_inputs([[2.0]], [[1.0], [3.0], [4.0]])
56
+ assert np.allclose(out, [[2.0], [6.0], [8.0]])
57
+
58
+
59
+ # ------------------------------------------------------------
60
+ # 4) Vector × vector (same shape)
61
+ # ------------------------------------------------------------
62
+ def test_product_vector_vector():
63
+ out = run_two_inputs([[2.0], [3.0]], [[4.0], [5.0]])
64
+ assert np.allclose(out, [[8.0], [15.0]])
65
+
66
+
67
+ # ------------------------------------------------------------
68
+ # 5) Matrix element-wise product (same shape)
69
+ # ------------------------------------------------------------
70
+ def test_product_matrix_elementwise():
71
+ A = [[1.0, 2.0], [3.0, 4.0]]
72
+ B = [[2.0, 0.5], [1.0, 2.0]]
73
+ out = run_two_inputs(A, B, multiplication="Element-wise (*)")
74
+ assert np.allclose(out, np.array(A) * np.array(B))
75
+
76
+
77
+ # ------------------------------------------------------------
78
+ # 6) Scalar × matrix (element-wise scalar broadcast)
79
+ # ------------------------------------------------------------
80
+ def test_product_scalar_matrix_elementwise():
81
+ out = run_two_inputs([[2.0]], [[1.0, 2.0], [3.0, 4.0]], multiplication="Element-wise (*)")
82
+ assert np.allclose(out, 2.0 * np.array([[1.0, 2.0], [3.0, 4.0]]))
83
+
84
+
85
+ # ------------------------------------------------------------
86
+ # 7) Incompatible shapes in element-wise must raise
87
+ # ------------------------------------------------------------
88
+ def test_product_shape_mismatch_elementwise():
89
+ m = Model()
90
+
91
+ s1 = Constant("s1", [[1.0], [2.0]]) # (2,1)
92
+ s2 = Constant("s2", [[3.0, 4.0, 5.0]]) # (1,3) -> mismatch
93
+ pr = Product("P", operations="*", multiplication="Element-wise (*)")
94
+
95
+ m.add_block(s1)
96
+ m.add_block(s2)
97
+ m.add_block(pr)
98
+
99
+ m.connect("s1", "out", "P", "in1")
100
+ m.connect("s2", "out", "P", "in2")
101
+
102
+ sim_cfg = SimulationConfig(0.1, 0.1, logging=["P.outputs.out"])
103
+ sim = Simulator(m, sim_cfg)
104
+
105
+ with pytest.raises(RuntimeError) as err:
106
+ sim.run(T=0.1)
107
+
108
+ assert "incompatible input shapes" in str(err.value).lower()
109
+
110
+
111
+ # ------------------------------------------------------------
112
+ # 8) Matrix multiplication (@) basic
113
+ # ------------------------------------------------------------
114
+ def test_product_matrix_multiplication():
115
+ A = [[1.0, 2.0],
116
+ [3.0, 4.0]] # (2,2)
117
+ B = [[5.0],
118
+ [6.0]] # (2,1)
119
+
120
+ out = run_two_inputs(A, B, operations="*", multiplication="Matrix (@)")
121
+ assert np.allclose(out, np.array(A) @ np.array(B))
122
+ assert out.shape == (2, 1)
123
+
124
+
125
+ # ------------------------------------------------------------
126
+ # 9) Matrix multiplication rejects division
127
+ # ------------------------------------------------------------
128
+ def test_product_matrix_mode_rejects_division():
129
+ with pytest.raises(ValueError):
130
+ Product("P", operations="/", multiplication="Matrix (@)")
131
+
132
+
133
+ # ------------------------------------------------------------
134
+ # 10) Invalid operations string
135
+ # ------------------------------------------------------------
136
+ def test_product_invalid_operations():
137
+ with pytest.raises(ValueError):
138
+ Product("P", operations="*+")
139
+
140
+
141
+ # ------------------------------------------------------------
142
+ # 11) Inferred num_inputs from operations
143
+ # ------------------------------------------------------------
144
+ def test_product_infer_num_inputs_from_operations():
145
+ pr = Product("P", operations="*/")
146
+ assert pr.num_inputs == 3
147
+
148
+
149
+ # ------------------------------------------------------------
150
+ # 12) Check output initialization
151
+ # ------------------------------------------------------------
152
+ def test_product_initial_output():
153
+ m = Model()
154
+
155
+ s1 = Constant("s1", [[2.0]])
156
+ s2 = Constant("s2", [[3.0]])
157
+
158
+ pr = Product("P", operations="*", multiplication="Element-wise (*)")
159
+
160
+ m.add_block(s1)
161
+ m.add_block(s2)
162
+ m.add_block(pr)
163
+
164
+ m.connect("s1", "out", "P", "in1")
165
+ m.connect("s2", "out", "P", "in2")
166
+
167
+ sim_cfg = SimulationConfig(0.1, 0.1, logging=["P.outputs.out"])
168
+ sim = Simulator(m, sim_cfg)
169
+ sim.initialize(0.0)
170
+
171
+ assert np.allclose(pr.outputs["out"], [[6.0]])
@@ -0,0 +1,166 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.core import Model, Simulator, SimulationConfig
5
+ from pySimBlocks.blocks.sources.step import Step
6
+ from pySimBlocks.blocks.sources.constant import Constant
7
+ from pySimBlocks.blocks.sources.ramp import Ramp
8
+ from pySimBlocks.blocks.operators.rate_limiter import RateLimiter
9
+
10
+
11
+ # ------------------------------------------------------------
12
+ def run_sim(src_block, rate_block, dt=0.1, T=0.3):
13
+ m = Model()
14
+ m.add_block(src_block)
15
+ m.add_block(rate_block)
16
+ m.connect(src_block.name, "out", rate_block.name, "in")
17
+
18
+ sim_cfg = SimulationConfig(dt, T, logging=[f"{rate_block.name}.outputs.out"])
19
+ sim = Simulator(m, sim_cfg)
20
+ logs = sim.run()
21
+ return logs[f"{rate_block.name}.outputs.out"]
22
+
23
+
24
+ # ------------------------------------------------------------
25
+ def test_rate_limiter_passthrough_without_initial_output():
26
+ src = Step("src", start_time=0.0, value_before=0.0, value_after=10.0)
27
+ R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0)
28
+
29
+ logs = run_sim(src, R, dt=0.1, T=0.2)
30
+
31
+ assert np.allclose(logs[0], [[10.0]])
32
+ assert np.allclose(logs[1], [[10.0]])
33
+
34
+
35
+ # ------------------------------------------------------------
36
+ def test_rate_limiter_scalar_with_initial_output():
37
+ src = Constant("src", 10.0)
38
+ R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0, initial_output=0.0)
39
+
40
+ logs = run_sim(src, R, dt=0.1, T=0.3)
41
+
42
+ assert np.allclose(logs[0], [[0.1]])
43
+ assert np.allclose(logs[1], [[0.2]])
44
+ assert np.allclose(logs[2], [[0.3]])
45
+
46
+
47
+ # ------------------------------------------------------------
48
+ def test_rate_limiter_no_active_limitation():
49
+ src = Ramp("src", slope=0.5, offset=1.0)
50
+ R = RateLimiter("R", rising_slope=5.0, falling_slope=-5.0)
51
+
52
+ logs = run_sim(src, R, dt=0.1, T=0.3)
53
+
54
+ for k in range(1, len(logs)):
55
+ assert np.allclose(logs[k] - logs[k - 1], [[0.05]])
56
+
57
+
58
+ # ------------------------------------------------------------
59
+ def test_rate_limiter_vector():
60
+ src = Step(
61
+ "src",
62
+ start_time=0.0,
63
+ value_before=[[0.0], [0.0]],
64
+ value_after=[[4.0], [-4.0]],
65
+ )
66
+
67
+ R = RateLimiter(
68
+ "R",
69
+ rising_slope=[1.0, 0.5],
70
+ falling_slope=[-1.0, -0.5],
71
+ initial_output=[[0.0], [0.0]],
72
+ )
73
+
74
+ logs = run_sim(src, R, dt=0.1, T=0.2)
75
+
76
+ assert np.allclose(logs[0], [[0.1], [-0.05]])
77
+ assert np.allclose(logs[1], [[0.2], [-0.1]])
78
+ assert np.allclose(logs[2], [[0.3], [-0.15]])
79
+
80
+
81
+ # ------------------------------------------------------------
82
+ def test_rate_limiter_matrix_scalar_slopes():
83
+ src = Step(
84
+ "src",
85
+ start_time=0.0,
86
+ value_before=[[0.0, 0.0],
87
+ [0.0, 0.0]],
88
+ value_after=[[10.0, -10.0],
89
+ [5.0, -5.0]],
90
+ )
91
+
92
+ R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0, initial_output=0.0)
93
+
94
+ logs = run_sim(src, R, dt=0.1, T=0.1)
95
+
96
+ # dt=0.1, slopes +/-1 => max delta per step = +/-0.1 everywhere
97
+ expected = np.array([[0.1, -0.1],
98
+ [0.1, -0.1]])
99
+ assert np.allclose(logs[0], expected)
100
+
101
+
102
+ # ------------------------------------------------------------
103
+ def test_rate_limiter_matrix_vector_slopes_broadcast_columns():
104
+ # input is (2,3)
105
+ src = Step(
106
+ "src",
107
+ start_time=0.0,
108
+ value_before=[[0.0, 0.0, 0.0],
109
+ [0.0, 0.0, 0.0]],
110
+ value_after=[[10.0, -10.0, 2.0],
111
+ [5.0, 6.0, -7.0]],
112
+ )
113
+
114
+ # slopes as (2,1) via 1D -> (2,1); should broadcast across 3 columns
115
+ R = RateLimiter(
116
+ "R",
117
+ rising_slope=[1.0, 0.5],
118
+ falling_slope=[-1.0, -0.5],
119
+ initial_output=0.0,
120
+ )
121
+
122
+ logs = run_sim(src, R, dt=0.1, T=0.1)
123
+
124
+ # row1 limited to +/-0.1, row2 limited to +/-0.05
125
+ expected = np.array([[0.1, -0.1, 0.1],
126
+ [0.05, 0.05, -0.05]])
127
+ assert np.allclose(logs[0], expected)
128
+
129
+
130
+ # ------------------------------------------------------------
131
+ def test_rate_limiter_missing_input():
132
+ R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0)
133
+ m = Model()
134
+ m.add_block(R)
135
+
136
+ sim_cfg = SimulationConfig(0.1, 0.1)
137
+ sim = Simulator(m, sim_cfg)
138
+
139
+ with pytest.raises(RuntimeError):
140
+ sim.initialize()
141
+
142
+
143
+ # ------------------------------------------------------------
144
+ def test_rate_limiter_invalid_slopes():
145
+ with pytest.raises(ValueError):
146
+ RateLimiter("R", rising_slope=-1.0, falling_slope=-1.0)
147
+
148
+ with pytest.raises(ValueError):
149
+ RateLimiter("R", rising_slope=1.0, falling_slope=1.0)
150
+
151
+
152
+ # ------------------------------------------------------------
153
+ def test_rate_limiter_input_shape_change_raises():
154
+ # Direct block test: once initialized with (1,1), shape cannot change
155
+ R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0, initial_output=0.0)
156
+
157
+ R.inputs["in"] = np.array([[1.0]])
158
+ R.initialize(0.0)
159
+ R.output_update(0.0, 0.1)
160
+
161
+ R.inputs["in"] = np.array([[1.0, 2.0],
162
+ [3.0, 4.0]])
163
+ with pytest.raises(ValueError) as err:
164
+ R.output_update(0.1, 0.1)
165
+
166
+ assert "shape changed" in str(err.value) or "shape" in str(err.value)