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
37 changes: 37 additions & 0 deletions source/isaaclab_tasks/test/core/test_rendering_deformable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 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

"""Rendering correctness tests for test-local Franka cloth deformable camera setup."""

# Launch Isaac Sim Simulator first for kit-based combinations.
from isaaclab.app import AppLauncher

app_launcher = AppLauncher(headless=True, enable_cameras=True)
simulation_app = app_launcher.app

from pathlib import Path # noqa: E402

import pytest # noqa: E402
from rendering_test_utils import ( # noqa: E402
PHYSICS_RENDERER_AOV_COMBINATIONS,
make_attach_comparison_properties_fixture,
make_determinism_fixture,
make_generate_html_report_fixture,
rendering_test_deformable,
)

pytestmark = pytest.mark.isaacsim_ci

_COMPARISON_SCORES: list[dict] = []

_determinism_fixture = make_determinism_fixture()
_generate_html_report_fixture = make_generate_html_report_fixture(_COMPARISON_SCORES, Path(__file__).stem + ".html")
_attach_comparison_properties_fixture = make_attach_comparison_properties_fixture(_COMPARISON_SCORES)


@pytest.mark.parametrize("physics_backend,renderer,data_type", PHYSICS_RENDERER_AOV_COMBINATIONS)
def test_rendering_deformable(physics_backend, renderer, data_type):
"""Test Franka cloth deformable rendering correctness."""
rendering_test_deformable(physics_backend, renderer, data_type, _COMPARISON_SCORES)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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

"""Kit-less rendering correctness tests for test-local Franka cloth deformable camera setup."""

from pathlib import Path

import pytest
from rendering_test_utils import (
KITLESS_PHYSICS_RENDERER_AOV_COMBINATIONS,
make_attach_comparison_properties_fixture,
make_determinism_fixture,
make_generate_html_report_fixture,
make_require_ovlibs_install_fixture,
rendering_test_deformable,
)

pytestmark = pytest.mark.isaacsim_ci

_COMPARISON_SCORES: list[dict] = []

_determinism_fixture = make_determinism_fixture()
_generate_html_report_fixture = make_generate_html_report_fixture(_COMPARISON_SCORES, Path(__file__).stem + ".html")
_attach_comparison_properties_fixture = make_attach_comparison_properties_fixture(_COMPARISON_SCORES)
_require_ovlibs_install_fixture = make_require_ovlibs_install_fixture()


@pytest.mark.parametrize("physics_backend,renderer,data_type", KITLESS_PHYSICS_RENDERER_AOV_COMBINATIONS)
def test_rendering_deformable_kitless(physics_backend, renderer, data_type):
"""Camera output must match golden images for the Franka cloth deformable test setup."""
rendering_test_deformable(physics_backend, renderer, data_type, _COMPARISON_SCORES)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
137 changes: 133 additions & 4 deletions source/isaaclab_tasks/test/rendering_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
# needs to be large enough to tolerate minor rendering noise while small enough to catch unexpected changes.
MAX_DIFFERENT_PIXELS_PERCENTAGE_BY_ENV_NAME = {
"cartpole": 1.0,
# Cloth and soft-body edges show more anti-aliasing noise than rigid cartpole geometry.
"franka_cloth_deformable": 5.0,
# Shadow-hand renderings (incl. ``Isaac-Reorient-Cube-Shadow-Camera-Direct``) show up to
# ~3.28 % per-pixel diff from anti-aliasing noise along the many finger/cube edges. 5.0 gives
# headroom above that without masking real regressions, which the SSIM gate still catches.
Expand Down Expand Up @@ -87,17 +89,20 @@


def _make_sensor_data_type_params(
physics_backend: str, renderer: str, sensor_data_types: list[str] = None
physics_backend: str,
renderer: str,
sensor_data_types: list[str] | None = None,
extra_marks: tuple[Any, ...] = (),
) -> list[pytest.param]:
"""Create golden-image parameter entries for every supported output type."""
sensor_data_types = sensor_data_types or _DEFAULT_SENSOR_DATA_TYPES
sensor_data_types = list(sensor_data_types or _DEFAULT_SENSOR_DATA_TYPES)
return [
pytest.param(
physics_backend,
f"{renderer}_renderer",
data_type,
id=f"{physics_backend}-{renderer}-{data_type}",
marks=_FLAKY_MARK,
marks=(_FLAKY_MARK, *extra_marks),
)
for data_type in sensor_data_types
]
Expand Down Expand Up @@ -197,7 +202,7 @@ def _redirect_ovrtx_renderer_log_to_stdout(env_cfg: Any) -> None:
# manager-based envs
scene = getattr(env_cfg, "scene", None)
if scene is not None:
for camera_name in ("base_camera", "wrist_camera"):
for camera_name in ("base_camera", "wrist_camera", "tiled_camera"):
camera_cfg = getattr(scene, camera_name, None)
if camera_cfg is not None:
camera_cfgs.append(camera_cfg)
Expand All @@ -217,6 +222,11 @@ def _physics_preset_name(physics_backend: str) -> str:
return "newton_mjwarp" if physics_backend == "newton" else physics_backend


def _physics_preset_name_deformable(physics_backend: str) -> str:
"""Map deformable-test physics labels to Hydra preset names."""
return "newton_mjwarp_vbd" if physics_backend == "newton" else physics_backend


def _normalize_tensor(tensor: torch.Tensor, data_type: str) -> torch.Tensor:
"""Convert camera output tensor to [0, 1] float32 for conversion to image."""
normalized = tensor.float()
Expand Down Expand Up @@ -770,3 +780,122 @@ def rendering_test_dexsuite_kuka(
# This invokes camera sensor and renderer cleanup explicitly before pytest teardown, otherwise OV
# native code could probably complain about leaks and trigger segmentation fault.
env = None


def _make_franka_cloth_camera_env_cfg(data_type: str):
"""Create a test-local Franka cloth camera env cfg without exposing a production task."""
import isaaclab.sim as sim_utils
from isaaclab.envs import mdp as env_mdp
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.sensors import CameraCfg
from isaaclab.utils.configclass import configclass

from isaaclab_tasks.core.lift.config.franka_soft.franka_cloth_env_cfg import FrankaClothEnvCfg, FrankaClothSceneCfg
from isaaclab_tasks.utils.presets import MultiBackendRendererCfg

@configclass
class TestFrankaClothCameraSceneCfg(FrankaClothSceneCfg):
"""Franka cloth scene with a test-only camera sensor."""

tiled_camera: CameraCfg = CameraCfg(
prim_path="/World/envs/env_.*/Camera",
offset=CameraCfg.OffsetCfg(
pos=(0.85, -0.55, 0.42),
rot=(0.52, 0.18, 0.27, 0.79),
convention="opengl",
),
data_types=[data_type],
spawn=sim_utils.PinholeCameraCfg(clipping_range=(0.01, 2.5)),
width=100,
height=100,
renderer_cfg=MultiBackendRendererCfg(),
)

@configclass
class TestFrankaClothCameraObservationsCfg:
"""Image-only observations for the local rendering test env."""

@configclass
class PolicyCfg(ObsGroup):
image = ObsTerm(
func=env_mdp.image,
params={"sensor_cfg": SceneEntityCfg("tiled_camera"), "data_type": data_type, "permute": True},
)

def __post_init__(self) -> None:
self.enable_corruption = False
self.concatenate_terms = True

policy: ObsGroup = PolicyCfg()

@configclass
class TestFrankaClothCameraEnvCfg(FrankaClothEnvCfg):
"""Test-only camera variant of ``Isaac-Lift-Cloth-Franka``."""

scene: TestFrankaClothCameraSceneCfg = TestFrankaClothCameraSceneCfg(
num_envs=4, env_spacing=2.5, replicate_physics=True
)
observations: TestFrankaClothCameraObservationsCfg = TestFrankaClothCameraObservationsCfg()

def __post_init__(self) -> None:
super().__post_init__()
self.commands.deformable_pose.debug_vis = False
self.events.reset_deformable.params["position_range"] = {
"x": (0.0, 0.0),
"y": (0.0, 0.0),
"z": (0.0, 0.0),
}

return TestFrankaClothCameraEnvCfg()
Comment on lines +785 to +851

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 @configclass classes defined inside a function called once per parametrized case

_make_franka_cloth_camera_env_cfg is invoked once for each of the 7 parametrized data_type values in a test session. Each call re-executes the @configclass decorator on three classes (TestFrankaClothCameraSceneCfg, TestFrankaClothCameraObservationsCfg, TestFrankaClothCameraEnvCfg) whose names are identical across invocations. If @configclass registers class names in any module-level registry (as some configclass frameworks do for serialisation/deserialization), repeated registration under the same name can silently overwrite entries or raise an error. Other rendering helpers in this file use pre-defined imported cfg classes (e.g. CartpoleCameraEnvCfg, ShadowHandCameraEnvCfg) to avoid this. Extracting the three inner classes to module level — parameterising them at construction time — would remove the risk.



def rendering_test_deformable(
physics_backend: str,
renderer: str,
data_type: str,
comparison_scores: list[dict],
) -> None:
if physics_backend == "ovphysx":
pytest.skip("ovphysx is not supported yet.")

from isaaclab.envs import ManagerBasedRLEnv

env_cfg = _make_franka_cloth_camera_env_cfg(data_type)
env_cfg = _apply_overrides_to_env_cfg(
env_cfg, [f"presets={_physics_preset_name_deformable(physics_backend)},{renderer}"]
)

env_cfg.scene.num_envs = 4

if renderer == "ovrtx_renderer":
_redirect_ovrtx_renderer_log_to_stdout(env_cfg)

test_name = "franka_cloth_deformable"
Comment on lines +870 to +875

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant num_envs=4 assignment — the scene cfg is already constructed with num_envs=4 inside _make_franka_cloth_camera_env_cfg. The identical re-assignment here adds noise without effect.

Suggested change
env_cfg.scene.num_envs = 4
if renderer == "ovrtx_renderer":
_redirect_ovrtx_renderer_log_to_stdout(env_cfg)
test_name = "franka_cloth_deformable"
if renderer == "ovrtx_renderer":
_redirect_ovrtx_renderer_log_to_stdout(env_cfg)
test_name = "franka_cloth_deformable"

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

env = None

try:
env = ManagerBasedRLEnv(env_cfg)

# After 15 steps, the cloth should have fallen down on top of the cube and deformed.
zero_actions = torch.zeros(env.num_envs, env.action_manager.total_action_dim, device=env.device)
for _ in range(15):
env.step(zero_actions)

maybe_save_stage(test_name, physics_backend, renderer, data_type)
validate_camera_outputs(
test_name,
physics_backend,
renderer,
{data_type: env.scene.sensors["tiled_camera"].data.output[data_type]},
max_different_pixels_percentage=MAX_DIFFERENT_PIXELS_PERCENTAGE_BY_ENV_NAME[test_name],
comparison_scores=comparison_scores,
)
finally:
if env is not None:
env.close()

# This invokes camera sensor and renderer cleanup explicitly before pytest teardown, otherwise OV
# native code could probably complain about leaks and trigger segmentation fault.
env = None
Loading