diff --git a/examples/reach_plan_sweep.py b/examples/reach_plan_sweep.py new file mode 100644 index 0000000..db67bcf --- /dev/null +++ b/examples/reach_plan_sweep.py @@ -0,0 +1,65 @@ +"""Script to run reach_plan_sweep.""" + +import argparse + +from isaaclab.app import AppLauncher + +parser = argparse.ArgumentParser(description="Sample dx/dy/dz/yaw around base reach pose and batch-plan with cuRobo.") +parser.add_argument("--pipeline_id", required=True, type=str) +parser.add_argument( + "--reach_skill_index", + default=0, + type=int, + help="Which reach skill to sweep at (0-based, globally across all subtasks)", +) +parser.add_argument("--num_samples", default=64, type=int) +parser.add_argument("--dx", default=0.01, type=float, help="dx range is [-dx, dx] meters") +parser.add_argument("--dy", default=0.01, type=float, help="dy range is [-dy, dy] meters") +parser.add_argument("--dz", default=0.01, type=float, help="dz range is [-dz, dz] meters") +parser.add_argument("--yaw_deg", default=10.0, type=float, help="yaw range is [-yaw_deg, yaw_deg] degrees") +parser.add_argument("--seed", default=42, type=int) +parser.add_argument("--top_k", default=10, type=int) +parser.add_argument( + "--ik_only", + action="store_true", + help="Use IK-only solving instead of full motion planning (faster reachability check)", +) + +AppLauncher.add_app_launcher_args(parser) +args_cli = parser.parse_args() + +# Launch Isaac Sim / IsaacLab app +app_launcher_args = vars(args_cli) +app_launcher = AppLauncher(app_launcher_args) +simulation_app = app_launcher.app + +import math + +import autosim_examples # noqa: F401 +from autosim import make_pipeline +from autosim.calibration.plan_sweep import ReachPlanSweepCfg, reach_plan_sweep +from autosim.calibration.pose_sampler import OffsetSampler + + +def main() -> None: + pipeline = make_pipeline(args_cli.pipeline_id) + reach_plan_sweep( + pipeline, + ReachPlanSweepCfg( + reach_skill_index=args_cli.reach_skill_index, + sampling=OffsetSampler( + num_samples=args_cli.num_samples, + dx_range=(-args_cli.dx, args_cli.dx), + dy_range=(-args_cli.dy, args_cli.dy), + dz_range=(-args_cli.dz, args_cli.dz), + yaw_range_rad=(-math.radians(args_cli.yaw_deg), math.radians(args_cli.yaw_deg)), + seed=args_cli.seed, + ), + top_k=args_cli.top_k, + ik_only=args_cli.ik_only, + ), + ) + + +if __name__ == "__main__": + main() diff --git a/source/autosim/autosim/calibration/__init__.py b/source/autosim/autosim/calibration/__init__.py new file mode 100644 index 0000000..0218ae0 --- /dev/null +++ b/source/autosim/autosim/calibration/__init__.py @@ -0,0 +1,9 @@ +from .plan_sweep import ReachPlanSweepCfg, reach_plan_sweep +from .pose_sampler import OffsetSampler, PoseSampler + +__all__ = [ + "PoseSampler", + "OffsetSampler", + "ReachPlanSweepCfg", + "reach_plan_sweep", +] diff --git a/source/autosim/autosim/calibration/plan_sweep.py b/source/autosim/autosim/calibration/plan_sweep.py new file mode 100644 index 0000000..3a84252 --- /dev/null +++ b/source/autosim/autosim/calibration/plan_sweep.py @@ -0,0 +1,257 @@ +import time +from dataclasses import dataclass, field +from typing import Any + +import isaaclab.utils.math as PoseUtils +import torch + +from autosim.core.pipeline import AutoSimPipeline +from autosim.core.registration import SkillRegistry + +from .pose_sampler import OffsetSampler, PoseSampler + + +@dataclass +class ReachPlanSweepCfg: + reach_skill_index: int = 0 + """Which reach skill to sweep at (0-based, globally across all subtasks).""" + sampling: PoseSampler = field(default_factory=OffsetSampler) + """Sampler used to generate candidate poses around the base pose.""" + top_k: int = 10 + """Number of top poses to print.""" + ik_only: bool = False + """If True, use IK-only solving instead of full motion planning. Much faster for reachability checking; does not produce trajectories.""" + + +def _tensor_to_list(x: torch.Tensor) -> list[float]: + return [float(v) for v in x.detach().cpu().flatten().tolist()] + + +def _fmt_pose(vals: list[float]) -> str: + """Format a 7-value pose as a Python list literal, ready to copy-paste into code.""" + return "[" + ", ".join(f"{v:.4f}" for v in vals) + "]" + + +def reach_plan_sweep(pipeline: AutoSimPipeline, cfg: ReachPlanSweepCfg) -> list[dict[str, Any]]: + """ + Execute the pipeline step by step. When the reach_skill_index-th reach skill + is encountered, capture the live robot and object state and sweep around the + reach target pose using cuRobo batch planning. + + All skills before the target reach skill are executed normally, so the + environment reflects the actual state at the point of interest. + + For multi-arm tasks (i.e. when `EnvExtraInfo.object_extra_reach_target_poses` is set), + extra EE goals are swept in lock-step with the main EE: both use the same sampled + offsets, so the relative configuration between arms is preserved across all candidates. + + Returns: + Top-k result rows sorted by plan quality. Each row contains: + "pose_oe": list[float] — main EE pose in object frame [x,y,z,qw,qx,qy,qz] + "extra_pose_oe/": list[float] — extra EE poses (multi-arm only) + "plan_success": bool + "traj_len": int | None — trajectory length (full planning only) + "position_error": float | None — IK position error (IK-only mode) + + Typical multi-reach workflow (each step is a separate script invocation): + # Step 1: sweep reach 0, note the best pose_oe from the printout + python reach_plan_sweep.py --reach_skill_index 0 ... + + # Manually update object_reach_target_poses[obj][0] in the pipeline code + + # Step 2: sweep reach 1; reach 0 now runs with the updated hard-coded pose + python reach_plan_sweep.py --reach_skill_index 1 ... + """ + + # modified from pipeline.run() to only execute the reach skill at the specified index + decompose_result = pipeline.decompose() + pipeline._check_skill_extra_cfg() + pipeline.reset_env() + + reach_skill_counter = 0 + reach_count_per_object: dict[str, int] = {} + + for subtask in decompose_result.subtasks: + for skill_info in subtask.skills: + skill = SkillRegistry.create( + skill_info.skill_type, + pipeline.cfg.skills.get(skill_info.skill_type).extra_cfg, + ) + + if pipeline._action_adapter.should_skip_apply(skill): + continue + + is_reach = skill_info.skill_type == "reach" + + if is_reach and reach_skill_counter == cfg.reach_skill_index: + obj_name = skill_info.target_object + obj_pose_idx = reach_count_per_object.get(obj_name, 0) + return _sweep(pipeline, cfg, obj_name, obj_pose_idx) + + goal = skill.extract_goal_from_info(skill_info, pipeline._env, pipeline._env_extra_info) + if is_reach: + reach_count_per_object[skill_info.target_object] = ( + reach_count_per_object.get(skill_info.target_object, 0) + 1 + ) + reach_skill_counter += 1 + + success, _ = pipeline._execute_single_skill(skill, goal) + if not success: + raise ValueError( + f"Skill '{skill_info.skill_type}' (step {skill_info.step}) failed before reaching target reach" + f" skill (index {cfg.reach_skill_index})." + ) + + raise ValueError( + f"reach_skill_index={cfg.reach_skill_index} is out of range: only {reach_skill_counter} reach skill(s) found in" + " the decompose result." + ) + + +def _sweep(pipeline: AutoSimPipeline, cfg: ReachPlanSweepCfg, obj_name: str, obj_pose_idx: int) -> list[dict[str, Any]]: + """ + Core sweep logic. Called once the environment is in the correct pre-reach state. + + Samples K candidate poses around the base reach target (object frame), transforms them to + robot root frame, then batch-plans with cuRobo. If `object_extra_reach_target_poses` is + defined for this object, extra EE goals are sampled with the same offsets (OffsetSampler + resets its RNG from `self.seed` on every `sample()` call, so identical offsets are applied + to every EE) and passed to the planner as `link_goals`. + + Args: + pipeline: The pipeline providing env, planner, and extra info. + cfg: Sweep configuration (sampler, top_k, ik_only). + obj_name: Name of the target object in the scene. + obj_pose_idx: Index into `object_reach_target_poses[obj_name]` — which reach call this is. + + Returns: + Top-k result rows (see `reach_plan_sweep` for row schema), sorted by plan quality. + """ + + env = pipeline._env + env_id = pipeline._env_id + env_extra_info = pipeline._env_extra_info + planner = pipeline._motion_planner + robot = pipeline._robot + + pose_list = env_extra_info.object_reach_target_poses[obj_name] + if not (0 <= obj_pose_idx < len(pose_list)): + raise ValueError(f"pose index {obj_pose_idx} out of range for object '{obj_name}' ({len(pose_list)} poses).") + + base_pose_oe = torch.as_tensor(pose_list[obj_pose_idx], device=env.device, dtype=torch.float32).view(7) + + # Sample object-frame candidate poses around base + poses_oe = cfg.sampling.sample(base_pose_oe) + k = int(poses_oe.shape[0]) + + # Read live object pose in world frame + obj_pose_w = env.scene[obj_name].data.root_pose_w[env_id] + obj_pos_w = obj_pose_w[:3].view(1, 3).repeat(k, 1) + obj_quat_w = obj_pose_w[3:].view(1, 4).repeat(k, 1) + + # object frame -> world frame -> robot root frame + target_pos_w, target_quat_w = PoseUtils.combine_frame_transforms( + obj_pos_w, obj_quat_w, poses_oe[:, :3], poses_oe[:, 3:] + ) + robot_root_pose_w = robot.data.root_pose_w[env_id] + rr_pos_w = robot_root_pose_w[:3].view(1, 3).repeat(k, 1) + rr_quat_w = robot_root_pose_w[3:].view(1, 4).repeat(k, 1) + target_pos_r, target_quat_r = PoseUtils.subtract_frame_transforms(rr_pos_w, rr_quat_w, target_pos_w, target_quat_w) + + # Build current joint state in planner joint order (name-based mapping) + state = pipeline._build_world_state() + full_sim_joint_names = state.sim_joint_names + full_q = state.robot_joint_pos + full_qd = state.robot_joint_vel + activate_q, activate_qd = [], [] + for joint_name in planner.target_joint_names: + if joint_name not in full_sim_joint_names: + raise ValueError(f"Planner joint '{joint_name}' not found in simulation joint names.") + idx = full_sim_joint_names.index(joint_name) + activate_q.append(full_q[idx]) + activate_qd.append(full_qd[idx]) + activate_q = torch.stack(activate_q, dim=0) + activate_qd = torch.stack(activate_qd, dim=0) + + # Build extra EE goals from object_extra_reach_target_poses (multi-arm only). + # OffsetSampler.sample() resets its RNG from self.seed on every call, so sample i of any + # extra EE receives the same dx/dy/dz/dyaw offset as sample i of the main EE — keeping all + # arms coherently displaced across the entire candidate batch. + extra_poses_oe_dict: dict[str, torch.Tensor] = {} + link_goals: dict[str, torch.Tensor] | None = None + + extra_pose_map = env_extra_info.object_extra_reach_target_poses.get(obj_name, {}) + if extra_pose_map: + extra_link_pos_r: dict[str, torch.Tensor] = {} + extra_link_quat_r: dict[str, torch.Tensor] = {} + for ee_name, ee_pose_list in extra_pose_map.items(): + if not (0 <= obj_pose_idx < len(ee_pose_list)): + raise ValueError( + f"extra pose index {obj_pose_idx} out of range for '{obj_name}'/'{ee_name}'" + f" ({len(ee_pose_list)} poses)." + ) + ee_base = torch.as_tensor(ee_pose_list[obj_pose_idx], device=env.device, dtype=torch.float32).view(7) + ee_poses_oe = cfg.sampling.sample(ee_base) + extra_poses_oe_dict[ee_name] = ee_poses_oe + + ee_pos_w, ee_quat_w = PoseUtils.combine_frame_transforms( + obj_pos_w, obj_quat_w, ee_poses_oe[:, :3], ee_poses_oe[:, 3:] + ) + ee_pos_r, ee_quat_r = PoseUtils.subtract_frame_transforms(rr_pos_w, rr_quat_w, ee_pos_w, ee_quat_w) + extra_link_pos_r[ee_name] = ee_pos_r + extra_link_quat_r[ee_name] = ee_quat_r + + link_goals = {ee: torch.cat([extra_link_pos_r[ee], extra_link_quat_r[ee]], dim=-1) for ee in extra_link_pos_r} + + # Batch plan + t0 = time.time() + if cfg.ik_only: + result = planner.solve_ik_batch(target_pos_r, target_quat_r, link_goals=link_goals) + else: + result = planner.plan_motion_batch(target_pos_r, target_quat_r, activate_q, activate_qd, link_goals=link_goals) + dt_ms = (time.time() - t0) * 1000.0 + + success = ( + result.success.detach().cpu().bool() if result.success is not None else torch.zeros((k,), dtype=torch.bool) + ) + pos_err = result.position_error.detach().cpu() if result.position_error is not None else None + traj_last = ( + result.path_buffer_last_tstep if (not cfg.ik_only and result.path_buffer_last_tstep is not None) else None + ) + + rows = [] + for i in range(k): + rows.append({ + "pose_oe": _tensor_to_list(poses_oe[i]), + **{f"extra_pose_oe/{ee}": _tensor_to_list(extra_poses_oe_dict[ee][i]) for ee in extra_poses_oe_dict}, + "plan_success": bool(success[i].item()), + "traj_len": int(traj_last[i]) if traj_last is not None else None, + "position_error": float(pos_err[i].item()) if pos_err is not None else None, + }) + + def _sort_key(r): + if not r["plan_success"]: + return (10**9, 10**9) + return (r["traj_len"] or 10**8, r["position_error"] or 10**8) + + top_k = sorted(rows, key=_sort_key)[: cfg.top_k] + + success_count = int(success.sum().item()) + _SEP = "─" * 80 + print(_SEP) + print(f" reach_plan_sweep │ object='{obj_name}' reach_skill_index={cfg.reach_skill_index}") + print(f" success {success_count}/{k} ({success_count / k:.1%}) │ time {dt_ms:.0f} ms") + print(_SEP) + print(f" top {len(top_k)} poses (object frame [x, y, z, qw, qx, qy, qz])") + print() + for rank, r in enumerate(top_k): + mark = "✓" if r["plan_success"] else "✗" + metric = f"traj_len={r['traj_len']}" if r["traj_len"] is not None else f"pos_err={r['position_error']:.4f}" + print(f" [{rank}] {mark} {_fmt_pose(r['pose_oe'])} # {metric}") + for ee in extra_poses_oe_dict: + print(f" {ee}: {_fmt_pose(r[f'extra_pose_oe/{ee}'])}") + if extra_poses_oe_dict: + print() + print(_SEP) + + return top_k diff --git a/source/autosim/autosim/calibration/pose_sampler.py b/source/autosim/autosim/calibration/pose_sampler.py new file mode 100644 index 0000000..2254e9f --- /dev/null +++ b/source/autosim/autosim/calibration/pose_sampler.py @@ -0,0 +1,76 @@ +from abc import ABC, abstractmethod + +import torch + + +class PoseSampler(ABC): + @abstractmethod + def sample(self, base_pose_oe: torch.Tensor) -> torch.Tensor: + """ + Sample candidate poses around a base pose in object frame. + + Args: + base_pose_oe: Tensor shape [7] in object frame [x,y,z,qw,qx,qy,qz] + + Returns: + poses_oe: Tensor [K, 7] of sampled poses in object frame + """ + + +class OffsetSampler(PoseSampler): + """Samples poses by adding uniform random dx/dy/dz/yaw offsets around a base pose.""" + + def __init__( + self, + num_samples: int = 64, + dx_range: tuple[float, float] = (-0.03, 0.03), + dy_range: tuple[float, float] = (-0.03, 0.03), + dz_range: tuple[float, float] = (-0.02, 0.02), + yaw_range_rad: tuple[float, float] = (-0.35, 0.35), + seed: int = 0, + ): + self.num_samples = num_samples + self.dx_range = dx_range + self.dy_range = dy_range + self.dz_range = dz_range + self.yaw_range_rad = yaw_range_rad + self.seed = seed + + def sample(self, base_pose_oe: torch.Tensor) -> torch.Tensor: + if base_pose_oe.shape != (7,): + raise ValueError(f"base_pose_oe must have shape [7], got {tuple(base_pose_oe.shape)}") + + g = torch.Generator(device=base_pose_oe.device) + g.manual_seed(int(self.seed)) + + k = self.num_samples + dx = torch.empty((k,), device=base_pose_oe.device).uniform_(*self.dx_range, generator=g) + dy = torch.empty((k,), device=base_pose_oe.device).uniform_(*self.dy_range, generator=g) + dz = torch.empty((k,), device=base_pose_oe.device).uniform_(*self.dz_range, generator=g) + dyaw = torch.empty((k,), device=base_pose_oe.device).uniform_(*self.yaw_range_rad, generator=g) + + pos = base_pose_oe[:3].view(1, 3).repeat(k, 1) + torch.stack([dx, dy, dz], dim=-1) + + # Apply yaw delta around object local +Z axis: q_new = q_base ⊗ q_delta + base_quat = base_pose_oe[3:].view(1, 4).repeat(k, 1) + half = dyaw * 0.5 + q_delta = torch.stack( + [torch.cos(half), torch.zeros_like(half), torch.zeros_like(half), torch.sin(half)], dim=-1 + ) + quat = self._quat_mul(base_quat, q_delta) + + return torch.cat([pos, quat], dim=-1) + + @staticmethod + def _quat_mul(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: + w1, x1, y1, z1 = q1.unbind(-1) + w2, x2, y2, z2 = q2.unbind(-1) + return torch.stack( + [ + w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2, + w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2, + w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2, + w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2, + ], + dim=-1, + ) diff --git a/source/autosim/autosim/capabilities/motion_planning/curobo/curobo_planner.py b/source/autosim/autosim/capabilities/motion_planning/curobo/curobo_planner.py index acd2d74..52c164f 100644 --- a/source/autosim/autosim/capabilities/motion_planning/curobo/curobo_planner.py +++ b/source/autosim/autosim/capabilities/motion_planning/curobo/curobo_planner.py @@ -91,7 +91,7 @@ def __init__( # Warm up planner self._logger.info("Warming up motion planner...") - self.motion_gen.warmup(enable_graph=True, warmup_js_trajopt=False) + self.motion_gen.warmup(enable_graph=self.cfg.use_cuda_graph, warmup_js_trajopt=False) # Read static world geometry once self._initialize_static_world() @@ -246,6 +246,137 @@ def plan_motion( self._logger.warning(f"planning failed: {result.status}") return None + def plan_motion_batch( + self, + target_pos: torch.Tensor, + target_quat: torch.Tensor, + current_q: torch.Tensor, + current_qd: torch.Tensor | None = None, + link_goals: dict[str, torch.Tensor] | None = None, + ): + """ + Plan trajectories for a batch of target poses from the same start joint state. + + This uses cuRobo's batch API (`MotionGen.plan_batch`) under the hood. + + Args: + target_pos: Tensor of shape [K, 3], in robot root frame. + target_quat: Tensor of shape [K, 4] in [qw, qx, qy, qz], in robot root frame. + current_q: Tensor of shape [dof], current joint positions. + current_qd: Tensor of shape [dof], current joint velocities. Defaults to zeros. + link_goals: Optional dict mapping extra link names to tensors of shape [K, 7] + ([x, y, z, qw, qx, qy, qz], robot root frame) for multi-arm robots. Each entry + specifies the simultaneous target pose of that link for every sample in the batch. + + Returns: + MotionGenResult (cuRobo). Check `result.success[k]` for each batch index. + + Note: + `time_dilation_factor` is always suppressed for batch planning because cuRobo's + `retime_trajectory` does not support batch results. + """ + + if target_pos.ndim != 2 or target_pos.shape[-1] != 3: + raise ValueError(f"target_pos must have shape [K, 3], got {tuple(target_pos.shape)}") + if target_quat.ndim != 2 or target_quat.shape[-1] != 4: + raise ValueError(f"target_quat must have shape [K, 4], got {tuple(target_quat.shape)}") + if target_pos.shape[0] != target_quat.shape[0]: + raise ValueError( + f"Batch size mismatch: target_pos has {target_pos.shape[0]}, target_quat has {target_quat.shape[0]}" + ) + k = target_pos.shape[0] + if link_goals is not None: + for ee_name, poses in link_goals.items(): + if poses.ndim != 2 or poses.shape != (k, 7): + raise ValueError(f"link_goals['{ee_name}'] must have shape [{k}, 7], got {tuple(poses.shape)}") + + if current_qd is None: + current_qd = torch.zeros_like(current_q) + + dof_needed = len(self.target_joint_names) + if len(current_q) < dof_needed: + pad = torch.zeros(dof_needed - len(current_q), dtype=current_q.dtype, device=current_q.device) + current_q = torch.concatenate([current_q, pad], axis=0) + current_qd = torch.concatenate([current_qd, torch.zeros_like(pad)], axis=0) + elif len(current_q) > dof_needed: + current_q = current_q[:dof_needed] + current_qd = current_qd[:dof_needed] + + goal = Pose( + position=self._to_curobo_device(target_pos), + quaternion=self._to_curobo_device(target_quat), + ) + + start_state = JointState( + position=self._to_curobo_device(current_q).view(1, -1), + velocity=self._to_curobo_device(current_qd).view(1, -1) * 0.0, + acceleration=self._to_curobo_device(current_qd).view(1, -1) * 0.0, + jerk=self._to_curobo_device(current_qd).view(1, -1) * 0.0, + joint_names=self.target_joint_names, + ).repeat_seeds(int(target_pos.shape[0])) + + link_poses = None + if link_goals is not None: + link_poses = { + ee_name: Pose( + position=self._to_curobo_device(poses[:, :3]), + quaternion=self._to_curobo_device(poses[:, 3:]), + ) + for ee_name, poses in link_goals.items() + } + # plan_batch does not support retime_trajectory (batch result); disable time_dilation_factor + batch_plan_config = self.plan_config.clone() + batch_plan_config.time_dilation_factor = None + return self.motion_gen.plan_batch(start_state, goal, batch_plan_config, link_poses=link_poses) + + def solve_ik_batch( + self, + target_pos: torch.Tensor, + target_quat: torch.Tensor, + link_goals: dict[str, torch.Tensor] | None = None, + ): + """ + Solve IK for a batch of target poses without trajectory optimization. + + Faster than plan_motion_batch for reachability checking since it skips + trajectory optimization entirely. + + Args: + target_pos: Tensor of shape [K, 3], in robot root frame. + target_quat: Tensor of shape [K, 4] in [qw, qx, qy, qz], in robot root frame. + link_goals: Optional dict mapping extra link names to tensors of shape [K, 7] + ([x, y, z, qw, qx, qy, qz], robot root frame) for multi-arm robots. + + Returns: + IKResult from cuRobo. Check result.success[k], result.position_error[k], + result.rotation_error[k] for each batch index. + """ + + if target_pos.ndim != 2 or target_pos.shape[-1] != 3: + raise ValueError(f"target_pos must have shape [K, 3], got {tuple(target_pos.shape)}") + if target_quat.ndim != 2 or target_quat.shape[-1] != 4: + raise ValueError(f"target_quat must have shape [K, 4], got {tuple(target_quat.shape)}") + k = target_pos.shape[0] + if link_goals is not None: + for ee_name, poses in link_goals.items(): + if poses.ndim != 2 or poses.shape != (k, 7): + raise ValueError(f"link_goals['{ee_name}'] must have shape [{k}, 7], got {tuple(poses.shape)}") + + goal = Pose( + position=self._to_curobo_device(target_pos), + quaternion=self._to_curobo_device(target_quat), + ) + link_poses = None + if link_goals is not None: + link_poses = { + ee_name: Pose( + position=self._to_curobo_device(poses[:, :3]), + quaternion=self._to_curobo_device(poses[:, 3:]), + ) + for ee_name, poses in link_goals.items() + } + return self.motion_gen.ik_solver.solve_batch(goal, link_poses=link_poses) + def reset(self): """reset the planner state""" diff --git a/source/autosim/autosim/decomposers/llm_decomposer/prompts/task_decompose.jinja b/source/autosim/autosim/decomposers/llm_decomposer/prompts/task_decompose.jinja index b79d6ac..1709b13 100644 --- a/source/autosim/autosim/decomposers/llm_decomposer/prompts/task_decompose.jinja +++ b/source/autosim/autosim/decomposers/llm_decomposer/prompts/task_decompose.jinja @@ -65,7 +65,7 @@ Output the task decomposition result in JSON format with the following fields: 2. **Pick-and-Place Pattern** (for moving objects to specific location): ``` - moveto(object) → reach(object) → grasp(object) → [moveto(target)](optional) → reach(target) → ungrasp(object) + moveto(object) → reach(object) → grasp(object) → lift(object) → [moveto(target)](optional) → reach(target) → ungrasp(object) ``` 3. **Press Pattern** (for pressing buttons):