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,185 @@
1
+ from pathlib import Path
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from pySimBlocks.blocks.sources.file_source import FileSource
7
+
8
+
9
+ def test_file_source_npz_single_array(tmp_path: Path):
10
+ path = tmp_path / "data.npz"
11
+ np.savez(path, y=np.array([[1.0, 2.0], [3.0, 4.0]]))
12
+
13
+ blk = FileSource("src", file_path=str(path), key="y")
14
+ blk.initialize(0.0)
15
+ assert np.allclose(blk.outputs["out"], [[1.0], [2.0]])
16
+
17
+ blk.output_update(0.0, 0.1)
18
+ assert np.allclose(blk.outputs["out"], [[1.0], [2.0]])
19
+
20
+ blk.output_update(0.1, 0.1)
21
+ assert np.allclose(blk.outputs["out"], [[3.0], [4.0]])
22
+
23
+
24
+ def test_file_source_npz_requires_key(tmp_path: Path):
25
+ path = tmp_path / "data.npz"
26
+ np.savez(path, a=np.array([1.0]), b=np.array([2.0]))
27
+
28
+ with pytest.raises(ValueError):
29
+ FileSource("src", file_path=str(path))
30
+
31
+
32
+ def test_file_source_npz_invalid_key(tmp_path: Path):
33
+ path = tmp_path / "data.npz"
34
+ np.savez(path, a=np.array([1.0]))
35
+
36
+ with pytest.raises(KeyError):
37
+ FileSource("src", file_path=str(path), key="missing")
38
+
39
+
40
+ def test_file_source_csv(tmp_path: Path):
41
+ path = tmp_path / "data.csv"
42
+ path.write_text("a,b\n1.0,2.0\n3.0,4.0\n", encoding="utf-8")
43
+
44
+ blk = FileSource("src", file_path=str(path), key="b")
45
+ blk.initialize(0.0)
46
+ assert np.allclose(blk.outputs["out"], [[2.0]])
47
+
48
+ blk.output_update(0.0, 0.1)
49
+ assert np.allclose(blk.outputs["out"], [[2.0]])
50
+
51
+ blk.output_update(0.1, 0.1)
52
+ assert np.allclose(blk.outputs["out"], [[4.0]])
53
+
54
+
55
+ def test_file_source_repeat_false_outputs_zeros_after_end(tmp_path: Path):
56
+ path = tmp_path / "data.npz"
57
+ np.savez(path, y=np.array([[5.0], [7.0]]))
58
+
59
+ blk = FileSource(
60
+ "src",
61
+ file_path=str(path),
62
+ key="y",
63
+ repeat=False,
64
+ )
65
+ blk.initialize(0.0)
66
+ assert np.allclose(blk.outputs["out"], [[5.0]])
67
+
68
+ blk.output_update(0.0, 0.1)
69
+ assert np.allclose(blk.outputs["out"], [[5.0]])
70
+
71
+ blk.output_update(0.1, 0.1)
72
+ assert np.allclose(blk.outputs["out"], [[7.0]])
73
+
74
+ blk.output_update(0.2, 0.1)
75
+ assert np.allclose(blk.outputs["out"], [[0.0]])
76
+
77
+
78
+ def test_file_source_repeat_true_restarts_after_end(tmp_path: Path):
79
+ path = tmp_path / "data.npy"
80
+ np.save(path, np.array([[10.0], [20.0]]))
81
+
82
+ blk = FileSource(
83
+ "src",
84
+ file_path=str(path),
85
+ repeat=True,
86
+ )
87
+ blk.initialize(0.0)
88
+ assert np.allclose(blk.outputs["out"], [[10.0]])
89
+
90
+ blk.output_update(0.0, 0.1)
91
+ assert np.allclose(blk.outputs["out"], [[10.0]])
92
+
93
+ blk.output_update(0.1, 0.1)
94
+ assert np.allclose(blk.outputs["out"], [[20.0]])
95
+
96
+ blk.output_update(0.2, 0.1)
97
+ assert np.allclose(blk.outputs["out"], [[10.0]])
98
+
99
+
100
+ def test_file_source_npy_key_not_allowed(tmp_path: Path):
101
+ path = tmp_path / "data.npy"
102
+ np.save(path, np.array([1.0, 2.0]))
103
+
104
+ with pytest.raises(ValueError):
105
+ FileSource("src", file_path=str(path), key="k")
106
+
107
+
108
+ def test_file_source_npy_use_time_not_allowed(tmp_path: Path):
109
+ path = tmp_path / "data.npy"
110
+ np.save(path, np.array([1.0, 2.0]))
111
+
112
+ with pytest.raises(ValueError):
113
+ FileSource("src", file_path=str(path), use_time=True)
114
+
115
+
116
+ def test_file_source_csv_missing_key(tmp_path: Path):
117
+ path = tmp_path / "data.csv"
118
+ path.write_text("a,b\n1.0,2.0\n", encoding="utf-8")
119
+
120
+ with pytest.raises(ValueError):
121
+ FileSource("src", file_path=str(path))
122
+
123
+
124
+ def test_file_source_invalid_extension(tmp_path: Path):
125
+ path = tmp_path / "data.txt"
126
+ path.write_text("1.0\n", encoding="utf-8")
127
+ with pytest.raises(ValueError):
128
+ FileSource("src", file_path=str(path))
129
+
130
+
131
+ def test_file_source_missing_file():
132
+ with pytest.raises(FileNotFoundError):
133
+ FileSource("src", file_path="does-not-exist.npz")
134
+
135
+
136
+ def test_file_source_npz_use_time_zoh(tmp_path: Path):
137
+ path = tmp_path / "data.npz"
138
+ t = np.array([0.0, 0.2, 0.5], dtype=float)
139
+ y = np.array([[10.0], [20.0], [50.0]], dtype=float)
140
+ np.savez(path, time=t, y=y)
141
+
142
+ blk = FileSource("src", file_path=str(path), key="y", use_time=True)
143
+
144
+ blk.initialize(0.0)
145
+ assert np.allclose(blk.outputs["out"], [[10.0]])
146
+
147
+ blk.output_update(0.19, 0.1)
148
+ assert np.allclose(blk.outputs["out"], [[10.0]])
149
+
150
+ blk.output_update(0.20, 0.1)
151
+ assert np.allclose(blk.outputs["out"], [[20.0]])
152
+
153
+ blk.output_update(0.8, 0.1)
154
+ assert np.allclose(blk.outputs["out"], [[50.0]])
155
+
156
+
157
+ def test_file_source_csv_use_time_zoh(tmp_path: Path):
158
+ path = tmp_path / "data.csv"
159
+ path.write_text(
160
+ "time,y\n0.0,1.0\n0.5,2.0\n1.0,3.0\n",
161
+ encoding="utf-8",
162
+ )
163
+
164
+ blk = FileSource("src", file_path=str(path), key="y", use_time=True)
165
+ blk.initialize(0.1)
166
+ assert np.allclose(blk.outputs["out"], [[1.0]])
167
+
168
+ blk.output_update(0.75, 0.1)
169
+ assert np.allclose(blk.outputs["out"], [[2.0]])
170
+
171
+
172
+ def test_file_source_npz_time_must_be_strictly_increasing(tmp_path: Path):
173
+ path = tmp_path / "data.npz"
174
+ np.savez(path, time=np.array([0.0, 0.2, 0.2]), y=np.array([1.0, 2.0, 3.0]))
175
+
176
+ with pytest.raises(ValueError):
177
+ FileSource("src", file_path=str(path), key="y", use_time=True)
178
+
179
+
180
+ def test_file_source_csv_use_time_requires_time_column(tmp_path: Path):
181
+ path = tmp_path / "data.csv"
182
+ path.write_text("y\n1.0\n2.0\n", encoding="utf-8")
183
+
184
+ with pytest.raises(KeyError):
185
+ FileSource("src", file_path=str(path), key="y", use_time=True)
@@ -0,0 +1,135 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.blocks.sources.function_source import FunctionSource
5
+
6
+
7
+ def test_function_source_single_output():
8
+ def f(t, dt):
9
+ return {"y": 2.0 * t + dt}
10
+
11
+ src = FunctionSource(name="f", function=f, output_keys=["y"])
12
+ src.initialize(0.0)
13
+ assert np.allclose(src.outputs["y"], [[0.0]])
14
+
15
+ src.output_update(1.0, 0.1)
16
+ assert np.allclose(src.outputs["y"], [[2.1]])
17
+
18
+
19
+ def test_function_source_multiple_outputs():
20
+ def f(t, dt):
21
+ return {
22
+ "y1": np.array([t, t + dt]),
23
+ "y2": np.array([[2.0 * t]]),
24
+ }
25
+
26
+ src = FunctionSource(name="f", function=f, output_keys=["y1", "y2"])
27
+ src.initialize(0.0)
28
+ src.output_update(0.2, 0.1)
29
+
30
+ assert src.outputs["y1"].shape == (2, 1)
31
+ assert np.allclose(src.outputs["y1"], [[0.2], [0.3]])
32
+ assert np.allclose(src.outputs["y2"], [[0.4]])
33
+
34
+
35
+ def test_function_source_signature_mismatch_raises():
36
+ def f(t, dt, u):
37
+ return {"out": np.array([[u]])}
38
+
39
+ src = FunctionSource(name="f", function=f)
40
+ with pytest.raises(ValueError):
41
+ src.initialize(0.0)
42
+
43
+
44
+ def test_function_source_function_error_is_wrapped():
45
+ def f(t, dt):
46
+ raise RuntimeError("boom")
47
+
48
+ src = FunctionSource(name="f", function=f)
49
+ with pytest.raises(RuntimeError) as err:
50
+ src.initialize(0.0)
51
+
52
+ assert "function call error" in str(err.value).lower()
53
+
54
+
55
+ def test_function_source_return_not_dict_raises():
56
+ def f(t, dt):
57
+ return np.array([[1.0]])
58
+
59
+ src = FunctionSource(name="f", function=f, output_keys=["out"])
60
+
61
+ with pytest.raises(RuntimeError) as err:
62
+ src.initialize(0.0)
63
+
64
+ assert "must return a dict" in str(err.value).lower()
65
+
66
+
67
+ def test_function_source_output_keys_mismatch_raises():
68
+ def f(t, dt):
69
+ return {"z": np.array([[1.0]])}
70
+
71
+ src = FunctionSource(name="f", function=f, output_keys=["out"])
72
+
73
+ with pytest.raises(RuntimeError) as err:
74
+ src.initialize(0.0)
75
+
76
+ assert "output keys mismatch" in str(err.value).lower()
77
+
78
+
79
+ def test_function_source_output_shape_change_raises_per_key():
80
+ def f(t, dt):
81
+ if t < 0.1:
82
+ return {"y1": np.array([[1.0]]), "y2": np.array([[2.0]])}
83
+ return {"y1": np.array([[1.0, 2.0]]), "y2": np.array([[2.0]])}
84
+
85
+ src = FunctionSource(name="f", function=f, output_keys=["y1", "y2"])
86
+ src.initialize(0.0)
87
+
88
+ with pytest.raises(ValueError) as err:
89
+ src.output_update(0.1, 0.1)
90
+
91
+ assert "shape changed" in str(err.value).lower()
92
+
93
+
94
+ def test_function_source_adapt_params_loads_function(tmp_path):
95
+ py_file = tmp_path / "my_function.py"
96
+ py_file.write_text(
97
+ "def my_source(t, dt):\n"
98
+ " return {'y': [[t + dt]]}\n",
99
+ encoding="utf-8",
100
+ )
101
+
102
+ adapted = FunctionSource.adapt_params(
103
+ {
104
+ "file_path": "my_function.py",
105
+ "function_name": "my_source",
106
+ "output_keys": ["y"],
107
+ },
108
+ params_dir=tmp_path,
109
+ )
110
+ src = FunctionSource(name="f", **adapted)
111
+
112
+ src.initialize(0.0)
113
+ src.output_update(0.2, 0.1)
114
+ assert np.allclose(src.outputs["y"], [[0.3]])
115
+
116
+
117
+ def test_function_source_adapt_params_missing_key_raises():
118
+ with pytest.raises(ValueError):
119
+ FunctionSource.adapt_params({"file_path": "foo.py"}, params_dir=None)
120
+
121
+
122
+ def test_function_source_adapt_params_sets_default_output_keys(tmp_path):
123
+ py_file = tmp_path / "my_function.py"
124
+ py_file.write_text(
125
+ "def my_source(t, dt):\n"
126
+ " return {'out': [[t + dt]]}\n",
127
+ encoding="utf-8",
128
+ )
129
+
130
+ adapted = FunctionSource.adapt_params(
131
+ {"file_path": "my_function.py", "function_name": "my_source"},
132
+ params_dir=tmp_path,
133
+ )
134
+
135
+ assert adapted["output_keys"] == ["out"]
@@ -0,0 +1,110 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.blocks.sources.ramp import Ramp
5
+
6
+
7
+ # ----------------------------------------------------------
8
+ # 1) Scalar slope and scalar start_time
9
+ # ----------------------------------------------------------
10
+ def test_ramp_scalar_before_start():
11
+ r = Ramp("r", slope=2.0, start_time=1.0, offset=0.0)
12
+ r.initialize(0.0)
13
+ assert np.allclose(r.outputs["out"], [[0.0]]) # before start_time
14
+
15
+
16
+ def test_ramp_scalar_after_start():
17
+ r = Ramp("r", slope=2.0, start_time=1.0, offset=0.0)
18
+ r.output_update(2.0, 0.1)
19
+ assert np.allclose(r.outputs["out"], [[2.0]]) # slope * (2 - 1) = 2
20
+
21
+
22
+ # ----------------------------------------------------------
23
+ # 2) Vector parameters (still valid)
24
+ # ----------------------------------------------------------
25
+ def test_ramp_vector_parameters():
26
+ r = Ramp(
27
+ "r",
28
+ slope=[1.0, 2.0],
29
+ start_time=[0.0, 1.0],
30
+ offset=[0.0, 10.0],
31
+ )
32
+ r.output_update(2.0, 0.1)
33
+ # first: 0 + 1*(2-0)=2
34
+ # second: 10 + 2*(2-1)=12
35
+ assert np.allclose(r.outputs["out"], [[2.0], [12.0]])
36
+
37
+
38
+ # ----------------------------------------------------------
39
+ # 3) Broadcasting scalar to match non-scalar shape
40
+ # (broadcast allowed ONLY for scalar (1,1))
41
+ # ----------------------------------------------------------
42
+ def test_ramp_broadcast_scalar_start_time():
43
+ r = Ramp(
44
+ "r",
45
+ slope=[1.0, 1.0, 1.0],
46
+ start_time=0.0, # scalar broadcast
47
+ offset=0.0, # scalar broadcast
48
+ )
49
+ r.output_update(2.0, 0.1)
50
+ assert np.allclose(r.outputs["out"], [[2.0], [2.0], [2.0]])
51
+
52
+
53
+ # ----------------------------------------------------------
54
+ # 4) Matrix parameters (Option B)
55
+ # ----------------------------------------------------------
56
+ def test_ramp_matrix_parameters_same_shape():
57
+ slope = np.array([[1.0, 2.0],
58
+ [3.0, 4.0]])
59
+ start_time = np.array([[0.0, 1.0],
60
+ [1.0, 0.0]])
61
+ offset = np.array([[10.0, 20.0],
62
+ [30.0, 40.0]])
63
+
64
+ r = Ramp("r", slope=slope, start_time=start_time, offset=offset)
65
+ r.output_update(2.0, 0.1)
66
+
67
+ # dt = max(0, t - start_time)
68
+ dt_mat = np.maximum(0.0, 2.0 - start_time)
69
+ expected = offset + slope * dt_mat
70
+ assert np.allclose(r.outputs["out"], expected)
71
+
72
+
73
+ def test_ramp_matrix_broadcast_scalar_offset():
74
+ slope = np.array([[1.0, 2.0],
75
+ [3.0, 4.0]])
76
+ start_time = np.array([[0.0, 1.0],
77
+ [1.0, 0.0]])
78
+
79
+ r = Ramp("r", slope=slope, start_time=start_time, offset=5.0) # scalar broadcast
80
+ r.output_update(2.0, 0.1)
81
+
82
+ dt_mat = np.maximum(0.0, 2.0 - start_time)
83
+ expected = np.full((2, 2), 5.0) + slope * dt_mat
84
+ assert np.allclose(r.outputs["out"], expected)
85
+
86
+
87
+ # ----------------------------------------------------------
88
+ # 5) Inconsistent shapes among non-scalars -> error
89
+ # ----------------------------------------------------------
90
+ def test_ramp_inconsistent_dimensions():
91
+ with pytest.raises(ValueError):
92
+ Ramp(
93
+ "r",
94
+ slope=np.zeros((2, 2)),
95
+ start_time=np.zeros((2, 3)), # mismatch
96
+ offset=0.0,
97
+ )
98
+
99
+
100
+ # ----------------------------------------------------------
101
+ # 6) Illegal ndims (>2) -> error
102
+ # ----------------------------------------------------------
103
+ def test_ramp_illegal_ndim():
104
+ with pytest.raises(ValueError):
105
+ Ramp(
106
+ "r",
107
+ slope=np.zeros((2, 2, 2)), # ndim=3 forbidden
108
+ start_time=0.0,
109
+ offset=0.0,
110
+ )
@@ -0,0 +1,113 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.blocks.sources.sinusoidal import Sinusoidal
5
+
6
+
7
+ # ----------------------------------------------------------
8
+ # 1) Scalar parameters
9
+ # ----------------------------------------------------------
10
+ def test_sinusoidal_scalar():
11
+ s = Sinusoidal("s", amplitude=2.0, frequency=1.0, offset=0.5, phase=0.0)
12
+ s.initialize(0.0)
13
+ assert np.allclose(s.outputs["out"], [[0.5]]) # sin(0)=0
14
+
15
+
16
+ def test_sinusoidal_scalar_time():
17
+ s = Sinusoidal("s", 2.0, 1.0, 0.0, 0.0)
18
+ s.output_update(0.25, 0.1) # sin(2π * 1 * 0.25) = sin(π/2) = 1
19
+ assert np.allclose(s.outputs["out"], [[2.0]])
20
+
21
+
22
+ # ----------------------------------------------------------
23
+ # 2) Vector parameters (still valid)
24
+ # ----------------------------------------------------------
25
+ def test_sinusoidal_vector():
26
+ s = Sinusoidal(
27
+ "s",
28
+ amplitude=[1.0, 2.0],
29
+ frequency=[1.0, 0.5],
30
+ offset=[0.0, 10.0],
31
+ phase=[0.0, np.pi / 2],
32
+ )
33
+ s.output_update(0.0, 0.1)
34
+ # out(0): [1*sin(0)+0, 2*sin(pi/2)+10] = [0, 12]
35
+ assert np.allclose(s.outputs["out"], [[0.0], [12.0]])
36
+
37
+
38
+ # ----------------------------------------------------------
39
+ # 3) Broadcasting scalar to match non-scalar shape
40
+ # (allowed only for scalar (1,1))
41
+ # ----------------------------------------------------------
42
+ def test_sinusoidal_broadcast_scalar_frequency():
43
+ s = Sinusoidal(
44
+ "s",
45
+ amplitude=[1.0, 1.0, 1.0],
46
+ frequency=0.5, # scalar broadcast
47
+ offset=0.0,
48
+ phase=0.0,
49
+ )
50
+ s.output_update(1.0, 0.1)
51
+ expected = np.sin(2 * np.pi * 0.5 * 1.0) * np.ones((3, 1))
52
+ assert np.allclose(s.outputs["out"], expected)
53
+
54
+
55
+ # ----------------------------------------------------------
56
+ # 4) Matrix parameters (Option B)
57
+ # ----------------------------------------------------------
58
+ def test_sinusoidal_matrix_same_shape():
59
+ A = np.array([[1.0, 2.0],
60
+ [3.0, 4.0]])
61
+ F = np.array([[1.0, 0.5],
62
+ [2.0, 1.0]])
63
+ O = np.array([[0.0, 10.0],
64
+ [20.0, 30.0]])
65
+ P = np.array([[0.0, np.pi / 2],
66
+ [np.pi, 0.0]])
67
+
68
+ s = Sinusoidal("s", amplitude=A, frequency=F, offset=O, phase=P)
69
+ s.output_update(0.0, 0.1)
70
+
71
+ expected = A * np.sin(2 * np.pi * F * 0.0 + P) + O
72
+ assert np.allclose(s.outputs["out"], expected)
73
+
74
+
75
+ def test_sinusoidal_matrix_broadcast_scalar_phase():
76
+ A = np.array([[1.0, 2.0],
77
+ [3.0, 4.0]])
78
+ F = np.array([[1.0, 0.5],
79
+ [2.0, 1.0]])
80
+
81
+ s = Sinusoidal("s", amplitude=A, frequency=F, offset=0.0, phase=0.0) # scalars broadcast
82
+ s.output_update(0.0, 0.1)
83
+
84
+ expected = A * np.sin(2 * np.pi * F * 0.0 + 0.0) + 0.0
85
+ assert np.allclose(s.outputs["out"], expected)
86
+
87
+
88
+ # ----------------------------------------------------------
89
+ # 5) Inconsistent parameter shapes among non-scalars -> error
90
+ # ----------------------------------------------------------
91
+ def test_sinusoidal_inconsistent_shapes():
92
+ with pytest.raises(ValueError):
93
+ Sinusoidal(
94
+ "s",
95
+ amplitude=np.zeros((2, 2)),
96
+ frequency=np.zeros((2, 3)), # mismatch
97
+ offset=0.0,
98
+ phase=0.0,
99
+ )
100
+
101
+
102
+ # ----------------------------------------------------------
103
+ # 6) Illegal ndims (>2) -> error
104
+ # ----------------------------------------------------------
105
+ def test_sinusoidal_illegal_ndim():
106
+ with pytest.raises(ValueError):
107
+ Sinusoidal(
108
+ "s",
109
+ amplitude=np.zeros((2, 2, 2)), # ndim=3 forbidden
110
+ frequency=1.0,
111
+ offset=0.0,
112
+ phase=0.0,
113
+ )
@@ -0,0 +1,76 @@
1
+ import numpy as np
2
+ import pytest
3
+
4
+ from pySimBlocks.blocks.sources.step import Step
5
+
6
+
7
+ def test_step_scalar_before_after():
8
+ s = Step("s", 0.0, 1.0, start_time=1.0)
9
+ s.initialize(0.0)
10
+ assert np.allclose(s.outputs["out"], [[0.0]])
11
+ s.initialize(2.0)
12
+ assert np.allclose(s.outputs["out"], [[1.0]])
13
+
14
+
15
+ def test_step_vector():
16
+ s = Step("s", [0, 0], [1, 2], start_time=1.0)
17
+ s.initialize(0.0)
18
+ assert np.allclose(s.outputs["out"], [[0.0], [0.0]])
19
+ s.initialize(2.0)
20
+ assert np.allclose(s.outputs["out"], [[1.0], [2.0]])
21
+
22
+
23
+ def test_step_matrix_same_shape():
24
+ vb = np.array([[0.0, 0.5],
25
+ [1.0, 1.5]])
26
+ va = np.array([[2.0, 2.5],
27
+ [3.0, 3.5]])
28
+ s = Step("s", vb, va, start_time=1.0)
29
+
30
+ s.initialize(0.0)
31
+ assert np.allclose(s.outputs["out"], vb)
32
+ s.initialize(2.0)
33
+ assert np.allclose(s.outputs["out"], va)
34
+
35
+
36
+ def test_step_scalar_broadcast_to_matrix_before():
37
+ # value_before scalar, value_after matrix -> broadcast before
38
+ vb = 0.0
39
+ va = np.array([[1.0, 2.0],
40
+ [3.0, 4.0]])
41
+ s = Step("s", vb, va, start_time=1.0)
42
+
43
+ s.initialize(0.0)
44
+ assert np.allclose(s.outputs["out"], np.zeros((2, 2)))
45
+ s.initialize(2.0)
46
+ assert np.allclose(s.outputs["out"], va)
47
+
48
+
49
+ def test_step_scalar_broadcast_to_matrix_after():
50
+ # value_before matrix, value_after scalar -> broadcast after
51
+ vb = np.array([[1.0, 2.0],
52
+ [3.0, 4.0]])
53
+ va = 0.0
54
+ s = Step("s", vb, va, start_time=1.0)
55
+
56
+ s.initialize(0.0)
57
+ assert np.allclose(s.outputs["out"], vb)
58
+ s.initialize(2.0)
59
+ assert np.allclose(s.outputs["out"], np.zeros((2, 2)))
60
+
61
+
62
+ def test_step_shape_mismatch_non_scalars():
63
+ vb = np.zeros((2, 2))
64
+ va = np.zeros((2, 3))
65
+ with pytest.raises(ValueError):
66
+ Step("s", vb, va, start_time=1.0)
67
+
68
+
69
+ def test_step_bad_start_time_type():
70
+ with pytest.raises(TypeError):
71
+ Step("s", 0, 1, start_time=[1.0])
72
+
73
+
74
+ def test_step_bad_ndim():
75
+ with pytest.raises(ValueError):
76
+ Step("s", np.zeros((2, 2, 2)), 1.0, start_time=1.0)