Loading an Articulation#
This tutorial loads a URDF as a generic objects.Articulation, inspects
its links and joints, and verifies the drive type applied to the constructed
physics entities. Generic articulations are passive by default: unlike
objects.Robot, their joints use drive_type="none" unless a drive is
configured explicitly.
The Code#
The complete example is available in
scripts/tutorials/sim/create_articulation.py.
Code for create_articulation.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17"""Load a passive articulation and open or close it with joint forces."""
18
19from __future__ import annotations
20
21import argparse
22
23import torch
24
25from dexsim.types import DriveType
26
27from embodichain.data import get_data_path
28from embodichain.lab.gym.utils.gym_utils import add_env_launcher_args_to_parser
29from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
30from embodichain.lab.sim.cfg import ArticulationCfg, RenderCfg
31from embodichain.lab.sim.objects import Articulation
32from embodichain.lab.visualization import visualization_cfg_from_args
33
34DRAWER_ASSET = "SlidingBoxDrawer/SlidingBoxDrawer.urdf"
35DRAWER_USER_QPOS_LIMITS = {"slide_rails": [0.0, 0.18]}
36DRAWER_JOINT_FORCE = 1.0
37JOINT_LIMIT_TOLERANCE = 1.0e-3
38
39
40def create_articulation(sim: SimulationManager) -> Articulation:
41 """Load a drawer articulation with the passive default drive.
42
43 Args:
44 sim: Simulation manager that owns the scene.
45
46 Returns:
47 The loaded drawer articulation.
48
49 Raises:
50 RuntimeError: If the constructed backend joints are not passive.
51 """
52 # Resolve the drawer URDF and configure its initial pose. ``drive_pros`` is
53 # intentionally omitted: ArticulationCfg defaults to drive_type="none".
54 articulation_cfg = ArticulationCfg(
55 uid="drawer",
56 fpath=get_data_path(DRAWER_ASSET),
57 init_pos=(0.0, 0.0, 0.05),
58 fix_base=True,
59 # The asset limit is [0.0, 0.2]; keep 90% of its travel range.
60 qpos_limits=DRAWER_USER_QPOS_LIMITS,
61 )
62
63 # Load one articulation instance into every simulation environment.
64 articulation: Articulation = sim.add_articulation(cfg=articulation_cfg)
65
66 # Query the constructed DexSim entities, not only the config object.
67 backend_drive_types = articulation.get_joint_drive_type()
68 expected_drive_types = [
69 [DriveType.NONE] * articulation.dof for _ in range(sim.num_envs)
70 ]
71 if backend_drive_types != expected_drive_types:
72 raise RuntimeError(
73 "Expected every articulation joint drive to be DriveType.NONE, "
74 f"but received {backend_drive_types!r}."
75 )
76
77 print(f"[INFO]: Loaded articulation with {articulation.dof} joint(s)", flush=True)
78 print(f"[INFO]: Joint names: {articulation.joint_names}", flush=True)
79 print(
80 f"[INFO]: Config drive type: {articulation.cfg.drive_pros.drive_type}",
81 flush=True,
82 )
83 print(f"[INFO]: Backend drive types: {backend_drive_types}", flush=True)
84 print(
85 f"[INFO]: Effective qpos limits: {articulation.get_qpos_limits()}", flush=True
86 )
87 return articulation
88
89
90def apply_drawer_force(articulation: Articulation, opening: bool) -> None:
91 """Apply a joint force that opens or closes the drawer.
92
93 Args:
94 articulation: Drawer articulation receiving the force.
95 opening: If True, apply positive force; otherwise apply negative force.
96 """
97 force = DRAWER_JOINT_FORCE if opening else -DRAWER_JOINT_FORCE
98 joint_forces = torch.full_like(articulation.get_qpos(), force)
99 articulation.set_qf(joint_forces)
100
101
102def run_simulation(
103 sim: SimulationManager,
104 articulation: Articulation,
105 max_steps: int | None = None,
106) -> None:
107 """Open and close the drawer by reversing force at its joint limits.
108
109 Args:
110 sim: Simulation manager to advance.
111 articulation: Drawer articulation whose joints are updated.
112 max_steps: Optional number of steps to run before returning.
113 """
114 if sim.is_use_gpu_physics:
115 sim.init_gpu_physics()
116
117 qpos_limits = articulation.get_qpos_limits()
118 closed_qpos = qpos_limits[..., 0]
119 open_qpos = qpos_limits[..., 1]
120 opening = True
121 step_count = 0
122 print(
123 f"[INFO]: Applying +{DRAWER_JOINT_FORCE:.1f} N to open the drawer",
124 flush=True,
125 )
126 try:
127 while max_steps is None or step_count < max_steps:
128 qpos = articulation.get_qpos()
129 if opening and torch.all(qpos >= open_qpos - JOINT_LIMIT_TOLERANCE).item():
130 print(f"[INFO]: Drawer reached open limit: {qpos}", flush=True)
131 opening = False
132 print(
133 f"[INFO]: Applying -{DRAWER_JOINT_FORCE:.1f} N to close the drawer",
134 flush=True,
135 )
136 elif (
137 not opening
138 and torch.all(qpos <= closed_qpos + JOINT_LIMIT_TOLERANCE).item()
139 ):
140 print(f"[INFO]: Drawer reached closed limit: {qpos}", flush=True)
141 opening = True
142 print(
143 f"[INFO]: Applying +{DRAWER_JOINT_FORCE:.1f} N to open the drawer",
144 flush=True,
145 )
146
147 apply_drawer_force(articulation, opening=opening)
148 sim.update(step=1)
149 step_count += 1
150 except KeyboardInterrupt:
151 print("\n[INFO]: Stopping simulation...")
152 finally:
153 articulation.set_qf(torch.zeros_like(articulation.get_qpos()))
154
155
156def main() -> None:
157 """Load and simulate a passive drawer articulation."""
158 parser = argparse.ArgumentParser(
159 description="Load an articulation with its default passive joint drive"
160 )
161 add_env_launcher_args_to_parser(parser)
162 parser.add_argument(
163 "--max-steps",
164 type=int,
165 default=None,
166 help="Optional number of simulation steps before exiting.",
167 )
168 args = parser.parse_args()
169 if args.max_steps is not None and args.max_steps < 1:
170 parser.error("--max-steps must be at least 1")
171
172 # Configure the simulation. Window creation is deferred until the asset is loaded.
173 sim_cfg = SimulationManagerCfg(
174 headless=args.headless,
175 sim_device=args.device,
176 num_envs=args.num_envs,
177 arena_space=2.0,
178 physics_dt=1.0 / 100.0,
179 render_cfg=RenderCfg(renderer=args.renderer),
180 visualization=visualization_cfg_from_args(args),
181 )
182 sim = SimulationManager(sim_cfg)
183
184 try:
185 articulation = create_articulation(sim)
186 print(f"[INFO]: Initial joint positions: {articulation.get_qpos()}", flush=True)
187
188 if not args.headless and not args.viser:
189 sim.open_window()
190
191 print("[INFO]: Running simulation. Press Ctrl+C to stop.", flush=True)
192 run_simulation(sim, articulation, max_steps=args.max_steps)
193 finally:
194 sim.destroy()
195
196
197if __name__ == "__main__":
198 main()
The Code Explained#
Configuring the simulation#
Create a SimulationManager in manual-update mode. The common launcher
arguments let the same script run on CPU or CUDA, in multiple environments, or
with native or browser visualization.
# Configure the simulation. Window creation is deferred until the asset is loaded.
sim_cfg = SimulationManagerCfg(
headless=args.headless,
sim_device=args.device,
num_envs=args.num_envs,
arena_space=2.0,
physics_dt=1.0 / 100.0,
render_cfg=RenderCfg(renderer=args.renderer),
visualization=visualization_cfg_from_args(args),
)
sim = SimulationManager(sim_cfg)
Loading the URDF#
Resolve the bundled drawer asset, then pass its path to
cfg.ArticulationCfg. The example intentionally does not set
drive_pros. Therefore the configuration uses the Articulation default,
drive_type="none". SimulationManager.add_articulation loads one drawer
into each configured environment and returns a batched
objects.Articulation handle.
The URDF defines slide_rails over [0.0, 0.2] metres. The example also
sets qpos_limits={"slide_rails": [0.0, 0.18]}, retaining 90% of the asset’s
travel while keeping the fully closed position valid. This becomes the
effective physics limit used by both the backend and the force-control loop.
# Resolve the drawer URDF and configure its initial pose. ``drive_pros`` is
# intentionally omitted: ArticulationCfg defaults to drive_type="none".
articulation_cfg = ArticulationCfg(
uid="drawer",
fpath=get_data_path(DRAWER_ASSET),
init_pos=(0.0, 0.0, 0.05),
fix_base=True,
# The asset limit is [0.0, 0.2]; keep 90% of its travel range.
qpos_limits=DRAWER_USER_QPOS_LIMITS,
)
# Load one articulation instance into every simulation environment.
articulation: Articulation = sim.add_articulation(cfg=articulation_cfg)
Verifying the constructed drive type#
Checking articulation.cfg.drive_pros.drive_type confirms the requested
configuration, but it does not prove what the physics backend received. The
example therefore calls objects.Articulation.get_joint_drive_type(),
which reads the drive type from every constructed DexSim entity. It raises an
error unless every joint in every environment is DriveType.NONE.
# Query the constructed DexSim entities, not only the config object.
backend_drive_types = articulation.get_joint_drive_type()
expected_drive_types = [
[DriveType.NONE] * articulation.dof for _ in range(sim.num_envs)
]
if backend_drive_types != expected_drive_types:
raise RuntimeError(
"Expected every articulation joint drive to be DriveType.NONE, "
f"but received {backend_drive_types!r}."
)
For the bundled drawer, the relevant output is:
[INFO]: Loaded articulation with 1 joint(s)
[INFO]: Config drive type: none
[INFO]: Backend drive types: [[<DriveType.NONE: ...>]]
[INFO]: Effective qpos limits: tensor([[[0.0000, 0.1800]]])
The numeric enum value represented by ... is backend-version dependent;
the semantic value is always DriveType.NONE.
Opening and closing the drawer#
The articulation state APIs are batched. For example,
objects.Articulation.get_qpos() returns a tensor with shape
(num_envs, dof), while objects.Articulation.get_qpos_limits() returns
(num_envs, dof, 2). The tutorial treats each lower limit as the closed
position and each upper limit as the open position.
Because this Articulation has drive_type="none", it does not track position
or velocity targets. Instead, the example uses objects.Articulation.set_qf()
to apply a +1 N generalized joint force while opening and a -1 N force
while closing. Applying external effort does not change the passive drive type.
def apply_drawer_force(articulation: Articulation, opening: bool) -> None:
"""Apply a joint force that opens or closes the drawer.
Args:
articulation: Drawer articulation receiving the force.
opening: If True, apply positive force; otherwise apply negative force.
"""
force = DRAWER_JOINT_FORCE if opening else -DRAWER_JOINT_FORCE
joint_forces = torch.full_like(articulation.get_qpos(), force)
articulation.set_qf(joint_forces)
The simulation loop reads the current joint position on every step. When every drawer instance reaches its upper limit, it reverses the force to close; at the lower limit, it reverses again to open. The force is cleared before the loop returns.
def run_simulation(
sim: SimulationManager,
articulation: Articulation,
max_steps: int | None = None,
) -> None:
"""Open and close the drawer by reversing force at its joint limits.
Args:
sim: Simulation manager to advance.
articulation: Drawer articulation whose joints are updated.
max_steps: Optional number of steps to run before returning.
"""
if sim.is_use_gpu_physics:
sim.init_gpu_physics()
qpos_limits = articulation.get_qpos_limits()
closed_qpos = qpos_limits[..., 0]
open_qpos = qpos_limits[..., 1]
opening = True
step_count = 0
print(
f"[INFO]: Applying +{DRAWER_JOINT_FORCE:.1f} N to open the drawer",
flush=True,
)
try:
while max_steps is None or step_count < max_steps:
qpos = articulation.get_qpos()
if opening and torch.all(qpos >= open_qpos - JOINT_LIMIT_TOLERANCE).item():
print(f"[INFO]: Drawer reached open limit: {qpos}", flush=True)
opening = False
print(
f"[INFO]: Applying -{DRAWER_JOINT_FORCE:.1f} N to close the drawer",
flush=True,
)
elif (
not opening
and torch.all(qpos <= closed_qpos + JOINT_LIMIT_TOLERANCE).item()
):
print(f"[INFO]: Drawer reached closed limit: {qpos}", flush=True)
opening = True
print(
f"[INFO]: Applying +{DRAWER_JOINT_FORCE:.1f} N to open the drawer",
flush=True,
)
apply_drawer_force(articulation, opening=opening)
sim.update(step=1)
step_count += 1
except KeyboardInterrupt:
print("\n[INFO]: Stopping simulation...")
finally:
articulation.set_qf(torch.zeros_like(articulation.get_qpos()))
The finite run reports both limit transitions:
[INFO]: Applying +1.0 N to open the drawer
[INFO]: Drawer reached open limit: tensor([[0.1800]])
[INFO]: Applying -1.0 N to close the drawer
[INFO]: Drawer reached closed limit: tensor([[0.]])
Run the interactive example from the repository root:
python scripts/tutorials/sim/create_articulation.py
For a finite, headless CPU verification:
python scripts/tutorials/sim/create_articulation.py \
--headless \
--device cpu \
--max-steps 210
To inspect the articulation in a browser:
python scripts/tutorials/sim/create_articulation.py --viser
Enabling a drive explicitly#
Passive joints react to contacts, gravity, friction, and externally applied
forces, but they do not track position or velocity targets. If a generic
articulation needs an actuator, opt in with
cfg.JointDrivePropertiesCfg:
from embodichain.lab.sim.cfg import ArticulationCfg, JointDrivePropertiesCfg
articulation_cfg = ArticulationCfg(
fpath="path/to/articulation.urdf",
drive_pros=JointDrivePropertiesCfg(
drive_type="force",
stiffness=1.0e4,
damping=1.0e3,
),
)
For a controllable robot, prefer cfg.RobotCfg and
SimulationManager.add_robot(); robots default to drive_type="force".
Attention
For USD assets, use_usd_properties=True preserves the drive types stored
in the USD file instead of applying the Articulation configuration default.
Next Steps#
Simulating a Robot — Load and control a driven robot
Simulating a Camera Sensor — Attach cameras and collect sensor data
Articulation — Articulation configuration and API reference