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 time
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ import parameters as prm
6
+ from emioapi import EmioMotors
7
+
8
+ from pySimBlocks.project import load_simulator_from_project
9
+ from pySimBlocks.real_time import RealTimeRunner
10
+
11
+ try:
12
+ BASE_DIR = Path(__file__).parent.resolve()
13
+ except Exception:
14
+ BASE_DIR = Path("")
15
+
16
+
17
+ # ------------------------------------------------------------------------------
18
+ # Process
19
+ # ------------------------------------------------------------------------------
20
+ def process_block_diagram(shared_markers_pos, shared_ref_ol, shared_ref_cl,
21
+ shared_control_mode, shared_update,
22
+ event_frame, event_measure):
23
+ """pySimBlocks runner process.
24
+ """
25
+ init_angles = np.array([0.7, 0, 0.7, 0])
26
+ motors = setup_motors(init_angles)
27
+
28
+ # init pySimBlocks runner
29
+ runner = setup_block_diagram()
30
+
31
+ # Initialize variables
32
+ measure = np.zeros((prm.nb_markers * 2, 1))
33
+ ref_ol = np.zeros((2, 1))
34
+ ref_cl = np.zeros((2, 1))
35
+ control_mode = np.zeros((1, 1))
36
+ command = np.zeros((2, 1))
37
+
38
+ # Initial time for dt measurement
39
+ t = time.perf_counter()
40
+ dt_init = 1/60
41
+ dt_list = []
42
+
43
+ while True:
44
+ event_frame.wait()
45
+ event_frame.clear()
46
+
47
+ send_motors_command(motors, command, init_angles)
48
+
49
+ event_measure.wait()
50
+ event_measure.clear()
51
+
52
+ if dt_init is not None:
53
+ t = time.perf_counter()
54
+ dt = dt_init
55
+ dt_init = None
56
+ else:
57
+ t2 = time.perf_counter()
58
+ dt = t2 - t
59
+ t = t2
60
+ dt_list.append(dt)
61
+
62
+ # data from camera
63
+ with shared_markers_pos.get_lock():
64
+ measure[:, 0] = shared_markers_pos[:]
65
+
66
+ with shared_update.get_lock():
67
+ if shared_update.value:
68
+ with shared_ref_ol.get_lock():
69
+ ref_ol[:, 0] = shared_ref_ol[:]
70
+ with shared_ref_cl.get_lock():
71
+ ref_cl[:, 0] = shared_ref_cl[:]
72
+ with shared_control_mode.get_lock():
73
+ control_mode[0,0] = shared_control_mode.value
74
+ shared_update.value = False
75
+
76
+ outs = runner.tick(
77
+ inputs={
78
+ "Camera": measure,
79
+ "Ref_ol": ref_ol,
80
+ "Ref_cl": ref_cl,
81
+ "Mode": control_mode
82
+ },
83
+ dt=dt,
84
+ pace=False
85
+ )
86
+ command = outs["Cmd"]
87
+
88
+
89
+ # ------------------------------------------------------------------------------
90
+ # Helpers
91
+ # ------------------------------------------------------------------------------
92
+ def setup_block_diagram():
93
+ sim, _plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
94
+ runner = RealTimeRunner(
95
+ sim,
96
+ input_blocks=["Camera", "Ref_cl", "Ref_ol", "Mode"],
97
+ output_blocks=["Cmd"],
98
+ target_dt=1/60
99
+ )
100
+ runner.initialize()
101
+ return runner
102
+
103
+
104
+ # -------------------------------------------------------
105
+ def setup_motors(init_angles=[0, 0, 0, 0]):
106
+ motors = EmioMotors()
107
+ while not motors.open():
108
+ print("Waiting for motors to open...")
109
+ time.sleep(1)
110
+ print("Motors opened successfully.")
111
+ motors.position_p_gain = [2000, 800, 2000, 800]
112
+ motors.position_i_gain = [0, 0, 0, 0]
113
+ motors.position_d_gain = [50, 0, 50, 0]
114
+ time.sleep(1)
115
+ motors.angles = init_angles
116
+ return motors
117
+
118
+ # -------------------------------------------------------
119
+ def send_motors_command(motors, command, init_angles=np.array([0, 0, 0, 0])):
120
+ command = command.flatten()
121
+ motors.angles = [command[0] + init_angles[0], init_angles[1],
122
+ command[1] + init_angles[2], init_angles[3]]
123
+
124
+ # -------------------------------------------------------
125
+ def get_motors_position(motors, init_angles=np.array([0, 0, 0, 0])):
126
+ motors_pos = np.array(motors.angles) - init_angles
127
+ return motors_pos[[0, 2]].reshape((2, 1))
128
+
129
+ # ------------------------------------------------------------------------------
130
+ # BLOCK DIAGRAM FUNCTIONS
131
+ # ------------------------------------------------------------------------------
132
+ def select_cmd(t, dt, u_cl, u_ol, mode):
133
+ try:
134
+ mode = mode.item()
135
+ except Exception:
136
+ return {"u": np.zeros((2,1))}
137
+
138
+ if mode == 0:
139
+ return {"u": u_ol}
140
+ elif mode == 1:
141
+ return {"u": u_cl}
142
+ else:
143
+ return {"u": np.zeros((2,1))}
144
+
145
+ # -------------------------------------------------------
146
+ def filter_first_order(t, dt, u, u_prev):
147
+ """Apply a first-order low-pass filter to the signal."""
148
+ if dt <= 0:
149
+ return {"u_filter" : np.zeros((2,1))}
150
+ cutoffFreq = 30. # Cutoff frequency in Hz
151
+ samplingFreq = 1 / dt
152
+
153
+ timeConstant = 1 / (2 * np.pi * cutoffFreq)
154
+ samplingPeriod = 1 / samplingFreq
155
+ alpha = samplingPeriod / (timeConstant + samplingPeriod)
156
+ uf = alpha * u + (1 - alpha) * u_prev
157
+
158
+ return {"u_filter" : uf}
159
+
160
+
161
+ if __name__ == "__main__":
162
+ setup_block_diagram()
@@ -0,0 +1,93 @@
1
+ import json
2
+
3
+ import cv2 as cv
4
+ import numpy as np
5
+ import parameters as prm
6
+ from emioapi._depthcamera import DepthCamera
7
+
8
+
9
+ # ------------------------------------------------------------------------------
10
+ # Process
11
+ # ------------------------------------------------------------------------------
12
+ def process_camera(shared_markers_pos, shared_start,
13
+ event_frame, event_measure):
14
+ """Update tracker position."""
15
+ camera = setup_camera()
16
+
17
+ init_pos = np.zeros((2 * prm.nb_markers))
18
+ pos = np.zeros((2 * prm.nb_markers))
19
+ start = False
20
+
21
+ while True:
22
+ # get frame from camera
23
+ ret = camera.get_frame()
24
+ event_frame.set()
25
+
26
+ if not ret:
27
+ raise RuntimeError("Camera frame not received")
28
+
29
+ pos = process_frame(camera, pos)
30
+
31
+ if start:
32
+ pos -= init_pos
33
+ with shared_markers_pos.get_lock():
34
+ shared_markers_pos[:] = pos
35
+ event_measure.set()
36
+
37
+ else:
38
+ init_pos = pos
39
+ with shared_start.get_lock():
40
+ start = shared_start.value
41
+
42
+ k = cv.waitKey(1)
43
+ if k == ord('q'):
44
+ camera.quit()
45
+ break
46
+
47
+
48
+ # ------------------------------------------------------------------------------
49
+ # Helpers
50
+ # ------------------------------------------------------------------------------
51
+ def setup_camera():
52
+ json_path = prm.data_path / "cameraparameter.json"
53
+ with json_path.open('r') as fp:
54
+ json_parameters = json.load(fp)
55
+ camera = DepthCamera(
56
+ show_video_feed=True,
57
+ tracking=True,
58
+ compute_point_cloud=False,
59
+ parameter=json_parameters)
60
+ camera.set_fps(60)
61
+ camera.set_depth_min(0)
62
+ camera.set_depth_max(1000)
63
+ camera.open()
64
+ return camera
65
+
66
+ # -------------------------------------------------------
67
+ def pixel_to_mm(points, depth):
68
+ ppx, ppy = 319.475, 240.962
69
+ fx, fy = 382.605, 382.605
70
+ points[:, 0] = ((points[:, 0] - ppx) / fx) * depth
71
+ points[:, 1] = ((points[:, 1] - ppy) / fy) * depth
72
+ points = np.column_stack((points[:, 2], -points[:, 1], points[:, 0]))
73
+ return points.copy()
74
+
75
+ # -------------------------------------------------------
76
+ def camera_to_sofa_order(points):
77
+ i_ymax = np.argmax(points[:, 1])
78
+ i_rest = [i for i in range(prm.nb_markers) if i != i_ymax]
79
+ i_sorted_z = sorted(i_rest, key=lambda i: points[i, 2])
80
+ new_order = [i_sorted_z[1], i_sorted_z[0], i_ymax]
81
+ return points[new_order].flatten()
82
+
83
+ # -------------------------------------------------------
84
+ def process_frame(camera, last_pos):
85
+ indices = [1, 2, 4, 5, 7, 8]
86
+ camera.process_frame()
87
+ if len(camera.trackers_pos) == prm.nb_markers:
88
+ pos = np.array(camera.trackers_camera).reshape(prm.nb_markers, 3).copy()
89
+ pos = pos.astype(np.float64)
90
+ pos = pixel_to_mm(pos, 249)
91
+ markers_pos = camera_to_sofa_order(pos)
92
+ return markers_pos[indices]
93
+ return last_pos
@@ -0,0 +1,231 @@
1
+ import tkinter as tk
2
+
3
+ import numpy as np
4
+ import parameters as prm
5
+
6
+
7
+ # -------------------------------------------------------------------------------
8
+ # Process
9
+ # -------------------------------------------------------------------------------
10
+ def process_gui(shared_ref_ol, shared_ref_cl, shared_start,
11
+ shared_control_mode, shared_update):
12
+ root = tk.Tk()
13
+ app = EmioRealTimeGUI(root, shared_ref_ol, shared_ref_cl,
14
+ shared_start, shared_control_mode, shared_update)
15
+ root.protocol("WM_DELETE_WINDOW", app.close_app)
16
+ root.mainloop()
17
+
18
+ # -------------------------------------------------------------------------------
19
+ # GUI Class
20
+ # -------------------------------------------------------------------------------
21
+ class EmioRealTimeGUI:
22
+ MOTOR_SCALE = np.pi / 2 / 100 # slider [-100..100] -> radians
23
+
24
+ def __init__(self, root, shared_ref_ol, shared_ref_cl,
25
+ shared_start, shared_control_mode, shared_update):
26
+ self.root = root
27
+ self.root.title('Emio Real Time Control')
28
+ self.root.geometry("500x350")
29
+
30
+ # ------- Variables -------
31
+ # shared
32
+ self.shared_ref_ol = shared_ref_ol
33
+ self.shared_ref_cl = shared_ref_cl
34
+ self.shared_start = shared_start
35
+ self.shared_control_mode = shared_control_mode
36
+ self.shared_update = shared_update
37
+
38
+ # local
39
+ self.start = False
40
+ self.active = True
41
+ self.control_mode = prm.ControlMode.OPEN_LOOP
42
+
43
+ # ------- Information Frame -------
44
+ info_frame = tk.Frame(root)
45
+ info_frame.pack()
46
+ info = ["Start", "Active", "Control"]
47
+ info_values = ["False", "True", "Open Loop"]
48
+ self.label_info = {}
49
+ for i, item in enumerate(info):
50
+ self.label_info[item] = tk.Label(
51
+ info_frame,
52
+ text=f"{item}: {info_values[i]}", font=("Arial", 12))
53
+ self.label_info[item].pack(side="left", padx=5, pady=5)
54
+
55
+ # ------- Control Frame -------
56
+ self.build_sliders()
57
+ self.build_buttons()
58
+
59
+
60
+ # --------------------------------------------------------------------------
61
+ # Build methods
62
+ # --------------------------------------------------------------------------
63
+ def build_sliders(self):
64
+ self.label_motors = []
65
+ self.sliders_motor = []
66
+ motor_label_frame = tk.Frame(self.root)
67
+ motor_label_frame.pack()
68
+ motor_slider_frame = tk.Frame(self.root)
69
+ motor_slider_frame.pack()
70
+
71
+ self.label_ref = []
72
+ self.sliders_ref = []
73
+ ref_label_frame = tk.Frame(self.root)
74
+ ref_label_frame.pack()
75
+ ref_slider_frame = tk.Frame(self.root)
76
+ ref_slider_frame.pack()
77
+
78
+ for i in range(2):
79
+ self.label_motors.append(
80
+ self._build_label(motor_label_frame, text=f"Motor {i + 1}: 0.00 (rad)")
81
+ )
82
+ self.sliders_motor.append(
83
+ self._build_slider(
84
+ motor_slider_frame,
85
+ command=lambda val, idx=i: self._motor_action(val, idx),
86
+ )
87
+ )
88
+
89
+ self.label_ref.append(
90
+ self._build_label(ref_label_frame, text=f"Ref {i + 1}: 0.00 (mm)")
91
+ )
92
+ self.sliders_ref.append(
93
+ self._build_slider(
94
+ ref_slider_frame,
95
+ command=lambda val, idx=i: self._ref_action(val, idx),
96
+ )
97
+ )
98
+
99
+ control_label_frame = tk.Frame(self.root)
100
+ control_label_frame.pack()
101
+ control_slider_frame = tk.Frame(self.root)
102
+ control_slider_frame.pack()
103
+
104
+ self.label_control = self._build_label(
105
+ control_label_frame, text="Desired Control Mode: Open Loop"
106
+
107
+ )
108
+ self.slider_control = self._build_slider(
109
+ control_slider_frame,
110
+ command=self.slider_control_action,
111
+ from_=0, to=1
112
+ )
113
+
114
+ # ----------------------------------------------------------
115
+ def build_buttons(self):
116
+ button_frame = tk.Frame(self.root)
117
+ button_frame.pack(pady=10)
118
+
119
+ buttons = [
120
+ ("Start", self.start_action),
121
+ ("Switch Control", self.control_mode_action),
122
+ ("Active", self.active_action),
123
+ ]
124
+ for name, action in buttons:
125
+ tk.Button(button_frame, text=name, command=action).pack(side="left", padx=3)
126
+
127
+
128
+ # --------------------------------------------------------------------------
129
+ # Slider actions
130
+ # --------------------------------------------------------------------------
131
+ def _motor_action(self, val, index: int):
132
+ if not self.start:
133
+ self.sliders_motor[index].set(0)
134
+ return
135
+ cmd = float(val) * self.MOTOR_SCALE # radians
136
+ self.label_motors[index].config(text=f"Motor {index + 1}: {cmd:.2f} (rad)")
137
+ if self.active:
138
+ mode = prm.ControlMode(0)
139
+ self._push_current_commands(mode)
140
+
141
+ # ----------------------------------------------------------
142
+ def _ref_action(self, val, index: int):
143
+ if not self.start:
144
+ self.sliders_ref[index].set(0)
145
+ return
146
+ cmd = float(val) # mm
147
+ self.label_ref[index].config(text=f"Ref {index + 1}: {cmd:.2f} (mm)")
148
+ if self.active:
149
+ mode = prm.ControlMode(1)
150
+ self._push_current_commands(mode)
151
+
152
+ # ----------------------------------------------------------
153
+ def slider_control_action(self, val):
154
+ if not self.start:
155
+ self.slider_control.set(0)
156
+ return
157
+ self.control_mode = prm.ControlMode(int(val))
158
+ self.label_control.config(text=f"Desired Control Mode: {self.control_mode.label}")
159
+
160
+ # --------------------------------------------------------------------------
161
+ # Button actions
162
+ # --------------------------------------------------------------------------
163
+ def start_action(self):
164
+ if not self.start:
165
+ self.start = True
166
+ self.label_info["Start"].config(text="Start: True")
167
+ with self.shared_start.get_lock():
168
+ self.shared_start.value = self.start
169
+ mode = prm.ControlMode(0)
170
+ self._push_current_commands(mode)
171
+
172
+ # ----------------------------------------------------------
173
+ def active_action(self):
174
+ if not self.start:
175
+ return
176
+ self.active = not self.active
177
+ self.label_info["Active"].config(text=f"Active: {self.active}")
178
+
179
+ if self.active:
180
+ with self.shared_control_mode.get_lock():
181
+ mode = prm.ControlMode(int(self.shared_control_mode.value))
182
+ self._push_current_commands(mode)
183
+
184
+ # ----------------------------------------------------------
185
+ def control_mode_action(self):
186
+ if not self.start:
187
+ return
188
+ with self.shared_control_mode.get_lock():
189
+ self.shared_control_mode.value = int(self.control_mode)
190
+ with self.shared_update.get_lock():
191
+ self.shared_update.value = True
192
+
193
+ if self.control_mode == prm.ControlMode.OPEN_LOOP:
194
+ self.label_info["Control"].config(text="Control: Open Loop")
195
+ else:
196
+ self.label_info["Control"].config(text=f"Control: {self.control_mode.short}")
197
+
198
+ # --------------------------------------------------------------------------
199
+ # Other methods
200
+ # --------------------------------------------------------------------------
201
+ def close_app(self):
202
+ self.root.destroy()
203
+
204
+ # ----------------------------------------------------------
205
+ def _build_slider(self, frame, command, from_=-100, to=100):
206
+ slider = tk.Scale(frame, from_=from_, to=to, orient="horizontal", command=command)
207
+ slider.set(0)
208
+ slider.pack(side="left", padx=3, pady=5)
209
+ return slider
210
+
211
+ # ----------------------------------------------------------
212
+ def _build_label(self, frame, text):
213
+ label = tk.Label(frame, text=text)
214
+ label.pack(side="left", padx=3, pady=5)
215
+ return label
216
+
217
+ # ----------------------------------------------------------
218
+ def _push_current_commands(self, mode):
219
+ if mode == prm.ControlMode.OPEN_LOOP:
220
+ pos = [float(self.sliders_motor[i].get()) for i in range(2)]
221
+ with self.shared_ref_ol.get_lock():
222
+ self.shared_ref_ol[0] = pos[0] * self.MOTOR_SCALE
223
+ self.shared_ref_ol[1] = pos[1] * self.MOTOR_SCALE
224
+ else:
225
+ ref = [float(self.sliders_ref[i].get()) for i in range(2)]
226
+ with self.shared_ref_cl.get_lock():
227
+ self.shared_ref_cl[0] = ref[0]
228
+ self.shared_ref_cl[1] = ref[1]
229
+
230
+ with self.shared_update.get_lock():
231
+ self.shared_update.value = True
@@ -0,0 +1,69 @@
1
+ import numpy as np
2
+ import cvxpy as cp
3
+ import control as ct
4
+
5
+
6
+ def get_lmi_controller(A, B, C):
7
+ """Compute the LMI controller.
8
+ """
9
+ nx = A.shape[0]
10
+ nu = B.shape[1]
11
+ ny = C.shape[0]
12
+
13
+ # # Define the decay factor and matrices for the LQR problem
14
+ decay = 0.99
15
+ Qy = 1e0 * np.diag(ny//2 * [1, 0.5])
16
+ Q = C.T @ Qy @ C
17
+ eig = max(abs(np.linalg.eigvals(Q)))
18
+ Q = Q + 1e-3*eig * np.eye(nx)
19
+ R = 1e3 * np.eye(nu)
20
+ Q_inv = np.linalg.inv(Q)
21
+ R_inv = np.linalg.inv(R)
22
+
23
+ # Define the optimization variables
24
+ X = cp.Variable((nx, nx), symmetric=True)
25
+ Y = cp.Variable((nu, nx))
26
+ delta = cp.Variable(nonneg=True)
27
+
28
+ gamma_decay = cp.bmat([
29
+ [-X, 1/decay * (A @ X - B @ Y).T],
30
+ [1/decay * (A @ X - B @ Y), -X]
31
+ ])
32
+ gamma_lqr = cp.bmat([
33
+ [-X, (A @ X - B @ Y).T, X, Y.T],
34
+ [(A @ X - B @ Y), -X, np.zeros((nx, nx)), np.zeros((nx, nu))],
35
+ [X, np.zeros((nx, nx)), -Q_inv, np.zeros((nx, nu))],
36
+ [Y, np.zeros((nu, nx)), np.zeros((nu, nx)), -R_inv]
37
+ ])
38
+
39
+ matrix = [-X]
40
+ matrix.append(gamma_decay)
41
+ matrix.append(gamma_lqr)
42
+ matrix.append(cp.bmat([[-delta * np.eye(nx), np.eye(nx)], [np.eye(nx), -X]]))
43
+ constraints = [mat << -1e-5 * np.eye(mat.shape[0]) for mat in matrix]
44
+
45
+ # obj = cp.Minimize(0)
46
+ obj = cp.Minimize(delta)
47
+ pb = cp.Problem(obj, constraints)
48
+ pb.solve(solver="MOSEK")
49
+ print(f"Problem status: {pb.status}")
50
+ if pb.status != cp.OPTIMAL:
51
+ raise Exception(f"Control problem not solved. Status: {pb.status}")
52
+
53
+ P = np.linalg.inv(X.value)
54
+ K = Y.value @ P
55
+ # Feedforward gain
56
+ C_ctr = C[[0, 1]]
57
+ G = np.linalg.inv(C_ctr @ np.linalg.inv(np.eye(nx) - A + B @ K) @ B)
58
+
59
+ return K, G
60
+
61
+ def get_lmi_observer(A, C):
62
+ """Compute the LMI observer.
63
+ """
64
+ nx = A.shape[0]
65
+ ny = C.shape[0]
66
+ Q = 8e2 * np.eye(nx) # State error cost
67
+ R = 1e1 * np.eye(ny) # noise cost
68
+ L = ct.dlqr(A.T, C.T, Q, R)[0].T
69
+ return L
@@ -0,0 +1,57 @@
1
+ import time
2
+ import multiprocessing
3
+
4
+ import parameters as prm
5
+ from block_diagram import process_block_diagram
6
+ from camera import process_camera
7
+ from gui_emio import process_gui
8
+
9
+
10
+ def main():
11
+
12
+ # shared variables
13
+ shared_markers_pos = multiprocessing.Array("d", 2 * prm.nb_markers * [0.])
14
+ shared_ref_ol = multiprocessing.Array("d", 2 * [0.0])
15
+ shared_ref_cl = multiprocessing.Array("d", 2 * [0.0])
16
+
17
+ # shared bool
18
+ shared_start = multiprocessing.Value("b", False)
19
+ shared_control_mode = multiprocessing.Value("i", 0)
20
+ shared_update = multiprocessing.Value("b", False)
21
+
22
+ # shared event
23
+ event_frame = multiprocessing.Event()
24
+ event_measure = multiprocessing.Event()
25
+
26
+ # Create processes
27
+ p1 = multiprocessing.Process(target=process_camera, args=(
28
+ shared_markers_pos, shared_start,
29
+ event_frame, event_measure))
30
+
31
+ p2 = multiprocessing.Process(target=process_gui, args=(
32
+ shared_ref_ol, shared_ref_cl,
33
+ shared_start, shared_control_mode, shared_update))
34
+
35
+ p3 = multiprocessing.Process(target=process_block_diagram, args=(
36
+ shared_markers_pos, shared_ref_ol, shared_ref_cl,
37
+ shared_control_mode, shared_update, event_frame, event_measure))
38
+
39
+ p1.start()
40
+ p2.start()
41
+ p3.start()
42
+
43
+ try:
44
+ while True:
45
+ time.sleep(5)
46
+ except KeyboardInterrupt:
47
+ p1.terminate()
48
+ p2.terminate()
49
+ p3.terminate()
50
+
51
+ p1.join()
52
+ p2.join()
53
+ p3.join()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,54 @@
1
+ from enum import IntEnum
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+
6
+ from lmi import get_lmi_controller, get_lmi_observer
7
+
8
+
9
+ class ControlMode(IntEnum):
10
+ OPEN_LOOP = 0
11
+ STATE_FEEDBACK = 1
12
+
13
+ @property
14
+ def short(self) -> str:
15
+ return {0: "OL", 1: "SF"}[int(self)]
16
+
17
+ @property
18
+ def label(self) -> str:
19
+ return {
20
+ 0: "Open Loop",
21
+ 1: "State Feedback",
22
+ }[int(self)]
23
+
24
+
25
+ data_path = Path(__file__).parent / "data"
26
+
27
+ nb_markers = 3
28
+
29
+ # ctr_data = np.load(data_path / "controller_order5_linear.npz")
30
+ # K = ctr_data["feedback_gain"]
31
+ # G = ctr_data["feedforward_gain"]
32
+
33
+ obs_data = np.load(data_path / "observer_order5_linear.npz")
34
+ # L = obs_data["observer_gain"]
35
+ A = obs_data["state_matrix"]
36
+ B = obs_data["input_matrix"]
37
+ C = obs_data["output_matrix"]
38
+
39
+ x0 = np.zeros((A.shape[0], 1))
40
+ Abar = np.block([
41
+ [A, B],
42
+ [np.zeros((B.shape[1], A.shape[1] + B.shape[1]))]
43
+ ])
44
+ Bbar = np.block([
45
+ [np.zeros((A.shape[0], B.shape[1]))],
46
+ [np.eye(B.shape[1])]
47
+ ])
48
+ Cbar = np.block([
49
+ [C, np.zeros((C.shape[0], B.shape[1]))]
50
+ ])
51
+
52
+ K, G = get_lmi_controller(Abar, Bbar, Cbar)
53
+ L = get_lmi_observer(A, C)
54
+
@@ -0,0 +1,10 @@
1
+ from numpy import array, dot
2
+
3
+ M = array([[1., 2., 0.], [-8., 3., 2.], [0., 1., 1.]])
4
+ P = M.T @ M
5
+ q = array([[3., 2., 3.]]) @ M
6
+ #
7
+ # from qpsolvers import solve_qp
8
+ # x = solve_qp(P, q, None, None, None, None, None, solver="clarabel")
9
+ # print(f"QP solution: x = {x}")
10
+ #
@@ -0,0 +1,14 @@
1
+ from pathlib import Path
2
+ from pySimBlocks.project import load_simulator_from_project
3
+ from pySimBlocks.project.plot_from_config import plot_from_config
4
+
5
+ try:
6
+ BASE_DIR = Path(__file__).parent.resolve()
7
+ except Exception:
8
+ BASE_DIR = Path("")
9
+
10
+ sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
11
+
12
+ logs = sim.run()
13
+ if True and plot_cfg is not None:
14
+ plot_from_config(logs, plot_cfg)