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,162 @@
1
+ import pytest
2
+
3
+ from pySimBlocks.gui.main_window import MainWindow
4
+
5
+ def test_main_window_opens(qtbot, tmp_path):
6
+
7
+ window = MainWindow(tmp_path)
8
+
9
+ qtbot.addWidget(window)
10
+ window.show()
11
+
12
+ qtbot.waitUntil(lambda: window.isVisible())
13
+
14
+ assert window.isVisible()
15
+ assert window.view is not None
16
+ assert window.blocks is not None
17
+ assert window.toolbar is not None
18
+ assert window.project_controller is not None
19
+ assert len(window.block_registry) > 0
20
+
21
+
22
+ @pytest.fixture
23
+ def minimal_project(tmp_path):
24
+ """Create a minimal pySimBlocks project.yaml."""
25
+ project_yaml = tmp_path / "project.yaml"
26
+ project_yaml.write_text(
27
+ """schema_version: 1
28
+ project:
29
+ name: test_gui_project
30
+ simulation:
31
+ dt: 0.01
32
+ T: 3.0
33
+ solver: fixed
34
+ logging:
35
+ - ref.outputs.out
36
+ - plant.outputs.y
37
+ - pid.outputs.u
38
+ plots:
39
+ - title: Ref vs Output
40
+ signals: [ref.outputs.out, plant.outputs.y]
41
+ - title: Command
42
+ signals: [pid.outputs.u]
43
+ diagram:
44
+ blocks:
45
+ - name: ref
46
+ category: sources
47
+ type: step
48
+ parameters:
49
+ value_before: [[0.0]]
50
+ value_after: [[1.0]]
51
+ start_time: 0.5
52
+ - name: error
53
+ category: operators
54
+ type: sum
55
+ parameters:
56
+ signs: +-
57
+ - name: pid
58
+ category: controllers
59
+ type: pid
60
+ parameters:
61
+ controller: PI
62
+ Kp: [[2.0]]
63
+ Ki: [[1.0]]
64
+ Kd: 0.0
65
+ - name: plant
66
+ category: systems
67
+ type: linear_state_space
68
+ parameters:
69
+ A: [[0.95]]
70
+ B: [[0.5]]
71
+ C: [[1.0]]
72
+ x0: [[0.0]]
73
+ connections:
74
+ - name: c1
75
+ ports: [ref.out, error.in1]
76
+ - name: c2
77
+ ports: [plant.y, error.in2]
78
+ - name: c3
79
+ ports: [error.out, pid.e]
80
+ - name: c4
81
+ ports: [pid.u, plant.u]
82
+ gui:
83
+ layout:
84
+ blocks:
85
+ ref:
86
+ x: 0.0
87
+ y: -80.0
88
+ orientation: normal
89
+ error:
90
+ x: 195.0
91
+ y: -71.0
92
+ orientation: normal
93
+ pid:
94
+ x: 364.0
95
+ y: -71.0
96
+ orientation: normal
97
+ plant:
98
+ x: 535.0
99
+ y: -70.0
100
+ orientation: normal
101
+ """
102
+ )
103
+
104
+ return tmp_path
105
+
106
+ def test_main_window_loads_project(qtbot, minimal_project):
107
+ """Test that MainWindow opens and auto-loads a project from project.yaml."""
108
+ window = MainWindow(minimal_project)
109
+ qtbot.addWidget(window)
110
+ window.show()
111
+ qtbot.waitUntil(lambda: window.isVisible())
112
+
113
+ assert window.isVisible()
114
+ assert window.project_controller.project_state.directory_path == minimal_project
115
+ assert window.view is not None
116
+ assert window.blocks is not None
117
+ assert window.toolbar is not None
118
+
119
+ assert len(window.block_registry) > 0
120
+ block_names = [item.instance.name for item in window.view.block_items.values()]
121
+ for expected in ["ref", "error", "pid", "plant"]:
122
+ assert expected in block_names
123
+
124
+ expected_connections = [
125
+ ("ref", "out", "error", "in1"),
126
+ ("plant", "y", "error", "in2"),
127
+ ("error", "out", "pid", "e"),
128
+ ("pid", "u", "plant", "u"),
129
+ ]
130
+
131
+ actual_connections = [
132
+ (
133
+ conn.src_block().name,
134
+ conn.src_port.name,
135
+ conn.dst_block().name,
136
+ conn.dst_port.name
137
+ )
138
+ for conn in window.view.connections.keys()
139
+ ]
140
+
141
+ for conn in expected_connections:
142
+ assert conn in actual_connections
143
+
144
+ def test_run_simulation(qtbot, minimal_project):
145
+ """
146
+ Smoke test: Load a minimal project and run the simulation via ToolBarView.
147
+ """
148
+ window = MainWindow(minimal_project)
149
+ qtbot.addWidget(window)
150
+ window.show()
151
+ qtbot.waitUntil(lambda: window.isVisible())
152
+
153
+ project_state = window.project_state
154
+ toolbar = window.toolbar
155
+
156
+ assert not bool(project_state.logs)
157
+
158
+ toolbar.on_run_sim()
159
+
160
+ assert bool(project_state.logs)
161
+ assert isinstance(project_state.logs, dict)
162
+ assert all(isinstance(v, list) for v in project_state.logs.values())
@@ -0,0 +1,16 @@
1
+ from pySimBlocks.gui.blocks.block_dialog_session import BlockDialogSession
2
+ from pySimBlocks.gui.blocks.controllers.pid import PIDMeta
3
+ from pySimBlocks.gui.models.block_instance import BlockInstance
4
+
5
+
6
+ def test_gather_params_keeps_inactive_values_for_cache():
7
+ meta = PIDMeta()
8
+ instance = BlockInstance(meta)
9
+ session = BlockDialogSession(meta, instance)
10
+
11
+ session.local_params["controller"] = "PD" # Ki inactive for PD
12
+ session.local_params["Ki"] = 10
13
+
14
+ gathered = meta.gather_params(session)
15
+
16
+ assert gathered["Ki"] == 10
@@ -0,0 +1,17 @@
1
+ from pySimBlocks.gui.blocks.sources.file_source import FileSourceMeta
2
+
3
+
4
+ def test_file_source_meta_conditional_parameters():
5
+ meta = FileSourceMeta()
6
+
7
+ npz_values = {"file_path": "data.npz"}
8
+ assert meta.is_parameter_active("key", npz_values) is True
9
+ assert meta.is_parameter_active("use_time", npz_values) is True
10
+
11
+ npy_values = {"file_path": "data.npy"}
12
+ assert meta.is_parameter_active("key", npy_values) is False
13
+ assert meta.is_parameter_active("use_time", npy_values) is False
14
+
15
+ csv_values = {"file_path": "data.csv"}
16
+ assert meta.is_parameter_active("key", csv_values) is True
17
+ assert meta.is_parameter_active("use_time", csv_values) is True
@@ -0,0 +1,28 @@
1
+ from pySimBlocks.gui.blocks.sources.function_source import FunctionSourceMeta
2
+ from pySimBlocks.gui.models.block_instance import BlockInstance
3
+
4
+
5
+ def test_function_source_meta_definition():
6
+ meta = FunctionSourceMeta()
7
+
8
+ assert meta.category == "sources"
9
+ assert meta.type == "function_source"
10
+ assert [p.name for p in meta.parameters] == [
11
+ "file_path",
12
+ "function_name",
13
+ "output_keys",
14
+ "sample_time",
15
+ ]
16
+ assert len(meta.inputs) == 0
17
+ assert len(meta.outputs) == 1
18
+ assert meta.outputs[0].name == "out"
19
+
20
+
21
+ def test_function_source_meta_resolves_dynamic_output_ports():
22
+ meta = FunctionSourceMeta()
23
+ instance = BlockInstance(meta)
24
+ instance.update_params({"output_keys": ["y", "z"]})
25
+ instance.resolve_ports()
26
+
27
+ outputs = [p for p in instance.ports if p.direction == "output"]
28
+ assert [p.name for p in outputs] == ["y", "z"]
@@ -0,0 +1,217 @@
1
+ import numpy as np
2
+ from pathlib import Path
3
+
4
+ from pySimBlocks import Model, Simulator, SimulationConfig
5
+ from pySimBlocks.blocks.sources import Step
6
+ from pySimBlocks.blocks.systems import LinearStateSpace
7
+ from pySimBlocks.blocks.operators import Sum
8
+ from pySimBlocks.blocks.controllers import Pid
9
+
10
+
11
+ # ---------------------------------------------------------------------
12
+ # Helpers
13
+ # ---------------------------------------------------------------------
14
+
15
+ def _dc_motor_discrete_matrices(dt: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
16
+ """
17
+ DC motor discrete-time model used in the Simulink reference dataset.
18
+
19
+ Parameters match the reference scripts:
20
+ R=0.1, L=0.5, J=0.01, K=0.1, a=0.001
21
+ """
22
+ R = 0.1
23
+ L = 0.5
24
+ J = 0.01
25
+ K = 0.1
26
+ a = 0.001
27
+
28
+ A = np.array([[1 - dt * R / L, -dt * K / L],
29
+ [dt * K / J, 1 - dt * a / J]])
30
+ B = np.array([[dt / L], [0]])
31
+ C = np.array([[0, 1]])
32
+ return A, B, C
33
+
34
+
35
+ def _load_simulink_npz() -> dict[str, np.ndarray]:
36
+ """
37
+ Loads the reference dataset shipped in the repository.
38
+
39
+ Expected location:
40
+ tests/data/simulink/simulation.npz
41
+ """
42
+ data_path = Path(__file__).resolve().parent / "data" / "simulink" / "dc_motor.npz"
43
+ if not data_path.exists():
44
+ raise FileNotFoundError(
45
+ f"Simulink reference dataset not found: {data_path}\n"
46
+ "Place simulation.npz under tests/data/simulink/ (or update this path)."
47
+ )
48
+ return dict(np.load(str(data_path)))
49
+
50
+
51
+ def _extract(logs: dict, key: str) -> np.ndarray:
52
+ v = np.array(logs[key])
53
+ return v.reshape(-1).astype(float)
54
+
55
+
56
+ def _assert_close(ref: np.ndarray, got: np.ndarray, *, atol: float, rtol: float, label: str):
57
+ assert ref.shape == got.shape, f"{label}: shape mismatch ref={ref.shape} got={got.shape}"
58
+ if not np.allclose(ref, got, atol=atol, rtol=rtol):
59
+ err = np.linalg.norm(ref - got)
60
+ mx = np.max(np.abs(ref - got))
61
+ raise AssertionError(
62
+ f"{label}: not close (atol={atol}, rtol={rtol}). "
63
+ f"L2={err:.3e}, max_abs={mx:.3e}"
64
+ )
65
+
66
+
67
+ # ---------------------------------------------------------------------
68
+ # Reference simulations (pySimBlocks)
69
+ # ---------------------------------------------------------------------
70
+
71
+ def _sim_openloop(A: np.ndarray, B: np.ndarray, C: np.ndarray, T: float, dt: float) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
72
+ ref = Step("command", start_time=1.0, value_before=0.0, value_after=1.0)
73
+ motor = LinearStateSpace("motor", A, B, C)
74
+
75
+ model = Model("openloop")
76
+ model.add_block(ref)
77
+ model.add_block(motor)
78
+ model.connect("command", "out", "motor", "u")
79
+
80
+ sim_cfg = SimulationConfig(dt, T, logging=["command.outputs.out", "motor.outputs.y"])
81
+ sim = Simulator(model, sim_cfg, verbose=False)
82
+ logs = sim.run()
83
+
84
+ t = _extract(logs, "time")
85
+ u = _extract(logs, "command.outputs.out")
86
+ w = _extract(logs, "motor.outputs.y")
87
+ return t, u, w
88
+
89
+
90
+ def _sim_pi(A: np.ndarray, B: np.ndarray, C: np.ndarray, Kp: np.ndarray, Ki: np.ndarray, T: float, dt: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
91
+ ref = Step("ref", start_time=1.0, value_before=0.0, value_after=1.0)
92
+ motor = LinearStateSpace("motor", A, B, C)
93
+ error = Sum("error", signs="+-")
94
+ pid = Pid("pid", Kp=Kp, Ki=Ki, controller="PI")
95
+
96
+ model = Model("pi")
97
+ for b in [ref, motor, error, pid]:
98
+ model.add_block(b)
99
+
100
+ model.connect("ref", "out", "error", "in1")
101
+ model.connect("motor", "y", "error", "in2")
102
+ model.connect("error", "out", "pid", "e")
103
+ model.connect("pid", "u", "motor", "u")
104
+
105
+ sim_cfg = SimulationConfig(dt, T, logging=["ref.outputs.out", "motor.outputs.y", "pid.outputs.u"])
106
+ sim = Simulator(model, sim_cfg, verbose=False)
107
+ logs = sim.run()
108
+
109
+ t = _extract(logs, "time")
110
+ r = _extract(logs, "ref.outputs.out")
111
+ w = _extract(logs, "motor.outputs.y")
112
+ u = _extract(logs, "pid.outputs.u")
113
+ return t, u, w, r
114
+
115
+
116
+ def _sim_pid(A: np.ndarray, B: np.ndarray, C: np.ndarray, Kp: np.ndarray, Ki: np.ndarray, Kd: np.ndarray,
117
+ T: float, dt: float, integration_method: str | None = None) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
118
+ ref = Step("ref", start_time=1.0, value_before=0.0, value_after=1.0)
119
+ motor = LinearStateSpace("motor", A, B, C)
120
+ error = Sum("error", signs="+-")
121
+
122
+ if integration_method is None:
123
+ pid = Pid("pid", Kp=Kp, Ki=Ki, Kd=Kd)
124
+ else:
125
+ pid = Pid("pid", Kp=Kp, Ki=Ki, Kd=Kd, integration_method=integration_method)
126
+
127
+ model = Model("pid")
128
+ for b in [ref, motor, error, pid]:
129
+ model.add_block(b)
130
+
131
+ model.connect("ref", "out", "error", "in1")
132
+ model.connect("motor", "y", "error", "in2")
133
+ model.connect("error", "out", "pid", "e")
134
+ model.connect("pid", "u", "motor", "u")
135
+
136
+ sim_cfg = SimulationConfig(dt, T, logging=["ref.outputs.out", "motor.outputs.y", "pid.outputs.u"])
137
+ sim = Simulator(model, sim_cfg, verbose=False)
138
+ logs = sim.run()
139
+
140
+ t = _extract(logs, "time")
141
+ r = _extract(logs, "ref.outputs.out")
142
+ w = _extract(logs, "motor.outputs.y")
143
+ u = _extract(logs, "pid.outputs.u")
144
+ return t, u, w, r
145
+
146
+
147
+ # ---------------------------------------------------------------------
148
+ # Regression tests vs Simulink reference
149
+ # ---------------------------------------------------------------------
150
+ def test_regression_openloop_matches_simulink():
151
+ data = _load_simulink_npz()
152
+
153
+ dt = 0.1
154
+ T = 30.0
155
+ A, B, C = _dc_motor_discrete_matrices(dt)
156
+
157
+ t, u, w = _sim_openloop(A, B, C, T, dt)
158
+
159
+ _assert_close(data["time"].reshape(-1), t, atol=1e-12, rtol=1e-12, label="time")
160
+ _assert_close(data["uol"].reshape(-1), u, atol=1e-12, rtol=1e-12, label="uol")
161
+ _assert_close(data["wol"].reshape(-1), w, atol=1e-12, rtol=1e-12, label="wol")
162
+
163
+
164
+ def test_regression_pi_matches_simulink():
165
+ data = _load_simulink_npz()
166
+
167
+ dt = 0.1
168
+ T = 30.0
169
+ A, B, C = _dc_motor_discrete_matrices(dt)
170
+
171
+ Kp = np.array([[0.001]])
172
+ Ki = np.array([[0.02]])
173
+
174
+ t, u, w, r = _sim_pi(A, B, C, Kp, Ki, T, dt)
175
+
176
+ _assert_close(data["time"].reshape(-1), t, atol=1e-12, rtol=1e-12, label="time")
177
+ _assert_close(data["r"].reshape(-1), r, atol=1e-12, rtol=1e-12, label="r (reference)")
178
+ _assert_close(data["upi"].reshape(-1), u, atol=1e-12, rtol=1e-12, label="upi")
179
+ _assert_close(data["wpi"].reshape(-1), w, atol=1e-12, rtol=1e-12, label="wpi")
180
+
181
+
182
+ def test_regression_pid_matches_simulink():
183
+ data = _load_simulink_npz()
184
+
185
+ dt = 0.1
186
+ T = 30.0
187
+ A, B, C = _dc_motor_discrete_matrices(dt)
188
+
189
+ Kp = np.array([[0.001]])
190
+ Ki = np.array([[0.02]])
191
+ Kd = np.array([[0.01]])
192
+
193
+ t, u, w, r = _sim_pid(A, B, C, Kp, Ki, Kd, T, dt)
194
+
195
+ _assert_close(data["time"].reshape(-1), t, atol=1e-12, rtol=1e-12, label="time")
196
+ _assert_close(data["r"].reshape(-1), r, atol=1e-12, rtol=1e-12, label="r (reference)")
197
+ _assert_close(data["upid"].reshape(-1), u, atol=1e-12, rtol=1e-12, label="upid")
198
+ _assert_close(data["wpid"].reshape(-1), w, atol=1e-12, rtol=1e-12, label="wpid")
199
+
200
+
201
+ def test_regression_pid_backward_euler_matches_simulink():
202
+ data = _load_simulink_npz()
203
+
204
+ dt = 0.1
205
+ T = 30.0
206
+ A, B, C = _dc_motor_discrete_matrices(dt)
207
+
208
+ Kp = np.array([[0.001]])
209
+ Ki = np.array([[0.02]])
210
+ Kd = np.array([[0.01]])
211
+
212
+ t, u, w, r = _sim_pid(A, B, C, Kp, Ki, Kd, T, dt, integration_method="euler backward")
213
+
214
+ _assert_close(data["time"].reshape(-1), t, atol=1e-12, rtol=1e-12, label="time")
215
+ _assert_close(data["r"].reshape(-1), r, atol=1e-12, rtol=1e-12, label="r (reference)")
216
+ _assert_close(data["upid_backward"].reshape(-1), u, atol=1e-12, rtol=1e-12, label="upid_backward")
217
+ _assert_close(data["wpid_backward"].reshape(-1), w, atol=1e-12, rtol=1e-12, label="wpid_backward")