Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/_static/css/environment-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
["IsaacContrib-Forge-NutThread-Direct", "rl_games", "", "", "", "tasks/factory/nut_thread.jpg"],
["IsaacContrib-Forge-PegInsert-Direct", "rl_games", "", "", "", "tasks/factory/peg_insert.jpg"],
["IsaacContrib-Franka-Pour", "rsl_rl", "", "", "", "tasks/manipulation/franka_pour.jpg"],
["IsaacContrib-Franka-Smoothie", "rsl_rl", "", "", "", "tasks/franka_smoothie.png"],
["IsaacContrib-Humanoid-AMP-Dance-Direct", "skrl", "", "", "", "tasks/others/humanoid_amp.jpg"],
["IsaacContrib-Humanoid-AMP-Run-Direct", "skrl", "", "", "", "tasks/others/humanoid_amp.jpg"],
["IsaacContrib-Humanoid-AMP-Walk-Direct", "skrl", "", "", "", "tasks/others/humanoid_amp.jpg"],
Expand Down
Binary file added docs/source/_static/tasks/franka_smoothie.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions scripts/environments/run_franka_smoothie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Run the repository-owned fruit, tap, lid and blender task with its scripted controller."""

from __future__ import annotations

import argparse
import contextlib
import os
from pathlib import Path


def main() -> None:
"""Step the scripted smoothie sequence, optionally rendering it live."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--headless", action="store_true")
parser.add_argument("--max_steps", type=int)
parser.add_argument("--seed", type=int, default=70044)
parser.add_argument("--robot_usd_path", type=Path, help="Optional local Franka asset instead of the bundled asset.")
playback = parser.add_mutually_exclusive_group()
playback.add_argument("--replay", type=Path, help="Replay joint actions from a previously saved .npz recording.")
playback.add_argument(
"--record",
type=Path,
help=("Run the live, fully controlled sequence and write its actions/stages to this .npz path after success."),
)
args = parser.parse_args()
if args.max_steps is not None and args.max_steps < 1:
parser.error("--max_steps must be positive.")
if not args.headless and not os.environ.get("DISPLAY"):
parser.error("Run from a graphical desktop terminal, or pass --headless.")
if args.robot_usd_path is not None:
if not args.robot_usd_path.is_file():
parser.error("--robot_usd_path must be an existing USD file.")
os.environ["ISAACLAB_FRANKA_POUR_ROBOT_USD_PATH"] = str(args.robot_usd_path.resolve())
os.environ.setdefault("PXR_WORK_THREAD_LIMIT", "1")
os.environ.setdefault("OMP_NUM_THREADS", "4")
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")

import numpy as np
import torch

from isaaclab.app import launch_simulation

from isaaclab_tasks.contrib.franka_smoothie.recorded_controller import RecordedSequenceController
from isaaclab_tasks.contrib.franka_smoothie.smoothie_controller import SmoothieSequenceController
from isaaclab_tasks.contrib.franka_smoothie.smoothie_env import SmoothieBlenderEnv
from isaaclab_tasks.contrib.franka_smoothie.smoothie_env_cfg import FrankaSmoothieEnvCfg

cfg = FrankaSmoothieEnvCfg()
cfg.seed = args.seed

with contextlib.ExitStack() as resources:
resources.enter_context(launch_simulation(cfg))
with torch.inference_mode():
env = SmoothieBlenderEnv(cfg)
resources.callback(env.close)
env.reset(seed=args.seed)

viewer = visuals = None
if not args.headless:
from isaaclab_visualizers.newton import NewtonGLVisualizer, NewtonGLVisualizerCfg

from isaaclab_tasks.contrib.franka_smoothie.smoothie_visuals import SmoothieVisuals

viewer = NewtonGLVisualizer(
NewtonGLVisualizerCfg(
enable_picking=False,
show_particles=False,
streaming_view=False,
window_width=1280,
window_height=800,
eye=(1.4, -1.2, 1.05),
lookat=(0.45, 0.0, 0.20),
)
)
resources.callback(viewer.close)
viewer.initialize(env.sim.get_scene_data_provider())
visuals = SmoothieVisuals()
resources.callback(visuals.close)

recording = args.record is not None
controller = (
RecordedSequenceController(env, recording_path=args.replay)
if args.replay is not None
else SmoothieSequenceController(env)
)
resources.callback(controller.close_trace)
recorded_actions: list[np.ndarray] = []
recorded_stages: list[str] = []
mode = "replaying recorded sequence" if args.replay is not None else "live sequence"
print(f"Running franka smoothie task ({mode}); seed {args.seed}", flush=True)
for step in range(args.max_steps or env.max_episode_length):
if viewer is not None and not viewer.is_running():
print("Window closed.", flush=True)
break
actions = controller.compute(step)
if recording:
recorded_actions.append(actions[0].detach().cpu().numpy().copy())
recorded_stages.append(controller.stage)
_, _, terminated, truncated, _ = env.step(actions)
done = bool(terminated[0] | truncated[0])
if viewer is not None and not done:
visuals.update(
viewer,
env.pose("cup")[0].cpu().numpy(),
float(env.task.fill_fraction[0]),
bool(env.task.tap_on[0]),
)
viewer.step(env.step_dt)
if step % 30 == 0 or done:
print(f"{(step + 1) * env.step_dt:.1f}s {controller.stage}", flush=True)
if done:
succeeded = bool(env.termination_manager.get_term("success")[0])
print(f"Terminated at step {step + 1}: {'success' if succeeded else 'failure'}.", flush=True)
if recording:
if not succeeded:
parser.error("Refusing to save a recording from a run that did not succeed.")
np.savez(
args.record,
actions=np.stack(recorded_actions).astype(np.float32),
stages=np.asarray(recorded_stages),
)
print(f"Saved recording to {args.record}.", flush=True)
break
else:
print("Reached step limit without terminating.", flush=True)


if __name__ == "__main__":
main()
9 changes: 9 additions & 0 deletions source/isaaclab_tasks/changelog.d/franka-smoothie.minor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Added
^^^^^

* Added ``IsaacContrib-Franka-Smoothie``, a contributed Franka task with a repository-owned
scripted demonstration for fruit pouring, visual tap filling, physical lid fastening,
docking, and pressing the blender button. The task uses no MPM solver and no
liquid particles; liquid filling is a visual scalar approximation. Optional
``--record`` and ``--replay`` runner arguments saved successful live joint-action
sequences and replayed them without per-step IK or live tracking gates.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
assets/*.usdc filter=lfs diff=lfs merge=lfs -text
assets/*.npz filter=lfs diff=lfs merge=lfs -text
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Include the USD geometry and textures required by this task.
!assets/*.usda
!assets/*.usdc
!assets/overrides/*.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Franka smoothie task

`IsaacContrib-Franka-Smoothie` runs the five-stage task in one physical scene:

1. Pour all 16 fruits from the basket into the cup and return the basket.
2. Place the cup under the tap, press its spring button to start filling, then press again to stop.
3. Pick up and screw on the lid.
4. Invert and dock the closed cup on the blender.
5. Press the physical blender button.

The tap fill is deliberately a visual mesh and a scalar: two simulated seconds
of eligible filling represents 86.4 mL. It has no liquid mass, liquid contacts,
MPM solver, or liquid particles. The robot and all manipulated objects retain
rigid-body physics. The controller uses joint actions; it does not attach or
teleport objects between phases. This is a scripted demonstration, not a trained
policy or a verified successful full-sequence dataset.

By default the runner uses the live scripted controller. All robot and scene
assets are bundled; no recording, checkpoint, or private output directory is
required. Optional playback uses a recording generated by `--record`.

Use a Linux machine with a CUDA-capable NVIDIA GPU and the repository’s uv
environment and a driver compatible with the CUDA version pinned by this branch.
The interactive viewer also needs a graphical desktop with OpenGL.
Install Git LFS and uv first. From this branch’s repository root, fetch the
bundled assets and install the pinned dependencies:

```bash
git lfs install
git lfs pull
uv sync --frozen
```

Then launch:

```bash
uv run --frozen python scripts/environments/run_franka_smoothie.py
```

The script opens a NewtonGL window and uses the bundled Franka robot asset,
including the arm collision proxies required by this task. To use another local
copy, add `--robot_usd_path /absolute/path/to/franka_panda.usda`. No Python code or
local task assets are loaded from an `outputs/` directory. The scene, controllers
and authored USD live in this package; `assets/overrides/` holds the small USD
layers that adjust a base mesh for this scene.

Pass `--headless` to run without a window and `--max_steps` to cap the number of
control steps. Close the window or press Ctrl+C to stop.

To record a successful live run and then replay its raw joint actions:

```bash
uv run --frozen python scripts/environments/run_franka_smoothie.py --headless --record /tmp/smoothie.npz
uv run --frozen python scripts/environments/run_franka_smoothie.py --replay /tmp/smoothie.npz
```

The runner saves the recording only if the success termination is reached.
Playback performs no IK or live tracking gates. The scene has no randomization;
`--seed` is passed through to the environment for reproducibility.

The task uses Newton with MJWarp at a 50 Hz control rate. Playback is tied to
this physics configuration and the bundled assets; changes to either require a
new recording. Contact dynamics can also vary across hardware and dependency
versions, so confirm the final success message when testing.

For a short headless launch check:

```bash
uv run --frozen python scripts/environments/run_franka_smoothie.py --headless --max_steps 100
```

For a complete sequence test, omit `--max_steps`. A successful run prints
`Terminated at step ...: success.`; a step-limit message only confirms that the
requested number of steps ran.

Run the task's contract tests with:

```bash
uv run --frozen --extra test python -m pytest source/isaaclab_tasks/test/contrib/test_franka_smoothie.py -q
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Franka fruit preparation with a visual tap and physical lid assembly."""

import gymnasium as gym

gym.register(
id="IsaacContrib-Franka-Smoothie",
entry_point=f"{__name__}.smoothie_env:SmoothieBlenderEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": f"{__name__}.smoothie_env_cfg:FrankaSmoothieEnvCfg",
"rsl_rl_cfg_entry_point": f"{__name__}.agents:FrankaSmoothiePPORunnerCfg",
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""PPO baseline for the contact-rich smoothie task."""

from isaaclab.utils.configclass import configclass

from isaaclab_tasks.core.cabinet.config.franka.agents.rsl_rl_ppo_cfg import CabinetPPORunnerCfg


@configclass
class FrankaSmoothiePPORunnerCfg(CabinetPPORunnerCfg):
"""PPO configuration for the rigid-body smoothie task's state observations."""

experiment_name = "franka_smoothie"
num_steps_per_env = 32
max_iterations = 3000
save_interval = 25
obs_groups = {"actor": ["policy"], "critic": ["policy"]}

def __post_init__(self):
self.actor.obs_normalization = True
self.critic.obs_normalization = True
Loading
Loading