Creating a new task
Build L0–L3 tabletop tasks from the environment template, load YCB / RoboTwin / PartNet / sketchfab assets, and add a scripted motion-planning solution. Everything below follows the design templates so contributed tasks work with the data pipeline unchanged.
Design templates
The repository ships four design templates that gate every contribution:
| Template | Path | What it documents |
|---|---|---|
| Model template | examples/baselines/_template/ | Dataset contract, agent interface, how to add a new baseline |
| Task template (L0–L3) | mani_skill/envs/tasks/_template/ | L0/L1/L2 vs L3 environments, object loading, level semantics |
| Robot template | mani_skill/agents/robots/_template/ | Import a new robot from a URDF |
| Motion-planning solution | mani_skill/examples/motionplanning/dual/ | Add a scripted solution for a task |
Task implementation contract
Each released task is a matched set of four components:
- one base environment registered as
TwoRobot{Task}-v1; - one standalone L3 environment registered as
TwoRobot{Task}L3-v1; - one base and one L3 motion-planning solution registered in
MP_SOLUTIONS; and - four aliases in the dataset task mapping.
The environment owns scene construction, episode initialization, success predicates, and
reward phases. Level utilities own only the controlled L1/L2 changes shared across tasks.
Motion-planning solutions access task geometry through the environment and produce the same
two-agent pd_joint_pos action interface used by recorded demonstrations.
Create the environment
Start from the template at
mani_skill/envs/tasks/_template/template_task.py:
-
Copy it to
mani_skill/envs/tasks/tabletop/dual_tasks/_NNN_your_task.py. - Change the
@register_envid, class name, and docstring. - Import them in
mani_skill/envs/tasks/tabletop/__init__.py. - Replace the assets in
_load_scene()— useobject_loader.py. - Update
_initialize_episode(),evaluate(), andcompute_dense_reward().
The L0/L1/L2 class is registered as TwoRobotTemplateTask-v1; the L3 class as
TwoRobotTemplateTaskL3-v1. Rename both before use.
Loading objects
Assets live under ~/.maniskill/data (see the
installation page). The template provides loaders for all
four namespaces:
| Asset set | Loader | Download |
|---|---|---|
| YCB | load_ycb_object | python -m mani_skill.utils.download_asset ycb |
| RoboTwin | load_robotwin_object | python -m mani_skill.utils.download_robotwin |
| PartNet-Mobility | load_partnet_object | python -m mani_skill.utils.download_partnet |
| sketchfab GLB | load_sketchfab_object | manual (see assets/sketchfab_README.md) |
from mani_skill.envs.tasks._template.object_loader import (
load_ycb_object, load_robotwin_object, load_partnet_object,
load_sketchfab_object, z_offset_to_table,
)
# YCB object (dynamic)
self.apple, _ = load_ycb_object(self.scene, "013_apple", position=[0, 0.5, 0],
scale=0.7, mass=0.5)
# RoboTwin container (static goal)
self.basket, _ = load_robotwin_object(self.scene, "076_breadbasket",
position=[0, -0.5, 0],
rotation=(np.pi / 2, 0, 0),
is_static=True)
# PartNet articulated object (microwave, cabinet, ...)
self.microwave, _ = load_partnet_object(self.scene, "microwave", "7119",
position=[0.3, 0.1, 0],
robot_base_position=[0, -1, 0])
# External GLB asset
self.scale, _ = load_sketchfab_object(self.scene, "balance_scale",
position=[0.2, 0.0, 0.0])
How levels work
| Level | What changes | Where it is implemented |
|---|---|---|
| L0 | base scene (nothing) | the environment class itself |
| L1 | same objects, rearranged layout | apply_l1_offset_xy(...) in _initialize_episode |
| L2 | same semantics, different instances | apply_l2_ycb_model_id(...) in __init__ |
| L3 | different semantics, reusable affordances | a separate ...L3-v1 class |
The level is chosen before gym.make via
configure_dual_task_level(level), or via the --l0/--l1/--l2 flags
of two_robot_run. Do not add level logic to the policy. Full semantics are on
the levels page.
Required method contract
Every task environment must implement:
_load_scene(options)— build table + load all actors/articulations_initialize_episode(env_idx, options)— randomize per-episode state_after_reconfigure(options)— compute z-offsets for table contactevaluate()— return dict with asuccesstensor (rule-based)_get_obs_extra(info)— extra observation fieldscompute_dense_reward(obs, action, info)— rewardcompute_normalized_dense_reward(obs, action, info)
Camera configuration (_default_sensor_configs, hi_res,
wrist_sensor) and the dual-agent setup are identical to the released tasks;
keep them as-is so the data pipeline (two_robot_run, collect_data,
h5_to_lerobot) works unchanged. Keep the same __init__ signature
(hi_res, wrist_sensor, robot_init_qpos_noise), the
same camera sets, RewardTracker phases, and rule-based evaluate()
with a success field.
Add a motion-planning solution
Every released task is four components — base env, L3 env, base solution, and L3 solution.
Copy the reference solution and implement solve():
cp mani_skill/examples/motionplanning/dual/solutions/two_robot_pick_cube_ycb.py \
mani_skill/examples/motionplanning/dual/solutions/_NNN_your_task.py
The runner calls solve(env, seed=None, debug=False, vis=False) and expects the
tuple (left_res, right_res):
from mani_skill.envs.tasks import YourTaskEnv
from mani_skill.examples.motionplanning.panda.motionplanner import (
PandaArmMotionPlanningSolver,
)
def solve(env: YourTaskEnv, seed=None, debug=False, vis=False):
env.reset(seed=seed)
left_planner = PandaArmMotionPlanningSolver(
env, debug=debug, vis=vis,
base_pose=env.unwrapped.agent.agents[0].robot.pose,
visualize_target_grasp_pose=vis, print_env_info=False,
multi_robot_id=0, # left robot
)
right_planner = PandaArmMotionPlanningSolver(
env, debug=debug, vis=vis,
base_pose=env.unwrapped.agent.agents[1].robot.pose,
visualize_target_grasp_pose=vis, print_env_info=False,
multi_robot_id=1, # right robot
)
env = env.unwrapped
# Always pass the other robot's gripper state to avoid collisions.
left_planner.move_to_pose_with_screw(
my_pose, other_gripper_state=right_planner.gripper_state)
left_planner.close_gripper(other_gripper_state=right_planner.gripper_state)
...
left_res = left_planner.open_gripper(other_gripper_state=right_planner.gripper_state)
return left_res, left_res
Key planner API (PandaArmMotionPlanningSolver):
| Method | Purpose |
|---|---|
move_to_pose_with_screw(pose, other_gripper_state=...) | plan + execute a screw-motion path to pose |
close_gripper(...) / open_gripper(...) | gripper commands (returns the step result tuple) |
follow_path(result) | execute a returned plan |
gripper_state | current gripper state (share it with the other planner) |
move_to_pose_with_screw(..., dry_run=True) | plan without executing |
Useful helpers: get_actor_obb(actor),
compute_grasp_info_by_obb(...) from ../panda/utils.py to build
grasp poses from an object's oriented bounding box.
Register the solution
In solutions/__init__.py:
from ._NNN_your_task import solve as solveYourTask
In two_robot_run.py, add to MP_SOLUTIONS:
MP_SOLUTIONS = {
...,
"TwoRobotYourTask-v1": solveYourTask,
}
If the task is L0/L1/L2, its L3 variant goes into solutions_l3/ and is
registered with the ...L3-v1 key.
Use is_l2_enabled() from
envs/tasks/tabletop/utils/L0_L3_utils.py inside the solution to adapt
waypoints when needed. env.reset(seed=seed) at the start keeps episodes
reproducible; the runner increments seed per trajectory. Always pass
other_gripper_state — the two planners share one scene and would otherwise
collide.
Add a robot
New robots (URDF agents) are contributed via the robot template under
mani_skill/agents/robots/_template/, which documents how to register a URDF
agent and its supported controllers. Robots such as the single-arm Panda, dual-arm Panda
pairs, xArm6+Allegro, Fetch, so100, and widowxai are already supported by
TableSceneBuilder.initialize().
Verify
python -m mani_skill.examples.motionplanning.dual.two_robot_run \
-e TwoRobotTemplateTask-v1 -n 1 --only-count-success --record-dir demos
Ready to share your task? Follow the contribution workflow to open a PR and request pairing for your accepted task.