Articulations¶
MJCF, URDF, and USD loaders produce a common ArticulationDesc.
Load a robot, then pass its data to world.add_articulation().
Load a robot¶
Choose the loader for your file:
# world is an existing KangSimWorld.
data: ke.asset.ArticulationDesc = world.load_mjcf("robot.xml", order="DFS")
# Or:
data = world.load_urdf("robot.urdf", order="DFS")
data = world.load_usd("robot.usd", order="DFS")
USD requires a native build with USE_USD enabled. It selects the default
prim; pass prim_path="/Robot" to select a different robot subtree.
Format |
Supported joints |
3-axis rotation |
|---|---|---|
MJCF |
Fixed, Revolute (hinge) |
3 orthogonal Revolute joints on one body |
URDF |
Fixed, Revolute, Prismatic |
3 Revolute joints with intermediate links |
USD |
Fixed, Revolute, Prismatic |
3 Revolute joints with intermediate links |
Native MJCF ball/slide and USD Spherical joint import are not supported.
Create the articulation¶
config = ke.physics.ArticulationConfig.free_base()
record = world.add_articulation(
data,
env_id=0,
obj_id=0,
name="robot",
config=config,
)
robot = record.articulation
For all three formats, choose ArticulationConfig.free_base() for a floating
root or ArticulationConfig.fixed_base() for a fixed root.
The following visualization example uses an MJCF asset:
Use the same order for loading and visualization.
robot_visual = visual.add_articulation_scene_graph(
0,
0,
mjcf_path,
path="/robot",
order="DFS",
material=robot_material,
)
Control example¶
Run the complete control example with an MJCF file:
python ./python/examples/mjcf_dof_control.py /path/to/robot.xml
Complete source: mjcf_dof_control.py
1"""Generic MJCF articulation DOF position-control viewer."""
2
3from __future__ import annotations
4
5import argparse
6import math
7from pathlib import Path
8
9import numpy as np
10
11import kangengine as ke
12from kangengine import imgui, keys
13
14
15def package_asset_path(*parts: str) -> str:
16 return str(Path(ke.__file__).resolve().parent / "assets" / Path(*parts))
17
18
19def default_mjcf_path() -> Path:
20 return Path(package_asset_path("characters", "kw", "kw.xml"))
21
22
23class MjcfDofControlApp(ke.App):
24 """Load an MJCF articulation and expose every DOF as an ImGui slider."""
25
26 window_title = "MJCF DOF Control"
27 object_name = "mjcf"
28 prim_base_path = "/mjcf"
29 camera_pos = (3.8, -5.4, 1.2)
30 camera_target = (0.0, 0.0, 0.45)
31 # None preserves per-geom MJCF rgba. Set an RGBA tuple to force a single
32 # override color for the whole articulation.
33 visual_color = None # np.array([1,1,1, 1.0])
34 ground_size = 10.0
35 root_pos = (0.0, 0.0, 1.5)
36 root_rot_xyzw = (0.0, 0.0, 0.0, 1.0)
37 fixed_base = False
38 order = "DFS"
39 sim_dt = 1.0 / 240.0
40 step_substeps = 4
41 default_kp = 120.0
42 default_kd = 12.0
43 default_anim_amp = 0.35
44 default_anim_speed = 1.0
45 default_contact_force_scale = 0.002
46 contact_force_threshold = 1e-3
47 visual_alpha_with_collision = 0.1
48 drag_force_debug_path = "/debug/mjcf_drag_force"
49 drag_force_target_debug_path = "/debug/mjcf_drag_force_target"
50
51 def __init__(self, mjcf_path: str | Path):
52 super().__init__()
53 self.mjcf_path = str(Path(mjcf_path).expanduser().resolve())
54
55 def setup(self):
56 self.timing = self.configure_timing(
57 ke.SimulationTimingConfig.from_dt(
58 physics_dt=self.sim_dt,
59 fixed_dt=self.sim_dt * self.step_substeps,
60 render_hz=60.0,
61 )
62 )
63 self.set_simulation_hotkeys_enabled(True)
64 self.elapsed = 0.0
65 self.kp = float(self.default_kp)
66 self.kd = float(self.default_kd)
67 self.animate = True
68 self.anim_amp = float(self.default_anim_amp)
69 self.anim_speed = float(self.default_anim_speed)
70 self.show_collision = False
71 self.show_contact_forces = False
72 self.contact_force_scale = float(self.default_contact_force_scale)
73 self.contact_force_view = None
74 self.contact_force_color = np.array([[1.0, 0.25, 0.05, 1.0]], dtype=np.float32)
75 self.empty_vec3 = np.empty((0, 3), dtype=np.float32)
76 self.empty_vec4 = np.empty((0, 4), dtype=np.float32)
77 red = ke.ColorLibrary.get(ke.ColorType.RED)
78 magenta = ke.ColorLibrary.get(ke.ColorType.MAGENTA)
79 self.drag_force_line_color = np.array(
80 [[red.r, red.g, red.b, red.a]], dtype=np.float32
81 )
82 self.drag_force_target_color = np.array(
83 [[magenta.r, magenta.g, magenta.b, magenta.a]], dtype=np.float32
84 )
85 self.drag_force_line_starts = np.empty((3, 3), dtype=np.float32)
86 self.drag_force_line_ends = np.empty((3, 3), dtype=np.float32)
87 self.drag_force_up_z = np.array([0.0, 0.0, 1.0], dtype=np.float32)
88 self.drag_force_up_y = np.array([0.0, 1.0, 0.0], dtype=np.float32)
89 self.drag_force_enabled = True
90 self.drag_force_stiffness = 750.0
91 self.drag_force_damping = 75.0
92 self.drag_force_max = 300.0
93 self.drag_force_arrow_scale = 0.003
94 self.show_drag_force_arrow = True
95 self._drag_force_body_id = None
96 self._drag_force_local_anchor = None
97 self._drag_force_target = None
98 self._drag_force_anchor_world = None
99 self._drag_force_vector = None
100 self._clear_drag_force_arrow()
101
102 self.configure_camera()
103 self.standard_materials = self.create_standard_materials()
104 self.debug_material = self.standard_materials.common
105 self.create_world()
106 self.load_articulation()
107 self._reset()
108 self.print_summary()
109
110 def configure_camera(self):
111 self.get_camera().set_camera_pos(ke.Vec3(*self.camera_pos))
112 self.get_camera().set_target_pos(ke.Vec3(*self.camera_target))
113
114 def create_world(self):
115 self.world = ke.sim.KangSimWorld(
116 num_envs=1,
117 sim_dt=self.timing.physics_dt,
118 add_ground=True,
119 )
120 self.visual = ke.visual.sim.SimWorldVisualizer(self, self.world)
121
122 self.ground_view = self.scene.add_ground(
123 "/ground",
124 scale=float(self.ground_size),
125 material=self.standard_materials.ground,
126 )
127
128 def load_articulation(self):
129 data = self.world.load_mjcf(self.mjcf_path, order=self.order)
130 self.obj_id = 0
131 config = (
132 ke.physics.ArticulationConfig.fixed_base()
133 if self.fixed_base
134 else ke.physics.ArticulationConfig.free_base()
135 )
136 self.robot = self.world.add_articulation(
137 data,
138 env_id=0,
139 obj_id=self.obj_id,
140 name=self.object_name,
141 config=config,
142 ).articulation
143
144 self.articulation_visual_view = self.visual.add_articulation_scene_graph(
145 0,
146 self.obj_id,
147 self.mjcf_path,
148 path=self.prim_base_path,
149 order=self.order,
150 material=self.standard_materials.pbr,
151 collision_path=f"{self.prim_base_path}_collision",
152 show_collision=self.show_collision,
153 color=(
154 None
155 if self.visual_color is None
156 else np.array(self.visual_color, dtype=np.float32)
157 ),
158 )
159 self.visual_body_prims = self.articulation_visual_view.prims
160 # self.collision_body_prims = self.articulation_visual_view.collision_visuals
161
162 self.num_dofs = self.robot.num_dofs()
163 self.dof_names = self.world.state.get_obj_dof_names(self.obj_id)
164 self.dof_limits = np.asarray(
165 self.world.state.get_obj_dof_limits(self.obj_id), dtype=np.float32
166 )
167 if self.dof_limits.shape != (self.num_dofs, 2):
168 self.dof_limits = np.tile(
169 np.array([-math.pi, math.pi], dtype=np.float32),
170 (self.num_dofs, 1),
171 )
172 self.targets = self.initial_targets()
173
174 def initial_targets(self) -> np.ndarray:
175 return np.zeros(self.num_dofs, dtype=np.float32)
176
177 def print_summary(self):
178 print(
179 f"{self.object_name} loaded: links={self.robot.num_links()} dofs={self.num_dofs}"
180 )
181 print("DOFs:", ", ".join(self.dof_names))
182 print("XML:", self.mjcf_path)
183
184 def _reset(self):
185 self.elapsed = 0.0
186 self.targets[:] = self.initial_targets()
187 self.world.set_root_state(
188 None,
189 self.obj_id,
190 np.array(self.root_pos, dtype=np.float32),
191 np.array(self.root_rot_xyzw, dtype=np.float32),
192 )
193 self.world.set_dof_state(None, self.obj_id, self.targets)
194 self.world.set_cmd(
195 None,
196 self.obj_id,
197 self.targets,
198 mode=ke.sim.ControlMode.POS,
199 kp=self.kp,
200 kd=self.kd,
201 )
202 self.world.step(substeps=0, apply_commands=False)
203 self.visual.sync()
204 self._update_contact_force_arrows()
205 self._clear_drag_force()
206
207 def _animated_targets(self):
208 out = np.zeros_like(self.targets)
209 for i in range(self.num_dofs):
210 lo, hi = self.slider_limits(i)
211 span = max(0.0, min(float(hi - lo) * 0.35, self.anim_amp))
212 center = 0.5 * float(lo + hi)
213 phase = self.elapsed * self.anim_speed + i * 0.75
214 out[i] = np.clip(center + span * math.sin(phase), lo, hi)
215 return out
216
217 def slider_limits(self, dof_index: int) -> tuple[float, float]:
218 lo, hi = self.dof_limits[dof_index]
219 if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo:
220 return -math.pi, math.pi
221 return float(lo), float(hi)
222
223 def pre_update(self):
224 if self.was_key_pressed(keys.R):
225 self._reset()
226 if self.was_key_pressed(keys.Q):
227 self.animate = not self.animate
228
229 def fixed_update(self, fixed_dt):
230 self.elapsed += fixed_dt
231 if self.animate:
232 self.targets[:] = self._animated_targets()
233
234 self.world.set_cmd(
235 None,
236 self.obj_id,
237 self.targets,
238 mode=ke.sim.ControlMode.POS,
239 kp=self.kp,
240 kd=self.kd,
241 )
242 self.world.advance(fixed_dt)
243
244 def pre_render(self):
245 self.visual.sync()
246 self._update_contact_force_arrows()
247 self._update_drag_force_arrow()
248 self.check_error()
249
250 def on_force_drag_begin(self, result, target):
251 if not self.drag_force_enabled or not result.hit:
252 self._clear_drag_force()
253 return
254
255 pick = self.articulation_visual_view.pick_body(result)
256 if pick is None:
257 self._clear_drag_force()
258 return
259
260 self._drag_force_body_id = int(pick.body_id)
261 body_state = self._drag_body_state(self._drag_force_body_id)
262 if body_state is None:
263 return
264 body_pos, body_rot, _, _ = body_state
265 hit_pos = self._vec3_to_np(result.position)
266 self._drag_force_local_anchor = self._quat_inverse_rotate_xyzw(
267 body_rot, hit_pos - body_pos
268 )
269 self._drag_force_target = self._vec3_to_np(target)
270 self._apply_drag_force()
271 self._update_drag_force_arrow()
272
273 def on_force_drag_update(self, result, target):
274 if not self.drag_force_enabled or self._drag_force_body_id is None:
275 self._clear_drag_force()
276 return
277 self._drag_force_target = self._vec3_to_np(target)
278 self._apply_drag_force()
279 self._update_drag_force_arrow()
280
281 def on_force_drag_end(self):
282 self._clear_drag_force()
283
284 def _apply_drag_force(self):
285 if (
286 self._drag_force_body_id is None
287 or self._drag_force_local_anchor is None
288 or self._drag_force_target is None
289 ):
290 return
291 body_state = self._drag_body_state(self._drag_force_body_id)
292 if body_state is None:
293 return
294 body_pos, body_rot, body_vel, body_ang_vel = body_state
295 radius = self._quat_rotate_xyzw(body_rot, self._drag_force_local_anchor)
296 anchor_world = body_pos + radius
297 point_vel = body_vel + np.cross(body_ang_vel, radius)
298 force = (self._drag_force_target - anchor_world) * float(
299 self.drag_force_stiffness
300 ) - point_vel * float(self.drag_force_damping)
301 norm = float(np.linalg.norm(force))
302 if norm > float(self.drag_force_max) > 0.0:
303 force *= float(self.drag_force_max) / norm
304 self._drag_force_anchor_world = anchor_world.astype(np.float32)
305 self._drag_force_vector = force.astype(np.float32)
306 self.world.set_body_force_at_position(
307 0,
308 self.obj_id,
309 self._drag_force_body_id,
310 force.astype(np.float32),
311 anchor_world.astype(np.float32),
312 )
313
314 def _clear_drag_force_arrow(self):
315 self.clear_debug_lines(self.drag_force_debug_path)
316 self.clear_debug_points(self.drag_force_target_debug_path)
317
318 def _update_drag_force_arrow(self):
319 if (
320 not self.show_drag_force_arrow
321 or self._drag_force_anchor_world is None
322 or self._drag_force_vector is None
323 or self._drag_force_target is None
324 ):
325 self._clear_drag_force_arrow()
326 return
327
328 start = self._drag_force_anchor_world
329 force = self._drag_force_vector
330 force_len = float(np.linalg.norm(force))
331 if force_len < 1e-5:
332 self._clear_drag_force_arrow()
333 return
334
335 direction = force / force_len
336 end = start + force * float(self.drag_force_arrow_scale)
337 shaft_len = float(np.linalg.norm(end - start))
338 head_len = min(max(shaft_len * 0.25, 0.04), 0.18)
339
340 side = np.cross(direction, self.drag_force_up_z)
341 if np.linalg.norm(side) < 1e-5:
342 side = np.cross(direction, self.drag_force_up_y)
343 side = side / max(float(np.linalg.norm(side)), 1e-8)
344
345 back = end - direction * head_len
346 left = back + side * head_len * 0.45
347 right = back - side * head_len * 0.45
348 self.drag_force_line_starts[0] = start
349 self.drag_force_line_starts[1] = end
350 self.drag_force_line_starts[2] = end
351 self.drag_force_line_ends[0] = end
352 self.drag_force_line_ends[1] = left
353 self.drag_force_line_ends[2] = right
354 self.log_debug_lines(
355 self.drag_force_debug_path,
356 self.drag_force_line_starts,
357 self.drag_force_line_ends,
358 self.drag_force_line_color,
359 3.0,
360 )
361 self.log_debug_points(
362 self.drag_force_target_debug_path,
363 self._drag_force_target.reshape(1, 3),
364 self.drag_force_target_color,
365 10.0,
366 )
367
368 def _drag_body_state(self, body_id: int):
369 body_pos = self._state_array(self.world.state.get_body_pos(self.obj_id)[0])
370 if body_id >= body_pos.shape[0]:
371 self._clear_drag_force()
372 return None
373 body_rot = self._state_array(self.world.state.get_body_rot(self.obj_id)[0])
374 body_vel = self._state_array(self.world.state.get_body_vel(self.obj_id)[0])
375 body_ang_vel = self._state_array(
376 self.world.state.get_body_ang_vel(self.obj_id)[0]
377 )
378 return (
379 body_pos[body_id],
380 body_rot[body_id],
381 body_vel[body_id],
382 body_ang_vel[body_id],
383 )
384
385 def _clear_drag_force(self):
386 if getattr(self, "_drag_force_body_id", None) is not None:
387 self.world.set_body_force(
388 0,
389 self.obj_id,
390 int(self._drag_force_body_id),
391 np.zeros(3, dtype=np.float32),
392 )
393 self._drag_force_body_id = None
394 self._drag_force_local_anchor = None
395 self._drag_force_target = None
396
397 def _clear_contact_force_arrows(self):
398 if self.contact_force_view is None:
399 return
400 self.contact_force_view.update_arrows(
401 self.empty_vec3,
402 self.empty_vec3,
403 self.empty_vec4,
404 )
405
406 def _update_contact_force_arrows(self):
407 if not self.show_contact_forces:
408 self._clear_contact_force_arrows()
409 return
410
411 # Body-aggregated force visualization.
412 # the arrows start at link origins instead of real contact points.
413 # body_pos = np.asarray(
414 # self.world.state.get_body_pos(self.obj_id)[0], dtype=np.float32
415 # )
416 # forces = np.asarray(
417 # self.world.state.get_contact_forces(self.obj_id)[0], dtype=np.float32
418 # )
419 # active = np.linalg.norm(forces, axis=1) > float(self.contact_force_threshold)
420 # starts = body_pos[active]
421 # ends = starts + forces[active] * float(self.contact_force_scale)
422
423 contacts = self.world.physics.get_contacts()
424 starts = []
425 ends = []
426 dt = max(float(self.world.sim_dt), 1e-8)
427 for contact in contacts:
428 position = self._vec3_to_np(contact.position)
429 force = self._vec3_to_np(contact.impulse) / dt
430 if np.linalg.norm(force) <= float(self.contact_force_threshold):
431 continue
432 starts.append(position)
433 ends.append(position + force * float(self.contact_force_scale))
434
435 if not starts:
436 self._clear_contact_force_arrows()
437 return
438
439 starts = np.asarray(starts, dtype=np.float32)
440 ends = np.asarray(ends, dtype=np.float32)
441 colors = np.repeat(self.contact_force_color, starts.shape[0], axis=0)
442
443 if self.contact_force_view is None:
444 self.contact_force_view = self.scene.log_arrows(
445 "/debug/contact_forces",
446 self.debug_material,
447 starts,
448 ends,
449 colors,
450 0.015,
451 12,
452 )
453 else:
454 self.contact_force_view.update_arrows(starts, ends, colors)
455
456 @staticmethod
457 def _vec3_to_np(value) -> np.ndarray:
458 return np.array(
459 [float(value.x), float(value.y), float(value.z)], dtype=np.float32
460 )
461
462 @staticmethod
463 def _state_array(value) -> np.ndarray:
464 if hasattr(value, "detach"):
465 value = value.detach().cpu().numpy()
466 return np.asarray(value, dtype=np.float32)
467
468 @staticmethod
469 def _quat_rotate_xyzw(quat, vector) -> np.ndarray:
470 q = np.asarray(quat, dtype=np.float32)
471 v = np.asarray(vector, dtype=np.float32)
472 qv = q[:3]
473 t = 2.0 * np.cross(qv, v)
474 return (v + q[3] * t + np.cross(qv, t)).astype(np.float32)
475
476 @classmethod
477 def _quat_inverse_rotate_xyzw(cls, quat, vector) -> np.ndarray:
478 q = np.asarray(quat, dtype=np.float32).copy()
479 q[:3] *= -1.0
480 return cls._quat_rotate_xyzw(q, vector)
481
482 def _set_visual_alpha(self, alpha: float):
483 if self.visual_color is None:
484 self.articulation_visual_view.set_alpha(alpha)
485 return
486 color = np.array(self.visual_color, dtype=np.float32).reshape(-1)
487 if color.size == 3:
488 color = np.concatenate([color, np.ones(1, dtype=np.float32)])
489 color = color[:4].copy()
490 color[3] = float(alpha)
491 self.articulation_visual_view.set_color(color)
492
493 def _set_collision_visible(self, visible: bool):
494 self.show_collision = bool(visible)
495 self.articulation_visual_view.set_collision_visible(self.show_collision)
496 self._set_visual_alpha(
497 self.visual_alpha_with_collision if self.show_collision else 1.0
498 )
499
500 def render(self):
501 imgui.begin(self.window_title)
502 state = "paused" if self.is_simulation_paused() else "running"
503 imgui.text(f"State: {state}")
504 imgui.text(
505 "Enter: play/pause Space: pause/step R: reset Q: auto motion"
506 )
507 imgui.text(f"Links: {self.robot.num_links()} DOFs: {self.num_dofs}")
508 imgui.text(Path(self.mjcf_path).name)
509 imgui.separator()
510 _, self.kp = imgui.slider_float("kp", self.kp, 0.0, 1000.0)
511 _, self.kd = imgui.slider_float("kd", self.kd, 0.0, 80.0)
512 _, self.animate = imgui.checkbox("Animate targets", self.animate)
513 _, self.anim_amp = imgui.slider_float("anim amplitude", self.anim_amp, 0.0, 1.5)
514 _, self.anim_speed = imgui.slider_float("anim speed", self.anim_speed, 0.0, 6.0)
515 changed, self.show_collision = imgui.checkbox(
516 "Show collision prims",
517 self.show_collision,
518 )
519 if changed:
520 self._set_collision_visible(self.show_collision)
521 changed, self.show_contact_forces = imgui.checkbox(
522 "Show contact forces",
523 self.show_contact_forces,
524 )
525 if changed:
526 self._update_contact_force_arrows()
527 _, self.contact_force_scale = imgui.slider_float(
528 "contact force scale",
529 self.contact_force_scale,
530 0.0,
531 0.02,
532 )
533 imgui.separator()
534 changed, self.drag_force_enabled = imgui.checkbox(
535 "Enable drag force",
536 self.drag_force_enabled,
537 )
538 if changed and not self.drag_force_enabled:
539 self._clear_drag_force()
540 changed, self.show_drag_force_arrow = imgui.checkbox(
541 "Show drag force arrow",
542 self.show_drag_force_arrow,
543 )
544 if changed and not self.show_drag_force_arrow:
545 self._clear_drag_force_arrow()
546 _, self.drag_force_stiffness = imgui.slider_float(
547 "drag force stiffness",
548 self.drag_force_stiffness,
549 0.0,
550 1000.0,
551 )
552 _, self.drag_force_damping = imgui.slider_float(
553 "drag force damping",
554 self.drag_force_damping,
555 0.0,
556 80.0,
557 )
558 _, self.drag_force_max = imgui.slider_float(
559 "drag force max",
560 self.drag_force_max,
561 0.0,
562 2000.0,
563 )
564 _, self.drag_force_arrow_scale = imgui.slider_float(
565 "drag force arrow scale",
566 self.drag_force_arrow_scale,
567 0.0005,
568 0.02,
569 )
570 if self._drag_force_body_id is not None:
571 imgui.text(f"Dragging body: {self._drag_force_body_id}")
572 imgui.separator()
573
574 for i, name in enumerate(self.dof_names):
575 lo, hi = self.slider_limits(i)
576 changed, value = imgui.slider_float(name, float(self.targets[i]), lo, hi)
577 if changed:
578 self.targets[i] = value
579 self.animate = False
580
581 pos = self.world.state.get_dof_pos(self.obj_id)[0]
582 imgui.separator()
583 imgui.text("Current DOF positions")
584 for name, value in zip(self.dof_names, pos):
585 imgui.text(f"{name}: {float(value): .3f}")
586 imgui.end()
587
588 def cleanup(self):
589 if hasattr(self, "visual"):
590 self.visual.release()
591 if hasattr(self, "world"):
592 self.world.release()
593
594
595def main():
596 parser = argparse.ArgumentParser()
597 parser.add_argument(
598 "mjcf_path",
599 nargs="?",
600 default=str(default_mjcf_path()),
601 help="Path to an MJCF XML file",
602 )
603 parser.add_argument(
604 "--fixed-base",
605 action="store_true",
606 help="Use a fixed root instead of the default free root.",
607 )
608 parser.add_argument("--order", default="DFS", choices=("DFS", "BFS"))
609 parser.add_argument("--width", type=int, default=1920)
610 parser.add_argument("--height", type=int, default=1080)
611 args = parser.parse_args()
612
613 class CliMjcfDofControlApp(MjcfDofControlApp):
614 fixed_base = args.fixed_base
615 order = args.order
616
617 app = CliMjcfDofControlApp(args.mjcf_path)
618 app.initialize(args.width, args.height, False, ke.UpAxis.Z)
619 app.start()
620
621
622if __name__ == "__main__":
623 main()
MJCF articulation |
Collision debug |
|---|---|
|
|
USD import requirements¶
Prepare a USD robot with:
Z-up,
metersPerUnit = 1, andkilogramsPerUnit = 1.One connected body tree with explicit positive mass and inertia.
convexHullcollision meshes and no joint connecting the root to the world.
Configure initial states, drives, simulation settings, and materials after loading. These scene settings are not fully reproduced by the importer. Unsupported joint or collider configurations raise errors; inspect warnings for omitted properties:
result: ke.asset.USDArticulationImportResult = ke.asset.USDLoader.parse_articulation(
usd_path="robot.usd", prim_path="/Robot"
)
print(result.diagnostics.warnings)

