-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Expand Visualizer Tests and Patch Visualizer Bugs #5075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -693,7 +693,13 @@ def update_visualizers(self, dt: float) -> None: | |
| logger.info("Visualizer not running: %s", type(viz).__name__) | ||
| visualizers_to_remove.append(viz) | ||
| continue | ||
| # Treat Kit transport pause as a simulation-level pause trigger. | ||
| # Intentionally no auto-resume bridge: unpause should be explicit. | ||
| if type(viz).__name__ == "KitVisualizer" and viz.is_training_paused() and self.is_playing(): | ||
| self.pause() | ||
| if viz.is_rendering_paused(): | ||
| # Keep polling viewer/UI events while rendering is paused so users can unpause. | ||
| viz.step(0.0) | ||
| continue | ||
|
Comment on lines
700
to
703
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Concretely:
Before this PR the rendering-pause branch simply called |
||
| while viz.is_training_paused() and viz.is_running(): | ||
| viz.step(0.0) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| "newton", | ||
| "PyOpenGL-accelerate", | ||
| "imgui-bundle>=1.92.5", | ||
| "munch", | ||
| ], | ||
| "rerun": [ | ||
| "newton", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,9 @@ | |||||||||||
|
|
||||||||||||
| import logging | ||||||||||||
| import socket | ||||||||||||
| import copy | ||||||||||||
|
|
||||||||||||
| import numpy as np | ||||||||||||
| import pytest | ||||||||||||
| import torch | ||||||||||||
| from isaaclab_visualizers.kit import KitVisualizer, KitVisualizerCfg | ||||||||||||
|
|
@@ -30,6 +32,8 @@ | |||||||||||
| CartpolePhysicsCfg, | ||||||||||||
| CartpoleSceneCfg, | ||||||||||||
| ) | ||||||||||||
| from isaaclab_tasks.direct.cartpole.cartpole_camera_env import CartpoleCameraEnv | ||||||||||||
| from isaaclab_tasks.direct.cartpole.cartpole_camera_presets_env_cfg import CartpoleCameraPresetsEnvCfg | ||||||||||||
|
|
||||||||||||
| # Set to False to only fail on visualizer errors; when True, also fail on warnings. | ||||||||||||
| ASSERT_VISUALIZER_WARNINGS = True | ||||||||||||
|
|
@@ -93,7 +97,8 @@ def _get_visualizer_cfg(visualizer_kind: str): | |||||||||||
| if visualizer_kind == "viser": | ||||||||||||
| __import__("newton") | ||||||||||||
| __import__("viser") | ||||||||||||
| return ViserVisualizerCfg(open_browser=False), ViserVisualizer | ||||||||||||
| port = _find_free_tcp_port(host="127.0.0.1") | ||||||||||||
| return ViserVisualizerCfg(open_browser=False, port=port), ViserVisualizer | ||||||||||||
|
Comment on lines
+100
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The viser application would ideally accept a |
||||||||||||
| if visualizer_kind == "rerun": | ||||||||||||
| __import__("newton") | ||||||||||||
| __import__("rerun") | ||||||||||||
|
|
@@ -215,6 +220,52 @@ def _run_smoke_test(cfg, expected_visualizer_cls, expected_backend: str, caplog) | |||||||||||
| SimulationContext.clear_instance() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _assert_non_black_tensor(image_tensor: torch.Tensor, *, min_nonzero_pixels: int = 1) -> None: | ||||||||||||
| """Assert camera-like tensor contains non-black pixels.""" | ||||||||||||
| assert isinstance(image_tensor, torch.Tensor), f"Expected torch.Tensor, got {type(image_tensor)!r}" | ||||||||||||
| assert image_tensor.numel() > 0, "Image tensor is empty." | ||||||||||||
| finite_tensor = torch.where(torch.isfinite(image_tensor), image_tensor, torch.zeros_like(image_tensor)) | ||||||||||||
| if finite_tensor.dtype.is_floating_point: | ||||||||||||
| nonzero = torch.count_nonzero(torch.abs(finite_tensor) > 1e-6).item() | ||||||||||||
| else: | ||||||||||||
| nonzero = torch.count_nonzero(finite_tensor > 0).item() | ||||||||||||
| assert nonzero >= min_nonzero_pixels, "Rendered frame appears black (no non-zero pixels)." | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _assert_non_black_frame_array(frame) -> None: | ||||||||||||
| """Assert viewer-captured frame has visible, non-black content.""" | ||||||||||||
| frame_arr = np.asarray(frame) | ||||||||||||
| assert frame_arr.size > 0, "Viewer returned an empty frame." | ||||||||||||
| if frame_arr.ndim == 2: | ||||||||||||
| color = frame_arr | ||||||||||||
| else: | ||||||||||||
| assert frame_arr.shape[-1] >= 3, f"Expected at least 3 channels, got shape {frame_arr.shape}." | ||||||||||||
| color = frame_arr[..., :3] | ||||||||||||
| finite = np.where(np.isfinite(color), color, 0) | ||||||||||||
| assert np.count_nonzero(finite) > 0, "Viewer frame appears fully black." | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _make_cartpole_camera_env(visualizer_kind: str, backend_kind: str) -> CartpoleCameraEnv: | ||||||||||||
| """Create cartpole camera env configured with selected visualizer and physics backend.""" | ||||||||||||
| env_cfg_root = CartpoleCameraPresetsEnvCfg() | ||||||||||||
| # PresetCfg wrappers may expose concrete presets either on the instance or class. | ||||||||||||
| env_cfg = getattr(env_cfg_root, "default", None) | ||||||||||||
| if env_cfg is None: | ||||||||||||
| env_cfg = getattr(type(env_cfg_root), "default", None) | ||||||||||||
| if env_cfg is None: | ||||||||||||
| raise RuntimeError( | ||||||||||||
| "CartpoleCameraPresetsEnvCfg does not expose a 'default' preset config. " | ||||||||||||
| f"Available attributes: {sorted(vars(env_cfg_root).keys())}" | ||||||||||||
| ) | ||||||||||||
| env_cfg = copy.deepcopy(env_cfg) | ||||||||||||
| env_cfg.scene.num_envs = 1 | ||||||||||||
| env_cfg.seed = 42 | ||||||||||||
| env_cfg.sim.physics, _ = _get_physics_cfg(backend_kind) | ||||||||||||
| visualizer_cfg, _ = _get_visualizer_cfg(visualizer_kind) | ||||||||||||
| env_cfg.sim.visualizer_cfgs = visualizer_cfg | ||||||||||||
| return CartpoleCameraEnv(env_cfg) | ||||||||||||
|
Comment on lines
+248
to
+266
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The function looks up Adding a if not isinstance(env_cfg, DirectRLEnvCfg):
raise RuntimeError(
f"CartpoleCameraPresetsEnvCfg.default is {type(env_cfg)!r}, expected a DirectRLEnvCfg subclass."
) |
||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @pytest.mark.isaacsim_ci | ||||||||||||
| @pytest.mark.parametrize("visualizer_kind", ["kit", "newton", "rerun", "viser"]) | ||||||||||||
| @pytest.mark.parametrize("backend_kind", ["physx", "newton"]) | ||||||||||||
|
|
@@ -224,5 +275,59 @@ def test_visualizer_backend_smoke(visualizer_kind: str, backend_kind: str, caplo | |||||||||||
| _run_smoke_test(cfg, expected_viz_cls, expected_backend, caplog) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @pytest.mark.isaacsim_ci | ||||||||||||
| @pytest.mark.parametrize("visualizer_kind", ["kit", "newton", "rerun", "viser"]) | ||||||||||||
| @pytest.mark.parametrize("backend_kind", ["physx", "newton"]) | ||||||||||||
| def test_cartpole_visualizer_non_black_camera_frame(visualizer_kind: str, backend_kind: str): | ||||||||||||
| """Cartpole tiled-camera output should not be black when visualizers are enabled.""" | ||||||||||||
| env = None | ||||||||||||
| try: | ||||||||||||
| sim_utils.create_new_stage() | ||||||||||||
| env = _make_cartpole_camera_env(visualizer_kind=visualizer_kind, backend_kind=backend_kind) | ||||||||||||
| env.sim._app_control_on_stop_handle = None # type: ignore[attr-defined] | ||||||||||||
| env.reset() | ||||||||||||
| actions = torch.zeros((env.num_envs, env.action_space.shape[-1]), device=env.device) | ||||||||||||
| for _ in range(_SMOKE_STEPS): | ||||||||||||
| env.step(action=actions) | ||||||||||||
| rgb = env._tiled_camera.data.output["rgb"] | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| _assert_non_black_tensor(rgb) | ||||||||||||
| finally: | ||||||||||||
| if env is not None: | ||||||||||||
| env.close() | ||||||||||||
| else: | ||||||||||||
| SimulationContext.clear_instance() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @pytest.mark.isaacsim_ci | ||||||||||||
| @pytest.mark.parametrize("backend_kind", ["physx", "newton"]) | ||||||||||||
| def test_newton_visualizer_non_black_viewer_frame(backend_kind: str): | ||||||||||||
| """Newton visualizer should produce at least one non-black viewer frame for Cartpole.""" | ||||||||||||
| env = None | ||||||||||||
| try: | ||||||||||||
| sim_utils.create_new_stage() | ||||||||||||
| env = _make_cartpole_camera_env(visualizer_kind="newton", backend_kind=backend_kind) | ||||||||||||
| env.sim._app_control_on_stop_handle = None # type: ignore[attr-defined] | ||||||||||||
| env.reset() | ||||||||||||
| actions = torch.zeros((env.num_envs, env.action_space.shape[-1]), device=env.device) | ||||||||||||
| for _ in range(max(_SMOKE_STEPS, 6)): | ||||||||||||
| env.step(action=actions) | ||||||||||||
|
|
||||||||||||
| newton_visualizers = [viz for viz in env.sim.visualizers if isinstance(viz, NewtonVisualizer)] | ||||||||||||
| assert newton_visualizers, "Expected an initialized Newton visualizer." | ||||||||||||
| viewer = getattr(newton_visualizers[0], "_viewer", None) | ||||||||||||
| assert viewer is not None, "Newton viewer was not created." | ||||||||||||
|
|
||||||||||||
| get_frame = getattr(viewer, "get_frame", None) | ||||||||||||
| if not callable(get_frame): | ||||||||||||
| pytest.skip("ViewerGL.get_frame is not available in this Newton version.") | ||||||||||||
| frame = get_frame() | ||||||||||||
| _assert_non_black_frame_array(frame) | ||||||||||||
| finally: | ||||||||||||
| if env is not None: | ||||||||||||
| env.close() | ||||||||||||
| else: | ||||||||||||
| SimulationContext.clear_instance() | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| if __name__ == "__main__": | ||||||||||||
| pytest.main([__file__, "-v", "--maxfail=1"]) | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type(viz).__name__ == "KitVisualizer"bypasses Python's type system. This will silently fail for any subclass ofKitVisualizer(since the class name would differ), and the string is a typo-prone magic constant with no import-time validation.A more idiomatic and robust approach is to add a capability method on
BaseVisualizer(e.g.needs_transport_pause_bridge() -> bool, returningFalseby default) and override it to returnTrueinKitVisualizer. That way the dispatch is polymorphic, avoids string comparison, and scales to future visualizer types without modifyingsimulation_context.py.