diff --git a/.ci/test-impact.toml b/.ci/test-impact.toml index 3f57a3086..0f7ad96ac 100644 --- a/.ci/test-impact.toml +++ b/.ci/test-impact.toml @@ -340,7 +340,7 @@ risk = "medium" [[rules]] id = "atomic-actions" paths = ["embodichain/lab/sim/atomic_actions/**"] -tests = ["tests/sim/atomic_actions/**"] +tests = ["tests/sim/atomic_actions/**", "tests/examples/atomic_action/test_support.py"] resource_hints = ["sim"] risk = "medium" @@ -361,8 +361,7 @@ risk = "medium" [[rules]] id = "atomic-action-tutorials" paths = ["scripts/tutorials/atomic_action/*.py"] -tests = ["tests/sim/atomic_actions/**"] -resource_hints = ["sim"] +tests = ["tests/visualization/test_example_tutorial_coverage.py"] risk = "medium" [[rules]] diff --git a/.gitignore b/.gitignore index 1da54924c..4a83151ef 100644 --- a/.gitignore +++ b/.gitignore @@ -192,7 +192,7 @@ outputs /output/ test_configs/* embodichain_data/ -scripts/tutorials/atomic_action/*.glb +examples/sim/atomic_action/*.glb wandb/ *.mp4 diff --git a/agent_context/topics/atomic-actions/affordance-sampling.md b/agent_context/topics/atomic-actions/affordance-sampling.md index 3c07d1a8e..a8345647f 100644 --- a/agent_context/topics/atomic-actions/affordance-sampling.md +++ b/agent_context/topics/atomic-actions/affordance-sampling.md @@ -26,15 +26,15 @@ Action/Goal inputs rather than Affordance variation. ## Current host boundary The only host integration is direct simulation. The PickUp, AxisAlign, -HandOver, OpenDoor, Press, Slide, and Twist tutorials map +HandOver, OpenDoor, Press, Slide, and Twist examples map `--affordance_branches` one-to-one to simulation rows and inject the context through the shared host helpers in -`scripts/tutorials/atomic_action/tutorial_utils.py`. HandOver samples pickup +`examples/sim/atomic_action/support.py`. HandOver samples pickup and receiving grasps in separate named streams while preserving opposite object ends and per-row arm assignment; its diagnostics retain the selected assignment's candidate metadata and control parts. Press samples local contact-frame roll; -the Twist tutorial explicitly declares `grasp_roll_range=(-pi, pi)` for parallel -branches. Press, Slide, and OpenDoor tutorials use `ik_interp` for their exact +the Twist example explicitly declares `grasp_roll_range=(-pi, pi)` for parallel +branches. Press, Slide, and OpenDoor examples use `ik_interp` for their exact Cartesian contact/path samples. The seed and attempt identity control Affordance selection, not upstream grasp @@ -42,7 +42,7 @@ generation. Antipodal approach-direction perturbations still use PyTorch's global RNG and each row has its own candidate pool. Candidate IDs and reuse flags are row-local; they do not certify cross-row uniqueness. One branch keeps sampling disabled. See the human-facing -[tutorial matrix](../../../docs/source/overview/sim/atomic_actions/affordance_sampling.md) +[example matrix](../../../docs/source/overview/sim/atomic_actions/affordance_sampling.md) for supported geometric freedom and CLI examples. Do not route this feature through Task Program, the Gym bridge, environment @@ -54,7 +54,7 @@ define an explicit expansion policy. - Sampling contracts and selection: `atomic_actions/affordance_sampling.py`. - Geometric samplers: `atomic_actions/affordance.py`. - Feasibility and diagnostics: matching file under `atomic_actions/primitives/`. -- Direct host examples: `scripts/tutorials/atomic_action/` and its shared - `tutorial_utils.py` sampling helpers. +- Direct host examples: `examples/sim/atomic_action/` and its shared + `support.py` sampling helpers. - Tests: `tests/sim/atomic_actions/test_affordance_sampling.py`, `test_affordance.py`, `test_actions.py`, `test_core.py`, and `test_engine.py`. diff --git a/agent_context/topics/atomic-actions/atomic-actions.md b/agent_context/topics/atomic-actions/atomic-actions.md index ad2fbf66c..0ac5c13e0 100644 --- a/agent_context/topics/atomic-actions/atomic-actions.md +++ b/agent_context/topics/atomic-actions/atomic-actions.md @@ -133,7 +133,11 @@ The example lives in `examples/sim/atomic_action/` and is shipped as ordinary distribution data with the rest of that tree. Its `__main__.py` owns typed configuration and validated selection; `host.py` owns compilation, expansion and output. Scene modules own physical setup and local defaults. -Tutorial files forwarding there are compatibility entries. Read the +All runnable demonstrations and helpers live there. Specialized examples such +as coordinated manipulation and observed recovery run by file path through +`embodichain run-example`. The tutorial directory contains only `single_arm.py` +(PickUp→Place) and `dual_arm.py` (HandOver), which reuse example setup while +explaining typed application composition. Read the [example README](../../../examples/sim/atomic_action/README.md) for supported combinations, fixture calibration, playback and output artifacts. diff --git a/agent_context/topics/motion-planning/motion-planning.md b/agent_context/topics/motion-planning/motion-planning.md index 3e8f4b18c..9cb68efc2 100644 --- a/agent_context/topics/motion-planning/motion-planning.md +++ b/agent_context/topics/motion-planning/motion-planning.md @@ -149,10 +149,9 @@ explicitly enabled factor that cannot produce a qpos variant raises rather than disappearing: `ik` without Jacobians, and `approach`, which owns Cartesian standoff poses the caller must replan. The resolved config is reported on the result. -`scripts/tutorials/atomic_action/place.py` is the demonstration host, hooked -the way the Affordance sampling tutorials are: shared helpers live in -`examples/sim/atomic_action/support.py` through the `tutorial_utils.py` adapter; -the tutorial declares only which named segments may move, +`examples/sim/atomic_action/place.py` is the demonstration host. Shared +helpers live directly in `examples/sim/atomic_action/support.py`; the example +declares which named segments may move, and one variant replays per simulation row. Phases come from the action's own `TrajectorySegment` ranges; only motion at or after the lift is retimed so the shared `clear_dynamics()` step stays aligned. `--variant_plot_dir` writes a diff --git a/agent_context/topics/motion-planning/planner-details.md b/agent_context/topics/motion-planning/planner-details.md index 0e6272fa7..26ce0238d 100644 --- a/agent_context/topics/motion-planning/planner-details.md +++ b/agent_context/topics/motion-planning/planner-details.md @@ -34,8 +34,11 @@ Preserve these coupled result rules in `motion_generator.py`: - Resampling is time-based, preserves each row's duration, recomputes velocities and invalidates accelerations. Unchanged samples retain native derivatives. - `uses_sparse_joint_waypoints` lets a backend receive `start_qpos` plus goals - without generic pre-interpolation. `preserve_plan_samples` keeps its native - grid and derivatives through normalization. TrapezoidalPlanner declares both. + without generic pre-interpolation. Free Cartesian goals become sparse IK + waypoints; only an explicit `is_linear` request adds Cartesian path samples. + This avoids treating interpolated samples as mandatory rest points. + `preserve_plan_samples` keeps its native grid and derivatives through + normalization. TrapezoidalPlanner declares both. - Failed rows hold the supplied start pose unless the backend declares `preserve_failed_plan_positions`; NeuralPlanner uses that capability to keep partial rollout evidence while reporting failure. diff --git a/docs/source/overview/sim/atomic_actions/affordance_sampling.md b/docs/source/overview/sim/atomic_actions/affordance_sampling.md index 9a4a08fb5..52fd51625 100644 --- a/docs/source/overview/sim/atomic_actions/affordance_sampling.md +++ b/docs/source/overview/sim/atomic_actions/affordance_sampling.md @@ -55,17 +55,17 @@ Explicit caller-provided grasp poses remain exact. Slide travel distance, Twist angle, and OpenDoor opening fraction are action or goal parameters and are not sampled by the Affordance layer. -## Parallel simulation tutorials +## Parallel simulation examples -The following tutorials map one Affordance branch to one simulation environment. +The following examples map one Affordance branch to one simulation environment. They share argument validation, sampling-context construction, and per-row -diagnostic logging in `scripts/tutorials/atomic_action/tutorial_utils.py`. +diagnostic logging in `examples/sim/atomic_action/support.py`. -| Entry point under `scripts/tutorials/atomic_action/` | Geometric augmentation | Sampling metadata | Preserved task inputs | +| Entry point under `examples/sim/atomic_action/` | Geometric augmentation | Sampling metadata | Preserved task inputs | |---|---|---|---| | `pickup.py` | Select antipodal candidates returned by `AntipodalGraspPoseGenerator.get_valid_grasp_poses()`, after pickup IK and symmetric-roll screening | `grasp`: candidate ID, candidate counts, reuse | Approach direction, pre-grasp distance, lift height | | `axis_align.py` | Select generator candidates within the action's preferred alignment-grasp set, then apply the existing symmetric/upright pose adjustments | `grasp`: candidate ID, candidate counts, reuse | Object internal axis and requested target axis | -| `hand_over.py` | Independently select generator candidates for pickup and receiving grasps; the first hand uses its near projected object end and the receiver uses the opposite end at the predicted middle pose | `pickup_grasp` and `receive_grasp`: candidate ID, candidate counts, reuse, executing arm | Nearest-arm assignment, opposite-end constraint, lift height, final target | +| `handover.py` | Independently select generator candidates for pickup and receiving grasps; the first hand uses its near projected object end and the receiver uses the opposite end at the predicted middle pose | `pickup_grasp` and `receive_grasp`: candidate ID, candidate counts, reuse, executing arm | Nearest-arm assignment, opposite-end constraint, lift height, final target | | `open_door.py` | Select generator candidates on the door handle and replan the hinge-following motion | `grasp`: candidate ID, candidate counts, reuse | Hinge geometry and requested opening fraction | | `press.py` | Sample a contact-frame roll in `[-pi, pi]` radians around the press pose's local z-axis | `roll`: per-row contact roll in radians | Contact position, local z-axis, approach distance, press distance | | `slide.py` | Select generator candidates on the handle for both pull and push; push plans from the handle pose observed after pull replay | `grasp`: candidate ID, candidate counts, reuse | Pull/push direction and translation distance | @@ -96,7 +96,7 @@ Every listed entry point accepts the same branch controls: For example: ```bash -python scripts/tutorials/atomic_action/pickup.py \ +embodichain run-example sim/atomic_action/pickup \ --affordance_branches 4 \ --sampling_seed 7 \ --sampling_attempt 0 \ @@ -110,9 +110,9 @@ also support the same roll augmentation. Press, Slide, and OpenDoor use `strategy="ik_interp"` to preserve their required Cartesian contact/path samples; their exact-path segments do not use the `--planner` backend. -Each tutorial logs the selected candidate and reuse status, or the sampled roll, +Each example logs the selected candidate and reuse status, or the sampled roll, for every row before replay. HandOver also logs the arm for each grasp stage. -The tutorials replay only when every row has a successful plan. Planning +The examples replay only when every row has a successful plan. Planning success is not a physical task-success measurement. The shared parser accepts `--num_envs=1` as the default sentinel; otherwise diff --git a/docs/source/overview/sim/atomic_actions/builtin_actions.md b/docs/source/overview/sim/atomic_actions/builtin_actions.md index a087bd7c1..85bfe9599 100644 --- a/docs/source/overview/sim/atomic_actions/builtin_actions.md +++ b/docs/source/overview/sim/atomic_actions/builtin_actions.md @@ -5,7 +5,7 @@ ```{currentmodule} embodichain.lab.sim.atomic_actions ``` -EmbodiChain ships thirteen built-in action implementations with stable skill IDs; +EmbodiChain ships built-in action implementations with stable skill IDs; `AtomicActionEngine` creates and registers a fresh instance of every built-in by default. Applications select them by stable skill ID rather than registering routine instances themselves. @@ -36,8 +36,9 @@ multi-stage in-hand manipulation require additional command types and segments. ## Visual catalog -The animations below are the focused simulator demos under -`scripts/tutorials/atomic_action/`. +The animations below are the runnable simulator demonstrations under +`examples/sim/atomic_action/`. The two teaching walkthroughs are described in +{doc}`/tutorial/atomic_actions`. ::::{grid} 1 2 2 2 :gutter: 2 @@ -366,7 +367,7 @@ Use an explicit pose for a fixed target, `SceneEntityPose` for a tracked target, or `(B, N, 4, 4)` for intermediate waypoints. The action does not command an end effector/hand resource. -**Example:** `scripts/tutorials/atomic_action/move_end_effector.py` +**Example:** `examples/sim/atomic_action/move_end_effector.py` (builtin-move-joints)= @@ -403,7 +404,7 @@ explicit_goal = JointPositionGoal(target=home_qpos) named_goal = JointPositionGoal(target="home") ``` -**Example:** `scripts/tutorials/atomic_action/move_joints.py` +**Example:** `examples/sim/atomic_action/move_joint.py` (builtin-pick-up)= @@ -473,10 +474,10 @@ generation or trajectory determinism. Explicit goal grasps and `ObjectSemantics.entity` without an ID is a deprecated compatibility path. Its live pose does not create an automatic scene dependency. -**Example:** `scripts/tutorials/atomic_action/pickup.py` currently exercises the +**Example:** `examples/sim/atomic_action/pickup.py` currently exercises the deprecated entity-only fallback. For canonical snapshot grounding and moving target recovery, see -`scripts/tutorials/atomic_action/moving_target_recovery.py`. +`examples/sim/atomic_action/moving_target_recovery.py`. ### Picking articulated targets @@ -531,7 +532,7 @@ Set `root_props=ArticulationRootPropertiesCfg(fixed_base=False)`. Left at the default, `plan_success` still reports success and the whole trajectory replays while the object never moves, so verify the target pose rather than the plan. -`scripts/tutorials/atomic_action/pickup_rubiks_cube.py` demonstrates both on a +`examples/sim/atomic_action/pickup_rubiks_cube.py` demonstrates both on a Rubik's cube whose `top_turn` joint is locked with a stiff position drive so the two layers grasp as one body. Grasps are sampled from the `lower_two_layers` link, the joint's parent. @@ -579,7 +580,7 @@ and `(B, 3)` respectively. Zero or non-finite axes are rejected, and exactly opposite axes use a deterministic 180-degree rotation rather than an unstable cross-product direction. -**Example:** `scripts/tutorials/atomic_action/axis_align.py` provides +**Example:** `examples/sim/atomic_action/axis_align.py` provides `--alignment upright` (align object-local X to world Z) and `--alignment horizontal_align` (align object-local X to world Y). @@ -618,7 +619,9 @@ sampling remains in `MotionPolicy`. In a vectorized batch, rows where another manipulator holds the same semantic object or live entity are marked unsuccessful and held in place. -**Example:** `scripts/tutorials/atomic_action/move_held_object.py` +**Examples:** `examples/sim/atomic_action/move_held_object.py` provides the fixed +cube scene (`--skill move_held_object`); +`examples/sim/atomic_action/move_held_object_cup.py` preserves the paper-cup sequence. (builtin-pour)= @@ -649,7 +652,7 @@ planner can chain the outbound and return legs. | Motion | rotate by `rotate_angle`, then reverse by the same angle, with the hand held at `grasp` | | Effect | none; the existing attachment is preserved | -**Example:** `scripts/tutorials/atomic_action/pour.py` compiles a horizontal +**Example:** `examples/sim/atomic_action/pour.py` compiles a horizontal `PickUp` followed by `Pour`. (builtin-push-object)= @@ -724,7 +727,7 @@ The bound `primary.grasp` endpoint must provide `open` and `grasp`. Important | `cartesian_waypoint_count` | Fixed-orientation translation keyframes per segment | | `preserve_current_object_orientation` | Keep the observed object orientation while using the target translation | -**Example:** `scripts/tutorials/atomic_action/place.py` +**Example:** `examples/sim/atomic_action/place.py` (builtin-assemble)= @@ -748,7 +751,7 @@ temporarily falls back to the affordance's `base_object_entity` with a deprecation warning; that fallback is not a scene dependency. Planning declares the same detach effect as a normal place. -**Example:** `scripts/tutorials/atomic_action/assemble.py` currently exercises +**Example:** `examples/sim/atomic_action/assemble.py` currently exercises the legacy `base_object_entity` fallback and is not the canonical `base_pose` form. It remains a compatibility example until the registry-backed tutorial migration. @@ -794,7 +797,7 @@ generic `ActionBinding`, and the action keeps the gripper closed for all arm motion segments. Applications that require force/contact confirmation must verify it externally. -**Example:** `scripts/tutorials/atomic_action/press.py` loads +**Example:** `examples/sim/atomic_action/press.py` loads `Microwave/microwave.urdf` and targets `button_link`, whose parent prismatic joint is `button_joint`. It requests `MotionPolicy.sample_count=256` with `PressOptions.hand_interp_steps=12`, then holds the final pose for 256 @@ -862,7 +865,7 @@ translation axis belongs to `SlideAffordance`; the bound `primary.grasp` endpoint must provide `open` and `grasp`. Reach, pull/push, and push-return use axis-aligned Cartesian samples rather than sparse joint-space endpoints. -**Example:** `scripts/tutorials/atomic_action/slide.py` +**Example:** `examples/sim/atomic_action/slide.py` plans and replays a pull first, then replans a push from the drawer's measured post-pull link pose. @@ -918,7 +921,7 @@ segment start plus one for every configured door-arc keyframe; the full motion policy therefore needs `sample_count >= 2 * hand_interp_steps + door_waypoint_count + 7`. -**Example:** `scripts/tutorials/atomic_action/open_door.py` configures only the +**Example:** `examples/sim/atomic_action/open_door.py` configures only the `handle_link` target in `Microwave/microwave.urdf`. Automatic traversal resolves `door_hinge` through `door_handle_fixed_joint`. Its absolute `--open_angle` value is normalized against the resolved hinge limits and passed as the goal's @@ -981,7 +984,7 @@ completion therefore means commanded motion completion only. Applications that need semantic success must observe button/contact or articulation state and verify it outside the side-effect-free planner. -**Example:** `scripts/tutorials/atomic_action/twist.py` loads +**Example:** `examples/sim/atomic_action/twist.py` loads `Microwave/microwave.urdf` and targets `knob_link`, whose parent revolute joint is `knob_joint`. It requests `MotionPolicy.sample_count=256` with `TwistOptions.hand_interp_steps=20`, then holds the final pose for 256 @@ -1040,7 +1043,7 @@ corresponding participant slots. Coordinated dual-arm planning with supported by the cuRobo backend; use the supported IK/interpolation path for this primitive. -**Example:** `scripts/tutorials/atomic_action/coordinated_pickment.py` +**Example:** `examples/sim/atomic_action/coordinated_pickment.py` (builtin-coordinated-placement)= @@ -1074,7 +1077,7 @@ The placing/support motion and grasp endpoints come exclusively from the corresponding participant slots. The same cuRobo restriction as coordinated pickment applies to dual-arm `strategy="motion_gen"` planning. -**Example:** `scripts/tutorials/atomic_action/coordinated_placement.py` +**Example:** `examples/sim/atomic_action/coordinated_placement.py` (builtin-hand-over)= @@ -1137,7 +1140,7 @@ interval between them as a likely path- or collision-planning failure. As with the other coordinated primitive, cuRobo does not currently support its dual-arm `strategy="motion_gen"` path. -**Example:** `scripts/tutorials/atomic_action/hand_over.py` +**Example:** `examples/sim/atomic_action/handover.py` ## Running the demos @@ -1146,9 +1149,9 @@ keyboard prompts and combine it with `--headless --device cpu` for a headless run that records video under `outputs/videos`: ```bash -python scripts/tutorials/atomic_action/move_end_effector.py --headless --auto_play --device cpu -python scripts/tutorials/atomic_action/pickup.py --headless --auto_play --device cpu -python scripts/tutorials/atomic_action/hand_over.py --headless --auto_play --device cpu +embodichain run-example sim/atomic_action/move_end_effector --headless --auto_play --device cpu +embodichain run-example sim/atomic_action/pickup --headless --auto_play --device cpu +embodichain run-example sim/atomic_action/handover --headless --auto_play --device cpu ``` The scripts share a `--planner` selector for the non-neural backends diff --git a/docs/source/overview/sim/atomic_actions/index.md b/docs/source/overview/sim/atomic_actions/index.md index f71a23013..0ec57074a 100644 --- a/docs/source/overview/sim/atomic_actions/index.md +++ b/docs/source/overview/sim/atomic_actions/index.md @@ -551,7 +551,7 @@ empty task state and scene snapshot. Supply an explicit context whenever goals depend on perceived entities or a previous verified attachment. Do not compile across a boundary where execution feedback changes a later goal. -For example, `scripts/tutorials/atomic_action/coordinated_placement.py` compiles +For example, `examples/sim/atomic_action/coordinated_placement.py` compiles the two pick-ups, executes them, rebuilds the held-object state from measured poses, and only then compiles placement. @@ -939,7 +939,7 @@ See {doc}`builtin_actions` for the shipped skill catalog and visual demos, and - {doc}`../motion/motion_generator` — the motion generator owned by the engine - {doc}`../sim_robot` — robot control parts and kinematic configuration - {doc}`/tutorial/atomic_actions` — static, closed-loop, and recovery examples -- `scripts/tutorials/atomic_action/moving_target_recovery.py` — runnable runner +- `examples/sim/atomic_action/moving_target_recovery.py` — runnable runner example that visibly moves a late-bound target, replans, and picks up the cube -- `scripts/tutorials/atomic_action/dynamic_obstacle_recovery.py` — cuRobo +- `examples/sim/atomic_action/dynamic_obstacle_recovery.py` — cuRobo collision-world revision and obstacle-pose replanning example diff --git a/docs/source/overview/sim/motion/trajectory_variants.md b/docs/source/overview/sim/motion/trajectory_variants.md index fbc4da19f..bc60256c1 100644 --- a/docs/source/overview/sim/motion/trajectory_variants.md +++ b/docs/source/overview/sim/motion/trajectory_variants.md @@ -79,7 +79,7 @@ environment where EmbodiChain is installed (see [Installation](../../../quick_start/install.md)): ```bash -python scripts/tutorials/atomic_action/place.py --num_envs 6 --device cuda +embodichain run-example sim/atomic_action/place --num_envs 6 --device cuda ``` The tutorial declares only which of the action's own named segments may move: @@ -114,7 +114,7 @@ replay them; ask for a hundred and run a single environment if you only want the figures: ```bash -python scripts/tutorials/atomic_action/place.py --headless --num_envs 1 --device cuda --trajectory_variants 100 --variant_plot_dir outputs/variants +embodichain run-example sim/atomic_action/place --headless --num_envs 1 --device cuda --trajectory_variants 100 --variant_plot_dir outputs/variants ``` The first figure plots every variant's arm joints against time. Curves separate diff --git a/docs/source/tutorial/atomic_actions.rst b/docs/source/tutorial/atomic_actions.rst index 58dc5bd8b..3b2c961ba 100644 --- a/docs/source/tutorial/atomic_actions.rst +++ b/docs/source/tutorial/atomic_actions.rst @@ -114,54 +114,123 @@ This extension contract is intentionally strict: a subclass that defines custom actions must rename that implementation to ``_plan()`` so the framework-owned collision-scene preparation cannot be bypassed. -Runnable examples ------------------ - -Focused examples live under ``scripts/tutorials/atomic_action``: - -* ``move_end_effector.py`` -* ``move_joints.py`` -* ``axis_align.py`` -* ``control_dt.py`` -* ``pickup.py`` -* ``move_held_object.py`` -* ``pour.py`` -* ``place.py`` -* ``assemble.py`` -* ``press.py`` -* ``slide.py`` -* ``twist.py`` -* ``coordinated_pickment.py`` -* ``coordinated_placement.py`` -* ``hand_over.py`` -* ``open_door.py`` -* ``moving_target_recovery.py`` -* ``dynamic_obstacle_recovery.py`` - -The scripts are interactive by default. Add ``--auto_play`` to skip prompts; -combine it with ``--headless --device cpu`` for a headless run that records -video under ``outputs/videos``: +Two executable walkthroughs +--------------------------- + +``scripts/tutorials/atomic_action`` contains exactly two teaching scripts. +Their robot, asset, TCP and recording helpers come from +``examples/sim/atomic_action``. The complete runnable scene catalog, including +recovery and coordinated manipulation, lives in that examples directory and is +installed with the package. Browse it with: + +.. code-block:: bash + + embodichain run-example --list + embodichain run-example sim/atomic_action --help + embodichain run-example sim/atomic_action/coordinated_pickment --help + +Single arm: PickUp followed by Place +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run the single-arm walkthrough from a source checkout: + +.. code-block:: bash + + python scripts/tutorials/atomic_action/single_arm.py --headless --auto_play --device cpu + +``single_arm.py`` creates the same cube and calibrated robot as the runnable +Place example. It then declares the hand's semantic ``open`` and ``grasp`` +commands and gives the engine an object-backed scene provider and grasp-pose +generator. The ``primary`` participant binds to the concrete ``arm`` and +``eef`` control parts. A ``GraspGoal`` requests acquisition; a subsequent +``PlaceGoal`` supplies the desired tool-pose waypoints: + +.. literalinclude:: ../../../scripts/tutorials/atomic_action/single_arm.py + :language: python + :start-after: # -- Resources -- + :end-before: # -- Planning -- + :dedent: 4 + +The observed robot and scene state enter through an explicit control period. +``compile()`` projects the first action's expected attachment into the next +planning context, allowing Place to plan its held-object motion. It does not +step physics or establish that the real grasp succeeded: + +.. literalinclude:: ../../../scripts/tutorials/atomic_action/single_arm.py + :language: python + :start-after: # -- Planning -- + :end-before: # -- Playback -- + :dedent: 4 + +The host subsequently passes ``compiled.trajectory`` to the shared +``replay_trajectory()`` helper. Its arrival intervals determine the simulation +steps. The source script shows the complete setup, lift-boundary callback, +terminal hold and ``run_tutorial()`` resource-cleanup boundary. + +Two arms: one HandOver invocation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The dual-arm walkthrough binds two motion/grasp pairs to one skill. Its default +can fixture uses Robotiq 2F-140; ``--is_horizontal`` selects the stick and +matching PGC-140-50 asset, TCP, grasp model and closing commands: .. code-block:: bash - python scripts/tutorials/atomic_action/move_end_effector.py --headless --auto_play --device cpu - python scripts/tutorials/atomic_action/control_dt.py --headless --auto_play --device cpu - python scripts/tutorials/atomic_action/pickup.py --headless --auto_play --device cpu - python scripts/tutorials/atomic_action/pour.py --headless --auto_play --device cpu - python scripts/tutorials/atomic_action/assemble.py --headless --auto_play --device cpu - python scripts/tutorials/atomic_action/hand_over.py --headless --auto_play --device cpu + python scripts/tutorials/atomic_action/dual_arm.py --headless --auto_play --device cpu + python scripts/tutorials/atomic_action/dual_arm.py --is_horizontal --headless --auto_play --device cpu + +``dual_arm.py`` obtains physical setup and fixture calibration from the +canonical HandOver example. It registers an ``open``/``grasp`` profile for each +hand and supplies both grasp-pose generators to the engine. The application +then constructs one goal and binds both participants: + +.. literalinclude:: ../../../scripts/tutorials/atomic_action/dual_arm.py + :language: python + :start-after: # -- Goal and binding -- + :end-before: # -- Planning -- + :dedent: 4 + +``source`` and ``destination`` are skill-local candidate participants. +HandOver chooses the nearer pickup arm, plans pickup and receiving grasps, and +coordinates hand closure, transfer, source release and final transport. The +application does not split that transfer into unrelated single-arm calls. +Both motion/hand resources must satisfy the skill's disjointness contract. + +As in the single-arm case, compile and playback remain separate. The same +``control_dt`` governs both arms. A successful compile reports planned +feasibility; use measured effect verification when physical task success must +be certified. The observed-execution and recovery sections below describe that +alternative to offline compilation. + +Runnable demonstrations +~~~~~~~~~~~~~~~~~~~~~~~ + +All per-skill and specialist demonstrations run from ``examples``: + +.. code-block:: bash + + embodichain run-example sim/atomic_action --skill pickup --headless + embodichain run-example sim/atomic_action --skill place --planner toppra --headless + embodichain run-example sim/atomic_action/handover --is_horizontal --headless --auto_play + embodichain run-example sim/atomic_action/assemble --headless --auto_play + embodichain run-example sim/atomic_action/coordinated_placement --headless --auto_play + embodichain run-example sim/atomic_action/moving_target_recovery --headless --auto_play + +The ``examples/sim/atomic_action/README.md`` lists fixed-scene selectors, +standalone demonstrations, supported backends and recording options. The cube +MoveHeldObject scene uses ``--skill move_held_object``; the distinct paper-cup +sequence is ``sim/atomic_action/move_held_object_cup``. -``control_dt.py`` compiles the same 40-waypoint ``ik_interp`` path twice. The -positions stay identical, while changing ``PlanningContext.control_dt`` from -``2 * physics_dt`` to ``8 * physics_dt`` makes every arrival interval and the -total trajectory duration four times longer. The script checks that relationship -before replaying the fast and slow trajectories in sequence. +``sim/atomic_action/control_dt`` demonstrates two explicit command periods. +It chooses a minimum sample count that permits the faster interpolation under +its motion limits, checks that both geometric paths agree, then checks that the +slower arrival intervals and duration scale with the requested period. The ``motion_generator`` variable in the snippets below is a configured :class:`~embodichain.lab.sim.motion.motion_generator.MotionGenerator`; its robot, planner, device, cache, and collision world become the resources owned by the engine. -All atomic-action tutorials use the shared scene setup, which creates exactly +The teaching scripts and runnable examples use the shared scene setup, which creates exactly one global ``sun`` light (``main_light``) for the whole simulation. The sun uses a common downward direction and is not duplicated for each vectorized arena; tutorial-specific light positions are no longer needed. @@ -169,7 +238,7 @@ arena; tutorial-specific light positions are no longer needed. Selecting a motion planner ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Every runnable atomic-action tutorial accepts the shared ``--planner`` switch. +The teaching scripts and runnable examples accept the shared ``--planner`` switch. It selects the non-neural backend used to construct its ``MotionGenerator``: @@ -187,14 +256,12 @@ It selects the non-neural backend used to construct its * - ``trapezoidal`` - :class:`~embodichain.lab.sim.motion.planners.TrapezoidalPlanner` - Deterministic trapezoidal joint-space timing and the default for atomic - action tutorials. Cartesian targets are - converted through the shared IK/interpolation path before planning; - quantity sampling retains every converted waypoint. When options are - resolved from the backend-neutral request, ``MotionGenerator`` treats - the requested count as a lower bound; an explicitly supplied - ``TrapezoidalPlanOptions.sample_interval`` remains authoritative. - Fixed-length composite skill phases resample that position path back to - their requested phase count before combining arm and hand commands. + examples. Free Cartesian goals become sparse IK waypoints; explicit + ``is_linear`` requests retain a dense Cartesian path. Required waypoints + remain exact. Backend-neutral sample counts are minima, and control-grid + retiming preserves native phase duration when combining arm and hand + commands. Explicit ``TrapezoidalPlanOptions.sample_interval`` stays + authoritative at the backend boundary. * - ``curobo`` - :class:`~embodichain.lab.sim.motion.planners.CuroboPlanner` - CUDA-backed Cartesian/joint planning with collision-world support when @@ -210,24 +277,23 @@ Unless noted below, omitting ``--planner`` selects ``trapezoidal``. .. code-block:: bash - python scripts/tutorials/atomic_action/move_end_effector.py --device cpu - python scripts/tutorials/atomic_action/move_end_effector.py --device cpu --planner toppra - python scripts/tutorials/atomic_action/move_end_effector.py --device cuda --planner curobo + embodichain run-example sim/atomic_action/move_end_effector --device cpu + embodichain run-example sim/atomic_action/move_end_effector --device cpu --planner toppra + embodichain run-example sim/atomic_action/move_end_effector --device cuda --planner curobo The selector does not override a skill's explicit motion contract. In particular, ``dynamic_obstacle_recovery.py`` is deliberately cuRobo-only because it updates a live collision world; ``coordinated_pickment.py``, -``coordinated_placement.py``, and ``hand_over.py`` use dual-arm planning that +``coordinated_placement.py``, and ``handover.py`` use dual-arm planning that currently rejects cuRobo. ``control_dt.py`` intentionally uses ``strategy="ik_interp"`` to compare control periods, so its planner choice is constructed for consistency but does not change that interpolation experiment. ``coordinated_pickment.py`` likewise retains its synchronized multi-arm IK/keyframe implementation; its selector validates the backend but does not replace those custom synchronized phases. -The exact Cartesian-linear portions of ``OpenDoor``, ``Press``, and ``Slide`` -likewise remain IK-grounded; their planner choice applies to the other -motion-generation portions. ``Twist`` uses the selected planner for each of -its pose segments. +The ``OpenDoor``, ``Press``, ``Slide`` and ``Twist`` examples select +``ik_interp`` to preserve their exact Cartesian/contact paths. Selecting a +backend does not replace those constrained IK phases. Control-part commands --------------------- @@ -490,7 +556,7 @@ and regenerated goal axes remain visible for comparison: .. code-block:: bash - python scripts/tutorials/atomic_action/moving_target_recovery.py --headless --auto_play --device cpu + embodichain run-example sim/atomic_action/moving_target_recovery --headless --auto_play --device cpu For collision-aware execution, register each pose-updatable obstacle with ``SceneCollisionRole.DYNAMIC`` and configure the same canonical registry IDs as @@ -514,7 +580,7 @@ eventually reported: .. code-block:: bash - python scripts/tutorials/atomic_action/dynamic_obstacle_recovery.py --headless --auto_play + embodichain run-example sim/atomic_action/dynamic_obstacle_recovery --headless --auto_play ``MotionPolicy.dynamic_collision_mode`` defaults to ``DynamicCollisionMode.AUTO``. Use ``DynamicCollisionMode.REQUIRED`` when diff --git a/embodichain/lab/sim/atomic_actions/primitives/open_door.py b/embodichain/lab/sim/atomic_actions/primitives/open_door.py index 8b2e4419a..6bac128ad 100644 --- a/embodichain/lab/sim/atomic_actions/primitives/open_door.py +++ b/embodichain/lab/sim/atomic_actions/primitives/open_door.py @@ -16,7 +16,7 @@ """OpenDoor atomic action implementation. -The ``scripts/tutorials/atomic_action/open_door.py`` example loads +The ``examples/sim/atomic_action/open_door.py`` example loads ``Microwave/microwave.urdf`` and resolves the ``handle_link`` target through ``door_handle_fixed_joint`` to ``door_hinge``. Its invocation requests ``MotionPolicy.sample_count=300``, ``hand_interp_steps=30``, and diff --git a/embodichain/lab/sim/motion/motion_generator.py b/embodichain/lab/sim/motion/motion_generator.py index 8571486c7..061357955 100644 --- a/embodichain/lab/sim/motion/motion_generator.py +++ b/embodichain/lab/sim/motion/motion_generator.py @@ -619,10 +619,11 @@ def generate( (``"motion_gen"``) or deterministic waypoint IK followed by joint-space interpolation (``"ik_interp"``). Unsupported backend target types raise rather than silently switching to interpolation. - Joint-only planners that own sparse waypoints may retain every converted - waypoint when quantity sampling is requested. For automatically resolved - trapezoidal options, the requested count is then treated as a lower bound; - an explicitly supplied planner option remains authoritative. + Joint-only planners that own sparse waypoints receive IK solutions only + at the requested Cartesian goals unless ``options.is_linear`` requests + a dense Cartesian path. Quantity sampling retains every required joint + waypoint. Automatically resolved trapezoidal options treat the requested + count as a lower bound; explicit planner options remain authoritative. Args: target_states: Batched planner waypoints. @@ -667,10 +668,13 @@ def _generate_with_planner( """Dispatch batched targets through the configured planner backend.""" move_types = {state.move_type for state in target_states} move_type = target_states[0].move_type + owns_sparse_waypoints = ( + getattr(self.planner, "uses_sparse_joint_waypoints", False) is True + ) uses_sparse_joint_waypoints = ( len(move_types) == 1 and move_type is MoveType.JOINT_MOVE - and getattr(self.planner, "uses_sparse_joint_waypoints", False) is True + and owns_sparse_waypoints ) should_preinterpolate = ( len(move_types) == 1 @@ -722,6 +726,7 @@ def _generate_with_planner( xpos_list=xpos_list, qpos_list=qpos_list, options=options, + sparse_cartesian=owns_sparse_waypoints and not options.is_linear, ) target_plan_states = [ PlanState(move_type=MoveType.JOINT_MOVE, qpos=qpos_interpolated[:, j]) @@ -1850,8 +1855,10 @@ def _interpolate_waypoints( xpos_list: torch.Tensor | None = None, qpos_list: torch.Tensor | None = None, options: MotionGenOptions | None = None, + *, + sparse_cartesian: bool = False, ) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor]: - """Prepare backend waypoints without deleting failed Cartesian targets.""" + """Prepare required IK waypoints, optionally without Cartesian densification.""" options = MotionGenOptions() if options is None else options # Normalize single-env inputs to batched form. @@ -1892,36 +1899,42 @@ def _interpolate_waypoints( # Generate trajectory if options.is_linear or qpos_list is None: - # ``calculate_point_allocations`` only handles single-env (N, 4, 4), - # so compute allocations per env and use the per-segment maximum so - # all envs can share the same interpolated pose count. - per_env_allocations = [ - calculate_point_allocations( - xpos_list[b], - step_size=options.interpolate_position_step, - angle_step=options.interpolate_angle_step, - device=self.device, - ) - for b in range(xpos_list.shape[0]) - ] - n_segments = xpos_list.shape[1] - 1 - interpolated_point_allocations = [ - max(alloc[i] for alloc in per_env_allocations) - for i in range(n_segments) - ] + if sparse_cartesian: + # A sparse joint planner owns timing between the requested + # goals. Densifying a free Cartesian move would turn every IK + # sample into a mandatory rest point for trapezoidal timing. + total_interpolated_poses = xpos_list.to(self._runtime_device()) + else: + # Allocations are per environment; share their segment maxima + # so the batch has one interpolated pose count. + per_env_allocations = [ + calculate_point_allocations( + xpos_list[b], + step_size=options.interpolate_position_step, + angle_step=options.interpolate_angle_step, + device=self.device, + ) + for b in range(xpos_list.shape[0]) + ] + n_segments = xpos_list.shape[1] - 1 + interpolated_point_allocations = [ + max(alloc[i] for alloc in per_env_allocations) + for i in range(n_segments) + ] - # Linear cartesian interpolation, batched across B envs. - total_interpolated_poses = [] - for i in range(n_segments): - seg = interpolate_xpos_batched( - xpos_list[:, i], - xpos_list[:, i + 1], - interpolated_point_allocations[i], - ) # (B, seg, 4, 4) - total_interpolated_poses.append(seg) - total_interpolated_poses = torch.cat(total_interpolated_poses, dim=1).to( - self._runtime_device() - ) # (B, M, 4, 4) + total_interpolated_poses = [] + for i in range(n_segments): + seg = interpolate_xpos_batched( + xpos_list[:, i], + xpos_list[:, i + 1], + interpolated_point_allocations[i], + ) # (B, seg, 4, 4) + total_interpolated_poses.append(seg) + total_interpolated_poses = torch.cat( + total_interpolated_poses, dim=1 + ).to( + self._runtime_device() + ) # (B, M, 4, 4) if control_part is None: raise ValueError("Cartesian interpolation requires control_part.") diff --git a/embodichain/lab/sim/motion/planners/base_planner.py b/embodichain/lab/sim/motion/planners/base_planner.py index a7dc48884..bff41b6d1 100644 --- a/embodichain/lab/sim/motion/planners/base_planner.py +++ b/embodichain/lab/sim/motion/planners/base_planner.py @@ -239,7 +239,9 @@ def __init__(self, cfg: BasePlannerCfg): When ``True``, :class:`~embodichain.lab.sim.motion.motion_generator.MotionGenerator` prepends the observed ``start_qpos`` before dispatching a joint target. This lets the planner own both path timing and derivative generation without - making every caller materialize the current state as a waypoint. + making every caller materialize the current state as a waypoint. Cartesian + preparation also keeps only requested goals unless ``is_linear`` explicitly + requests a dense Cartesian path. """ preserve_plan_samples: bool = False diff --git a/examples/README.md b/examples/README.md index e84ba6cb9..f6d897a56 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,6 +7,7 @@ embodichain run-example --list embodichain run-example sim/motion/planners/curobo_planner --help embodichain run-example ods/simple_bc_demo/train --help embodichain run-example sim/atomic_action --skill place --variants 8 +embodichain run-example sim/atomic_action/coordinated_pickment --help ``` A directory with `__main__.py` can be selected by its directory path. All arguments @@ -28,3 +29,8 @@ distributions preserve this same tree and its resources; wheels install it under `share/embodichain/examples/`. The CLI locates the installed data through the distribution metadata and works outside the repository. Example code is not installed inside the `embodichain` Python package. + +The [Atomic Skill example catalog](sim/atomic_action/README.md) owns all per-skill, +coordinated-manipulation and recovery demonstrations. The two source-checkout +walkthroughs under `scripts/tutorials/atomic_action/` teach single-arm and dual-arm +API composition; they reuse the canonical example setup. diff --git a/examples/sim/atomic_action/README.md b/examples/sim/atomic_action/README.md index 585ea2ac0..7ff362bf9 100644 --- a/examples/sim/atomic_action/README.md +++ b/examples/sim/atomic_action/README.md @@ -21,6 +21,21 @@ The twelve scenes are `pickup`, `place`, `pour`, `press`, `slide`, `twist`, Pickup, Place, MoveJoint, and Handover also accept `franka` and `ur10`; other scenes currently require UR5. Unsupported selectors fail before simulation. +The HandOver can scene uses Robotiq 2F-140. The standalone horizontal-stick +variant selects two DH PGC-140-50 grippers, their short-pad TCPs and a 50 mm +grasp model. Run it with either planner: + +```bash +embodichain run-example sim/atomic_action/handover --is_horizontal --planner trapezoidal --headless --auto_play +embodichain run-example sim/atomic_action/handover --is_horizontal --planner toppra --headless --auto_play +``` + +PGC uses a 25 mm inward command per finger. Its TCP rotates the grasp-frame +opening axis onto the hand's Y axis and places the grasp center at local +Z = 0.12 m, within the short finger pads. These are coupled to the selected +asset and grasp model; changing only the mounted mesh leaves the old grasp +geometry in use. + `--variants N` defaults to one and creates exactly N parallel environments. Variant zero retains the reference. There is no collection loop or implicit batching. `--expansion both` is the default for N > 1: each added execution uses @@ -30,6 +45,41 @@ MoveJoint and PushObject support trajectory expansion only; explicitly selecting and must equal N; `--no-expansion` requires N = 1. The old fixed-bank `--recipe-indices` selector has been removed. +## Standalone demonstrations + +All Atomic Skill demonstrations live in this directory. The selector above +owns the twelve fixed scenes and their expansion/output host. Specialized +workflows also run directly through the generic example launcher: + +| Example path after `embodichain run-example` | Demonstration | +| --- | --- | +| `sim/atomic_action/move_end_effector` | Required multi-waypoint tool poses | +| `sim/atomic_action/control_dt` | Compare explicit control periods | +| `sim/atomic_action/pickup_rubiks_cube` | Grasp a locked articulation link | +| `sim/atomic_action/move_held_object_cup` | Approach, pick up, and transport a paper cup | +| `sim/atomic_action/assemble` | Place a can on a base object | +| `sim/atomic_action/coordinated_pickment` | Two arms grasp and transport one shared object | +| `sim/atomic_action/coordinated_placement` | Pick two objects and coordinate their placement | +| `sim/atomic_action/moving_target_recovery` | Observed PickUp recovery after a target moves | +| `sim/atomic_action/dynamic_obstacle_recovery` | cuRobo collision-world invalidation and replanning | + +```bash +embodichain run-example sim/atomic_action/coordinated_pickment --object plastic_tray --headless --auto_play +embodichain run-example sim/atomic_action/coordinated_placement --headless --auto_play +embodichain run-example sim/atomic_action/move_held_object_cup --headless --auto_play +``` + +Standalone demonstrations keep their own arguments; use their `--help` rather +than passing fixed-scene expansion options such as `--variants`. Their +`--auto_play` option skips prompts and records a native video. `--headless` +alone skips the viewer and prompts without enabling recording. + +The two teaching scripts in `scripts/tutorials/atomic_action/single_arm.py` and +`dual_arm.py` explain PickUp→Place and HandOver using the typed engine API. +They reuse this directory's physical setup and calibration. The tutorial +chapter at `docs/source/tutorial/atomic_actions.rst` walks through their code; +per-skill commands, additional scenes, recovery and shared helpers belong here. + ## Fixed-scene expansion Affordance sampling happens before planning. The CLI uses one upstream approach @@ -106,19 +156,23 @@ local frame; the upper-left 3x3 block is rotation. | UR5 + Robotiq 2F-140, Handover | `UR5_ROBOTIQ_2F_140_HANDOVER_TCP` | 0.21 m, with a 90-degree local-Z rotation | | Franka + Robotiq 2F-140, Handover | `FRANKA_ROBOTIQ_2F_140_HANDOVER_TCP` | 0.21 m, with a 90-degree local-Z rotation | | UR10 + Robotiq 2F-140 | `UR10_ROBOTIQ_2F_140_TCP` | 0.21 m, with its mounting rotation | +| UR5 + PGC-140-50, horizontal HandOver | `UR5_PGC_140_50_HANDOVER_TCP` | 0.12 m, with a 90-degree local-Z rotation | +| Franka + PGC-140-50, horizontal HandOver | `FRANKA_PGC_140_50_HANDOVER_TCP` | 0.12 m, with a 90-degree local-Z rotation | +| UR10 + PGC-140-50, horizontal HandOver | `UR10_PGC_140_50_HANDOVER_TCP` | 0.12 m, with a 90-degree local-Z rotation | For `--skill place --robot franka`, edit `FRANKA_PGI_TCP`. This calibrates the single-arm Franka/PGI tool without affecting UR5, Robotiq, or the separately authored handover calibration. Each solver receives an independent matrix. For `--skill handover --robot franka`, edit `FRANKA_ROBOTIQ_2F_140_HANDOVER_TCP`; the corresponding UR5 calibration is -`UR5_ROBOTIQ_2F_140_HANDOVER_TCP`. The generic dual helper retains PGI unless -the scene explicitly selects `gripper="robotiq_2f_140"`. +`UR5_ROBOTIQ_2F_140_HANDOVER_TCP`. Horizontal HandOver selects the corresponding +`*_PGC_140_50_HANDOVER_TCP` pose instead. The generic dual helper retains its +robot's default hand unless the scene explicitly selects a gripper. The named tool points preserve the existing sample offsets; they are sample calibrations, not measured guarantees for another physical fixture. Sample scenes select named tool points instead of embedding a generic `tcp_z`. Python helpers still accept explicit PGI `tcp_z` overrides, including Franka; an override changes local Z while preserving the fixture's rotation -and lateral offsets. Robotiq keeps its own full mounting pose. TCP calibration -changes the FK/IK tool reference, not the gripper mesh or physical length. +and lateral offsets. Robotiq and PGC keep their full mounting poses. TCP +calibration changes the FK/IK tool reference, not the gripper mesh or physical length. diff --git a/scripts/tutorials/atomic_action/assemble.py b/examples/sim/atomic_action/assemble.py similarity index 99% rename from scripts/tutorials/atomic_action/assemble.py rename to examples/sim/atomic_action/assemble.py index 5c49c58ef..3e6f4b984 100644 --- a/scripts/tutorials/atomic_action/assemble.py +++ b/examples/sim/atomic_action/assemble.py @@ -53,12 +53,12 @@ from embodichain.lab.sim.objects import RigidObject, Robot from embodichain.lab.sim.shapes import CubeCfg, MeshCfg from embodichain.utils import logger -from scripts.tutorials.atomic_action.scenario_utils import ( +from examples.sim.atomic_action.scenario import ( add_dual_tutorial_robot, add_support_surface, settle_object, ) -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( TutorialRobot, broadcast_pose_batch, clone_local_pose_from_first_env, diff --git a/scripts/tutorials/atomic_action/control_dt.py b/examples/sim/atomic_action/control_dt.py similarity index 94% rename from scripts/tutorials/atomic_action/control_dt.py rename to examples/sim/atomic_action/control_dt.py index a3bac70f9..f5ac92f7c 100644 --- a/scripts/tutorials/atomic_action/control_dt.py +++ b/examples/sim/atomic_action/control_dt.py @@ -34,7 +34,7 @@ MotionPolicy, ) from embodichain.utils import logger -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( add_tutorial_robot, create_toppra_motion_generator, create_tutorial_argument_parser, @@ -44,7 +44,10 @@ run_tutorial, ) -SAMPLE_COUNT = 40 +# At 20 ms, 100 samples leave 1.98 s for the 0.30 rad move, sufficient for +# the default 0.5 rad/s² minimum-jerk acceleration budget. Both periods can +# therefore use the same geometric samples without limit-driven expansion. +SAMPLE_COUNT = 100 FAST_CONTROL_STEPS = 2 SLOW_CONTROL_STEPS = 8 RESET_STEPS = 20 diff --git a/scripts/tutorials/atomic_action/coordinated_pickment.py b/examples/sim/atomic_action/coordinated_pickment.py similarity index 99% rename from scripts/tutorials/atomic_action/coordinated_pickment.py rename to examples/sim/atomic_action/coordinated_pickment.py index c5657cb04..6c4d0397b 100644 --- a/scripts/tutorials/atomic_action/coordinated_pickment.py +++ b/examples/sim/atomic_action/coordinated_pickment.py @@ -48,7 +48,7 @@ from embodichain.lab.sim.shapes import MeshCfg, MeshCollisionCfg from embodichain.utils import logger from embodichain.utils.math import matrix_from_euler -from scripts.tutorials.atomic_action.scenario_utils import ( +from examples.sim.atomic_action.scenario import ( add_dual_tutorial_robot, add_support_surface, compute_world_bounds, @@ -58,7 +58,7 @@ rotate_pose_about_world_z, settle_object, ) -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( TutorialRobot, broadcast_pose_batch, clone_local_pose_from_first_env, diff --git a/scripts/tutorials/atomic_action/coordinated_placement.py b/examples/sim/atomic_action/coordinated_placement.py similarity index 99% rename from scripts/tutorials/atomic_action/coordinated_placement.py rename to examples/sim/atomic_action/coordinated_placement.py index f9c1ea7c7..2d55f8c32 100644 --- a/scripts/tutorials/atomic_action/coordinated_placement.py +++ b/examples/sim/atomic_action/coordinated_placement.py @@ -61,7 +61,7 @@ build_support_object_target_pose, build_placing_object_target_pose, ) -from scripts.tutorials.atomic_action.scenario_utils import ( +from examples.sim.atomic_action.scenario import ( add_dual_tutorial_robot, compute_local_bounds, create_manual_object_semantics, @@ -71,7 +71,7 @@ resolve_cached_data_path, settle_object, ) -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( TutorialRobot, broadcast_pose_batch, clone_local_pose_from_first_env, diff --git a/scripts/tutorials/atomic_action/dynamic_obstacle_recovery.py b/examples/sim/atomic_action/dynamic_obstacle_recovery.py similarity index 99% rename from scripts/tutorials/atomic_action/dynamic_obstacle_recovery.py rename to examples/sim/atomic_action/dynamic_obstacle_recovery.py index 608366009..8adb75050 100644 --- a/scripts/tutorials/atomic_action/dynamic_obstacle_recovery.py +++ b/examples/sim/atomic_action/dynamic_obstacle_recovery.py @@ -60,7 +60,7 @@ from embodichain.lab.sim.shapes import CubeCfg from embodichain.lab.visualization import SceneOverlays, TrajectoryOverlay from embodichain.utils import logger -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( add_tutorial_robot, create_tutorial_argument_parser, create_tutorial_simulation, diff --git a/examples/sim/atomic_action/handover.py b/examples/sim/atomic_action/handover.py index 42bacb25c..6c28a59b8 100644 --- a/examples/sim/atomic_action/handover.py +++ b/examples/sim/atomic_action/handover.py @@ -96,6 +96,16 @@ finger_thickness=0.0075, palm_depth=0.10, ) +HORIZONTAL_HAND_CLOSE_QPOS = 0.025 +HORIZONTAL_GRIPPER_MODEL = ParallelJawGripperModelCfg( + model_id="dh_pgc_140_50", + min_opening_width=0.002, + max_opening_width=0.050, + finger_length=0.030, + finger_width=0.030, + finger_thickness=0.012, + palm_depth=0.105, +) HANDOVER_SAMPLE_INTERVAL = 220 HANDOVER_HAND_INTERP_STEPS = 10 HANDOVER_PRE_GRASP_DISTANCE = 0.08 @@ -124,7 +134,7 @@ def parse_arguments() -> argparse.Namespace: parser.add_argument( "--is_horizontal", action="store_true", - help="Use the horizontal WaterBasin object instead of the vertical soda can.", + help="Hand over a horizontal stick with PGC-140-50 grippers.", ) return parse_affordance_sampling_arguments(parser) @@ -132,14 +142,16 @@ def parse_arguments() -> argparse.Namespace: def create_dual_robot( sim: SimulationManager, robot_type: TutorialRobot, + *, + is_horizontal: bool = False, ) -> Robot: - """Create the selected dual-arm robot with two Robotiq 2F-140 grippers.""" + """Mount PGC-140-50 for the stick, or Robotiq 2F-140 for the soda can.""" return add_dual_tutorial_robot( sim, robot_type=robot_type, uid=f"Dual{robot_type.title()}HandOver", urdf_name=f"dual_{robot_type}_hand_over", - gripper="robotiq_2f_140", + gripper="pgc_140_50" if is_horizontal else "robotiq_2f_140", solver="pytorch" if robot_type == "franka" else "ur", ur_ik_nearest_weight=(1.0, 4.0, 1.0, 1.0, 1.0, 1.0), hand_stiffness=1e3, @@ -218,11 +230,13 @@ def run_handover_demo( planner=getattr(args, "planner", "trapezoidal"), ) + is_horizontal = bool(getattr(args, "is_horizontal", False)) + close_qpos = HORIZONTAL_HAND_CLOSE_QPOS if is_horizontal else HAND_CLOSE_QPOS left_open, left_close = get_hand_open_close_qpos( - robot, hand_control_part="left_eef", close_qpos=HAND_CLOSE_QPOS + robot, hand_control_part="left_eef", close_qpos=close_qpos ) right_open, right_close = get_hand_open_close_qpos( - robot, hand_control_part="right_eef", close_qpos=HAND_CLOSE_QPOS + robot, hand_control_part="right_eef", close_qpos=close_qpos ) final_pose = torch.eye(4, dtype=torch.float32) @@ -237,7 +251,9 @@ def run_handover_demo( n_sample=args.n_sample, force_refresh=args.force_reannotate, approach_direction_samples=getattr(args, "grasp_approach_samples", 4), - gripper_model=HANDOVER_GRIPPER_MODEL, + gripper_model=( + HORIZONTAL_GRIPPER_MODEL if is_horizontal else HANDOVER_GRIPPER_MODEL + ), ) engine = create_simulation_atomic_action_engine( motion_generator=motion_gen, @@ -333,7 +349,9 @@ def run(args: argparse.Namespace) -> None: args, arena_space=3.0, ) - robot = create_dual_robot(sim, args.robot) + robot = create_dual_robot( + sim, args.robot, is_horizontal=bool(getattr(args, "is_horizontal", False)) + ) run_handover_demo(args, sim, robot) serve_tutorial_scene(sim, args) diff --git a/scripts/tutorials/atomic_action/move_end_effector.py b/examples/sim/atomic_action/move_end_effector.py similarity index 97% rename from scripts/tutorials/atomic_action/move_end_effector.py rename to examples/sim/atomic_action/move_end_effector.py index 65114dc37..61247d81d 100644 --- a/scripts/tutorials/atomic_action/move_end_effector.py +++ b/examples/sim/atomic_action/move_end_effector.py @@ -34,7 +34,7 @@ MotionPolicy, ) from embodichain.utils import logger -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( add_tutorial_robot, broadcast_pose_batch, broadcast_waypoint_pose_batch, @@ -46,7 +46,7 @@ replay_trajectory, run_tutorial, ) -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( initialize_benchmark_simulation, ) diff --git a/scripts/tutorials/atomic_action/move_held_object.py b/examples/sim/atomic_action/move_held_object_cup.py similarity index 98% rename from scripts/tutorials/atomic_action/move_held_object.py rename to examples/sim/atomic_action/move_held_object_cup.py index abbe3fbe4..6c703bea9 100644 --- a/scripts/tutorials/atomic_action/move_held_object.py +++ b/examples/sim/atomic_action/move_held_object_cup.py @@ -42,7 +42,7 @@ from embodichain.lab.sim.objects import RigidObject from embodichain.lab.sim.shapes import MeshCfg, MeshCollisionCfg from embodichain.utils import logger -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( add_tutorial_robot, broadcast_pose_batch, clone_local_pose_from_first_env, @@ -60,7 +60,7 @@ replay_trajectory, run_tutorial, ) -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( compute_pick_close_end_step, initialize_benchmark_simulation, make_eef_pose_at, diff --git a/scripts/tutorials/atomic_action/moving_target_recovery.py b/examples/sim/atomic_action/moving_target_recovery.py similarity index 99% rename from scripts/tutorials/atomic_action/moving_target_recovery.py rename to examples/sim/atomic_action/moving_target_recovery.py index ab4474c18..e5b0f28cf 100644 --- a/scripts/tutorials/atomic_action/moving_target_recovery.py +++ b/examples/sim/atomic_action/moving_target_recovery.py @@ -58,7 +58,7 @@ from embodichain.lab.sim.objects import RigidObject from embodichain.lab.sim.shapes import CubeCfg from embodichain.utils import logger -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( add_tutorial_robot, create_curobo_motion_generator, create_tutorial_argument_parser, diff --git a/scripts/tutorials/atomic_action/pickup_rubiks_cube.py b/examples/sim/atomic_action/pickup_rubiks_cube.py similarity index 98% rename from scripts/tutorials/atomic_action/pickup_rubiks_cube.py rename to examples/sim/atomic_action/pickup_rubiks_cube.py index 41bf91db9..9c9354853 100644 --- a/scripts/tutorials/atomic_action/pickup_rubiks_cube.py +++ b/examples/sim/atomic_action/pickup_rubiks_cube.py @@ -25,7 +25,7 @@ * :class:`~embodichain.lab.sim.objects.Articulation` has no whole-body ``get_vertices()``; grasp geometry is read per link with ``get_link_vert_face()``, the same source - ``scripts/tutorials/atomic_action/slide.py`` uses for its handle affordance. + ``examples/sim/atomic_action/slide.py`` uses for its handle affordance. Grasps are then sampled by the shared parallel-jaw generator. The turn joint is locked with a stiff position drive so the cube behaves as a @@ -56,7 +56,7 @@ from embodichain.lab.sim.cfg import ArticulationCfg, ArticulationRootPropertiesCfg from embodichain.lab.sim.objects import Articulation from embodichain.utils import logger -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( add_tutorial_robot, create_curobo_motion_generator, create_parallel_jaw_grasp_pose_generator, diff --git a/examples/sim/atomic_action/scenario.py b/examples/sim/atomic_action/scenario.py index 19d18974e..1b952b462 100644 --- a/examples/sim/atomic_action/scenario.py +++ b/examples/sim/atomic_action/scenario.py @@ -93,7 +93,7 @@ def create_dual_tutorial_robot_cfg( uid: str, urdf_name: str, tcp_z: float | None = None, - gripper: Literal["robotiq_2f_140"] | None = None, + gripper: Literal["robotiq_2f_140", "pgc_140_50"] | None = None, solver: Literal["ur", "pytorch"] = "ur", ur_ik_nearest_weight: Sequence[float] | None = None, pytorch_num_samples: int = 30, @@ -110,10 +110,10 @@ def create_dual_tutorial_robot_cfg( Franka retains its preset FEP analytical solver and redundancy search. ``solver="ur"`` selects the analytical solver for UR5 and UR10, while ``solver="pytorch"`` explicitly selects numerical IK for any preset. - UR5 and Franka default to the PGI hand; Handover explicitly selects the - six-joint Robotiq 2F-140, which UR10 already uses. Its TCP includes the - mounting rotation and an independently calibrated tool offset. Control-part - names and downstream action bindings stay identical across robot choices. + UR5 and Franka default to the PGI hand; Handover explicitly selects either + Robotiq 2F-140 or PGC-140-50. Each TCP includes the mounting rotation and an + independently calibrated tool offset. Control-part names and downstream + action bindings stay identical across robot choices. Args: robot_type: Arm family to mount on both sides. @@ -122,8 +122,8 @@ def create_dual_tutorial_robot_cfg( tcp_z: Optional explicit PGI local-Z override for Python callers. Omission selects the fixture's handover TCP; UR10/Robotiq uses its separate calibrated mounting transform. - gripper: Explicit Handover gripper selection. Robotiq uses its full - fixture TCP and does not consume the PGI ``tcp_z`` override. + gripper: Explicit Handover gripper selection. Each uses its full + fixture TCP instead of the PGI ``tcp_z`` override. solver: UR solver selection; Franka retains its preset unless ``"pytorch"`` is explicitly selected. ur_ik_nearest_weight: Optional nearest-solution weights for UR IK. @@ -139,23 +139,28 @@ def create_dual_tutorial_robot_cfg( Returns: A dual-arm robot configuration with two matching grippers. """ - if gripper not in (None, "robotiq_2f_140"): + if gripper not in (None, "robotiq_2f_140", "pgc_140_50"): raise ValueError(f"Unknown dual sample gripper {gripper!r}") base_cfg = create_tutorial_robot_cfg(robot_type) - if gripper == "robotiq_2f_140": + if gripper is not None: + hand_path, hand_pattern, hand_dof = ( + ("DH_PGC_140_50/DH_PGC_140_50.urdf", "finger1_joint", 2) + if gripper == "pgc_140_50" + else (ROBOTIQ_2F_140_URDF_PATH, ROBOTIQ_HAND_JOINT_PATTERN, 6) + ) old_hand_patterns = tuple(base_cfg.control_parts["eef"]) hand_component = base_cfg.urdf_cfg.components["hand"] base_cfg.urdf_cfg.add_component( "hand", - get_data_path(ROBOTIQ_2F_140_URDF_PATH), + get_data_path(hand_path), hand_component["transform"], **hand_component.get("params", {}), ) - base_cfg.control_parts["eef"] = [ROBOTIQ_HAND_JOINT_PATTERN] + base_cfg.control_parts["eef"] = [hand_pattern] base_cfg.init_qpos = ( - base_cfg.init_qpos[: len(base_cfg.control_parts["arm"])] + [0.0] * 6 + base_cfg.init_qpos[: len(base_cfg.control_parts["arm"])] + [0.0] * hand_dof ) - # Drop retired PGI drive selectors before adding Robotiq's six joints. + # Remove selectors belonging to the replaced hand before adding drives. for field in base_cfg.joint_drive_props.__dataclass_fields__: rules = getattr(base_cfg.joint_drive_props, field) if isinstance(rules, dict): @@ -251,7 +256,7 @@ def add_dual_tutorial_robot( uid: str, urdf_name: str, tcp_z: float | None = None, - gripper: Literal["robotiq_2f_140"] | None = None, + gripper: Literal["robotiq_2f_140", "pgc_140_50"] | None = None, solver: Literal["ur", "pytorch"] = "ur", ur_ik_nearest_weight: Sequence[float] | None = None, pytorch_num_samples: int = 30, @@ -273,8 +278,8 @@ def add_dual_tutorial_robot( tcp_z: Optional explicit PGI local-Z override for Python callers. Omission selects the fixture's handover TCP; UR10/Robotiq uses its separate calibrated mounting transform. - gripper: Explicit Handover gripper selection. Robotiq uses its full - fixture TCP and does not consume the PGI ``tcp_z`` override. + gripper: Explicit Handover gripper selection. Each uses its full + fixture TCP instead of the PGI ``tcp_z`` override. solver: UR solver selection; Franka retains its preset unless ``"pytorch"`` is explicitly selected. ur_ik_nearest_weight: Optional nearest-solution weights for UR IK. diff --git a/examples/sim/atomic_action/support.py b/examples/sim/atomic_action/support.py index 801d8fad5..9e3cc046c 100644 --- a/examples/sim/atomic_action/support.py +++ b/examples/sim/atomic_action/support.py @@ -146,7 +146,7 @@ DEFAULT_TUTORIAL_SUN_INTENSITY = 5.0 _FRANKA_TUTORIAL_BASE_ROTATION = (0.0, 0.0, 180.0) _GRIPPER_CONTACT_LINK_PATTERN = ( - r"(?:.*_)?(?:gripper_finger[12]_link_1|" + r"(?:.*_)?(?:gripper_finger[12]_link_1|finger[12]_link|" r"(?:left|right)_(?:outer|inner)_(?:finger(?:_pad)?|knuckle))" ) TOP_DOWN_EEF_ROTATION = ( @@ -1606,7 +1606,7 @@ def get_hand_open_close_qpos( ) -> tuple[torch.Tensor, torch.Tensor]: """Return open and closed positions for a tutorial parallel gripper. - PGI hands use their lower position limit as the open command. A Robotiq + PGI and PGC hands use their lower position limit as the open command. A Robotiq 2F-140 hand is recognized from its six expanded finger/knuckle joint names; its open command is zero and a scalar close magnitude is negated for the three knuckle joints according to the URDF mimic directions. Supplying a diff --git a/examples/sim/atomic_action/tcp_presets.py b/examples/sim/atomic_action/tcp_presets.py index ded76f2ed..fd8ae9210 100644 --- a/examples/sim/atomic_action/tcp_presets.py +++ b/examples/sim/atomic_action/tcp_presets.py @@ -83,13 +83,34 @@ (0.0, 0.0, 0.0, 1.0), ) +# PGC finger pads close along the mounted hand's Y axis. Rotate the grasp +# frame's opening axis onto it and center the short pads at local Z = 0.12 m. +UR5_PGC_140_50_HANDOVER_TCP = ( + (0.0, -1.0, 0.0, 0.0), + (1.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.12), + (0.0, 0.0, 0.0, 1.0), +) +FRANKA_PGC_140_50_HANDOVER_TCP = ( + (0.0, -1.0, 0.0, 0.0), + (1.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.12), + (0.0, 0.0, 0.0, 1.0), +) +UR10_PGC_140_50_HANDOVER_TCP = ( + (0.0, -1.0, 0.0, 0.0), + (1.0, 0.0, 0.0, 0.0), + (0.0, 0.0, 1.0, 0.12), + (0.0, 0.0, 0.0, 1.0), +) + def sample_tool_tcp( robot_type: RobotPreset, *, point: ToolPoint = "default", tcp_z: float | None = None, - gripper: Literal["robotiq_2f_140"] | None = None, + gripper: Literal["robotiq_2f_140", "pgc_140_50"] | None = None, ) -> list[list[float]]: """Resolve a mounted gripper's TCP as an independent homogeneous matrix. @@ -99,7 +120,7 @@ def sample_tool_tcp( calibrations where they differ from the general tool point. tcp_z: Optional explicit local-Z calibration override for Python callers. The remaining translation and mounting rotation are preserved. - gripper: Optional explicit Robotiq selection for the Handover fixture. + gripper: Optional explicit mounted gripper for the Handover fixture. Omission uses the robot's existing sample gripper calibration. Returns: @@ -111,13 +132,21 @@ def sample_tool_tcp( if point not in ("default", "grasp", "handover"): raise ValueError(f"Unknown sample tool point {point!r}") if gripper is not None: - if gripper != "robotiq_2f_140": + if gripper not in ("robotiq_2f_140", "pgc_140_50"): raise ValueError(f"Unknown sample gripper {gripper!r}") - poses = { - "ur5": UR5_ROBOTIQ_2F_140_HANDOVER_TCP, - "franka": FRANKA_ROBOTIQ_2F_140_HANDOVER_TCP, - "ur10": UR10_ROBOTIQ_2F_140_TCP, - } + poses = ( + { + "ur5": UR5_PGC_140_50_HANDOVER_TCP, + "franka": FRANKA_PGC_140_50_HANDOVER_TCP, + "ur10": UR10_PGC_140_50_HANDOVER_TCP, + } + if gripper == "pgc_140_50" + else { + "ur5": UR5_ROBOTIQ_2F_140_HANDOVER_TCP, + "franka": FRANKA_ROBOTIQ_2F_140_HANDOVER_TCP, + "ur10": UR10_ROBOTIQ_2F_140_TCP, + } + ) if robot_type not in poses: raise ValueError(f"Unknown sample robot/fixture {robot_type!r}") pose = poses[robot_type] @@ -145,5 +174,8 @@ def sample_tool_tcp( "UR10_ROBOTIQ_2F_140_TCP", "UR5_ROBOTIQ_2F_140_HANDOVER_TCP", "FRANKA_ROBOTIQ_2F_140_HANDOVER_TCP", + "UR5_PGC_140_50_HANDOVER_TCP", + "FRANKA_PGC_140_50_HANDOVER_TCP", + "UR10_PGC_140_50_HANDOVER_TCP", "sample_tool_tcp", ] diff --git a/scripts/benchmark/atomic_action/move_end_effector_benchmark.py b/scripts/benchmark/atomic_action/move_end_effector_benchmark.py index 39edb2f69..9be999aa4 100644 --- a/scripts/benchmark/atomic_action/move_end_effector_benchmark.py +++ b/scripts/benchmark/atomic_action/move_end_effector_benchmark.py @@ -244,7 +244,7 @@ def run_all_benchmarks(args: argparse.Namespace | None = None) -> Path: MotionGenCfg, ) from embodichain.lab.sim.motion.planners import ToppraPlannerCfg - from scripts.tutorials.atomic_action.move_end_effector import ( + from examples.sim.atomic_action.move_end_effector import ( create_robot, initialize_simulation, ) @@ -323,7 +323,7 @@ def main() -> None: if __name__ == "__main__": - from scripts.tutorials.atomic_action.tutorial_utils import run_tutorial + from examples.sim.atomic_action.support import run_tutorial run_tutorial(main) diff --git a/scripts/benchmark/atomic_action/move_held_object_benchmark.py b/scripts/benchmark/atomic_action/move_held_object_benchmark.py index 765ead9ec..36a098325 100644 --- a/scripts/benchmark/atomic_action/move_held_object_benchmark.py +++ b/scripts/benchmark/atomic_action/move_held_object_benchmark.py @@ -186,7 +186,7 @@ def _prepare_held_state( PickUpOptions, SceneSnapshot, ) - from scripts.tutorials.atomic_action.move_held_object import ( + from examples.sim.atomic_action.move_held_object_cup import ( get_hand_open_close_qpos, make_pre_pick_eef_pose, ) @@ -288,7 +288,7 @@ def _run_case( HeldObjectPoseGoal, MotionPolicy, ) - from scripts.tutorials.atomic_action.move_held_object import ( + from examples.sim.atomic_action.move_held_object_cup import ( compute_pick_close_end_step, ) @@ -662,7 +662,7 @@ def run_all_benchmarks(args: argparse.Namespace | None = None) -> Path: MotionGenCfg, ) from embodichain.lab.sim.motion.planners import ToppraPlannerCfg - from scripts.tutorials.atomic_action.move_held_object import ( + from examples.sim.atomic_action.move_held_object_cup import ( create_robot, initialize_simulation, ) @@ -790,7 +790,7 @@ def main() -> None: if __name__ == "__main__": - from scripts.tutorials.atomic_action.tutorial_utils import run_tutorial + from examples.sim.atomic_action.support import run_tutorial run_tutorial(main) diff --git a/scripts/benchmark/atomic_action/move_joints_benchmark.py b/scripts/benchmark/atomic_action/move_joints_benchmark.py index 2c89635ab..5cfcd74f4 100644 --- a/scripts/benchmark/atomic_action/move_joints_benchmark.py +++ b/scripts/benchmark/atomic_action/move_joints_benchmark.py @@ -252,7 +252,7 @@ def run_all_benchmarks(args: argparse.Namespace | None = None) -> Path: MotionGenCfg, ) from embodichain.lab.sim.motion.planners import ToppraPlannerCfg - from scripts.tutorials.atomic_action.move_joints import ( + from examples.sim.atomic_action.move_joint import ( create_robot, initialize_simulation, ) @@ -336,7 +336,7 @@ def main() -> None: if __name__ == "__main__": - from scripts.tutorials.atomic_action.tutorial_utils import run_tutorial + from examples.sim.atomic_action.support import run_tutorial run_tutorial(main) diff --git a/scripts/benchmark/atomic_action/pickup_benchmark.py b/scripts/benchmark/atomic_action/pickup_benchmark.py index c40af5e56..96421a7ae 100644 --- a/scripts/benchmark/atomic_action/pickup_benchmark.py +++ b/scripts/benchmark/atomic_action/pickup_benchmark.py @@ -133,7 +133,7 @@ def _run_case( MotionPolicy, SceneSnapshot, ) - from scripts.tutorials.atomic_action.pickup import ( + from examples.sim.atomic_action.pickup import ( get_hand_open_close_qpos, initialize_pre_pick_robot_pose, compute_pick_close_end_step, @@ -475,7 +475,7 @@ def run_all_benchmarks(args: argparse.Namespace | None = None) -> Path: MotionGenCfg, ) from embodichain.lab.sim.motion.planners import ToppraPlannerCfg - from scripts.tutorials.atomic_action.pickup import ( + from examples.sim.atomic_action.pickup import ( create_robot, initialize_simulation, ) @@ -595,7 +595,7 @@ def main() -> None: if __name__ == "__main__": - from scripts.tutorials.atomic_action.tutorial_utils import run_tutorial + from examples.sim.atomic_action.support import run_tutorial run_tutorial(main) diff --git a/scripts/benchmark/atomic_action/place_benchmark.py b/scripts/benchmark/atomic_action/place_benchmark.py index 307e6628c..2c7a7fbb2 100644 --- a/scripts/benchmark/atomic_action/place_benchmark.py +++ b/scripts/benchmark/atomic_action/place_benchmark.py @@ -184,7 +184,7 @@ def _prepare_held_state( MotionPolicy, SceneSnapshot, ) - from scripts.tutorials.atomic_action.place import ( + from examples.sim.atomic_action.place import ( get_hand_open_close_qpos, initialize_pre_pick_robot_pose, ) @@ -271,7 +271,7 @@ def _run_case( PlaceGoal, PlaceOptions, ) - from scripts.tutorials.atomic_action.place import ( + from examples.sim.atomic_action.place import ( compute_pick_close_end_step, initialize_pre_pick_robot_pose, ) @@ -642,7 +642,7 @@ def run_all_benchmarks(args: argparse.Namespace | None = None) -> Path: MotionGenCfg, ) from embodichain.lab.sim.motion.planners import ToppraPlannerCfg - from scripts.tutorials.atomic_action.place import ( + from examples.sim.atomic_action.place import ( create_robot, initialize_simulation, ) @@ -769,7 +769,7 @@ def main() -> None: if __name__ == "__main__": - from scripts.tutorials.atomic_action.tutorial_utils import run_tutorial + from examples.sim.atomic_action.support import run_tutorial run_tutorial(main) diff --git a/scripts/tutorials/atomic_action/axis_align.py b/scripts/tutorials/atomic_action/axis_align.py deleted file mode 100644 index 15170b6af..000000000 --- a/scripts/tutorials/atomic_action/axis_align.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the axis_align atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import axis_align as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/dual_arm.py b/scripts/tutorials/atomic_action/dual_arm.py new file mode 100644 index 000000000..4b05692f5 --- /dev/null +++ b/scripts/tutorials/atomic_action/dual_arm.py @@ -0,0 +1,148 @@ +# ---------------------------------------------------------------------------- +# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- + +"""Teach two-participant resource binding with a complete HandOver action. + +Run with --is_horizontal for the PGC stick fixture, or omit it for the Robotiq +can fixture. The runnable scene catalog lives in examples/sim/atomic_action. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[3] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +import torch + +from embodichain.lab.sim.atomic_actions import ( + ControlPartCommandProfile, + HandOverGoal, + HandOverOptions, + MotionPolicy, + create_simulation_atomic_action_engine, +) +from examples.sim.atomic_action import handover as scene +from examples.sim.atomic_action.support import ( + clone_local_pose_from_first_env, + create_affordance_sampling_context, + create_antipodal_semantics, + create_parallel_jaw_grasp_pose_generator, + create_tutorial_motion_generator, + create_tutorial_simulation, + get_hand_open_close_qpos, + prepare_tutorial_scene, + replay_trajectory, + run_tutorial, +) + + +def main() -> None: + """Bind two arm/hand participants and let one skill plan their transfer.""" + args = scene.parse_arguments() + + # -- Scene setup -- + sim = create_tutorial_simulation(args, arena_space=3.0) + robot = scene.create_dual_robot(sim, args.robot, is_horizontal=args.is_horizontal) + scene.create_support_surface(sim) + obj = scene.create_handover_object(sim, args) + sim.prepare() + clone_local_pose_from_first_env(obj) + for _ in range(50): + sim.update(step=10) + obj.clear_dynamics() + semantics = create_antipodal_semantics(obj, label="handover") + + # -- Resources -- + # Asset, TCP, grasp geometry and closing commands are one fixture choice. + close = ( + scene.HORIZONTAL_HAND_CLOSE_QPOS + if args.is_horizontal + else scene.HAND_CLOSE_QPOS + ) + model = ( + scene.HORIZONTAL_GRIPPER_MODEL + if args.is_horizontal + else scene.HANDOVER_GRIPPER_MODEL + ) + profiles = {} + for part in ("left_eef", "right_eef"): + opened, closed = get_hand_open_close_qpos( + robot, hand_control_part=part, close_qpos=close + ) + profiles[part] = ControlPartCommandProfile.joint_positions( + open=opened, grasp=closed + ) + generator = create_parallel_jaw_grasp_pose_generator( + n_sample=args.n_sample, + force_refresh=args.force_reannotate, + approach_direction_samples=getattr(args, "grasp_approach_samples", 4), + gripper_model=model, + ) + engine = create_simulation_atomic_action_engine( + motion_generator=create_tutorial_motion_generator(robot, args.planner), + scene_entities=(obj,), + control_profiles=profiles, + grasp_pose_generators={"left_eef": generator, "right_eef": generator}, + ) + + # -- Goal and binding -- + target = torch.eye(4, dtype=torch.float32, device=sim.device) + target[:3, 3] = torch.tensor(scene.FINAL_OBJECT_XYZ, device=sim.device) + invocation = engine.make_invocation( + "hand_over", + HandOverGoal(semantics, target_pose=target), + control_parts={ + "source": {"motion": "left_arm", "grasp": "left_eef"}, + "destination": {"motion": "right_arm", "grasp": "right_eef"}, + }, + motion_policy=MotionPolicy(strategy="motion_gen", sample_count=220), + skill_options=HandOverOptions( + pre_grasp_distance=0.08, lift_height=0.15, hand_interp_steps=10 + ), + ) + + # -- Planning -- + wait = prepare_tutorial_scene(sim, args, "Inspect both arms, then press Enter...") + context = engine.initial_context( + control_dt=sim.sim_config.physics_dt, + affordance_sampling=create_affordance_sampling_context(args), + ) + compiled = engine.compile((invocation,), context) + if not bool(compiled.plan_success.all()): + raise RuntimeError("The dual-arm walkthrough could not plan the transfer.") + + # -- Playback -- + # source/destination are action-local candidate participants. HandOver + # chooses the nearer pickup arm and coordinates both hands inside its plan. + replay_trajectory( + sim, + robot, + compiled.trajectory, + args, + video_prefix="dual_arm_walkthrough", + hold_steps=120, + look_at=scene.HANDOVER_RECORD_LOOK_AT, + ) + if wait: + input("Press Enter to finish the walkthrough...") + + +if __name__ == "__main__": + run_tutorial(main) diff --git a/scripts/tutorials/atomic_action/hand_over.py b/scripts/tutorials/atomic_action/hand_over.py deleted file mode 100644 index 7858ff7ad..000000000 --- a/scripts/tutorials/atomic_action/hand_over.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the hand_over atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import handover as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/move_joints.py b/scripts/tutorials/atomic_action/move_joints.py deleted file mode 100644 index c88e0d374..000000000 --- a/scripts/tutorials/atomic_action/move_joints.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the move_joints atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import move_joint as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/open_door.py b/scripts/tutorials/atomic_action/open_door.py deleted file mode 100644 index 7fecaa1fc..000000000 --- a/scripts/tutorials/atomic_action/open_door.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the open_door atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import open_door as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/pickup.py b/scripts/tutorials/atomic_action/pickup.py deleted file mode 100644 index bb38fbc5d..000000000 --- a/scripts/tutorials/atomic_action/pickup.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the pickup atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import pickup as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/place.py b/scripts/tutorials/atomic_action/place.py deleted file mode 100644 index d7d1b8aab..000000000 --- a/scripts/tutorials/atomic_action/place.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the place atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import place as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/pour.py b/scripts/tutorials/atomic_action/pour.py deleted file mode 100644 index 141609185..000000000 --- a/scripts/tutorials/atomic_action/pour.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the pour atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import pour as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/press.py b/scripts/tutorials/atomic_action/press.py deleted file mode 100644 index b8ddc8a26..000000000 --- a/scripts/tutorials/atomic_action/press.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the press atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import press as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/scenario_utils.py b/scripts/tutorials/atomic_action/scenario_utils.py deleted file mode 100644 index 3821fbbd6..000000000 --- a/scripts/tutorials/atomic_action/scenario_utils.py +++ /dev/null @@ -1,30 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the scenario_utils atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import scenario as _implementation - -sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/single_arm.py b/scripts/tutorials/atomic_action/single_arm.py new file mode 100644 index 000000000..8d9c53ca8 --- /dev/null +++ b/scripts/tutorials/atomic_action/single_arm.py @@ -0,0 +1,141 @@ +# ---------------------------------------------------------------------------- +# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ---------------------------------------------------------------------------- + +"""Teach single-arm Atomic Skills by compiling PickUp followed by Place. + +Scene geometry and robot calibration belong to examples/sim/atomic_action. +This walkthrough exposes the typed application code that composes those skills. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[3] +if str(_REPO_ROOT) not in sys.path: + sys.path.insert(0, str(_REPO_ROOT)) + +from embodichain.lab.sim.atomic_actions import ( + ControlPartCommandProfile, + GraspGoal, + MotionPolicy, + PickUpOptions, + PlaceGoal, + PlaceOptions, + create_simulation_atomic_action_engine, +) +from examples.sim.atomic_action import place as scene +from examples.sim.atomic_action.support import ( + add_tutorial_robot, + broadcast_waypoint_pose_batch, + create_antipodal_semantics, + create_parallel_jaw_grasp_pose_generator, + create_tutorial_argument_parser, + create_tutorial_motion_generator, + create_tutorial_simulation, + get_hand_open_close_qpos, + initialize_pre_pick_robot_pose, + make_clear_dynamics_callback, + prepare_tutorial_scene, + replay_trajectory, + run_tutorial, +) + + +def main() -> None: + """Build the scene, bind one arm/hand, compile two goals, and play the result.""" + parser = create_tutorial_argument_parser( + "Single-arm walkthrough: PickUp -> Place.", features=("grasp_sampling",) + ) + args = parser.parse_args() + + # -- Scene setup -- + sim = create_tutorial_simulation(args) + robot = add_tutorial_robot(sim, args.robot, tcp_point="grasp") + obj = scene.create_pick_object(sim) + sim.prepare() + hand_open, hand_close = get_hand_open_close_qpos(robot) + initialize_pre_pick_robot_pose(robot, obj, hand_open) + semantics = create_antipodal_semantics(obj, label="cube") + + # -- Resources -- + engine = create_simulation_atomic_action_engine( + motion_generator=create_tutorial_motion_generator(robot, args.planner), + scene_entities=(obj,), + control_profiles={ + "eef": ControlPartCommandProfile.joint_positions( + open=hand_open, grasp=hand_close + ) + }, + grasp_pose_generators={ + "eef": create_parallel_jaw_grasp_pose_generator( + n_sample=args.n_sample, force_refresh=args.force_reannotate + ) + }, + ) + binding = {"primary": {"motion": "arm", "grasp": "eef"}} + policy = MotionPolicy(strategy="motion_gen", sample_count=120) + + # -- Goals -- + pick = engine.make_invocation( + "pick_up", + GraspGoal(semantics), + control_parts=binding, + motion_policy=policy, + skill_options=PickUpOptions( + pre_grasp_distance=0.15, lift_height=0.16, hand_interp_steps=12 + ), + ) + place = engine.make_invocation( + "place", + PlaceGoal( + broadcast_waypoint_pose_batch( + scene.make_place_eef_poses(sim.device), robot.get_qpos().shape[0] + ) + ), + control_parts=binding, + motion_policy=policy, + skill_options=PlaceOptions(lift_height=0.10, hand_interp_steps=12), + ) + + # -- Planning -- + wait = prepare_tutorial_scene(sim, args, "Inspect the scene, then press Enter...") + context = engine.initial_context(control_dt=sim.sim_config.physics_dt) + compiled = engine.compile((pick, place), context) + if not bool(compiled.plan_success.all()): + raise RuntimeError("The single-arm walkthrough could not plan both skills.") + + # -- Playback -- + # compile() projects the PickUp attachment for planning Place. Actual + # physical grasp/release happens only when the host plays these commands. + replay_trajectory( + sim, + robot, + compiled.trajectory, + args, + video_prefix="single_arm_walkthrough", + hold_steps=120, + on_trajectory_step=make_clear_dynamics_callback( + obj, compiled.segment(0, "lift").start + ), + ) + if wait: + input("Press Enter to finish the walkthrough...") + + +if __name__ == "__main__": + run_tutorial(main) diff --git a/scripts/tutorials/atomic_action/slide.py b/scripts/tutorials/atomic_action/slide.py deleted file mode 100644 index dc5608780..000000000 --- a/scripts/tutorials/atomic_action/slide.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the slide atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import slide as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/tutorial_utils.py b/scripts/tutorials/atomic_action/tutorial_utils.py deleted file mode 100644 index 01ad0c5a2..000000000 --- a/scripts/tutorials/atomic_action/tutorial_utils.py +++ /dev/null @@ -1,30 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the tutorial_utils atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import support as _implementation - -sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/atomic_action/twist.py b/scripts/tutorials/atomic_action/twist.py deleted file mode 100644 index fab348226..000000000 --- a/scripts/tutorials/atomic_action/twist.py +++ /dev/null @@ -1,34 +0,0 @@ -# ---------------------------------------------------------------------------- -# Copyright (c) 2021-2026 DexForce Technology Co., Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ---------------------------------------------------------------------------- - -"""Standalone compatibility entry for the twist atomic example.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -_REPO_ROOT = Path(__file__).resolve().parents[3] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) - -from examples.sim.atomic_action import twist as _implementation - -if __name__ == "__main__": - _implementation.run_tutorial(_implementation.main) -else: - # Keep imports and monkeypatches on the shared implementation module. - sys.modules[__name__] = _implementation diff --git a/scripts/tutorials/visualization/README.md b/scripts/tutorials/visualization/README.md index d152a6c78..465df53b4 100644 --- a/scripts/tutorials/visualization/README.md +++ b/scripts/tutorials/visualization/README.md @@ -71,8 +71,8 @@ python scripts/tutorials/sim/create_softbody.py --physics newton --viser python scripts/tutorials/sim/create_cloth.py --physics newton --viser ``` -The atomic-action tutorials receive the same options through -`tutorial_utils.py`. Gym tutorials pass the generated visualization +The Atomic Skill examples and two teaching walkthroughs receive the same +options through `examples/sim/atomic_action/support.py`. Gym tutorials pass the generated visualization configuration into their environment configuration. Gizmo examples accept `--viser`, which uses a headless simulation with browser-native transform controls. Omit `--viser` to use the DexSim native window instead. diff --git a/tests/ci/test_select_tests.py b/tests/ci/test_select_tests.py index 715209622..7e95f2625 100644 --- a/tests/ci/test_select_tests.py +++ b/tests/ci/test_select_tests.py @@ -93,8 +93,8 @@ def test_repository_manifest_selectors_resolve() -> None: "tests/lab/scripts/test_run_task.py", ), ( - "scripts/tutorials/atomic_action/pickup.py", - "tests/sim/atomic_actions/test_core.py", + "scripts/tutorials/atomic_action/single_arm.py", + "tests/visualization/test_example_tutorial_coverage.py", ), ( "scripts/tutorials/visualization/viser_scene.py", diff --git a/tests/sim/atomic_actions/test_tutorial_utils.py b/tests/examples/atomic_action/test_support.py similarity index 94% rename from tests/sim/atomic_actions/test_tutorial_utils.py rename to tests/examples/atomic_action/test_support.py index 7d1162959..d5491656e 100644 --- a/tests/sim/atomic_actions/test_tutorial_utils.py +++ b/tests/examples/atomic_action/test_support.py @@ -41,20 +41,20 @@ RigidBodyMaterialCfg, ) from embodichain.lab.sim.motion.solvers import BaseSolver -from scripts.tutorials.atomic_action.dynamic_obstacle_recovery import ( +from examples.sim.atomic_action.dynamic_obstacle_recovery import ( _animate_obstacle_to_pose, _blocking_obstacle_pose, _maximum_path_deviation, _minimum_cuboid_clearance, _obstacle_motion_trigger_command, ) -from scripts.tutorials.atomic_action.coordinated_pickment import ( +from examples.sim.atomic_action.coordinated_pickment import ( compute_left_to_right_arm_direction, ) -from scripts.tutorials.atomic_action.scenario_utils import ( +from examples.sim.atomic_action.scenario import ( create_dual_tutorial_robot_cfg, ) -from scripts.tutorials.atomic_action.tutorial_utils import ( +from examples.sim.atomic_action.support import ( DEFAULT_TUTORIAL_SUN_DIRECTION, DEFAULT_TUTORIAL_SUN_INTENSITY, NEWTON_GRASP_CONTACT_DAMPING, @@ -117,10 +117,10 @@ "coordinated_pickment", "coordinated_placement", "dynamic_obstacle_recovery", - "hand_over", + "handover", "move_end_effector", - "move_held_object", - "move_joints", + "move_held_object_cup", + "move_joint", "moving_target_recovery", "open_door", "pickup", @@ -134,8 +134,8 @@ "assemble", "axis_align", "coordinated_placement", - "hand_over", - "move_held_object", + "handover", + "move_held_object_cup", "pickup", "place", "pour", @@ -154,7 +154,7 @@ SCENE_FREE_TUTORIAL_MODULES = ( "control_dt", "move_end_effector", - "move_joints", + "move_joint", ) TUTORIAL_PRISMATIC_JOINT_AXIS = torch.tensor([2.0, 2.0, 1.0]) TUTORIAL_REVOLUTE_JOINT_AXIS = torch.tensor([-1.0, 2.0, 2.0]) @@ -268,7 +268,7 @@ def _run_obstacle_animation(*, pace_wall_time: bool) -> tuple[MagicMock, MagicMo def test_atomic_action_tutorial_uses_native_mujoco_contact_settings() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.tutorial_utils") + module = importlib.import_module("examples.sim.atomic_action.support") default_cfg = module._tutorial_physics_cfg("default") newton_cfg = module._tutorial_physics_cfg("newton") @@ -348,7 +348,7 @@ def test_articulation_tutorial_semantics_resolve_signed_parent_joint_axis( neighbor_offset: tuple[float, float, float], expected_axis: tuple[float, float, float], ) -> None: - module = importlib.import_module(f"scripts.tutorials.atomic_action.{module_name}") + module = importlib.import_module(f"examples.sim.atomic_action.{module_name}") geometry = _tutorial_axis_geometry(neighbor_offset) sampled_geometry = _tutorial_articulation_geometry(geometry) articulation = _TutorialArticulation(geometry) @@ -755,7 +755,7 @@ def test_curobo_motion_generator_factory_selects_curobo_backend() -> None: robot = MagicMock(uid="tutorial_robot") with patch( - "scripts.tutorials.atomic_action.tutorial_utils.MotionGenerator" + "examples.sim.atomic_action.support.MotionGenerator" ) as motion_generator_cls: result = create_curobo_motion_generator(robot, use_cuda_graph=False) @@ -835,17 +835,15 @@ def test_run_tutorial_uses_deferred_simulation_cleanup() -> None: with ( patch( - "scripts.tutorials.atomic_action.tutorial_utils." - "SimulationManager.is_instantiated", + "examples.sim.atomic_action.support." "SimulationManager.is_instantiated", return_value=True, ), patch( - "scripts.tutorials.atomic_action.tutorial_utils." - "SimulationManager.get_instance", + "examples.sim.atomic_action.support." "SimulationManager.get_instance", return_value=sim, ), patch( - "scripts.tutorials.atomic_action.tutorial_utils." + "examples.sim.atomic_action.support." "SimulationManager.flush_cleanup_queue" ) as flush_cleanup_queue, ): @@ -863,7 +861,7 @@ def test_tutorial_motion_generator_factory_selects_requested_backend( robot = MagicMock(uid="tutorial_robot") with patch( - "scripts.tutorials.atomic_action.tutorial_utils.MotionGenerator" + "examples.sim.atomic_action.support.MotionGenerator" ) as motion_generator_cls: result = create_tutorial_motion_generator(robot, planner) @@ -884,7 +882,7 @@ def test_tutorial_motion_generator_factory_defaults_to_trapezoidal() -> None: robot = MagicMock(uid="tutorial_robot") with patch( - "scripts.tutorials.atomic_action.tutorial_utils.MotionGenerator" + "examples.sim.atomic_action.support.MotionGenerator" ) as motion_generator_cls: create_tutorial_motion_generator(robot) @@ -898,15 +896,13 @@ def test_tutorial_simulation_uses_one_global_sun_light() -> None: with ( patch( - "scripts.tutorials.atomic_action.tutorial_utils.SimulationManager", + "examples.sim.atomic_action.support.SimulationManager", return_value=simulation, ), - patch("scripts.tutorials.atomic_action.tutorial_utils.SimulationManagerCfg"), - patch("scripts.tutorials.atomic_action.tutorial_utils.RenderCfg"), - patch("scripts.tutorials.atomic_action.tutorial_utils.LightCfg") as light_cfg, - patch( - "scripts.tutorials.atomic_action.tutorial_utils.visualization_cfg_from_args" - ), + patch("examples.sim.atomic_action.support.SimulationManagerCfg"), + patch("examples.sim.atomic_action.support.RenderCfg"), + patch("examples.sim.atomic_action.support.LightCfg") as light_cfg, + patch("examples.sim.atomic_action.support.visualization_cfg_from_args"), ): result = create_tutorial_simulation(args) @@ -955,7 +951,7 @@ def test_shared_tutorial_planner_selection_excludes_neural_backend() -> None: def test_handover_tutorial_defaults_to_cpu_for_grasp_planning() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.hand_over") + module = importlib.import_module("examples.sim.atomic_action.handover") with patch("sys.argv", ["hand_over.py"]): args = module.parse_arguments() @@ -964,7 +960,7 @@ def test_handover_tutorial_defaults_to_cpu_for_grasp_planning() -> None: def test_assemble_tutorial_uses_center_grasp_for_laid_soda_can() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.assemble") + module = importlib.import_module("examples.sim.atomic_action.assemble") assert module.PICKUP_OBJECT_PART == "center" assert "pick_object_part=PICKUP_OBJECT_PART" in inspect.getsource( @@ -996,7 +992,7 @@ def test_arm_direction_uses_selected_robot_solver_roots() -> None: def test_all_atomic_action_tutorials_accept_both_robot_choices( module_name: str, ) -> None: - module = importlib.import_module(f"scripts.tutorials.atomic_action.{module_name}") + module = importlib.import_module(f"examples.sim.atomic_action.{module_name}") with patch("sys.argv", [f"{module_name}.py"]): default_args = module.parse_arguments() @@ -1018,7 +1014,7 @@ def test_all_atomic_action_tutorials_accept_both_robot_choices( def test_place_tutorial_registers_pick_object_with_simulation_engine_factory() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.place") + module = importlib.import_module("examples.sim.atomic_action.place") args = Namespace( auto_play=True, force_reannotate=False, @@ -1114,7 +1110,7 @@ def test_add_tutorial_robot_authors_newton_contacts_before_spawn() -> None: robot_cfg = SimpleNamespace(link_attrs=None) with patch( - "scripts.tutorials.atomic_action.tutorial_utils.create_tutorial_robot_cfg", + "examples.sim.atomic_action.support.create_tutorial_robot_cfg", return_value=robot_cfg, ): result = add_tutorial_robot(sim, "ur5") @@ -1169,7 +1165,7 @@ def test_articulation_contact_tutorials_author_newton_material_before_spawn( group_name: str, contact_link: str, ) -> None: - module = importlib.import_module(f"scripts.tutorials.atomic_action.{module_name}") + module = importlib.import_module(f"examples.sim.atomic_action.{module_name}") sim = MagicMock() sim.is_newton_backend = True @@ -1201,7 +1197,7 @@ def test_place_cube_uses_backend_scoped_contact_material( is_newton_backend: bool, expected_material_type: type[RigidBodyMaterialCfg], ) -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.place") + module = importlib.import_module("examples.sim.atomic_action.place") sim = MagicMock() sim.is_newton_backend = is_newton_backend obj = MagicMock() @@ -1233,7 +1229,7 @@ def test_place_cube_uses_backend_scoped_contact_material( def test_move_held_object_cup_starts_above_the_ground() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.move_held_object") + module = importlib.import_module("examples.sim.atomic_action.move_held_object_cup") sim = MagicMock() sim.is_newton_backend = True obj = MagicMock() @@ -1251,7 +1247,7 @@ def test_move_held_object_cup_starts_above_the_ground() -> None: def test_rigid_scene_tutorials_use_simulation_engine_factory( module_name: str, ) -> None: - module = importlib.import_module(f"scripts.tutorials.atomic_action.{module_name}") + module = importlib.import_module(f"examples.sim.atomic_action.{module_name}") source = inspect.getsource(module) assert "create_simulation_atomic_action_engine(" in source @@ -1263,7 +1259,7 @@ def test_rigid_scene_tutorials_use_simulation_engine_factory( def test_static_pose_tutorials_do_not_construct_scene_snapshots( module_name: str, ) -> None: - module = importlib.import_module(f"scripts.tutorials.atomic_action.{module_name}") + module = importlib.import_module(f"examples.sim.atomic_action.{module_name}") source = inspect.getsource(module) assert "SceneSnapshot" not in source @@ -1271,7 +1267,7 @@ def test_static_pose_tutorials_do_not_construct_scene_snapshots( def test_axis_align_tutorial_exposes_upright_and_horizontal_modes() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.axis_align") + module = importlib.import_module("examples.sim.atomic_action.axis_align") with patch("sys.argv", ["axis_align.py"]): upright_args = module.parse_arguments() @@ -1294,7 +1290,7 @@ def test_axis_align_tutorial_exposes_upright_and_horizontal_modes() -> None: def test_pour_tutorial_uses_configured_pickup_and_local_rotation_axis() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.pour") + module = importlib.import_module("examples.sim.atomic_action.pour") with patch("sys.argv", ["pour.py"]): default_args = module.parse_arguments() @@ -1322,7 +1318,7 @@ def test_replay_timed_trajectory_uses_arrival_intervals() -> None: dt=torch.tensor([[0.0, 0.2, 0.25]]), ) - with patch("scripts.tutorials.atomic_action.tutorial_utils.time.sleep"): + with patch("examples.sim.atomic_action.support.time.sleep"): replay_trajectory( sim, robot, @@ -1349,7 +1345,7 @@ def test_replay_timed_trajectory_settles_once_after_newton_native_grasp() -> Non dt=torch.tensor([[0.0, 0.1, 0.1]]), ) - with patch("scripts.tutorials.atomic_action.tutorial_utils.time.sleep"): + with patch("examples.sim.atomic_action.support.time.sleep"): replay_trajectory( sim, robot, @@ -1387,7 +1383,7 @@ def test_replay_timed_trajectory_settles_after_each_native_hand_phase() -> None: dt=torch.tensor([[0.0, 0.1, 0.1, 0.1, 0.1, 0.1]]), ) - with patch("scripts.tutorials.atomic_action.tutorial_utils.time.sleep"): + with patch("examples.sim.atomic_action.support.time.sleep"): replay_trajectory( sim, robot, @@ -1426,7 +1422,7 @@ def test_obstacle_animation_interpolates_and_reaches_target_pose() -> None: def test_obstacle_animation_paces_live_viewer_in_wall_time() -> None: with patch( - "scripts.tutorials.atomic_action.dynamic_obstacle_recovery.time.sleep" + "examples.sim.atomic_action.dynamic_obstacle_recovery.time.sleep" ) as sleep: _run_obstacle_animation(pace_wall_time=True) @@ -1450,7 +1446,7 @@ def test_blocking_pose_targets_the_selected_initial_path_waypoint() -> None: def test_dynamic_obstacle_recovery_keeps_strict_collision_contract() -> None: module = importlib.import_module( - "scripts.tutorials.atomic_action.dynamic_obstacle_recovery" + "examples.sim.atomic_action.dynamic_obstacle_recovery" ) main_source = inspect.getsource(module.main) diff --git a/tests/sim/atomic_actions/test_newton_collision_pipeline_schedule.py b/tests/sim/atomic_actions/test_newton_collision_pipeline_schedule.py index 2ea7c730b..e10d839f7 100644 --- a/tests/sim/atomic_actions/test_newton_collision_pipeline_schedule.py +++ b/tests/sim/atomic_actions/test_newton_collision_pipeline_schedule.py @@ -26,7 +26,7 @@ @pytest.mark.no_sim def test_atomic_action_tutorial_disables_external_collision_pipeline() -> None: - module = importlib.import_module("scripts.tutorials.atomic_action.tutorial_utils") + module = importlib.import_module("examples.sim.atomic_action.support") physics_cfg = module._tutorial_physics_cfg("newton") diff --git a/tests/sim/motion/test_motion_generator_batched.py b/tests/sim/motion/test_motion_generator_batched.py index 72a36c757..2a41b4b39 100644 --- a/tests/sim/motion/test_motion_generator_batched.py +++ b/tests/sim/motion/test_motion_generator_batched.py @@ -182,31 +182,64 @@ def test_trapezoidal_generator_holds_stationary_sparse_joint_goal() -> None: def test_trapezoidal_generator_covers_dense_ik_waypoints() -> None: - """Quantity sampling grows to retain all Cartesian-to-joint waypoints.""" - generator = _trapezoidal_generator() - generator.robot = SimpleNamespace( - compute_fk=lambda *, qpos, name, to_matrix: torch.eye( - 4, dtype=qpos.dtype, device=qpos.device - ).expand(qpos.shape[0], -1, -1), - get_qvel_limits=lambda *, name=None: torch.full((1, 2), 10.0), - ) - dense_qpos = torch.linspace(0.0, 0.5, 6, dtype=torch.float64).reshape(1, 6, 1) - dense_qpos = dense_qpos.expand(-1, -1, 2).clone() - generator._interpolate_waypoints = Mock( - return_value=(dense_qpos, None, torch.ones(1, dtype=torch.bool)) - ) + """An explicit Cartesian line retains its required dense IK waypoints.""" + generator = _trapezoidal_generator(robot=_CartesianRobot()) + goal = torch.eye(4).repeat(2, 1, 1) + goal[:, 0, 3] = 0.5 result = generator.generate( - [PlanState.from_xpos(torch.eye(4, dtype=torch.float64).unsqueeze(0))], + [PlanState.from_xpos(goal)], MotionGenOptions( - start_qpos=torch.zeros(1, 2, dtype=torch.float64), + start_qpos=torch.zeros(2, 1), + control_part="arm", sample_count=3, is_interpolate=True, + is_linear=True, + # A 0.5 m line at 0.125 m spacing requests four IK samples. + interpolate_position_step=0.125, ), ) + assert result.success.tolist() == [True, True] assert result.positions is not None - assert result.positions.shape[1] == dense_qpos.shape[1] + expected = torch.linspace(0.0, 0.5, 4).view(1, 4, 1).expand(2, -1, -1) + torch.testing.assert_close(result.positions, expected) + + +@pytest.mark.parametrize("explicit_options", [False, True]) +def test_trapezoidal_cartesian_goals_keep_sparse_joint_timing( + explicit_options: bool, +) -> None: + """IK preparation must not turn free-motion samples into mandatory stops.""" + generator = _trapezoidal_generator(robot=_CartesianRobot()) + start = torch.zeros(2, 1) + goals = [torch.eye(4).repeat(2, 1, 1) for _ in range(2)] + for goal, position in zip(goals, (0.1, 0.2)): + goal[:, 0, 3] = position + options = MotionGenOptions( + start_qpos=start, + control_part="arm", + sample_count=9, + is_interpolate=True, + interpolate_position_step=0.01, + acceleration_limit=0.5, + plan_opts=( + TrapezoidalPlanOptions(sample_interval=9, backend="torch") + if explicit_options + else None + ), + ) + expected = generator.generate( + [PlanState.from_qpos(goal[:, 0, 3:4]) for goal in goals], options + ) + + result = generator.generate([PlanState.from_xpos(goal) for goal in goals], options) + + assert result.success.tolist() == [True, True] + torch.testing.assert_close(result.dt, expected.dt) + torch.testing.assert_close(result.positions, expected.positions) + torch.testing.assert_close(result.velocities, expected.velocities) + torch.testing.assert_close(result.accelerations, expected.accelerations) def test_trapezoidal_generator_preserves_native_derivatives() -> None: diff --git a/tests/test_example_package.py b/tests/test_example_package.py index a89b62383..8ff228794 100644 --- a/tests/test_example_package.py +++ b/tests/test_example_package.py @@ -41,6 +41,15 @@ def test_distribution_preserves_the_ordinary_examples_tree(tmp_path: Path) -> No "README.md", "sim/atomic_action/__main__.py", "sim/atomic_action/host.py", + "sim/atomic_action/assemble.py", + "sim/atomic_action/control_dt.py", + "sim/atomic_action/coordinated_pickment.py", + "sim/atomic_action/coordinated_placement.py", + "sim/atomic_action/dynamic_obstacle_recovery.py", + "sim/atomic_action/move_end_effector.py", + "sim/atomic_action/move_held_object_cup.py", + "sim/atomic_action/moving_target_recovery.py", + "sim/atomic_action/pickup_rubiks_cube.py", "sim/motion/planners/curobo_planner.py", "ods/simple_bc_demo/train.py", "ods/simple_bc_demo/configs/task.yaml",