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"""Open and close a passive drawer with joint forces.
18
19The default demonstrates a URDF drawer. Pass ``--asset usd`` to load
20``DrawerUSD/drawer_001.usdc`` and move only its top drawer.
21"""
22
23from __future__ import annotations
24
25import argparse
26from typing import Literal
27from embodichain.cli.sim import add_sim_args_to_parser
28
29
30def build_parser() -> argparse.ArgumentParser:
31 """Build CLI options without initializing simulation resources."""
32 parser = argparse.ArgumentParser(
33 description="Load an articulation with its default passive joint drive"
34 )
35 add_sim_args_to_parser(parser)
36 parser.add_argument(
37 "--asset",
38 choices=("urdf", "usd"),
39 default="urdf",
40 help="Drawer asset to demonstrate; usd moves the top drawer of DrawerUSD.",
41 )
42 parser.add_argument(
43 "--max-steps",
44 type=int,
45 default=None,
46 help="Optional number of simulation steps before exiting.",
47 )
48 return parser
49
50
51if __name__ == "__main__":
52 # Parse before importing optional simulation/planning dependencies.
53 _cli_args = build_parser().parse_args()
54
55
56import torch
57
58from dexsim.types import DriveType
59
60from embodichain.data import get_data_path
61from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
62from embodichain.lab.sim.cfg import (
63 ArticulationCfg,
64 ArticulationRootPropertiesCfg,
65 DefaultRigidBodyPropertiesCfg,
66 JointDrivePropertiesCfg,
67 RenderCfg,
68 RigidBodyPhysicsCfg,
69 physics_cfg_for_backend,
70)
71from embodichain.lab.sim.objects import Articulation
72from embodichain.lab.visualization import visualization_cfg_from_args
73
74DRAWER_ASSET = "SlidingBoxDrawer/SlidingBoxDrawer.urdf"
75USD_DRAWER_ASSET = "DrawerUSD/drawer_001.usdc"
76DRAWER_USER_QPOS_LIMITS = {"slide_rails": [0.0, 0.18]}
77DRAWER_JOINT_FORCE_LIMIT = 1.0
78DRAWER_POSITION_GAIN = 20.0
79DRAWER_VELOCITY_GAIN = 4.0
80JOINT_POSITION_TOLERANCE = 1.0e-3
81JOINT_VELOCITY_TOLERANCE = 1.0e-2
82
83
84def create_articulation(
85 sim: SimulationManager, asset: Literal["urdf", "usd"] = "urdf"
86) -> Articulation:
87 """Load a drawer articulation with the passive default drive.
88
89 Args:
90 sim: Simulation manager that owns the scene.
91 asset: URDF drawer or the USD cabinet with three drawers.
92
93 Returns:
94 The loaded drawer articulation.
95
96 Raises:
97 ValueError: If the asset selector is unknown.
98 RuntimeError: If the constructed backend joints are not passive.
99 """
100 if asset not in ("urdf", "usd"):
101 raise ValueError(f"Unknown drawer asset: {asset!r}.")
102 is_usd = asset == "usd"
103 # Resolve the selected asset and explicitly request the passive drive used by
104 # this tutorial while retaining all unconfigured asset properties.
105 articulation_cfg = ArticulationCfg(
106 uid="drawer",
107 fpath=get_data_path(USD_DRAWER_ASSET if is_usd else DRAWER_ASSET),
108 asset_physics_mode="overlay",
109 # The USD cabinet extends 0.31 m below its root; lift it clear of the ground.
110 init_pos=(0.0, 0.0, 0.8 if is_usd else 0.05),
111 root_props=ArticulationRootPropertiesCfg(fixed_base=True),
112 joint_drive_props=JointDrivePropertiesCfg(drive_type="none"),
113 # Keep 90% of the URDF travel; retain the USD's named per-drawer limits.
114 qpos_limits=None if is_usd else DRAWER_USER_QPOS_LIMITS,
115 # Newton currently has no body-level damping setting. Remove the
116 # Default backend's damping so both passive models use zero damping.
117 attrs=RigidBodyPhysicsCfg(
118 rigid_props=DefaultRigidBodyPropertiesCfg(
119 linear_damping=0.0,
120 angular_damping=0.0,
121 )
122 ),
123 )
124
125 # Load one articulation instance into every simulation environment.
126 articulation: Articulation = sim.add_articulation(cfg=articulation_cfg)
127 sim.prepare()
128
129 # Query the constructed DexSim entities, not only the config object.
130 backend_drive_types = articulation.get_joint_drive_type()
131 expected_drive_types = [
132 [DriveType.NONE] * articulation.dof for _ in range(sim.num_envs)
133 ]
134 if backend_drive_types != expected_drive_types:
135 raise RuntimeError(
136 "Expected every articulation joint drive to be DriveType.NONE, "
137 f"but received {backend_drive_types!r}."
138 )
139
140 print(f"[INFO]: Loaded articulation with {articulation.dof} joint(s)", flush=True)
141 print(f"[INFO]: Joint names: {articulation.joint_names}", flush=True)
142 print(
143 f"[INFO]: Config drive type: {articulation.cfg.joint_drive_props.drive_type}",
144 flush=True,
145 )
146 print(f"[INFO]: Backend drive types: {backend_drive_types}", flush=True)
147 print(
148 f"[INFO]: Effective qpos limits: {articulation.get_qpos_limits()}", flush=True
149 )
150 return articulation
151
152
153def apply_drawer_force(
154 articulation: Articulation,
155 target_qpos: torch.Tensor,
156 joint_ids: list[int] | None = None,
157) -> None:
158 """Apply effort-limited PD control toward a drawer position.
159
160 Args:
161 articulation: Drawer articulation receiving the force.
162 target_qpos: Target positions for every environment and selected joint.
163 joint_ids: Joint indices to control; None selects all joints.
164 """
165 qpos = articulation.get_qpos()
166 qvel = articulation.get_qvel()
167 if joint_ids is not None:
168 qpos, qvel = qpos[:, joint_ids], qvel[:, joint_ids]
169 position_error = target_qpos - qpos
170 joint_forces = DRAWER_POSITION_GAIN * position_error - DRAWER_VELOCITY_GAIN * qvel
171 joint_forces = torch.clamp(
172 joint_forces,
173 min=-DRAWER_JOINT_FORCE_LIMIT,
174 max=DRAWER_JOINT_FORCE_LIMIT,
175 )
176 articulation.set_qf(joint_forces, joint_ids=joint_ids)
177
178
179def run_simulation(
180 sim: SimulationManager,
181 articulation: Articulation,
182 max_steps: int | None = None,
183 *,
184 joint_name: str = "slide_rails",
185 open_at_lower_limit: bool = False,
186) -> None:
187 """Open and close the drawer with effort-limited position tracking.
188
189 Args:
190 sim: Simulation manager to advance.
191 articulation: Drawer articulation whose joints are updated.
192 max_steps: Optional number of steps to run before returning.
193 joint_name: Drawer joint to move; all other joints remain passive.
194 open_at_lower_limit: Whether opening moves toward the lower joint limit.
195 """
196 joint_ids = [articulation.joint_names.index(joint_name)]
197 qpos_limits = articulation.get_qpos_limits(joint_ids=joint_ids)
198 closed_qpos = qpos_limits[..., 1 if open_at_lower_limit else 0]
199 open_qpos = qpos_limits[..., 0 if open_at_lower_limit else 1]
200 opening = True
201 target_qpos = open_qpos
202 step_count = 0
203 print(
204 f"[INFO]: Tracking {joint_name!r} toward the open position with effort limited to "
205 f"+/-{DRAWER_JOINT_FORCE_LIMIT:.1f} N",
206 flush=True,
207 )
208 try:
209 while max_steps is None or step_count < max_steps:
210 qpos = articulation.get_qpos()[:, joint_ids]
211 qvel = articulation.get_qvel()[:, joint_ids]
212 settled = torch.all(
213 (torch.abs(qpos - target_qpos) <= JOINT_POSITION_TOLERANCE)
214 & (torch.abs(qvel) <= JOINT_VELOCITY_TOLERANCE)
215 ).item()
216 if settled:
217 reached_position = "open" if opening else "closed"
218 print(
219 f"[INFO]: Drawer settled at {reached_position} position: "
220 f"qpos={qpos}, qvel={qvel}",
221 flush=True,
222 )
223 opening = not opening
224 target_qpos = open_qpos if opening else closed_qpos
225 target_position = "open" if opening else "closed"
226 print(
227 f"[INFO]: Tracking the {target_position} position",
228 flush=True,
229 )
230
231 apply_drawer_force(
232 articulation, target_qpos=target_qpos, joint_ids=joint_ids
233 )
234 sim.update(step=1)
235 step_count += 1
236 except KeyboardInterrupt:
237 print("\n[INFO]: Stopping simulation...")
238 finally:
239 articulation.set_qf(
240 torch.zeros_like(articulation.get_qpos()[:, joint_ids]),
241 joint_ids=joint_ids,
242 )
243
244
245def main(args: argparse.Namespace | None = None) -> None:
246 """Load and simulate a passive drawer articulation."""
247 parser = build_parser()
248 if args is None:
249 args = parser.parse_args()
250 if args.max_steps is not None and args.max_steps < 1:
251 parser.error("--max-steps must be at least 1")
252
253 open_native_window = not args.headless and not args.viser
254
255 # Construct the World without a window so Spawn can finish first. The
256 # requested native window is opened explicitly after create_articulation().
257 sim_cfg = SimulationManagerCfg(
258 headless=True,
259 sim_device=args.device,
260 num_envs=args.num_envs,
261 arena_space=2.0,
262 physics_dt=1.0 / 100.0,
263 physics_cfg=physics_cfg_for_backend(args.physics),
264 render_cfg=RenderCfg(renderer=args.renderer),
265 visualization=visualization_cfg_from_args(args),
266 )
267 sim = SimulationManager(sim_cfg)
268
269 try:
270 articulation = create_articulation(sim, asset=args.asset)
271 print(f"[INFO]: Initial joint positions: {articulation.get_qpos()}", flush=True)
272
273 if open_native_window:
274 sim.open_window()
275
276 print("[INFO]: Running simulation. Press Ctrl+C to stop.", flush=True)
277 run_simulation(
278 sim,
279 articulation,
280 max_steps=args.max_steps,
281 joint_name="top_slide" if args.asset == "usd" else "slide_rails",
282 open_at_lower_limit=args.asset == "usd",
283 )
284 finally:
285 sim.destroy(exit_process=False)
286
287
288if __name__ == "__main__":
289 try:
290 main(_cli_args)
291 finally:
292 SimulationManager.flush_cleanup_queue()
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.
def create_articulation(
sim: SimulationManager, asset: Literal["urdf", "usd"] = "urdf"
) -> Articulation:
"""Load a drawer articulation with the passive default drive.
Args:
sim: Simulation manager that owns the scene.
asset: URDF drawer or the USD cabinet with three drawers.
Returns:
The loaded drawer articulation.
Raises:
ValueError: If the asset selector is unknown.
RuntimeError: If the constructed backend joints are not passive.
"""
if asset not in ("urdf", "usd"):
raise ValueError(f"Unknown drawer asset: {asset!r}.")
is_usd = asset == "usd"
# Resolve the selected asset and explicitly request the passive drive used by
# this tutorial while retaining all unconfigured asset properties.
articulation_cfg = ArticulationCfg(
uid="drawer",
fpath=get_data_path(USD_DRAWER_ASSET if is_usd else DRAWER_ASSET),
asset_physics_mode="overlay",
# The USD cabinet extends 0.31 m below its root; lift it clear of the ground.
init_pos=(0.0, 0.0, 0.8 if is_usd else 0.05),
root_props=ArticulationRootPropertiesCfg(fixed_base=True),
joint_drive_props=JointDrivePropertiesCfg(drive_type="none"),
# Keep 90% of the URDF travel; retain the USD's named per-drawer limits.
qpos_limits=None if is_usd else DRAWER_USER_QPOS_LIMITS,
# Newton currently has no body-level damping setting. Remove the
# Default backend's damping so both passive models use zero damping.
attrs=RigidBodyPhysicsCfg(
rigid_props=DefaultRigidBodyPropertiesCfg(
linear_damping=0.0,
angular_damping=0.0,
)
),
)
# Load one articulation instance into every simulation environment.
articulation: Articulation = sim.add_articulation(cfg=articulation_cfg)
sim.prepare()
# 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}."
)
print(f"[INFO]: Loaded articulation with {articulation.dof} joint(s)", flush=True)
print(f"[INFO]: Joint names: {articulation.joint_names}", flush=True)
print(
f"[INFO]: Config drive type: {articulation.cfg.joint_drive_props.drive_type}",
flush=True,
)
print(f"[INFO]: Backend drive types: {backend_drive_types}", flush=True)
print(
f"[INFO]: Effective qpos limits: {articulation.get_qpos_limits()}", flush=True
)
return articulation
def apply_drawer_force(
articulation: Articulation,
target_qpos: torch.Tensor,
joint_ids: list[int] | None = None,
) -> None:
"""Apply effort-limited PD control toward a drawer position.
Args:
articulation: Drawer articulation receiving the force.
target_qpos: Target positions for every environment and selected joint.
joint_ids: Joint indices to control; None selects all joints.
"""
qpos = articulation.get_qpos()
qvel = articulation.get_qvel()
if joint_ids is not None:
qpos, qvel = qpos[:, joint_ids], qvel[:, joint_ids]
position_error = target_qpos - qpos
joint_forces = DRAWER_POSITION_GAIN * position_error - DRAWER_VELOCITY_GAIN * qvel
joint_forces = torch.clamp(
joint_forces,
min=-DRAWER_JOINT_FORCE_LIMIT,
max=DRAWER_JOINT_FORCE_LIMIT,
)
articulation.set_qf(joint_forces, joint_ids=joint_ids)
def run_simulation(
sim: SimulationManager,
articulation: Articulation,
max_steps: int | None = None,
*,
joint_name: str = "slide_rails",
open_at_lower_limit: bool = False,
) -> None:
"""Open and close the drawer with effort-limited position tracking.
Args:
sim: Simulation manager to advance.
articulation: Drawer articulation whose joints are updated.
max_steps: Optional number of steps to run before returning.
joint_name: Drawer joint to move; all other joints remain passive.
open_at_lower_limit: Whether opening moves toward the lower joint limit.
"""
joint_ids = [articulation.joint_names.index(joint_name)]
qpos_limits = articulation.get_qpos_limits(joint_ids=joint_ids)
closed_qpos = qpos_limits[..., 1 if open_at_lower_limit else 0]
open_qpos = qpos_limits[..., 0 if open_at_lower_limit else 1]
opening = True
target_qpos = open_qpos
step_count = 0
print(
f"[INFO]: Tracking {joint_name!r} toward the open position with effort limited to "
f"+/-{DRAWER_JOINT_FORCE_LIMIT:.1f} N",
flush=True,
)
try:
while max_steps is None or step_count < max_steps:
qpos = articulation.get_qpos()[:, joint_ids]
qvel = articulation.get_qvel()[:, joint_ids]
settled = torch.all(
(torch.abs(qpos - target_qpos) <= JOINT_POSITION_TOLERANCE)
& (torch.abs(qvel) <= JOINT_VELOCITY_TOLERANCE)
).item()
if settled:
reached_position = "open" if opening else "closed"
print(
f"[INFO]: Drawer settled at {reached_position} position: "
f"qpos={qpos}, qvel={qvel}",
flush=True,
)
opening = not opening
target_qpos = open_qpos if opening else closed_qpos
target_position = "open" if opening else "closed"
print(
f"[INFO]: Tracking the {target_position} position",
flush=True,
)
apply_drawer_force(
articulation, target_qpos=target_qpos, joint_ids=joint_ids
)
sim.update(step=1)
step_count += 1
except KeyboardInterrupt:
print("\n[INFO]: Stopping simulation...")
finally:
articulation.set_qf(
torch.zeros_like(articulation.get_qpos()[:, joint_ids]),
joint_ids=joint_ids,
)
def main(args: argparse.Namespace | None = None) -> None:
"""Load and simulate a passive drawer articulation."""
parser = build_parser()
if args is None:
args = parser.parse_args()
if args.max_steps is not None and args.max_steps < 1:
parser.error("--max-steps must be at least 1")
open_native_window = not args.headless and not args.viser
# Construct the World without a window so Spawn can finish first. The
# requested native window is opened explicitly after create_articulation().
sim_cfg = SimulationManagerCfg(
headless=True,
sim_device=args.device,
num_envs=args.num_envs,
arena_space=2.0,
physics_dt=1.0 / 100.0,
physics_cfg=physics_cfg_for_backend(args.physics),
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
joint_drive_props. 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.
Verifying the constructed drive type#
Checking articulation.cfg.joint_drive_props.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,
target_qpos: torch.Tensor,
joint_ids: list[int] | None = None,
) -> None:
"""Apply effort-limited PD control toward a drawer position.
Args:
articulation: Drawer articulation receiving the force.
target_qpos: Target positions for every environment and selected joint.
joint_ids: Joint indices to control; None selects all joints.
"""
qpos = articulation.get_qpos()
qvel = articulation.get_qvel()
if joint_ids is not None:
qpos, qvel = qpos[:, joint_ids], qvel[:, joint_ids]
position_error = target_qpos - qpos
joint_forces = DRAWER_POSITION_GAIN * position_error - DRAWER_VELOCITY_GAIN * qvel
joint_forces = torch.clamp(
joint_forces,
min=-DRAWER_JOINT_FORCE_LIMIT,
max=DRAWER_JOINT_FORCE_LIMIT,
)
articulation.set_qf(joint_forces, joint_ids=joint_ids)
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,
*,
joint_name: str = "slide_rails",
open_at_lower_limit: bool = False,
) -> None:
"""Open and close the drawer with effort-limited position tracking.
Args:
sim: Simulation manager to advance.
articulation: Drawer articulation whose joints are updated.
max_steps: Optional number of steps to run before returning.
joint_name: Drawer joint to move; all other joints remain passive.
open_at_lower_limit: Whether opening moves toward the lower joint limit.
"""
joint_ids = [articulation.joint_names.index(joint_name)]
qpos_limits = articulation.get_qpos_limits(joint_ids=joint_ids)
closed_qpos = qpos_limits[..., 1 if open_at_lower_limit else 0]
open_qpos = qpos_limits[..., 0 if open_at_lower_limit else 1]
opening = True
target_qpos = open_qpos
step_count = 0
print(
f"[INFO]: Tracking {joint_name!r} toward the open position with effort limited to "
f"+/-{DRAWER_JOINT_FORCE_LIMIT:.1f} N",
flush=True,
)
try:
while max_steps is None or step_count < max_steps:
qpos = articulation.get_qpos()[:, joint_ids]
qvel = articulation.get_qvel()[:, joint_ids]
settled = torch.all(
(torch.abs(qpos - target_qpos) <= JOINT_POSITION_TOLERANCE)
& (torch.abs(qvel) <= JOINT_VELOCITY_TOLERANCE)
).item()
if settled:
reached_position = "open" if opening else "closed"
print(
f"[INFO]: Drawer settled at {reached_position} position: "
f"qpos={qpos}, qvel={qvel}",
flush=True,
)
opening = not opening
target_qpos = open_qpos if opening else closed_qpos
target_position = "open" if opening else "closed"
print(
f"[INFO]: Tracking the {target_position} position",
flush=True,
)
apply_drawer_force(
articulation, target_qpos=target_qpos, joint_ids=joint_ids
)
sim.update(step=1)
step_count += 1
except KeyboardInterrupt:
print("\n[INFO]: Stopping simulation...")
finally:
articulation.set_qf(
torch.zeros_like(articulation.get_qpos()[:, joint_ids]),
joint_ids=joint_ids,
)
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",
joint_drive_props=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 file-backed assets, asset_physics_mode="preserve" keeps source
physics, while "overlay" applies explicitly configured values.
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