Skip to content

Commit f1f6a4c

Browse files
committed
Planar HSA as a class
1 parent b094c37 commit f1f6a4c

File tree

7 files changed

+431
-171
lines changed

7 files changed

+431
-171
lines changed

examples/simulate_class_planar_hsa.py

Lines changed: 166 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,50 @@
22
import jax.numpy as jnp
33
from jax import jit, vmap
44

5+
jax.config.update("jax_enable_x64", True) # double precision
6+
57
import jsrm
68
from jsrm.systems.class_planar_hsa import PlanarHSA
79
from jsrm.parameters.hsa_params import PARAMS_FPU_CONTROL, PARAMS_FPU_HYSTERESIS_CONTROL
810

9-
from typing import Callable
1011
from jax import Array
1112

1213
import numpy as onp
1314

1415
from diffrax import Tsit5
16+
import matplotlib.pyplot as plt
17+
1518
import cv2 # importing cv2
1619

1720
from pathlib import Path
1821

19-
jax.config.update("jax_enable_x64", True) # double precision
22+
from os import PathLike
23+
2024
jnp.set_printoptions(
2125
threshold=jnp.inf,
2226
linewidth=jnp.inf,
2327
formatter={"float_kind": lambda x: "0" if x == 0 else f"{x:.2e}"},
2428
)
2529

30+
COLORS = {
31+
"base": "white",
32+
"backbone": "black",
33+
"rod": "blue",
34+
"platform": "green",
35+
}
2636

2737
def draw_robot(
2838
robot: PlanarHSA,
2939
q: Array,
30-
width: int = 700,
31-
height: int = 700,
40+
width: int= 700,
41+
height: int= 700,
3242
num_points: int = 50,
43+
show: bool = False,
3344
) -> onp.ndarray:
3445
"""
3546
Draw the robot in OpenCV.
3647
Args:
37-
robot:
48+
robot: PlanarHSA instance
3849
q: configuration as shape (3, )
3950
width: image width
4051
height: image height
@@ -43,7 +54,7 @@ def draw_robot(
4354
# plotting in OpenCV
4455
h, w = height, width # img height and width
4556
ppm = h / (
46-
2.0 * jnp.sum(robot.params["lpc"] + robot.params["l"] + robot.params["ldc"])
57+
2.0 * jnp.sum(robot.lpc + robot.L + robot.ldc)
4758
) # pixel per meter
4859
base_color = (0, 0, 0) # black base color in BGR
4960
backbone_color = (255, 0, 0) # blue robot color in BGR
@@ -52,27 +63,33 @@ def draw_robot(
5263

5364
batched_forward_kinematics_virtual_backbone_fn = vmap(
5465
robot.forward_kinematics_virtual_backbone_fn,
55-
in_axes=(None, 0), out_axes=-1
66+
in_axes=(None, 0),
67+
out_axes=-1
5668
)
5769
batched_forward_kinematics_rod_fn = vmap(
5870
robot.forward_kinematics_rod_fn,
59-
in_axes=(None, 0, None), out_axes=-1
71+
in_axes=(None, 0, None),
72+
out_axes=-1
6073
)
6174
batched_forward_kinematics_platform_fn = vmap(
6275
robot.forward_kinematics_platform_fn,
63-
in_axes=(None, 0), out_axes=0
76+
in_axes=(None, 0),
77+
out_axes=0
6478
)
6579

66-
L_max = jnp.sum(robot.params["l"]) # total length of the robot
6780
# we use for plotting N points along the length of the robot
68-
s_ps = jnp.linspace(0, L_max, num_points)
81+
s_ps = jnp.linspace(0, robot.Lmax, num_points)
6982

7083
# poses along the robot of shape (3, N)
71-
chiv_ps = batched_forward_kinematics_virtual_backbone_fn(q, s_ps) # poses of virtual backbone
84+
chiv_ps = batched_forward_kinematics_virtual_backbone_fn(
85+
q, s_ps
86+
) # poses of virtual backbone
7287
chiL_ps = batched_forward_kinematics_rod_fn(q, s_ps, 0) # poses of left rod
7388
chiR_ps = batched_forward_kinematics_rod_fn(q, s_ps, 1) # poses of left rod
7489
# poses of the platforms
75-
chip_ps = batched_forward_kinematics_platform_fn(q, jnp.arange(0, robot.num_segments))
90+
chip_ps = batched_forward_kinematics_platform_fn(
91+
q, jnp.arange(0, robot.num_segments)
92+
)
7693

7794
img = 255 * onp.ones((w, h, 3), dtype=jnp.uint8) # initialize background to white
7895
uv_robot_origin = onp.array(
@@ -106,15 +123,15 @@ def chi2u(chi: Array) -> Array:
106123
# add the first point of the proximal cap and the last point of the distal cap
107124
chiv_ps = jnp.concatenate(
108125
[
109-
(chiv_ps[:, 0] - jnp.array([0.0, 0.0, params["lpc"][0]])).reshape(3, 1),
126+
(chiv_ps[:, 0] - jnp.array([0.0, 0.0, robot.lpc[0]])).reshape(3, 1),
110127
chiv_ps,
111128
(
112129
chiv_ps[:, -1]
113130
+ jnp.array(
114131
[
115132
chiv_ps[2, -1],
116-
-jnp.sin(chiv_ps[2, -1]) * params["ldc"][-1],
117-
jnp.cos(chiv_ps[2, -1]) * params["ldc"][-1],
133+
-jnp.sin(chiv_ps[2, -1]) * robot.ldc[-1],
134+
jnp.cos(chiv_ps[2, -1]) * robot.ldc[-1],
118135
]
119136
)
120137
).reshape(3, 1),
@@ -130,15 +147,15 @@ def chi2u(chi: Array) -> Array:
130147
# add the first point of the proximal cap and the last point of the distal cap
131148
chiL_ps = jnp.concatenate(
132149
[
133-
(chiL_ps[:, 0] - jnp.array([0.0, 0.0, params["lpc"][0]])).reshape(3, 1),
150+
(chiL_ps[:, 0] - jnp.array([0.0, 0.0, robot.lpc[0]])).reshape(3, 1),
134151
chiL_ps,
135152
(
136153
chiL_ps[:, -1]
137154
+ jnp.array(
138155
[
139156
chiL_ps[2, -1],
140-
-jnp.sin(chiL_ps[2, -1]) * params["ldc"][-1],
141-
jnp.cos(chiL_ps[2, -1]) * params["ldc"][-1],
157+
-jnp.sin(chiL_ps[2, -1]) * robot.ldc[-1],
158+
jnp.cos(chiL_ps[2, -1]) * robot.ldc[-1],
142159
]
143160
)
144161
).reshape(3, 1),
@@ -152,20 +169,20 @@ def chi2u(chi: Array) -> Array:
152169
isClosed=False,
153170
color=rod_color,
154171
thickness=10,
155-
# thickness=2*int(ppm * params["rout"].mean(axis=0)[0])
172+
# thickness=2*int(ppm * robot.params["rout"].mean(axis=0)[0])
156173
)
157174
# add the first point of the proximal cap and the last point of the distal cap
158175
chiR_ps = jnp.concatenate(
159176
[
160-
(chiR_ps[:, 0] - jnp.array([0.0, 0.0, params["lpc"][0]])).reshape(3, 1),
177+
(chiR_ps[:, 0] - jnp.array([0.0, 0.0, robot.lpc[0]])).reshape(3, 1),
161178
chiR_ps,
162179
(
163180
chiR_ps[:, -1]
164181
+ jnp.array(
165182
[
166183
chiR_ps[2, -1],
167-
-jnp.sin(chiR_ps[2, -1]) * params["ldc"][-1],
168-
jnp.cos(chiR_ps[2, -1]) * params["ldc"][-1],
184+
-jnp.sin(chiR_ps[2, -1]) * robot.ldc[-1],
185+
jnp.cos(chiR_ps[2, -1]) * robot.ldc[-1],
169186
]
170187
)
171188
).reshape(3, 1),
@@ -174,38 +191,43 @@ def chi2u(chi: Array) -> Array:
174191
)
175192
curve_rod_right = onp.array(batched_chi2u(chiR_ps))
176193
cv2.polylines(img, [curve_rod_right], isClosed=False, color=rod_color, thickness=10)
177-
194+
178195
# draw the platform
179196
for i in range(chip_ps.shape[0]):
180197
# iterate over the platforms
181198
platform_R = jnp.array(
182199
[
183-
[jnp.cos(chip_ps[i, 0]), -jnp.sin(chip_ps[i, 0])],
184-
[jnp.sin(chip_ps[i, 0]), jnp.cos(chip_ps[i, 0])],
200+
[1, 0, 0],
201+
[0,jnp.cos(chip_ps[i, 0]), -jnp.sin(chip_ps[i, 0])],
202+
[0,jnp.sin(chip_ps[i, 0]), jnp.cos(chip_ps[i, 0])],
185203
]
186204
) # rotation matrix for the platform
187-
platform_llc = chip_ps[i, 1:] + platform_R @ jnp.array(
188-
[
189-
-params["pcudim"][i, 1] / 2, # go half the width to the left
190-
-params["pcudim"][i, 2] / 2, # go half the height down
205+
platform_llc = chip_ps[i, :] + platform_R @ jnp.array(
206+
[
207+
0,
208+
-robot.pcudim[i, 0] / 2, # go half the width to the left
209+
-robot.pcudim[i, 1] / 2, # go half the height down
191210
]
192211
) # lower left corner of the platform
193-
platform_ulc = chip_ps[i, 1:] + platform_R @ jnp.array(
212+
platform_ulc = chip_ps[i, :] + platform_R @ jnp.array(
194213
[
195-
-params["pcudim"][i, 1] / 2, # go half the width to the left
196-
+params["pcudim"][i, 2] / 2, # go half the height down
214+
0,
215+
-robot.pcudim[i, 0] / 2, # go half the width to the left
216+
+robot.pcudim[i, 1] / 2, # go half the height down
197217
]
198-
) # upper left corner of the platform
199-
platform_urc = chip_ps[i, 1:] + platform_R @ jnp.array(
218+
) # upper left corner of the platform
219+
platform_urc = chip_ps[i, :] + platform_R @ jnp.array(
200220
[
201-
+params["pcudim"][i, 1] / 2, # go half the width to the left
202-
+params["pcudim"][i, 2] / 2, # go half the height down
221+
0,
222+
+robot.pcudim[i, 0] / 2, # go half the width to the left
223+
+robot.pcudim[i, 1] / 2, # go half the height down
203224
]
204225
) # upper right corner of the platform
205-
platform_lrc = chip_ps[i, 1:] + platform_R @ jnp.array(
226+
platform_lrc = chip_ps[i, :] + platform_R @ jnp.array(
206227
[
207-
+params["pcudim"][i, 1] / 2, # go half the width to the left
208-
-params["pcudim"][i, 2] / 2, # go half the height down
228+
0,
229+
+robot.pcudim[i, 0] / 2, # go half the width to the left
230+
-robot.pcudim[i, 1] / 2, # go half the height down
209231
]
210232
) # lower right corner of the platform
211233
platform_curve = jnp.stack(
@@ -216,9 +238,59 @@ def chi2u(chi: Array) -> Array:
216238
cv2.fillPoly(
217239
img, [onp.array(batched_chi2u(platform_curve))], color=platform_color
218240
)
241+
242+
if show:
243+
win = "Planar HSA"
244+
# fenêtre redimensionnable (utile sur macOS/Linux/HiDPI)
245+
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
246+
cv2.imshow(win, img)
247+
# attend jusqu'à une touche (ferme si on appuie sur ESC ou 'q')
248+
key = cv2.waitKey(0) & 0xFF
249+
if key in (27, ord('q')):
250+
cv2.destroyWindow(win)
219251

220252
return img
221253

254+
def animate_robot( #TODO: correct this implementation
255+
robot: PlanarHSA,
256+
filepath: PathLike,
257+
video_ts: Array,
258+
q_ts: Array,
259+
video_width: int = 700,
260+
video_height: int = 700,
261+
):
262+
"""
263+
Animate the robot and save the video.
264+
Args:
265+
filepath: path to save the video
266+
video_ts: time stamps of the video
267+
q_ts: configuration time series of shape (N, 3)
268+
video_width: video width
269+
video_height: video height
270+
"""
271+
# create video
272+
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
273+
filepath.parent.mkdir(parents=True, exist_ok=True)
274+
video_dt = jnp.mean(video_ts[1:] - video_ts[:-1]).item()
275+
print(f"Rendering video with dt={video_dt} and {video_ts.shape[0]} frames")
276+
video = cv2.VideoWriter(
277+
str(filepath),
278+
fourcc,
279+
1 / video_dt, # fps
280+
(video_width, video_height),
281+
)
282+
283+
for time_idx, t in enumerate(video_ts):
284+
q = q_ts[time_idx]
285+
img = draw_robot(
286+
robot,
287+
q,
288+
video_width,
289+
video_height,
290+
)
291+
video.write(img)
292+
293+
video.release()
222294

223295
if __name__ == "__main__":
224296
num_segments = 1
@@ -250,6 +322,7 @@ def chi2u(chi: Array) -> Array:
250322
strain_selector=strain_selector,
251323
consider_hysteresis=consider_hysteresis,
252324
)
325+
print(f"Planar HSA with {num_segments} segments and {num_rods_per_segment} rods per segment initialized.")
253326

254327
# =====================================================
255328
# Simulation upon time
@@ -266,8 +339,15 @@ def chi2u(chi: Array) -> Array:
266339
img = draw_robot(
267340
robot,
268341
q = q0,
342+
show=False,
269343
)
270344

345+
win = "Planar HSA"
346+
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
347+
cv2.imshow(win, img)
348+
key = cv2.waitKey(0) & 0xFF
349+
if key in (27, ord('q')):
350+
cv2.destroyWindow(win)
271351

272352
# Simulation time parameters
273353
t0 = 0.0
@@ -276,4 +356,49 @@ def chi2u(chi: Array) -> Array:
276356
skip_step = 100 # how many time steps to skip in between video frames
277357

278358
# Solver
279-
solver = Tsit5()
359+
solver = Tsit5()
360+
361+
actuation_args = (phi, None, True) # actuation arguments
362+
363+
ts, q_ts, q_d_ts = robot.resolve_upon_time(
364+
q0=q0,
365+
qd0=qd0,
366+
u0=phi,
367+
t0=t0,
368+
t1=t1,
369+
dt=dt,
370+
skip_steps=skip_step,
371+
solver=solver,
372+
max_steps=None,
373+
)
374+
375+
# create video
376+
video_width, video_height = 700, 700 # img height and width
377+
video_path = Path(__file__).parent / "videos" / "planar_hsa.mp4"
378+
video_path.parent.mkdir(parents=True, exist_ok=True)
379+
380+
animate_robot(
381+
robot,
382+
video_path,
383+
video_ts=ts,
384+
q_ts=q_ts,
385+
video_width=video_width,
386+
video_height=video_height,
387+
)
388+
print(f"Video saved at {video_path}")
389+
390+
# Lecture et affichage de la vidéo générée
391+
cap = cv2.VideoCapture(str(video_path))
392+
if not cap.isOpened():
393+
print(f"Erreur lors de l'ouverture de la vidéo {video_path}")
394+
else:
395+
while True:
396+
ret, frame = cap.read()
397+
if not ret:
398+
break
399+
cv2.imshow("Animation Planar HSA", frame)
400+
key = cv2.waitKey(int(1000 / (1 / dt / skip_step)))
401+
if key in (27, ord('q')):
402+
break
403+
cap.release()
404+
cv2.destroyAllWindows()

0 commit comments

Comments
 (0)