Skip to content
Merged
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
36 changes: 36 additions & 0 deletions .github/workflows/nightly-isaacsim-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ jobs:
CURRENT_PIN: ${{ steps.pin.outputs.current }}
CANDIDATE_PIN: ${{ steps.pin.outputs.candidate }}
DIGEST: ${{ steps.pin.outputs.digest }}
BRANCH_CHANGED: ${{ steps.pin.outputs.branch_changed }}
run: |
set -euo pipefail

Expand All @@ -186,6 +187,8 @@ jobs:
title="[CI] Bump Isaac Sim image to $short_digest"
body_file="$RUNNER_TEMP/isaacsim-image-update.md"
{
echo "# Description"
echo
echo "This automated draft updates CI to the current Isaac Sim nightly image."
echo
echo "| Field | Value |"
Expand All @@ -198,6 +201,30 @@ jobs:
echo "Source: https://registry.ngc.nvidia.com/orgs/0947644777160149/teams/internal/containers/isaac-sim/tags"
echo
echo "New PRs are opened as drafts so maintainers can merge after the CI results are acceptable."
echo
echo "## Type of change"
echo
echo "- Infrastructure update"
echo
echo "## Release backport"
echo
echo '- [ ] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop`'
echo
echo "## Screenshots"
echo
echo "Not applicable."
echo
echo "## Checklist"
echo
echo "Docker and GPU tests run on demand. This workflow comments \`run-ci\` after opening the pull request or updating its commit."
echo
echo '- [ ] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html)'
echo '- [ ] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format`'
echo '- [ ] I have made corresponding changes to the documentation'
echo '- [ ] My changes generate no new warnings'
echo '- [ ] I have added tests that prove my fix is effective or that my feature works'
echo '- [ ] I have added a changelog fragment under `source/<pkg>/changelog.d/` for every touched package (do **not** edit `CHANGELOG.rst` or bump `extension.toml` — CI handles that)'
echo '- [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there'
} > "$body_file"

repository_owner=${REPOSITORY%%/*}
Expand All @@ -207,6 +234,7 @@ jobs:
-f head="$repository_owner:$UPDATE_BRANCH" \
--jq '.[0].number // empty')

pr_created=false
if [ -n "$pr_number" ]; then
pr_url=$(gh api --method PATCH "repos/$REPOSITORY/pulls/$pr_number" \
-f title="$title" \
Expand All @@ -222,10 +250,18 @@ jobs:
-F body=@"$body_file" \
-F draft=true \
--jq '.html_url')
pr_number=${pr_url##*/}
pr_created=true
echo "Opened draft PR: $pr_url"
echo "Draft PR: $pr_url" >> "$GITHUB_STEP_SUMMARY"
fi

if [ "$BRANCH_CHANGED" = "true" ] || [ "$pr_created" = "true" ]; then
gh api --method POST "repos/$REPOSITORY/issues/$pr_number/comments" \
-f body='run-ci' >/dev/null
echo "Triggered CI on PR #$pr_number."
fi

- name: Report no-op or dry run
if: ${{ steps.pin.outputs.changed != 'true' || inputs.dry_run }}
env:
Expand Down
5 changes: 5 additions & 0 deletions source/isaaclab/changelog.d/fix-osc-link-velocity.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed
^^^^^

* Fixed operational-space controller velocity feedback to use the same link origins as the end-effector pose and
Jacobian in the action term and integration tests.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allowed standalone smoke tests that reached readiness before the startup deadline to complete their full soak interval.
Original file line number Diff line number Diff line change
Expand Up @@ -712,10 +712,10 @@ def _compute_ee_pose(self):

def _compute_ee_velocity(self):
"""Computes the velocity of the ee frame in root frame."""
# Extract end-effector velocity in the world frame
self._ee_vel_w[:] = self._asset.data.body_vel_w.torch[:, self._ee_body_idx, :]
# Match the link-origin reference point used by the pose and Jacobian.
self._ee_vel_w[:] = self._asset.data.body_link_vel_w.torch[:, self._ee_body_idx, :]
# Compute the relative velocity in the world frame
relative_vel_w = self._ee_vel_w - self._asset.data.root_vel_w.torch
relative_vel_w = self._ee_vel_w - self._asset.data.root_link_vel_w.torch

# Convert ee velocities from world to root frame
root_quat_w = self._asset.data.root_quat_w.torch
Expand Down
2 changes: 1 addition & 1 deletion source/isaaclab/test/app/standalone_script_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ def read_available_output(timeout: float) -> bool:
_terminate_process_group(process)
returncode = process.poll()
break
if now - start_time >= startup_timeout:
if ready_at is None and now - start_time >= startup_timeout:
_terminate_process_group(process)
returncode = process.poll()
break
Expand Down
32 changes: 32 additions & 0 deletions source/isaaclab/test/app/test_standalone_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import sys
from dataclasses import replace
from pathlib import Path
from unittest import mock

import pytest
import standalone_script_cases as script_cases
Expand Down Expand Up @@ -371,6 +372,37 @@ def test_subprocess_supervisor_soaks_then_stops_process_group():
assert result.elapsed < 2.0


def test_subprocess_supervisor_completes_soak_after_startup_deadline(monkeypatch):
"""Readiness just before the startup deadline must still receive the full soak."""
process = mock.Mock(returncode=None)
process.poll.side_effect = lambda: process.returncode
process.communicate.return_value = (b"", None)
selector = mock.Mock()
now = 0.0
poll_times = iter((299.0, 300.0, 304.0))

def select(timeout):
nonlocal now
if timeout == 0.0:
return []
now = next(poll_times)
if now == 299.0:
return [(mock.Mock(fileobj=process.stdout), script_cases.selectors.EVENT_READ)]
return []

selector.select.side_effect = select
monkeypatch.setattr(script_cases.subprocess, "Popen", lambda *args, **kwargs: process)
monkeypatch.setattr(script_cases.selectors, "DefaultSelector", lambda: selector)
monkeypatch.setattr(script_cases.os, "read", lambda *args: b"READY\n")
monkeypatch.setattr(script_cases.time, "monotonic", lambda: now)
monkeypatch.setattr(script_cases, "_terminate_process_group", lambda process: setattr(process, "returncode", -15))

result = run_until_ready(["demo.py"], r"READY", startup_timeout=300.0, soak_time=5.0)
assert result.ready
assert result.stopped_after_soak
assert result.elapsed == 304.0


def test_subprocess_supervisor_ignores_fatal_output_after_intentional_teardown(monkeypatch):
"""Fatal-looking output caused by intentional teardown must not fail a healthy launch."""

Expand Down
73 changes: 70 additions & 3 deletions source/isaaclab/test/controllers/test_operational_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

"""Rest everything follows."""

from types import SimpleNamespace

import numpy as np
import pytest
import torch
Expand All @@ -31,6 +33,7 @@
##
from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg
from isaaclab.envs.mdp.actions.actions_cfg import OperationalSpaceControllerActionCfg
from isaaclab.envs.mdp.actions.task_space_actions import OperationalSpaceControllerAction
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import SceneEntityCfg
Expand Down Expand Up @@ -861,6 +864,23 @@ def test_franka_hybrid_variable_kp_impedance(sim):
)


@pytest.mark.isaacsim_ci
def test_task_frame_conversion_preserves_absolute_target():
"""A rounded pose command must resolve to the same target through either reference frame."""
osc_cfg = OperationalSpaceControllerCfg(target_types=["pose_abs"])
osc = OperationalSpaceController(osc_cfg, num_envs=1, device="cpu")
target_b = torch.tensor([[0.5, -0.4, 0.6, 0.707, 0.0, 0.0, 0.707]])
command = target_b.clone()
resolved_targets = []
for frame in ("root", "task"):
converted_command, task_frame_pose_b = _convert_to_task_frame(osc, command, target_b, frame)
osc.set_command(converted_command, current_task_frame_pose_b=task_frame_pose_b)
resolved_targets.append(osc.desired_ee_pose_b.clone())

torch.testing.assert_close(resolved_targets[0], resolved_targets[1], atol=1e-6, rtol=0.0)
torch.testing.assert_close(command, target_b, atol=0.0, rtol=0.0)


@pytest.mark.isaacsim_ci
def test_franka_taskframe_pose_abs(sim):
"""Test absolute pose control in task frame with fixed impedance and inertial dynamics decoupling."""
Expand Down Expand Up @@ -1336,6 +1356,51 @@ class _FloatingBaseOscEnvCfg(ManagerBasedEnvCfg):
sim: sim_utils.SimulationCfg = sim_utils.SimulationCfg(dt=0.01)


@pytest.mark.isaacsim_ci
@pytest.mark.parametrize("feedback_source", ["test_helper", "action"])
def test_franka_velocity_feedback_matches_jacobian(sim, feedback_source):
"""Both OSC callers must measure velocity at the link origin used by the Jacobian."""
sim_context, num_envs, robot_cfg, *_ = sim
robot = Articulation(cfg=robot_cfg)
sim_context.reset()
arm_joint_ids, _ = robot.find_joints("panda_joint.*")
ee_frame_idx = robot.find_bodies("panda_hand")[0][0]

joint_vel = torch.zeros_like(robot.data.default_joint_vel.torch)
joint_vel[:, arm_joint_ids] = torch.linspace(0.1, 0.7, len(arm_joint_ids), device=sim_context.device)
robot.write_joint_state_to_sim_index(position=robot.data.default_joint_pos.torch, velocity=joint_vel)
sim_context.step(render=False)
robot.update(sim_context.get_physics_dt())

# Angular motion and the hand's COM offset must expose the reference-point mismatch.
assert not torch.allclose(
robot.data.body_com_vel_w.torch[:, ee_frame_idx, :3],
robot.data.body_link_vel_w.torch[:, ee_frame_idx, :3],
atol=1e-4,
rtol=1e-4,
)
if feedback_source == "test_helper":
states = _update_states(robot, ee_frame_idx, arm_joint_ids, sim_context, None, num_envs)
jacobian_b, _, _, _, ee_vel_b, _, _, _, _, joint_vel = states
else:
env = SimpleNamespace(scene={"robot": robot}, sim=sim_context, num_envs=num_envs, device=sim_context.device)
action_cfg = OperationalSpaceControllerActionCfg(
asset_name="robot",
joint_names=["panda_joint.*"],
body_name="panda_hand",
controller_cfg=OperationalSpaceControllerCfg(target_types=["pose_abs"]),
)
action_term = OperationalSpaceControllerAction(action_cfg, env)
action_term._compute_ee_jacobian()
action_term._compute_ee_velocity()
jacobian_b, ee_vel_b = action_term._jacobian_b, action_term._ee_vel_b
joint_vel = robot.data.joint_vel.torch[:, arm_joint_ids]

# With a stationary fixed base, the link twist must equal J(q) * q_dot.
expected_vel_b = torch.bmm(jacobian_b, joint_vel.unsqueeze(-1)).squeeze(-1)
torch.testing.assert_close(ee_vel_b, expected_vel_b, atol=1e-4, rtol=1e-4)


@pytest.mark.isaacsim_ci
def test_floating_base_osc_action_term_indexing():
"""Regression test for #4999 / PR #5107: verify OperationalSpaceControllerAction uses correct
Expand Down Expand Up @@ -1635,9 +1700,9 @@ def _update_states(
)
ee_pose_b = torch.cat([ee_pos_b, ee_quat_b], dim=-1)

# Compute the current velocity of the end-effector
ee_vel_w = robot.data.body_vel_w.torch[:, ee_frame_idx, :] # Extract end-effector velocity in the world frame
root_vel_w = robot.data.root_vel_w.torch # Extract root velocity in the world frame
# Match the link-origin reference point used by the pose and Jacobian.
ee_vel_w = robot.data.body_link_vel_w.torch[:, ee_frame_idx, :]
root_vel_w = robot.data.root_link_vel_w.torch
relative_vel_w = ee_vel_w - root_vel_w # Compute the relative velocity in the world frame
ee_lin_vel_b = quat_apply_inverse(robot.data.root_quat_w.torch, relative_vel_w[:, 0:3]) # From world to root frame
ee_ang_vel_b = quat_apply_inverse(robot.data.root_quat_w.torch, relative_vel_w[:, 3:6])
Expand Down Expand Up @@ -1754,6 +1819,8 @@ def _convert_to_task_frame(
# Convert target commands from base to the task frame
command = command.clone()
task_frame_pose_b = ee_target_pose_b.clone()
# Rounded goal quaternions must define a unit rotation when used as a reference frame.
task_frame_pose_b[:, 3:] /= torch.linalg.vector_norm(task_frame_pose_b[:, 3:], dim=-1, keepdim=True)

cmd_idx = 0
for target_type in osc.cfg.target_types:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed
^^^^^

* Fixed Franka Lift and Reorient reset sampling with updated Franka assets by explicitly selecting the convex-hull
arm colliders used by the tasks' clearance criteria.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fixed
^^^^^

* Fixed the Franka Pour task selecting the primitive robot collider variant,
which omitted the arm collision meshes required by the task.
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ class PourSceneCfg(InteractiveSceneCfg):
)
robot = FRANKA_PANDA_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
robot.spawn.usd_path = FRANKA_POUR_ROBOT_USD_PATH
robot.spawn.variants = {"Colliders": "convex_hulls"}
robot.spawn.func = spawn_franka_with_arm_collisions
robot.spawn.articulation_props.enabled_self_collisions = True
robot.actuators = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
# legacy asset so the upstream franka tasks keep their demos and baselines.
FRANKA_PANDA_LIFT_CFG = FRANKA_PANDA_CFG.copy()
FRANKA_PANDA_LIFT_CFG.spawn.usd_path = f"{ISAACLAB_NUCLEUS_DIR}/Robots/FrankaEmika/franka_panda.usda"
# Reset clearance was calibrated for these arm meshes; the asset's primitive colliders intersect the ground.
FRANKA_PANDA_LIFT_CFG.spawn.variants = {"Colliders": "convex_hulls"}
FRANKA_PANDA_LIFT_CFG.actuators = {
# Inspired by libfranka's joint_impedance_control.cpp. ``actuator_velocity_limit``
# remains the soft task-limit snapshot; ``joint_velocity_limit`` is the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def test_reset_dataset_contract_stores_root_relative_robot_asset_path():
robot_asset = _reset_dataset_task_contract(cfg)["robot_asset"]
assert robot_asset == "Robots/FrankaEmika/franka_panda.usda"
assert f"{ISAACLAB_NUCLEUS_DIR}/{robot_asset}" == FRANKA_POUR_ROBOT_ASSET_ID
assert cfg.scene.robot.spawn.variants == {"Colliders": "convex_hulls"}


def test_capacity_resolution_only_updates_world_dependent_solver_limits():
Expand Down
27 changes: 27 additions & 0 deletions source/isaaclab_tasks/test/core/test_lift_env_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
import pytest
import torch

from pxr import Usd

from isaaclab.managers import CommandTerm
from isaaclab.sim import select_usd_variants

from isaaclab_tasks.core.lift import mdp
from isaaclab_tasks.core.lift.config.franka.franka_env_cfg import FrankaLiftEnvCfg, FrankaReorientEnvCfg
from isaaclab_tasks.core.lift.config.franka_soft.franka_soft_env_cfg import FrankaSoftEnvCfg
from isaaclab_tasks.core.lift.mdp.commands.pose_commands import (
CableUniformPoseCommand,
Expand Down Expand Up @@ -58,6 +62,29 @@ def test_franka_soft_robot_physics_variant_matches_backend(
assert cfg.scene.robot.spawn.variants == {"Physics": expected_physics}


@pytest.mark.parametrize("cfg_type", [FrankaLiftEnvCfg, FrankaReorientEnvCfg])
def test_franka_rigid_tasks_select_collision_meshes_for_reset_clearance(cfg_type) -> None:
"""Reset validation keeps the original arm meshes when the asset defaults to capsules."""
cfg = cfg_type()
stage = Usd.Stage.CreateInMemory()
robot = stage.DefinePrim("/Robot", "Xform")
colliders = robot.GetVariantSets().AddVariantSet("Colliders")
for selection, prim_path, prim_type in (
("convex_hulls", "/Robot/link1_c/link1_c", "Mesh"),
("primitives", "/Robot/link1_capsule", "Capsule"),
):
colliders.AddVariant(selection)
colliders.SetVariantSelection(selection)
with colliders.GetVariantEditContext():
stage.DefinePrim(prim_path, prim_type)
colliders.SetVariantSelection("primitives")

select_usd_variants("/Robot", cfg.scene.robot.spawn.variants or {}, stage=stage)

assert stage.GetPrimAtPath("/Robot/link1_c/link1_c").IsValid()
assert not stage.GetPrimAtPath("/Robot/link1_capsule").IsValid()


def test_camera_normalization_is_stationary() -> None:
"""RGB and depth normalization must not depend on per-frame statistics."""
rgb = torch.tensor([0.0, 127.5, 255.0])
Expand Down
Loading