From 160d508536fccd1e4e4ccba3e4e160dbc10f2bf1 Mon Sep 17 00:00:00 2001 From: Mustafa H <34825877+StafaH@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:10:38 -0700 Subject: [PATCH 1/3] [Core] Refactor and cleanup core subpackage for 3.0 [2/N] (#7959) # Description Second PR in a series that splits the core cleanup in #7949 into small, reviewable pieces. This PR contains only comment and style changes in `source/isaaclab/isaaclab`; there is no logic change. - Comments that restate the next line of code are removed (e.g. `# get stage handle`, `# import logger`, `# return the prim`), along with commented-out code. - Comments are kept when they explain intent or ordering constraints, units (e.g. `N-m/rad --> N-m/deg`), deprecation plans, tensor shapes and formulas, non-obvious semantics (e.g. the same random value per body), the `FactoryBase` dispatch, TODO/FIXME items, and section markers in long functions. - The `# extract the used quantities (to enable type-hinting)` boilerplate is removed from the core package; the typed local variable already states its purpose. - Blank lines inside function bodies that `ruff format` does not require are removed. - Empty `dict()` / `list()` calls are replaced with `{}` / `[]`. - A duplicated license header in `utils/warp/fabric.py` is removed. This PR only deletes comments. Comments that the reference branch rewrites alongside code changes will ship in the PRs that change that code. Verification: - Every file's AST (including docstrings) is identical to `develop` apart from the `dict()`/`list()` literals (520 files). - Importing every module under `isaaclab` gives the same result as on `develop`. - `ruff check` / `ruff format` are clean. ## Type of change - Code cleanup (non-breaking, no functional change) - [x] Backport this pull request to the active release branch after it merges into `develop` ## Checklist - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source//changelog.d/` for every touched package (`.skip`) - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../changelog.d/core-cleanup-comments.skip | 1 + .../isaaclab/actuators/actuator_pd.py | 2 - source/isaaclab/isaaclab/app/app_launcher.py | 21 ----- .../isaaclab/isaaclab/app/settings_manager.py | 1 - source/isaaclab/isaaclab/assets/asset_base.py | 4 - .../isaaclab/benchmark/benchmark_core.py | 3 - source/isaaclab/isaaclab/benchmark/capture.py | 1 - .../isaaclab/isaaclab/benchmark/formatters.py | 2 - .../isaaclab/benchmark/method_benchmark.py | 2 - .../benchmark/recorders/record_gpu_info.py | 3 - .../recorders/record_version_info.py | 5 -- source/isaaclab/isaaclab/cli/commands/envs.py | 14 ---- .../isaaclab/isaaclab/cli/commands/format.py | 1 - .../isaaclab/isaaclab/cli/commands/install.py | 6 -- source/isaaclab/isaaclab/cli/commands/misc.py | 1 - source/isaaclab/isaaclab/cli/utils.py | 11 --- source/isaaclab/isaaclab/cloner/clone_plan.py | 1 - .../isaaclab/controllers/differential_ik.py | 1 - .../isaaclab/controllers/operational_space.py | 2 +- source/isaaclab/isaaclab/controllers/utils.py | 4 - .../isaaclab/devices/gamepad/se2_gamepad.py | 2 +- .../isaaclab/devices/gamepad/se3_gamepad.py | 2 +- .../isaaclab/devices/haply/se3_haply.py | 2 +- .../isaaclab/devices/keyboard/se2_keyboard.py | 2 +- .../isaaclab/devices/keyboard/se3_keyboard.py | 2 +- .../devices/spacemouse/se2_spacemouse.py | 2 +- .../devices/spacemouse/se3_spacemouse.py | 2 +- .../isaaclab/isaaclab/envs/direct_marl_env.py | 9 +- .../isaaclab/isaaclab/envs/direct_rl_env.py | 9 -- .../isaaclab/envs/leapp_deployment_env.py | 1 - .../isaaclab/envs/manager_based_env.py | 9 -- .../isaaclab/envs/manager_based_rl_env.py | 11 --- .../envs/manager_based_rl_mimic_env.py | 2 +- .../envs/mdp/actions/binary_joint_actions.py | 1 - .../envs/mdp/actions/joint_actions.py | 1 - .../mdp/actions/joint_actions_to_limits.py | 1 - .../envs/mdp/actions/non_holonomic_actions.py | 1 - .../mdp/actions/rmpflow_task_space_actions.py | 1 - .../mdp/actions/surface_gripper_actions.py | 1 - .../envs/mdp/actions/task_space_actions.py | 2 - .../envs/mdp/actions/tendon_actions.py | 1 - .../envs/mdp/commands/velocity_command.py | 1 - .../isaaclab/isaaclab/envs/mdp/curriculums.py | 7 -- source/isaaclab/isaaclab/envs/mdp/events.py | 60 -------------- .../isaaclab/envs/mdp/observations.py | 33 -------- source/isaaclab/isaaclab/envs/mdp/rewards.py | 26 ------ .../isaaclab/envs/mdp/terminations.py | 9 -- .../isaaclab/isaaclab/envs/mimic_env_cfg.py | 2 +- .../isaaclab/envs/ui/base_env_window.py | 2 +- .../isaaclab/isaaclab/envs/ui/empty_window.py | 2 +- .../isaaclab/managers/action_manager.py | 4 +- .../isaaclab/managers/command_manager.py | 4 +- .../isaaclab/managers/curriculum_manager.py | 7 +- .../isaaclab/managers/event_manager.py | 21 +---- .../isaaclab/managers/manager_base.py | 3 - .../isaaclab/managers/manager_term_cfg.py | 2 +- .../isaaclab/managers/observation_manager.py | 36 ++++---- .../isaaclab/managers/recorder_manager.py | 6 +- .../isaaclab/managers/reward_manager.py | 9 +- .../isaaclab/managers/scene_entity_cfg.py | 1 - .../isaaclab/managers/termination_manager.py | 8 +- .../isaaclab/scene/interactive_scene.py | 47 +++++------ .../isaaclab/sensors/camera/camera.py | 3 +- .../contact_sensor/base_contact_sensor.py | 3 - source/isaaclab/isaaclab/sensors/kernels.py | 1 - .../multi_mesh_ray_caster_camera_cfg.py | 1 - .../isaaclab/isaaclab/sensors/sensor_base.py | 6 -- .../sim/converters/asset_converter_base.py | 4 - .../isaaclab/isaaclab/sim/schemas/schemas.py | 82 ------------------- .../sim/spawners/from_files/from_files.py | 9 -- .../isaaclab/sim/spawners/lights/lights.py | 2 - .../isaaclab/sim/spawners/meshes/meshes.py | 13 --- .../isaaclab/sim/spawners/sensors/sensors.py | 11 --- .../isaaclab/sim/spawners/shapes/shapes.py | 6 -- .../sim/spawners/wrappers/wrappers.py | 1 - source/isaaclab/isaaclab/sim/utils/legacy.py | 1 - source/isaaclab/isaaclab/sim/utils/prims.py | 30 ------- source/isaaclab/isaaclab/sim/utils/queries.py | 30 ------- .../isaaclab/isaaclab/sim/utils/semantics.py | 3 - source/isaaclab/isaaclab/sim/utils/stage.py | 12 --- .../isaaclab/isaaclab/sim/utils/transforms.py | 15 ---- .../isaaclab/terrains/height_field/utils.py | 2 - .../isaaclab/terrains/terrain_generator.py | 3 +- .../isaaclab/terrains/terrain_importer.py | 10 +-- .../terrains/trimesh/mesh_terrains.py | 9 -- .../isaaclab/terrains/trimesh/utils.py | 6 -- source/isaaclab/isaaclab/terrains/utils.py | 4 - .../isaaclab/ui/widgets/image_plot.py | 2 - .../isaaclab/isaaclab/ui/widgets/line_plot.py | 1 - .../ui/widgets/manager_live_visualizer.py | 5 +- .../isaaclab/ui/widgets/ui_visualizer_base.py | 6 -- .../ui/xr_widgets/instruction_widget.py | 2 - .../ui/xr_widgets/scene_visualization.py | 2 - .../teleop_visualization_manager.py | 1 - .../isaaclab/isaaclab/utils/backend_utils.py | 3 - .../isaaclab/utils/datasets/episode_data.py | 3 +- .../datasets/hdf5_dataset_file_handler.py | 2 - source/isaaclab/isaaclab/utils/dict.py | 7 +- .../isaaclab/utils/leapp/leapp_semantics.py | 1 - source/isaaclab/isaaclab/utils/math.py | 5 -- source/isaaclab/isaaclab/utils/mesh.py | 1 - .../isaaclab/utils/modifiers/modifier_cfg.py | 2 +- source/isaaclab/isaaclab/utils/sensors.py | 1 - source/isaaclab/isaaclab/utils/string.py | 5 -- source/isaaclab/isaaclab/utils/timer.py | 4 +- source/isaaclab/isaaclab/utils/warp/fabric.py | 4 - .../isaaclab/isaaclab/utils/warp/kernels.py | 4 - source/isaaclab/isaaclab/utils/warp/ops.py | 7 -- 108 files changed, 79 insertions(+), 696 deletions(-) create mode 100644 source/isaaclab/changelog.d/core-cleanup-comments.skip diff --git a/source/isaaclab/changelog.d/core-cleanup-comments.skip b/source/isaaclab/changelog.d/core-cleanup-comments.skip new file mode 100644 index 000000000000..d77f90730482 --- /dev/null +++ b/source/isaaclab/changelog.d/core-cleanup-comments.skip @@ -0,0 +1 @@ +Removed comments that restate the code and replaced empty dict()/list() calls with literals in the core package. diff --git a/source/isaaclab/isaaclab/actuators/actuator_pd.py b/source/isaaclab/isaaclab/actuators/actuator_pd.py index 09cb45f2d526..70d2d21ecd31 100644 --- a/source/isaaclab/isaaclab/actuators/actuator_pd.py +++ b/source/isaaclab/isaaclab/actuators/actuator_pd.py @@ -27,7 +27,6 @@ RemotizedPDActuatorCfg, ) -# import logger logger = logging.getLogger(__name__) """ @@ -506,7 +505,6 @@ def compute( control_action.joint_positions = self.positions_delay_buffer.compute(control_action.joint_positions) control_action.joint_velocities = self.velocities_delay_buffer.compute(control_action.joint_velocities) control_action.joint_efforts = self.efforts_delay_buffer.compute(control_action.joint_efforts) - # compte actuator model return super().compute(control_action, joint_pos, joint_vel) diff --git a/source/isaaclab/isaaclab/app/app_launcher.py b/source/isaaclab/isaaclab/app/app_launcher.py index cc431512eea6..b6134e852e82 100644 --- a/source/isaaclab/isaaclab/app/app_launcher.py +++ b/source/isaaclab/isaaclab/app/app_launcher.py @@ -668,7 +668,6 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: default=argparse.SUPPRESS, help=("When set, caps the nums of envs shown in the launched visualizers."), ) - # special flag for backwards compatibility # Corresponding to the beginning of the function, # if we have removed -h/--help handling, we add it back. @@ -787,7 +786,6 @@ def _config_resolution(self, launcher_args: dict): Args: launcher_args: A dictionary of all input arguments passed to the class object. """ - # Handle core settings livestream_arg, livestream_env = self._resolve_livestream_settings(launcher_args) self._resolve_visualizer_settings(launcher_args) # XR must be resolved before headless so that XR can prevent @@ -796,17 +794,9 @@ def _config_resolution(self, launcher_args: dict): self._resolve_headless_settings(launcher_args, livestream_arg, livestream_env) self._resolve_camera_settings(launcher_args) self._resolve_viewport_settings(launcher_args) - - # Handle device and distributed settings self._resolve_device_settings(launcher_args) - - # Handle experience file settings self._resolve_experience_file(launcher_args) - - # Handle animation recording settings self._resolve_anim_recording_settings(launcher_args) - - # Handle additional arguments self._resolve_kit_args(launcher_args) # Prepare final simulation app config @@ -1137,7 +1127,6 @@ def _resolve_device_settings(self, launcher_args: dict): # set environment variables to limit CPU threads os.environ["PXR_WORK_THREAD_LIMIT"] = str(num_threads_per_process) os.environ["OPENBLAS_NUM_THREADS"] = str(num_threads_per_process) - # pass command line variable to kit sys.argv.append(f"--/plugins/carb.tasking.plugin/threadCount={num_threads_per_process}") # ``/physics/cudaDevice`` is resolved by CUDA, so the masked index is correct there. @@ -1204,8 +1193,6 @@ def _resolve_experience_file(self, launcher_args: dict): kit_app_exp_path = os.path.join(os.path.dirname(_isaacsim_for_paths.__file__), "apps") os.environ["EXP_PATH"] = kit_app_exp_path isaaclab_app_exp_path = str(ISAACLAB_ROOT / "apps") - # For Isaac Sim 4.5 compatibility, we use the 4.5 app files in a different folder - # if launcher_args.get("use_isaacsim_45", False): if self.is_isaac_sim_version_5(): isaaclab_app_exp_path = os.path.join(isaaclab_app_exp_path, "isaacsim_5") @@ -1359,7 +1346,6 @@ def _create_app(self): self._app = SimulationApp(self._sim_app_config, experience=self._sim_experience_file) report_activity(None) - # enable sys stdout and stderr sys.stdout = sys.__stdout__ # add Isaac Lab modules back to sys.modules @@ -1404,20 +1390,13 @@ def _load_extensions(self): # Publish whether Kit has an interactive GUI (local window, livestream, or XR). # SimulationContext and renderers consume this setting during their initialization. settings.set_bool("/isaaclab/has_gui", not self._headless or self._livestream >= 1 or self._xr) - - # set setting to indicate Isaac Lab's offscreen_render pipeline should be enabled settings.set_bool("/isaaclab/render/offscreen", self._offscreen_render) - - # set setting to indicate Isaac Lab's render_viewport pipeline should be enabled settings.set_bool("/isaaclab/render/active_viewport", self._render_viewport) - - # set setting to indicate XR mode is enabled settings.set_bool("/isaaclab/xr/enabled", self._xr) # set setting to indicate XR auto-start mode -- when running headless # (no Kit GUI) the AR profile must be enabled programmatically so that # the OpenXR session starts without user interaction settings.set_bool("/isaaclab/xr/auto_start", self._headless and self._xr) - # set setting to indicate video recording mode settings.set_bool("/isaaclab/video/enabled", self._video_enabled) # set setting to indicate no RTX sensors are used (set to True when RTX sensor is created) diff --git a/source/isaaclab/isaaclab/app/settings_manager.py b/source/isaaclab/isaaclab/app/settings_manager.py index 8281c627a910..85499b085d5b 100644 --- a/source/isaaclab/isaaclab/app/settings_manager.py +++ b/source/isaaclab/isaaclab/app/settings_manager.py @@ -38,7 +38,6 @@ def __new__(cls): """Singleton pattern - always return the same instance, stored in sys.modules to survive reloads.""" # Check if instance exists in sys.modules (survives module reloads) instance = sys.modules.get(_SINGLETON_KEY) - if instance is None: instance = super().__new__(cls) sys.modules[_SINGLETON_KEY] = instance diff --git a/source/isaaclab/isaaclab/assets/asset_base.py b/source/isaaclab/isaaclab/assets/asset_base.py index 51db15260b05..ead7133ee580 100644 --- a/source/isaaclab/isaaclab/assets/asset_base.py +++ b/source/isaaclab/isaaclab/assets/asset_base.py @@ -93,19 +93,15 @@ def __init__(self, cfg: AssetBaseCfg): self._check_shapes = __debug__ else: self._check_shapes = not self.cfg.disable_shape_checks - # flag for whether the asset is initialized self._is_initialized = False - # register various callback functions self._register_callbacks() # add handle for debug visualization (this is set to a valid handle inside set_debug_vis) self._debug_vis_handle = None - # set initial state of debug visualization self.set_debug_vis(self.cfg.debug_vis) def __del__(self): """Unsubscribe from the callbacks.""" - # clear events handles self._clear_callbacks() """ diff --git a/source/isaaclab/isaaclab/benchmark/benchmark_core.py b/source/isaaclab/isaaclab/benchmark/benchmark_core.py index 8610333fab57..c9b3a01031cd 100644 --- a/source/isaaclab/isaaclab/benchmark/benchmark_core.py +++ b/source/isaaclab/isaaclab/benchmark/benchmark_core.py @@ -274,7 +274,6 @@ def __init__( "workflow_metadata provided, but missing expected 'metadata' entry. Metadata will not be read." ) - # Whether to use recorders to collect metrics. self._use_recorders = use_recorders self._use_frametime_recorders = frametime_recorders @@ -472,11 +471,9 @@ def _finalize_impl(self) -> tuple[Path, ...]: if self._bundle is None and any(key == "schema" for key, _ in self._metrics): raise RuntimeError("The schema formatter requires an attached benchmark bundle.") - # Stop collecting frametime recorders. for recorder in self._frametime_recorders.values(): recorder.stop_collecting() - # Add measurements and metadata from recorders to the phases. if self._use_recorders: for recorder_name, measurement_data in self._manual_recorders.items(): data = measurement_data.get_data() diff --git a/source/isaaclab/isaaclab/benchmark/capture.py b/source/isaaclab/isaaclab/benchmark/capture.py index b3f95af443d9..182ffcc73573 100644 --- a/source/isaaclab/isaaclab/benchmark/capture.py +++ b/source/isaaclab/isaaclab/benchmark/capture.py @@ -294,7 +294,6 @@ def run_config_from_env_cfg(env_cfg: object) -> RunConfig: if physics is None: physics_cfg = getattr(getattr(env_cfg, "sim", None), "physics", None) raise ValueError(f"Unsupported concrete physics config: {type(physics_cfg).__name__}.") - return RunConfig( physics_backend=physics, rendering_backend=rendering or "none", diff --git a/source/isaaclab/isaaclab/benchmark/formatters.py b/source/isaaclab/isaaclab/benchmark/formatters.py index 3df7f54ce5b8..1c8cb3a0bce7 100644 --- a/source/isaaclab/isaaclab/benchmark/formatters.py +++ b/source/isaaclab/isaaclab/benchmark/formatters.py @@ -137,7 +137,6 @@ def finalize(self, output_path: str, output_filename: str, **kwargs) -> None: # Append test name to measurement name as OVAT needs to uniquely identify for test_phase in self.data: test_name = test_phase.get_metadata_field("workflow_name") - # Store the test name if test_name != self.test_name: if self.test_name: logger.warning( @@ -538,7 +537,6 @@ def finalize(self, output_path: str, output_filename: str, **kwargs) -> None: """ multi_phase = len(self._test_phases) > 1 for test_phase in self._test_phases: - # Retrieve useful metadata from test_phase phase_name = test_phase.get_metadata_field("phase") osmo_kpis: dict[str, object] = {} diff --git a/source/isaaclab/isaaclab/benchmark/method_benchmark.py b/source/isaaclab/isaaclab/benchmark/method_benchmark.py index e13f488cadb2..2b386689875c 100644 --- a/source/isaaclab/isaaclab/benchmark/method_benchmark.py +++ b/source/isaaclab/isaaclab/benchmark/method_benchmark.py @@ -194,7 +194,6 @@ def run_benchmarks( benchmarks: List of benchmark definitions to run. target_object: Object containing the methods to benchmark. """ - print(f"\nBenchmarking {len(benchmarks)} methods...") print(f"Config: {self._config.num_iterations} iterations, {self._config.warmup_steps} warmup steps") print( @@ -437,7 +436,6 @@ def _benchmark_property( Returns: Dict with timing results, or None if property not found. """ - # Check if property exists if inspect.getattr_static(target_data, prop_name, None) is None: return None diff --git a/source/isaaclab/isaaclab/benchmark/recorders/record_gpu_info.py b/source/isaaclab/isaaclab/benchmark/recorders/record_gpu_info.py index 9655470d98c2..841c23d6c36b 100644 --- a/source/isaaclab/isaaclab/benchmark/recorders/record_gpu_info.py +++ b/source/isaaclab/isaaclab/benchmark/recorders/record_gpu_info.py @@ -55,8 +55,6 @@ def _get_hardware_info(self) -> None: self._device_count = torch.cuda.device_count() self._gpu_hardware_info["device_count"] = self._device_count self._gpu_hardware_info["current_device"] = torch.cuda.current_device() - - # Collect info for all devices self._gpu_hardware_info["devices"] = [] for i in range(self._device_count): gpu_props = torch.cuda.get_device_properties(i) @@ -88,7 +86,6 @@ def _get_hardware_info(self) -> None: cuda_version = getattr(torch_version, "cuda", None) self._gpu_hardware_info["cuda_version"] = cuda_version if cuda_version else "Unknown" - # Initialize pynvml for GPU utilization monitoring (all devices) with contextlib.suppress(Exception): import pynvml diff --git a/source/isaaclab/isaaclab/benchmark/recorders/record_version_info.py b/source/isaaclab/isaaclab/benchmark/recorders/record_version_info.py index 4355e27066f4..3739deda3406 100644 --- a/source/isaaclab/isaaclab/benchmark/recorders/record_version_info.py +++ b/source/isaaclab/isaaclab/benchmark/recorders/record_version_info.py @@ -91,7 +91,6 @@ def _record(self, key: str, version: str | None, *, nullable: bool = False) -> N self._version_info[key] = version def _get_version_info(self) -> None: - # isaaclab self._record("isaaclab", self._get_version("isaaclab")) # warp - try config.version first, then __version__ @@ -103,10 +102,7 @@ def _get_version_info(self) -> None: self._record("kit", version, nullable=True) self._record("isaacsim", self._get_isaacsim_version() if version else None, nullable=True) - # torch self._record("torch", self._get_version("torch")) - - # numpy self._record("numpy", self._get_version("numpy")) # IsaacLab sub-packages @@ -183,7 +179,6 @@ def _get_git_info(self) -> None: ) if result.returncode == 0: self._dev_info["commit_date"] = result.stdout.strip() - # Check if working directory is dirty result = subprocess.run( ["git", "status", "--porcelain"], diff --git a/source/isaaclab/isaaclab/cli/commands/envs.py b/source/isaaclab/isaaclab/cli/commands/envs.py index 3f59997a2aa2..87a58ffafd91 100644 --- a/source/isaaclab/isaaclab/cli/commands/envs.py +++ b/source/isaaclab/isaaclab/cli/commands/envs.py @@ -518,7 +518,6 @@ def command_setup_conda(env_name: str) -> None: _reject_downloaded_isaac_sim("conda") - # Check if conda is installed. if not shutil.which("conda"): print_error("Conda could not be found. Please install conda and try again.") sys.exit(1) @@ -544,7 +543,6 @@ def command_setup_conda(env_name: str) -> None: print("\tThis warning can be ignored if you plan to install Isaac Sim via pip.") print("\tDownloaded Isaac Sim packages use their bundled Python and cannot be combined with conda.") - # Check if the environment exists. conda_env = _sanitized_conda_env() result = run_command(["conda", "env", "list", "--json"], capture_output=True, text=True, check=False, env=conda_env) if '"' + env_name + '"' in result.stdout: @@ -576,13 +574,11 @@ def command_setup_conda(env_name: str) -> None: if temp_yml.exists(): temp_yml.unlink() - # Now configure activation scripts. conda_prefix = _get_conda_prefix(env_name) if not conda_prefix: print_error(f"Could not determine prefix for env {env_name}") return - # Setup Isaac Lab and Isaac Sim environment variables through conda hooks. _write_conda_env_hooks(conda_prefix) if not is_windows(): @@ -617,7 +613,6 @@ def _check_venv_python_version(env_path: Path, required_ver: str) -> None: python_exe = env_path / "Scripts" / "python.exe" else: python_exe = env_path / "bin" / "python" - if not python_exe.exists(): return @@ -654,7 +649,6 @@ def command_setup_uv(env_name: str) -> None: """ _reject_downloaded_isaac_sim("uv") - # Check if uv is installed. if not shutil.which("uv"): print_error("uv could not be found. Please install uv and try again.") print_error("uv can be installed here:") @@ -688,11 +682,7 @@ def command_setup_uv(env_name: str) -> None: if active_venv: env_path = Path(active_venv) print_info(f"Detected active virtual environment: {env_path}") - - # Validate Python version. _check_venv_python_version(env_path, py_ver) - - # Inject Isaac Lab hooks into the existing environment. _write_uv_env_hooks(env_path) print_info("Added Isaac Lab environment hooks to the active virtual environment.") @@ -705,17 +695,13 @@ def command_setup_uv(env_name: str) -> None: return env_path = ISAACLAB_ROOT / env_name - - # Check if the environment exists. if not env_path.exists(): print_info(f"Creating uv environment named '{env_name}'...") run_command(["uv", "venv", "--clear", "--seed", "--python", py_ver, str(env_path)]) else: print_info(f"uv environment '{env_name}' already exists.") - # Validate Python version of existing environment. _check_venv_python_version(env_path, py_ver) - # Setup Isaac Lab and Isaac Sim environment variables through uv activation hooks. _write_uv_env_hooks(env_path) print_info("Added environment hooks to uv activation scripts.") diff --git a/source/isaaclab/isaaclab/cli/commands/format.py b/source/isaaclab/isaaclab/cli/commands/format.py index b26b93af90e7..95dd11eb7b89 100644 --- a/source/isaaclab/isaaclab/cli/commands/format.py +++ b/source/isaaclab/isaaclab/cli/commands/format.py @@ -28,7 +28,6 @@ def _run_pre_commit() -> None: if result.returncode == 0: pre_commit_module = True - # If pre-commit is not installed, install it. if not pre_commit_module: print_info('Pre-commit not found. Installing "pre-commit" module...') diff --git a/source/isaaclab/isaaclab/cli/commands/install.py b/source/isaaclab/isaaclab/cli/commands/install.py index f022c4f8b61a..d41146d2327d 100644 --- a/source/isaaclab/isaaclab/cli/commands/install.py +++ b/source/isaaclab/isaaclab/cli/commands/install.py @@ -75,7 +75,6 @@ def _install_system_deps() -> None: if is_windows(): return - # Check if cmake is already installed. if shutil.which("cmake"): print_info("cmake is already installed.") else: @@ -274,7 +273,6 @@ def _ensure_pink_ik_dependencies_installed(python_exe: str, pip_cmd: list[str], ) if probe_result.returncode == 0: return - print_info("Pink IK dependency probe failed. Force-installing the cmeel pinocchio and DAQP stack.") pink_ik_stack = _pink_ik_stack() install_result = _run_package_install( @@ -329,7 +327,6 @@ def _ensure_cuda_torch() -> None: print_info(f"PyTorch {want_torch} already installed.") return - # Clean install torch. print_info(f"Installing torch=={torch_ver} and torchvision=={tv_ver} ({cuda_tag}) from {index_url}...") # uv pip uninstall does not accept -y @@ -380,7 +377,6 @@ def _ensure_newton() -> None: _run_package_install(pip_cmd + ["install", requirement, *([schemas] if schemas else [])]) -# Isaac Sim install settings. NVIDIA_INDEX_URL = "https://pypi.nvidia.com" @@ -1101,7 +1097,6 @@ def _repoint_prebundle_packages() -> None: print_debug(f"Repointed {prebundled} -> {venv_pkg}") except OSError as exc: print_warning(f"Could not repoint {prebundled}: {exc} — skipping.") - if repointed: print_info( f"Repointed {repointed} prebundled package(s) in Isaac Sim to the active environment's site-packages." @@ -1165,7 +1160,6 @@ def command_install(install_type: str = "all") -> None: os.environ.setdefault("PIP_RETRIES", _PACKAGE_INDEX_RETRIES) os.environ.setdefault("UV_HTTP_RETRIES", _PACKAGE_INDEX_RETRIES) - # Install system dependencies first. _install_system_deps() print_info("Installing extensions inside the Isaac Lab repository...") diff --git a/source/isaaclab/isaaclab/cli/commands/misc.py b/source/isaaclab/isaaclab/cli/commands/misc.py index db333ac22c1d..0aa217ed40ce 100644 --- a/source/isaaclab/isaaclab/cli/commands/misc.py +++ b/source/isaaclab/isaaclab/cli/commands/misc.py @@ -29,7 +29,6 @@ def command_run_isaacsim(sim_args: list[str]) -> None: Args: sim_args: Additional arguments passed to the Isaac Sim executable. """ - isaacsim_exe = extract_isaacsim_exe() print_info(f"Running Isaac Sim from: {isaacsim_exe}") diff --git a/source/isaaclab/isaaclab/cli/utils.py b/source/isaaclab/isaaclab/cli/utils.py index 4df4fc29fecd..e96028b51833 100644 --- a/source/isaaclab/isaaclab/cli/utils.py +++ b/source/isaaclab/isaaclab/cli/utils.py @@ -264,7 +264,6 @@ def _escape_for_cmd_exe(cmd: list[str] | tuple[str, ...]) -> list[str]: parts.append("".join(f"^{c}" if c in _CMD_METACHARACTERS else c for c in s)) else: parts.append(s) - return ["cmd.exe", "/c", " ".join(parts)] @@ -409,14 +408,12 @@ def extract_python_exe() -> str: python_exe = Path(venv_prefix) / "bin" / "python3" else: print_debug("extract_python_exe(): No VIRTUAL_ENV found.") - # Try conda python. if not python_exe or not Path(python_exe).exists(): if python_exe: print_debug( f'extract_python_exe(): Venv python "{python_exe}" does not exist, trying to find conda python...' ) - conda_prefix = os.environ.get("CONDA_PREFIX") if conda_prefix: print_debug(f"extract_python_exe(): Found CONDA_PREFIX: {conda_prefix}") @@ -445,7 +442,6 @@ def extract_python_exe() -> str: print_debug(f"extract_python_exe(): Found repo-local venv python: {candidate}") python_exe = candidate break - # Try kit python. if not python_exe or not Path(python_exe).exists(): print_debug("extract_python_exe(): Checking for Kit python...") @@ -503,7 +499,6 @@ def extract_isaacsim_path(*, required: bool = True) -> Path | None: """ # Use the sym-link path to Isaac Sim directory. isaacsim_path = DEFAULT_ISAAC_SIM_PATH - # If above path is not available, try to find the path using python. if not isaacsim_path.exists(): # Use the current interpreter to probe for isaacsim — avoids a recursive extract_python_exe call. @@ -535,17 +530,14 @@ def extract_isaacsim_path(*, required: bool = True) -> Path | None: except Exception: pass - # Check if there is a path available. if not isaacsim_path.exists(): if not required: return None - # Throw an error if no path is found. print_error(f"Unable to find the Isaac Sim directory: '{isaacsim_path}'") print("\tThis could be due to the following reasons:") print("\t1. Conda environment is not activated.") print("\t2. Isaac Sim package is not installed.") print(f"\t3. Isaac Sim directory is not available at the default path: {DEFAULT_ISAAC_SIM_PATH}") - # Exit. sys.exit(1) return isaacsim_path @@ -584,7 +576,6 @@ def extract_isaacsim_exe() -> list[str]: return ["isaacsim", "isaacsim.exp.full"] except Exception: pass - print_error(f"No Isaac Sim executable found at path: {isaacsim_path}") sys.exit(1) @@ -621,13 +612,11 @@ def determine_python_version() -> str: print_warning(f"Unable to determine Isaac Sim version. Defaulting to python={python_version}.") return python_version - # We found some Isaac Sim if isaacsim_version.startswith("5."): python_version = "3.11" elif isaacsim_version.startswith("6."): python_version = "3.12" else: - # We don't recognize the IsaacSim version. print_error(f"Unsupported Isaac Sim version: {isaacsim_version}") raise RuntimeError(f"Unsupported Isaac Sim version: {isaacsim_version}") diff --git a/source/isaaclab/isaaclab/cloner/clone_plan.py b/source/isaaclab/isaaclab/cloner/clone_plan.py index de580bbdfef3..a00eb78c7c82 100644 --- a/source/isaaclab/isaaclab/cloner/clone_plan.py +++ b/source/isaaclab/isaaclab/cloner/clone_plan.py @@ -307,7 +307,6 @@ def make_clone_plan( the flat prototype-to-env mapping, whose ``cfg_rows`` maps each replicated cfg to the rows it owns, and whose ``global_paths`` names shared scene assets. """ - cfgs = tuple(cfgs) global_paths = _minimal_roots(global_paths) sim = sim_utils.SimulationContext.instance() diff --git a/source/isaaclab/isaaclab/controllers/differential_ik.py b/source/isaaclab/isaaclab/controllers/differential_ik.py index 41e7ed94ca0e..82de20031d23 100644 --- a/source/isaaclab/isaaclab/controllers/differential_ik.py +++ b/source/isaaclab/isaaclab/controllers/differential_ik.py @@ -146,7 +146,6 @@ def set_command( # this is only needed for display purposes if ee_quat is None: raise ValueError("End-effector orientation can not be None for `position_*` command type!") - # compute targets if self.cfg.use_relative_mode: if ee_pos is None: raise ValueError("End-effector position can not be None for `position_rel` command type!") diff --git a/source/isaaclab/isaaclab/controllers/operational_space.py b/source/isaaclab/isaaclab/controllers/operational_space.py index e9348637b563..4ba607fd315b 100644 --- a/source/isaaclab/isaaclab/controllers/operational_space.py +++ b/source/isaaclab/isaaclab/controllers/operational_space.py @@ -94,7 +94,7 @@ def __init__(self, cfg: OperationalSpaceControllerCfg, num_envs: int, device: st raise ValueError("Inertia conditioning thresholds must satisfy 0 < lower < upper <= 1.") # resolve tasks-pace target dimensions - self.target_list = list() + self.target_list = [] for command_type in self.cfg.target_types: if command_type == "pose_rel": self.target_list.append(6) diff --git a/source/isaaclab/isaaclab/controllers/utils.py b/source/isaaclab/isaaclab/controllers/utils.py index f6ec8e5e63c3..300431d79785 100644 --- a/source/isaaclab/isaaclab/controllers/utils.py +++ b/source/isaaclab/isaaclab/controllers/utils.py @@ -18,7 +18,6 @@ from ..sim.utils import enable_extension, get_extension_path -# import logger logger = logging.getLogger(__name__) # NOTE: As of Isaac Sim 6.0, ``isaacsim.robot_motion.lula`` (and ``isaacsim.robot_motion.motion_generation``) @@ -123,14 +122,11 @@ def change_revolute_to_fixed_regex(urdf_path: str, fixed_joints: list[str], verb with open(urdf_path) as file: content = file.read() - - # Find all revolute joints in the URDF revolute_joints = re.findall(r'', content) for joint in revolute_joints: # Check if this joint matches any of the fixed joint patterns should_fix = any(re.match(pattern, joint) for pattern in fixed_joints) - if should_fix: old_str = f'' new_str = f'' diff --git a/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py b/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py index 500302a62996..0407d0fc2404 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py +++ b/source/isaaclab/isaaclab/devices/gamepad/se2_gamepad.py @@ -90,7 +90,7 @@ def __init__( # (positive, negative), (x, y, yaw) self._base_command_raw = np.zeros([2, 3]) # dictionary for additional callbacks - self._additional_callbacks = dict() + self._additional_callbacks = {} def __del__(self): """Unsubscribe from gamepad events.""" diff --git a/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py b/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py index fbcafdccc847..b6e7110f65df 100644 --- a/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py +++ b/source/isaaclab/isaaclab/devices/gamepad/se3_gamepad.py @@ -94,7 +94,7 @@ def __init__( # (positive, negative), (x, y, z, roll, pitch, yaw) self._delta_pose_raw = np.zeros([2, 6]) # dictionary for additional callbacks - self._additional_callbacks = dict() + self._additional_callbacks = {} def __del__(self): """Unsubscribe from gamepad events.""" diff --git a/source/isaaclab/isaaclab/devices/haply/se3_haply.py b/source/isaaclab/isaaclab/devices/haply/se3_haply.py index c417480ae9de..f7b9a2f47556 100644 --- a/source/isaaclab/isaaclab/devices/haply/se3_haply.py +++ b/source/isaaclab/isaaclab/devices/haply/se3_haply.py @@ -102,7 +102,7 @@ def __init__(self, cfg: HaplyDeviceCfg, retargeters: list[RetargeterBase] | None self.feedback_force = {"x": 0.0, "y": 0.0, "z": 0.0} self.force_lock = threading.Lock() - self._additional_callbacks = dict() + self._additional_callbacks = {} # Button state tracking self._prev_buttons = {"a": False, "b": False, "c": False} diff --git a/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py b/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py index 25563e925e9a..9edb3d4817cd 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py +++ b/source/isaaclab/isaaclab/devices/keyboard/se2_keyboard.py @@ -75,7 +75,7 @@ def __init__(self, cfg: Se2KeyboardCfg): # command buffers self._base_command = np.zeros(3) # dictionary for additional callbacks - self._additional_callbacks = dict() + self._additional_callbacks = {} def __del__(self): """Release the keyboard interface.""" diff --git a/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py b/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py index 20e6dccf7174..74a03cf4cd4f 100644 --- a/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py +++ b/source/isaaclab/isaaclab/devices/keyboard/se3_keyboard.py @@ -82,7 +82,7 @@ def __init__(self, cfg: Se3KeyboardCfg): self._delta_pos = np.zeros(3) # (x, y, z) self._delta_rot = np.zeros(3) # (roll, pitch, yaw) # dictionary for additional callbacks - self._additional_callbacks = dict() + self._additional_callbacks = {} def __del__(self): """Release the keyboard interface.""" diff --git a/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py b/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py index ff1050c069b4..556ce5a76997 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/se2_spacemouse.py @@ -63,7 +63,7 @@ def __init__(self, cfg: Se2SpaceMouseCfg): # command buffers self._base_command = np.zeros(3) # dictionary for additional callbacks - self._additional_callbacks = dict() + self._additional_callbacks = {} # run a thread for listening to device updates self._thread = threading.Thread(target=self._run_device) self._thread.daemon = True diff --git a/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py b/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py index 49ce997da486..39aa314efe98 100644 --- a/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py +++ b/source/isaaclab/isaaclab/devices/spacemouse/se3_spacemouse.py @@ -77,7 +77,7 @@ def __init__(self, cfg: Se3SpaceMouseCfg): self._delta_pos = np.zeros(3) # (x, y, z) self._delta_rot = np.zeros(3) # (roll, pitch, yaw) # dictionary for additional callbacks - self._additional_callbacks = dict() + self._additional_callbacks = {} # run a thread for listening to device updates self._thread = threading.Thread(target=self._run_device) self._thread.daemon = True diff --git a/source/isaaclab/isaaclab/envs/direct_marl_env.py b/source/isaaclab/isaaclab/envs/direct_marl_env.py index 76de2df11a8c..24447978c3ae 100644 --- a/source/isaaclab/isaaclab/envs/direct_marl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_marl_env.py @@ -29,7 +29,6 @@ from .utils.spaces import sample_space, spec_to_gym_space from .utils.video_recorder import VideoRecorder -# import logger logger = logging.getLogger(__name__) @@ -187,14 +186,12 @@ def _init_sim(self, render_mode: str | None = None, **kwargs): if self.sim.has_gui and self.cfg.ui_window_class_type is not None: self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") else: - # if no window, then we don't need to store the window self._window = None # allocate dictionary to store metrics self.extras = {agent: {} for agent in self.cfg.possible_agents} # initialize data and constants - # -- counter for simulation steps self._sim_step_counter = 0 # -- controls camera/Kit rendering in step(). # When False, the Kit app loop (app.update()) and camera/RTX sensor updates are @@ -211,7 +208,6 @@ def _init_sim(self, render_mode: str | None = None, **kwargs): # setup the observation, state and action spaces self._configure_env_spaces() - # setup noise cfg for adding action and observation noise if self.cfg.action_noise_model: self._action_noise_model: dict[AgentID, NoiseModel] = { @@ -234,7 +230,7 @@ def _init_sim(self, render_mode: str | None = None, **kwargs): if "startup" in self.event_manager.available_modes: self.event_manager.apply(mode="startup") self.has_rtx_sensors = self.sim.get_setting("/isaaclab/render/rtx_sensors") - # print the environment information + print("[INFO]: Completed setting up the environment...") def __del__(self, _sys=sys): @@ -493,7 +489,6 @@ def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn: if agent in self._observation_noise_model: self.obs_dict[agent] = self._observation_noise_model[agent](obs) - # return observations, rewards, resets and extras return self.obs_dict, self.reward_dict, self.terminated_dict, self.time_out_dict, self.extras def state(self) -> StateType | None: @@ -715,7 +710,6 @@ def _reset_idx(self, env_ids: Sequence[int]): env_step_count = self._sim_step_counter // self.cfg.decimation self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) - # reset noise models if self.cfg.action_noise_model: for noise_model in self._action_noise_model.values(): noise_model.reset(env_ids) @@ -723,7 +717,6 @@ def _reset_idx(self, env_ids: Sequence[int]): for noise_model in self._observation_noise_model.values(): noise_model.reset(env_ids) - # reset the episode length buffer self.episode_length_buf[env_ids] = 0 self.sim.render_context.reset_scene_state_cadence() diff --git a/source/isaaclab/isaaclab/envs/direct_rl_env.py b/source/isaaclab/isaaclab/envs/direct_rl_env.py index 9422d89d97ec..3dd3ece63f4d 100644 --- a/source/isaaclab/isaaclab/envs/direct_rl_env.py +++ b/source/isaaclab/isaaclab/envs/direct_rl_env.py @@ -30,7 +30,6 @@ from .utils.spaces import sample_space, spec_to_gym_space from .utils.video_recorder import VideoRecorder -# import logger logger = logging.getLogger(__name__) @@ -197,14 +196,12 @@ def _init_sim(self, render_mode: str | None = None, **kwargs): if self.sim.has_gui and self.cfg.ui_window_class_type is not None: self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") else: - # if no window, then we don't need to store the window self._window = None # allocate dictionary to store metrics self.extras = {} # initialize data and constants - # -- counter for simulation steps self._sim_step_counter = 0 # -- controls camera/Kit rendering in step(). # When False, the Kit app loop (app.update()) and camera/RTX sensor updates are @@ -220,7 +217,6 @@ def _init_sim(self, render_mode: str | None = None, **kwargs): self.reset_terminated = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) self.reset_time_outs = torch.zeros_like(self.reset_terminated) self.reset_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.sim.device) - # setup the action and observation spaces for Gym self._configure_gym_env_spaces() @@ -260,7 +256,6 @@ def _init_sim(self, render_mode: str | None = None, **kwargs): if self.cfg.num_rerenders_on_reset == 0: self.cfg.num_rerenders_on_reset = 1 - # print the environment information print("[INFO]: Completed setting up the environment...") def __del__(self, _sys=sys): @@ -482,7 +477,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: for recorder in self.video_recorders: recorder.step() - # update observations self.obs_buf = self._get_observations() # add observation noise @@ -490,7 +484,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: if self.cfg.observation_noise_model: self.obs_buf["policy"] = self._observation_noise_model(self.obs_buf["policy"]) - # return observations, rewards, resets and extras return self.obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras @staticmethod @@ -732,13 +725,11 @@ def _reset_idx(self, env_ids: Sequence[int]): env_step_count = self._sim_step_counter // self.cfg.decimation self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) - # reset noise models if self.cfg.action_noise_model: self._action_noise_model.reset(env_ids) if self.cfg.observation_noise_model: self._observation_noise_model.reset(env_ids) - # reset the episode length buffer self.episode_length_buf[env_ids] = 0 self.sim.render_context.reset_scene_state_cadence() diff --git a/source/isaaclab/isaaclab/envs/leapp_deployment_env.py b/source/isaaclab/isaaclab/envs/leapp_deployment_env.py index 0decf9c7b270..e75d42d9ac46 100644 --- a/source/isaaclab/isaaclab/envs/leapp_deployment_env.py +++ b/source/isaaclab/isaaclab/envs/leapp_deployment_env.py @@ -163,7 +163,6 @@ def __init__(self, cfg: Any, leapp_yaml_path: str): cfg: A ``ManagerBasedRLEnvCfg`` (or compatible) task config. leapp_yaml_path: Path to the LEAPP ``.yaml`` pipeline description. """ - cfg.scene.num_envs = 1 cfg.validate() self.cfg = cfg diff --git a/source/isaaclab/isaaclab/envs/manager_based_env.py b/source/isaaclab/isaaclab/envs/manager_based_env.py index e35059279f0c..bb9d1d282eb1 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_env.py @@ -30,7 +30,6 @@ ) from .utils.video_recorder import VideoRecorder -# import logger logger = logging.getLogger(__name__) @@ -147,7 +146,6 @@ def _init_sim(self): print(f"\tPhysics step-size : {self.physics_dt}") print(f"\tRendering step-size : {self.physics_dt * self.cfg.sim.render_interval}") print(f"\tEnvironment step-size : {self.step_dt}") - if self.cfg.sim.render_interval < self.cfg.decimation: msg = ( f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " @@ -222,13 +220,10 @@ def _init_sim(self): if self.sim.has_gui and self.cfg.ui_window_class_type is not None: self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") else: - # if no window, then we don't need to store the window self._window = None self.has_rtx_sensors = self.sim.get_setting("/isaaclab/render/rtx_sensors") - # initialize observation buffers self.obs_buf = {} - # export IO descriptors if requested if self.cfg.export_io_descriptors: self.export_IO_descriptors() @@ -497,7 +492,6 @@ def reset_to( self.seed(seed) self._reset_idx(env_ids) - # set the state self.scene.reset_to(state, env_ids, is_relative=is_relative) @@ -544,7 +538,6 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: Returns: A tuple containing the observations and extras. """ - # process actions self.action_manager.process_action(action.to(self.device)) self.recorder_manager.record_pre_step() @@ -589,11 +582,9 @@ def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: for recorder in self.video_recorders: recorder.step() - # -- compute observations self.obs_buf = self.observation_manager.compute(update_history=True) self.recorder_manager.record_post_step() - # return observations and extras return self.obs_buf, self.extras @staticmethod diff --git a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py index 1f04e094da7d..794254a43e15 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_env.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause -# needed to import for allowing type-hinting: np.ndarray | None from __future__ import annotations import math @@ -203,7 +202,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: Returns: A tuple containing the observations, rewards, resets (terminated and truncated) and extras. """ - # process actions self.action_manager.process_action(action.to(self.device)) self.recorder_manager.record_pre_step() @@ -211,7 +209,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: # check if we need to do rendering within the physics loop # note: uses cached property to avoid settings lookup every step is_rendering = self.sim.is_rendering - # perform physics stepping if self._physics_handles_decimation: self._sim_step_counter += self.cfg.decimation @@ -250,7 +247,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: self.reset_buf = self.termination_manager.compute() self.reset_terminated = self.termination_manager.terminated self.reset_time_outs = self.termination_manager.time_outs - # -- reward computation self.reward_buf = self.reward_manager.compute(dt=self.step_dt) if len(self.recorder_manager.active_terms) > 0: @@ -264,7 +260,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: # capture the terminal observation before reset and expose it for Same-Step autoreset. if self.cfg.compute_final_obs: self.extras["final_obs"] = self.observation_manager.compute() - # trigger recorder terms for pre-reset calls self.recorder_manager.record_pre_reset(reset_env_ids) self._reset_idx(reset_env_ids) @@ -273,8 +268,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: if self.render_enabled and is_rendering and self.has_rtx_sensors and self.cfg.num_rerenders_on_reset > 0: for _ in range(self.cfg.num_rerenders_on_reset): self.sim.render() - - # trigger recorder terms for post-reset calls self.recorder_manager.record_post_reset(reset_env_ids) # -- handle episode reset requested from visualizer UI controls @@ -291,9 +284,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: self._reset_idx(manual_reset_ids) self.recorder_manager.record_post_reset(manual_reset_ids) - # -- update command self.command_manager.compute(dt=self.step_dt) - # -- step interval events if "interval" in self.event_manager.available_modes: self.event_manager.apply(mode="interval", dt=self.step_dt) # -- advance video recorders (after render and resets, before final obs) @@ -303,7 +294,6 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: # note: done after reset to get the correct observations for reset envs self.obs_buf = self.observation_manager.compute(update_history=True) - # return observations, rewards, resets and extras return self.obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras def render(self, recompute: bool = False) -> np.ndarray | None: @@ -446,7 +436,6 @@ def _reset_idx(self, env_ids: Sequence[int]): info = self.recorder_manager.reset(env_ids) self.extras["log"].update(info) - # reset the episode length buffer self.episode_length_buf[env_ids] = 0 self.sim.render_context.reset_scene_state_cadence() diff --git a/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py b/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py index e74f634d6ea1..f5c8fc0dc85d 100644 --- a/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py +++ b/source/isaaclab/isaaclab/envs/manager_based_rl_mimic_env.py @@ -117,7 +117,7 @@ def get_object_poses(self, env_ids: Sequence[int] | None = None): env_ids = slice(None) rigid_object_states = self.scene.get_state(is_relative=True)["rigid_object"] - object_pose_matrix = dict() + object_pose_matrix = {} for obj_name, obj_state in rigid_object_states.items(): object_pose_matrix[obj_name] = PoseUtils.make_pose( obj_state["root_pose"][env_ids, :3], PoseUtils.matrix_from_quat(obj_state["root_pose"][env_ids, 3:7]) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py index 0d05bb3c05ea..873dba7d9c6b 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/binary_joint_actions.py @@ -20,7 +20,6 @@ from ...utils.io_descriptors import GenericActionIODescriptor from . import actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py index ca5cee718de2..98f5a947d66e 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions.py @@ -20,7 +20,6 @@ from ...utils.io_descriptors import GenericActionIODescriptor from . import actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py index 6dd26f25dfe9..9a9d296fc993 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/joint_actions_to_limits.py @@ -21,7 +21,6 @@ from ...utils.io_descriptors import GenericActionIODescriptor from . import actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py index de1d88f25ecc..aed8f330afe8 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/non_holonomic_actions.py @@ -21,7 +21,6 @@ from ...utils.io_descriptors import GenericActionIODescriptor from . import actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py index 91cd18ec7ba8..50793abb20a2 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/rmpflow_task_space_actions.py @@ -21,7 +21,6 @@ from ... import ManagerBasedEnv from . import rmpflow_actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py index 00712d505b8b..30f3c64553a2 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/surface_gripper_actions.py @@ -19,7 +19,6 @@ from ... import ManagerBasedEnv from . import actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py index a9c47e91fe9a..e9fd9da411a9 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/task_space_actions.py @@ -27,7 +27,6 @@ from ...utils.io_descriptors import GenericActionIODescriptor from . import actions_cfg -# import logger logger = logging.getLogger(__name__) @@ -619,7 +618,6 @@ def _resolve_nullspace_joint_pos_targets(self): ValueError: If the nullspace joint pos targets are not set when null space control is set to 'position'. ValueError: If an invalid value is set for nullspace joint pos targets. """ - if self.cfg.nullspace_joint_pos_target != "none" and self.cfg.controller_cfg.nullspace_control != "position": raise ValueError("Nullspace joint targets can only be set when null space control is set to 'position'.") diff --git a/source/isaaclab/isaaclab/envs/mdp/actions/tendon_actions.py b/source/isaaclab/isaaclab/envs/mdp/actions/tendon_actions.py index aa9251747549..f89eaf171d40 100644 --- a/source/isaaclab/isaaclab/envs/mdp/actions/tendon_actions.py +++ b/source/isaaclab/isaaclab/envs/mdp/actions/tendon_actions.py @@ -22,7 +22,6 @@ from ...utils.io_descriptors import GenericActionIODescriptor from . import actions_cfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py b/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py index 905c1f5a01d8..6f857bb81cb7 100644 --- a/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py +++ b/source/isaaclab/isaaclab/envs/mdp/commands/velocity_command.py @@ -22,7 +22,6 @@ from ... import ManagerBasedEnv from .commands_cfg import NormalVelocityCommandCfg, UniformVelocityCommandCfg -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/envs/mdp/curriculums.py b/source/isaaclab/isaaclab/envs/mdp/curriculums.py index 0f66e1b76075..422943363d31 100644 --- a/source/isaaclab/isaaclab/envs/mdp/curriculums.py +++ b/source/isaaclab/isaaclab/envs/mdp/curriculums.py @@ -41,11 +41,9 @@ def __call__( weight: float, num_steps: int, ) -> float: - # update term settings if env.common_step_counter > num_steps: self._term_cfg.weight = weight env.reward_manager.set_term_cfg(term_name, self._term_cfg) - return self._term_cfg.weight @@ -130,11 +128,8 @@ def resample_bucket_range( def __init__(self, cfg: CurriculumTermCfg, env: ManagerBasedRLEnv): super().__init__(cfg, env) - # resolve term configuration if "address" not in cfg.params: raise ValueError("The 'address' parameter must be specified in the curriculum term configuration.") - - # store current address self._address: str = cfg.params["address"] # store accessor functions self._get_fn: callable = None @@ -227,11 +222,9 @@ def _process_accessors(self, root: ManagerBasedRLEnv, path: str) -> tuple[callab else: container = getattr(container, container_path) - # save the container and the last part of the path self._container = container self._last_path = path_parts[-1] # for "a.b[2].c", this is "c", while for "a.b[2]" it is 2 - # build the getter and setter if isinstance(self._container, tuple): get_value = lambda: self._container[self._last_path] # noqa: E731 diff --git a/source/isaaclab/isaaclab/envs/mdp/events.py b/source/isaaclab/isaaclab/envs/mdp/events.py index 8e4ca55c8832..904927378ba1 100644 --- a/source/isaaclab/isaaclab/envs/mdp/events.py +++ b/source/isaaclab/isaaclab/envs/mdp/events.py @@ -77,7 +77,6 @@ def randomize_rigid_body_scale( " Please ensure that the event term is called before the simulation starts by using the 'usd' mode." ) - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] if any(cls.__name__ == "Articulation" for cls in type(asset).__mro__): @@ -96,10 +95,8 @@ def randomize_rigid_body_scale( # acquire stage stage = env.sim.stage - # resolve prim paths for spawning and cloning prim_paths = sim_utils.find_matching_prim_paths(asset.cfg.prim_path) - # sample scale values if isinstance(scale_range, dict): range_list = [scale_range.get(key, (1.0, 1.0)) for key in ["x", "y", "z"]] ranges = torch.tensor(range_list, device="cpu") @@ -124,12 +121,9 @@ def randomize_rigid_body_scale( for i, env_id in enumerate(env_ids): # path to prim to randomize prim_path = prim_paths[env_id] + relative_child_path - # spawn single instance prim_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) - # get the attribute to randomize scale_spec = prim_spec.GetAttributeAtPath(prim_path + ".xformOp:scale") - # if the scale attribute does not exist, create it has_scale_attr = scale_spec is not None if not has_scale_attr: scale_spec = Sdf.AttributeSpec(prim_spec, prim_path + ".xformOp:scale", Sdf.ValueTypeNames.Double3) @@ -193,7 +187,6 @@ def __init__( self.num_shapes_per_body.append(link_physx_view.max_shapes) # ``body_ids`` are public IDs; convert once before deriving backend-ordered shape ranges. self._backend_body_ids = asset.map_body_ids_to_backend(asset_cfg.body_ids) - # ensure the parsing is correct num_shapes = sum(self.num_shapes_per_body) expected_shapes = asset.root_view.max_shapes if num_shapes != expected_shapes: @@ -228,14 +221,9 @@ def __call__( bucket_ids = torch.randint(0, num_buckets, (len(env_ids), total_num_shapes), device="cpu") material_samples = self.material_buckets[bucket_ids] - # retrieve material buffer from the physics simulation materials = wp.to_torch(self.asset.root_view.get_material_properties()) - - # update material buffer with new samples if self.num_shapes_per_body is not None: - # sample material properties from the given ranges for body_id in self._backend_body_ids: - # obtain indices of shapes for the body start_idx = sum(self.num_shapes_per_body[:body_id]) end_idx = start_idx + self.num_shapes_per_body[body_id] # assign the new materials @@ -245,7 +233,6 @@ def __call__( # assign all the materials materials[env_ids] = material_samples[:] - # apply to simulation self.asset.root_view.set_material_properties( wp.from_torch(materials, dtype=wp.float32), wp.from_torch(env_ids, dtype=wp.int32) ) @@ -612,7 +599,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) - # extract the used quantities (to enable type-hinting) self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] @@ -694,7 +680,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): """ super().__init__(cfg, env) - # extract the used quantities (to enable type-hinting) self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] # check for valid operation @@ -819,7 +804,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): super().__init__(cfg, env) - # extract the used quantities (to enable type-hinting) self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] @@ -915,7 +899,6 @@ def __call__( elif operation == "abs": inertias[:, :, self._inertia_idx] = random_values[..., None] - # set the inertia tensors into the physics simulation self.asset.set_inertias_index(inertias=inertias, body_ids=body_ids, env_ids=env_ids) @@ -1174,7 +1157,6 @@ def __call__( ) self.default_margin[env_ids] = margin[env_ids] margin_view[env_ids] = margin[env_ids] - if contact_offset_distribution_params is not None: current_margin = self.default_margin contact_offset = torch.zeros_like(self.default_gap) @@ -1190,7 +1172,6 @@ def __call__( self.default_gap[env_ids] = gap[env_ids] gap_view = wp.to_torch(self._sim_bind_shape_gap) gap_view[env_ids] = gap[env_ids] - if rest_offset_distribution_params is not None or contact_offset_distribution_params is not None: self._newton_manager.add_model_change(self._notify_shape_properties) @@ -1415,7 +1396,6 @@ def _call_newton( gravity[env_ids] += random_values elif operation == "scale": gravity[env_ids] *= random_values - self._newton_manager.add_model_change(self._notify_model_properties) def _init_physx(self, env: ManagerBasedEnv): @@ -1486,7 +1466,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): """ super().__init__(cfg, env) - # extract the used quantities (to enable type-hinting) self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] @@ -1566,7 +1545,6 @@ def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: data, params, dim_0_ids=None, dim_1_ids=actuator_indices, operation=operation, distribution=distribution ) - # Loop through actuators and randomize gains for actuator_name, actuator in self._gain_actuators.items(): group_joint_indices = self._group_joint_indices[actuator_name] if isinstance(self.asset_cfg.joint_ids, slice): @@ -1684,7 +1662,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): """ super().__init__(cfg, env) - # extract the used quantities (to enable type-hinting) self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] self.asset: Articulation = env.scene[self.asset_cfg.name] @@ -1808,7 +1785,6 @@ def __call__( env_ids=env_ids, ) - # joint armature if armature_distribution_params is not None: armature = _randomize_prop_by_op( self.asset.data.default_joint_armature.torch.clone(), @@ -1822,7 +1798,6 @@ def __call__( armature[env_ids_for_slice, joint_ids], joint_ids=joint_ids, env_ids=env_ids ) - # joint position limits if lower_limit_distribution_params is not None or upper_limit_distribution_params is not None: joint_pos_limits = self.default_joint_pos_limits.clone() # -- randomize the lower limits @@ -1853,7 +1828,6 @@ def __call__( "Randomization term 'randomize_joint_parameters' is setting lower joint limits that are greater" " than upper joint limits. Please check the distribution parameters for the joint position limits." ) - # set the position limits into the physics simulation self.asset.write_joint_position_limit_to_sim_index( limits=joint_pos_limits, joint_ids=joint_ids, env_ids=env_ids, warn_limit_violation=False ) @@ -1885,7 +1859,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): """ super().__init__(cfg, env) - # extract the used quantities (to enable type-hinting) self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] # check for valid operation @@ -1948,7 +1921,6 @@ def __call__( stiffness=stiffness[env_ids[:, None], tendon_ids], fixed_tendon_ids=tendon_ids, env_ids=env_ids ) - # damping if damping_distribution_params is not None: damping = _randomize_prop_by_op( self.asset.data.fixed_tendon_damping.torch.clone(), @@ -1979,7 +1951,6 @@ def __call__( else: raise NotImplementedError("Limit stiffness is not support in Newton.") - # position limits if lower_limit_distribution_params is not None or upper_limit_distribution_params is not None: if _backend == "physx": limit = self.asset.data.fixed_tendon_pos_limits.torch.clone() @@ -2017,7 +1988,6 @@ def __call__( else: raise NotImplementedError("Position limits is not yet implemented with Newton.") - # rest length if rest_length_distribution_params is not None: if _backend == "physx": rest_length = _randomize_prop_by_op( @@ -2050,7 +2020,6 @@ def __call__( else: raise NotImplementedError("Offset is not supported in Newton.") - # write the fixed tendon properties into the simulation self.asset.write_fixed_tendon_properties_to_sim_index(env_ids=env_ids) @@ -2068,7 +2037,6 @@ def apply_external_force_torque( applied to the bodies by calling ``asset.set_external_force_and_torque``. The forces and torques are only applied when ``asset.write_data_to_sim()`` is called in the environment. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject | Articulation = env.scene[asset_cfg.name] # resolve environment ids if env_ids is None: @@ -2111,7 +2079,6 @@ def push_by_setting_velocity( are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. The values are tuples of the form ``(min, max)``. If the dictionary does not contain a key, the velocity is set to zero for that axis. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject | Articulation = env.scene[asset_cfg.name] # velocities @@ -2159,14 +2126,11 @@ def __call__( velocity_range: dict[str, tuple[float, float]], asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), ): - # extract the used quantities (to enable type-hinting) asset: RigidObject | Articulation = env.scene[asset_cfg.name] - # get default root state # tensor indexing already returns a copy, and the values are only read below default_root_pose = asset.data.default_root_pose.torch[env_ids] default_root_vel = asset.data.default_root_vel.torch[env_ids] - # poses ranges = self._pose_ranges rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=asset.device) @@ -2178,8 +2142,6 @@ def __call__( rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=asset.device) velocities = default_root_vel + rand_samples - - # set into the physics simulation asset.write_root_pose_to_sim_index(root_pose=torch.cat([positions, orientations], dim=-1), env_ids=env_ids) asset.write_root_velocity_to_sim_index(root_velocity=velocities, env_ids=env_ids) @@ -2211,7 +2173,6 @@ def reset_root_state_with_random_orientation( The values are tuples of the form ``(min, max)``. If the dictionary does not contain a particular key, the position is set to zero for that axis. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject | Articulation = env.scene[asset_cfg.name] # get default root state default_root_pose = asset.data.default_root_pose.torch[env_ids].clone() @@ -2267,11 +2228,9 @@ def reset_root_state_from_terrain( Raises: ValueError: If the terrain does not have valid flat patches under the key "init_pos". """ - # access the used quantities (to enable type-hinting) asset: RigidObject | Articulation = env.scene[asset_cfg.name] terrain: TerrainImporter = env.scene.terrain - # obtain all flat patches corresponding to the valid poses valid_positions: torch.Tensor = terrain.flat_patches.get("init_pos") if valid_positions is None: raise ValueError( @@ -2283,7 +2242,6 @@ def reset_root_state_from_terrain( ids = torch.randint(0, valid_positions.shape[2], size=(len(env_ids),), device=env.device) positions = valid_positions[terrain.terrain_levels[env_ids], terrain.terrain_types[env_ids], ids] positions += asset.data.default_root_pose.torch[env_ids, :3] - # sample random orientations range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["roll", "pitch", "yaw"]] ranges = torch.tensor(range_list, device=asset.device) @@ -2316,7 +2274,6 @@ def reset_joints_by_scale( This function samples random values from the given ranges and scales the default joint positions and velocities by these values. The scaled values are then set into the physics simulation. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] # cast env_ids to allow broadcasting @@ -2357,7 +2314,6 @@ def reset_joints_by_offset( This function samples random values from the given ranges and biases the default joint positions and velocities by these values. The biased values are then set into the physics simulation. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] # cast env_ids to allow broadcasting @@ -2421,7 +2377,6 @@ class reset_joints_within_limits_range(ManagerTermBase): """ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): - # initialize the base class super().__init__(cfg, env) # check if the cfg has the required parameters @@ -2442,7 +2397,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): " Please use 'abs' or 'scale'." ) - # extract the used quantities (to enable type-hinting) self._asset: Articulation = env.scene[asset_cfg.name] default_joint_pos = self._asset.data.default_joint_pos.torch[0] default_joint_vel = self._asset.data.default_joint_vel.torch[0] @@ -2518,27 +2472,22 @@ def __call__( joint_pos = self._asset.data.default_joint_pos.torch[env_ids].clone() joint_vel = self._asset.data.default_joint_vel.torch[env_ids].clone() - # sample random joint positions for each joint if len(self._pos_joint_ids) > 0: joint_pos_shape = (len(env_ids), len(self._pos_joint_ids)) joint_pos[:, self._pos_joint_ids] = math_utils.sample_uniform( self._pos_ranges[:, 0], self._pos_ranges[:, 1], joint_pos_shape, device=joint_pos.device ) - # clip the joint positions to the joint limits joint_pos_limits = self._asset.data.soft_joint_pos_limits.torch[0, self._pos_joint_ids] joint_pos = joint_pos.clamp(joint_pos_limits[:, 0], joint_pos_limits[:, 1]) - # sample random joint velocities for each joint if len(self._vel_joint_ids) > 0: joint_vel_shape = (len(env_ids), len(self._vel_joint_ids)) joint_vel[:, self._vel_joint_ids] = math_utils.sample_uniform( self._vel_ranges[:, 0], self._vel_ranges[:, 1], joint_vel_shape, device=joint_vel.device ) - # clip the joint velocities to the joint limits joint_vel_limits = self._asset.data.soft_joint_vel_limits.torch[0, self._vel_joint_ids] joint_vel = joint_vel.clamp(-joint_vel_limits, joint_vel_limits) - # set into the physics simulation self._asset.write_joint_position_to_sim_index(position=joint_pos, env_ids=env_ids) self._asset.write_joint_velocity_to_sim_index(velocity=joint_vel, env_ids=env_ids) @@ -2562,7 +2511,6 @@ def reset_nodal_state_uniform( dictionary are ``x``, ``y``, ``z``. The values are tuples of the form ``(min, max)``. If the dictionary does not contain a key, the position or velocity is set to zero for that axis. """ - # extract the used quantities (to enable type-hinting) asset: DeformableObject = env.scene[asset_cfg.name] # get default root state nodal_state = asset.data.default_nodal_state_w.torch[env_ids].clone() @@ -2679,8 +2627,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): # read parameters from the configuration asset_cfg: SceneEntityCfg = cfg.params.get("asset_cfg") - - # obtain the asset entity asset = env.scene[asset_cfg.name] # join all bodies in the asset @@ -2732,7 +2678,6 @@ def rep_texture_randomization(): ) return prims_group.node - # Register the event to the replicator with rep.trigger.on_custom_event(event_name=event_name): rep_texture_randomization() else: @@ -2893,8 +2838,6 @@ def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): if compare_versions(version, "1.12.4") < 0: colors = cfg.params.get("colors") event_name = cfg.params.get("event_name") - - # parse the colors into replicator format if isinstance(colors, dict): # (r, g, b) - low, high --> (low_r, low_g, low_b) and (high_r, high_g, high_b) color_low = [colors[key][0] for key in ["r", "g", "b"]] @@ -2911,7 +2854,6 @@ def rep_color_randomization(): return prims_group.node - # Register the event to the replicator with rep.trigger.on_custom_event(event_name=event_name): rep_color_randomization() else: @@ -3008,8 +2950,6 @@ def _randomize_prop_by_op( Raises: NotImplementedError: If the operation or distribution is not supported. """ - # resolve shape - # -- dim 0 if dim_0_ids is None: n_dim_0 = data.shape[0] dim_0_ids = slice(None) diff --git a/source/isaaclab/isaaclab/envs/mdp/observations.py b/source/isaaclab/isaaclab/envs/mdp/observations.py index 24c119894265..383b391555f3 100644 --- a/source/isaaclab/isaaclab/envs/mdp/observations.py +++ b/source/isaaclab/isaaclab/envs/mdp/observations.py @@ -45,7 +45,6 @@ @generic_io_descriptor(units="m", axes=["Z"], observation_type="RootState", on_inspect=[record_shape, record_dtype]) def base_pos_z(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Root height in the simulation world frame.""" - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return asset.data.root_pos_w.torch[:, 2].unsqueeze(-1) @@ -55,7 +54,6 @@ def base_pos_z(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg( ) def base_lin_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Root linear velocity in the asset's root frame.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.root_lin_vel_b.torch @@ -65,7 +63,6 @@ def base_lin_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCf ) def base_ang_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Root angular velocity in the asset's root frame.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.root_ang_vel_b.torch @@ -75,7 +72,6 @@ def base_ang_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCf ) def projected_gravity(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Gravity projection on the asset's root frame.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.projected_gravity_b.torch @@ -85,7 +81,6 @@ def projected_gravity(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEnt ) def root_pos_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Asset root position in the environment frame.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.root_pos_w.torch - env.scene.env_origins @@ -102,9 +97,7 @@ def root_quat_w( the quaternion has non-negative real component. This is because both ``q`` and ``-q`` represent the same orientation. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - quat = asset.data.root_quat_w.torch # make the quaternion real-part positive if configured return math_utils.quat_unique(quat) if make_quat_unique else quat @@ -115,7 +108,6 @@ def root_quat_w( ) def root_lin_vel_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Asset root linear velocity in the environment frame.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.root_lin_vel_w.torch @@ -125,7 +117,6 @@ def root_lin_vel_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntity ) def root_ang_vel_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Asset root angular velocity in the environment frame.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.root_ang_vel_w.torch @@ -152,10 +143,7 @@ def body_pose_w( The poses of bodies in articulation [num_env, 7 * num_bodies]. Pose order is [x,y,z,qw,qx,qy,qz]. Output is stacked horizontally per body. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - - # access the body poses in world frame pose = asset.data.body_pose_w.torch[:, asset_cfg.body_ids, :7] if isinstance(asset_cfg.body_ids, (slice, int)): pose = pose.clone() # if slice or int, make a copy to avoid modifying original data @@ -180,9 +168,7 @@ def body_projected_gravity_b( The unit vector direction of gravity projected onto body_name's frame. Gravity projection vector order is [x,y,z]. Output is stacked horizontally per body. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - body_quat = asset.data.body_quat_w.torch[:, asset_cfg.body_ids] # ``GRAVITY_VEC_W`` carries the per-env world-frame gravity in m/s^2 (Newton # backend) or scene-wide gravity (PhysX backend). @@ -204,7 +190,6 @@ def joint_pos(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg(" Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their positions returned. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return asset.data.joint_pos.torch[:, asset_cfg.joint_ids] @@ -219,7 +204,6 @@ def joint_pos_rel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityC Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their positions returned. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return ( asset.data.joint_pos.torch[:, asset_cfg.joint_ids] - asset.data.default_joint_pos.torch[:, asset_cfg.joint_ids] @@ -234,7 +218,6 @@ def joint_pos_limit_normalized( Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their normalized positions returned. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return math_utils.scale_transform( asset.data.joint_pos.torch[:, asset_cfg.joint_ids], @@ -251,7 +234,6 @@ def joint_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg(" Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their velocities returned. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return asset.data.joint_vel.torch[:, asset_cfg.joint_ids] @@ -266,7 +248,6 @@ def joint_vel_rel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityC Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their velocities returned. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return ( asset.data.joint_vel.torch[:, asset_cfg.joint_ids] - asset.data.default_joint_vel.torch[:, asset_cfg.joint_ids] @@ -288,7 +269,6 @@ def joint_effort(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCf Returns: The joint effort (N or N-m) for joint_names in asset_cfg, shape is [num_env,num_joints]. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return asset.actuators.applied_effort.torch[:, asset_cfg.joint_ids] @@ -303,7 +283,6 @@ def height_scan(env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg, offset: float The provided offset (Defaults to 0.5) is subtracted from the returned values. """ - # extract the used quantities (to enable type-hinting) sensor: RayCaster = env.scene.sensors[sensor_cfg.name] # height scan: height = sensor_height - hit_point_z - offset return sensor.data.pos_w.torch[:, 2].unsqueeze(1) - sensor.data.ray_hits_w.torch[..., 2] - offset @@ -314,7 +293,6 @@ def body_incoming_wrench(env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg) -> to This is the 6-D wrench (force followed by torque) applied to the body link by the incoming joint force. """ - # extract the used quantities (to enable type-hinting) sensor: JointWrenchSensor = env.scene.sensors[sensor_cfg.name] sensor_data = sensor.data force_data = sensor_data.force @@ -416,19 +394,13 @@ def image( Returns: The images produced at the last time-step """ - # extract the used quantities (to enable type-hinting) sensor: Camera | RayCasterCamera = env.scene.sensors[sensor_cfg.name] - - # obtain the input image images = sensor.data.output[data_type] - # depth image conversion if (data_type == "distance_to_camera") and convert_perspective_to_orthogonal: images = math_utils.orthogonalize_perspective_depth(images, sensor.data.intrinsic_matrices) - if normalize: images = normalize_camera_image(images, data_type) - if permute: images = images.permute(0, 3, 1, 2) @@ -478,10 +450,8 @@ class image_features(ManagerTermBase): """ def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedEnv): - # initialize the base class super().__init__(cfg, env) - # extract parameters from the configuration self.model_zoo_cfg: dict = cfg.params.get("model_zoo_cfg") # type: ignore self.model_name: str = cfg.params.get("model_name", "resnet18") # type: ignore self.model_device: str = cfg.params.get("model_device", env.device) # type: ignore @@ -521,7 +491,6 @@ def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedEnv): else: model_config = self.model_zoo_cfg[self.model_name] - # Retrieve the model, preprocess and inference functions self._model = model_config["model"]() self._reset_fn = model_config.get("reset") self._inference_fn = model_config["inference"] @@ -556,7 +525,6 @@ def __call__( image_device = image_data.device # forward the images through the model features = self._inference_fn(self._model, image_data, **(inference_kwargs or {})) - # observation terms must be flat after the environment batch dimension return features.flatten(start_dim=1).detach().to(image_device) @@ -632,7 +600,6 @@ def _inference(model, images: torch.Tensor) -> torch.Tensor: features = model.backbone.model(pixel_values=image_proc, interpolate_pos_encoding=True) return features.last_hidden_state[:, 1:] - # return the model, preprocess and inference functions return {"model": _load_model, "inference": _inference} def _prepare_resnet_model(self, model_name: str, model_device: str) -> dict: diff --git a/source/isaaclab/isaaclab/envs/mdp/rewards.py b/source/isaaclab/isaaclab/envs/mdp/rewards.py index 0e75986ffd44..3327adfb31eb 100644 --- a/source/isaaclab/isaaclab/envs/mdp/rewards.py +++ b/source/isaaclab/isaaclab/envs/mdp/rewards.py @@ -54,9 +54,7 @@ class is_terminated_term(ManagerTermBase): """ def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): - # initialize the base class super().__init__(cfg, env) - # find and store the termination terms term_keys = cfg.params.get("term_keys", ".*") self._term_names = env.termination_manager.find_terms(term_keys) @@ -66,7 +64,6 @@ def __call__(self, env: ManagerBasedRLEnv, term_keys: str | list[str] = ".*") -> for term in self._term_names: # Sums over terminations term values to account for multiple terminations in the same step reset_buf += env.termination_manager.get_term(term) - return (reset_buf * (~env.termination_manager.time_outs)).float() @@ -104,14 +101,12 @@ def __call__(self, env: ManagerBasedRLEnv) -> torch.Tensor: def lin_vel_z_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize z-axis base linear velocity using L2 squared kernel.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return torch.square(asset.data.root_lin_vel_b.torch[:, 2]) def ang_vel_xy_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize xy-axis base angular velocity using L2 squared kernel.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return torch.sum(torch.square(asset.data.root_ang_vel_b.torch[:, :2]), dim=1) @@ -121,7 +116,6 @@ def flat_orientation_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = Scen This is computed by penalizing the xy-components of the projected gravity vector. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return torch.sum(torch.square(asset.data.projected_gravity_b.torch[:, :2]), dim=1) @@ -138,7 +132,6 @@ def base_height_l2( For flat terrain, target height is in the world frame. For rough terrain, sensor readings can adjust the target height to account for the terrain. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] if sensor_cfg is not None: sensor: RayCaster = env.scene[sensor_cfg.name] @@ -169,14 +162,12 @@ def joint_torques_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEn Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint torques contribute to the term. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return torch.sum(torch.square(asset.actuators.applied_effort.torch[:, asset_cfg.joint_ids]), dim=1) def joint_vel_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: """Penalize joint velocities on the articulation using an L1-kernel.""" - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return torch.sum(torch.abs(asset.data.joint_vel.torch[:, asset_cfg.joint_ids]), dim=1) @@ -188,7 +179,6 @@ def joint_vel_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntity Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint velocities contribute to the term. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return torch.sum(torch.square(asset.data.joint_vel.torch[:, asset_cfg.joint_ids]), dim=1) @@ -200,14 +190,12 @@ def joint_acc_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntity Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint accelerations contribute to the term. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] return torch.sum(torch.square(asset.data.joint_acc.torch[:, asset_cfg.joint_ids]), dim=1) def joint_deviation_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Penalize joint positions that deviate from the default one.""" - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] # compute out of limits constraints angle = ( @@ -221,7 +209,6 @@ def joint_pos_target_l2(env: ManagerBasedRLEnv, target: float, asset_cfg: SceneE The joint positions are wrapped to ``[-pi, pi]`` before the deviation is computed. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] joint_pos = wrap_to_pi(asset.data.joint_pos.torch[:, asset_cfg.joint_ids]) return torch.sum(torch.square(joint_pos - target), dim=1) @@ -232,9 +219,7 @@ def joint_pos_limits(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEn This is computed as a sum of the absolute value of the difference between the joint position and the soft limits. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - # compute out of limits constraints out_of_limits = -( asset.data.joint_pos.torch[:, asset_cfg.joint_ids] - asset.data.soft_joint_pos_limits.torch[:, asset_cfg.joint_ids, 0] @@ -256,9 +241,7 @@ def joint_vel_limits( Args: soft_ratio: The ratio of the soft limits to be used. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - # compute out of limits constraints out_of_limits = ( torch.abs(asset.data.joint_vel.torch[:, asset_cfg.joint_ids]) - asset.data.soft_joint_vel_limits.torch[:, asset_cfg.joint_ids] * soft_ratio @@ -282,7 +265,6 @@ def applied_torque_limits(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = Sc Currently, this only works for explicit actuators since we manually compute the applied torques. For implicit actuators, we currently cannot retrieve the applied torques from the physics engine. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] # compute out of limits constraints # TODO: We need to fix this to support implicit joints. @@ -310,7 +292,6 @@ def action_l2(env: ManagerBasedRLEnv) -> torch.Tensor: def undesired_contacts(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: """Penalize undesired contacts as the number of violations that are above a threshold.""" - # extract the used quantities (to enable type-hinting) contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] # check if contact force is above threshold net_contact_forces = contact_sensor.data.net_normal_forces_w_history.torch @@ -334,7 +315,6 @@ def desired_contacts(env, sensor_cfg: SceneEntityCfg, threshold: float = 1.0) -> def contact_forces(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: """Penalize contact forces as the amount of violations of the net contact force.""" - # extract the used quantities (to enable type-hinting) contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] net_contact_forces = contact_sensor.data.net_normal_forces_w_history.torch # compute the violation @@ -354,9 +334,7 @@ def track_lin_vel_xy_exp( env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: """Reward tracking of linear velocity commands (xy axes) using exponential kernel.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - # compute the error lin_vel_error = torch.sum( torch.square(env.command_manager.get_command(command_name)[:, :2] - asset.data.root_lin_vel_b.torch[:, :2]), dim=1, @@ -368,9 +346,7 @@ def track_ang_vel_z_exp( env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: """Reward tracking of angular velocity commands (yaw) using exponential kernel.""" - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] - # compute the error ang_vel_error = torch.square( env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_b.torch[:, 2] ) @@ -389,7 +365,6 @@ def position_command_error(env: ManagerBasedRLEnv, command_name: str, asset_cfg: asset's root pose) and the current position of the asset's body in the world frame. The command is expected to be a pose command whose first three entries are the desired position in the root frame. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] command = env.command_manager.get_command(command_name) # obtain the desired and current positions in the world frame @@ -419,7 +394,6 @@ def orientation_command_error(env: ManagerBasedRLEnv, command_name: str, asset_c frame. The command is expected to be a pose command whose entries ``[3:7]`` are the desired orientation quaternion in the root frame. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] command = env.command_manager.get_command(command_name) # obtain the desired and current orientations in the world frame diff --git a/source/isaaclab/isaaclab/envs/mdp/terminations.py b/source/isaaclab/isaaclab/envs/mdp/terminations.py index 964a43f00c38..6188036a677b 100644 --- a/source/isaaclab/isaaclab/envs/mdp/terminations.py +++ b/source/isaaclab/isaaclab/envs/mdp/terminations.py @@ -70,7 +70,6 @@ def bad_orientation( This is computed by checking the angle between the projected gravity vector and the z-axis. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return torch.acos(-asset.data.projected_gravity_b.torch[:, 2]).abs() > limit_angle @@ -83,7 +82,6 @@ def root_height_below_minimum( Note: This is currently only supported for flat terrains, i.e. the minimum height is in the world frame. """ - # extract the used quantities (to enable type-hinting) asset: RigidObject = env.scene[asset_cfg.name] return asset.data.root_pos_w.torch[:, 2] < minimum_height @@ -95,7 +93,6 @@ def root_height_below_minimum( def joint_pos_out_of_limit(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: """Terminate when the asset's joint positions are outside of the soft joint limits.""" - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] if asset_cfg.joint_ids is None: asset_cfg.joint_ids = slice(None) @@ -114,7 +111,6 @@ def joint_pos_out_of_manual_limit( Note: This function is similar to :func:`joint_pos_out_of_limit` but allows the user to specify the bounds manually. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] if asset_cfg.joint_ids is None: asset_cfg.joint_ids = slice(None) @@ -140,7 +136,6 @@ def __init__(self, cfg: TerminationTermCfg, env: ManagerBasedRLEnv): self._joint_ids = joint_ids def __call__(self, env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: - # compute any violations limits = self._asset.data.soft_joint_vel_limits.torch[:, self._joint_ids] return torch.any(torch.abs(self._asset.data.joint_vel.torch[:, self._joint_ids]) > limits, dim=1) @@ -149,9 +144,7 @@ def joint_vel_out_of_manual_limit( env: ManagerBasedRLEnv, max_velocity: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") ) -> torch.Tensor: """Terminate when the asset's joint velocities are outside the provided limits.""" - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] - # compute any violations return torch.any(torch.abs(asset.data.joint_vel.torch[:, asset_cfg.joint_ids]) > max_velocity, dim=1) @@ -164,7 +157,6 @@ def joint_effort_out_of_limit( the computed torques to the joint limits. Hence, we check if the computed torques are equal to the applied torques. If they are not, it means that clipping has occurred. """ - # extract the used quantities (to enable type-hinting) asset: Articulation = env.scene[asset_cfg.name] # check if any joint effort is out of limit out_of_limits = ~torch.isclose( @@ -181,7 +173,6 @@ def joint_effort_out_of_limit( def illegal_contact(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: """Terminate when the contact force on the sensor exceeds the force threshold.""" - # extract the used quantities (to enable type-hinting) contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] net_contact_forces = contact_sensor.data.net_normal_forces_w_history.torch # check if any contact force exceeds the threshold diff --git a/source/isaaclab/isaaclab/envs/mimic_env_cfg.py b/source/isaaclab/isaaclab/envs/mimic_env_cfg.py index 27ac22eae061..ac31ca5ab822 100644 --- a/source/isaaclab/isaaclab/envs/mimic_env_cfg.py +++ b/source/isaaclab/isaaclab/envs/mimic_env_cfg.py @@ -247,7 +247,7 @@ def generate_runtime_subtask_constraints(self): - A "selected_src_demo_ind" and "transform" field are used to ensure the transforms used by both subtasks are the same. """ - task_constraints_dict = dict() + task_constraints_dict = {} if self.constraint_type == SubTaskConstraintType.SEQUENTIAL: constrained_task_spec_key, constrained_subtask_ind = self.eef_subtask_constraint_tuple[1] assert isinstance(constrained_subtask_ind, int) diff --git a/source/isaaclab/isaaclab/envs/ui/base_env_window.py b/source/isaaclab/isaaclab/envs/ui/base_env_window.py index ef861ac0c791..cfac5b2c6e53 100644 --- a/source/isaaclab/isaaclab/envs/ui/base_env_window.py +++ b/source/isaaclab/isaaclab/envs/ui/base_env_window.py @@ -80,7 +80,7 @@ def __init__(self, env: ManagerBasedEnv, window_name: str = "IsaacLab"): # keep a dictionary of stacks so that child environments can add their own UI elements # this can be done by using the `with` context manager - self.ui_window_elements = dict() + self.ui_window_elements = {} # create main frame self.ui_window_elements["main_frame"] = self.ui_window.frame with self.ui_window_elements["main_frame"]: diff --git a/source/isaaclab/isaaclab/envs/ui/empty_window.py b/source/isaaclab/isaaclab/envs/ui/empty_window.py index bc12f862d1b0..5c5963b150a8 100644 --- a/source/isaaclab/isaaclab/envs/ui/empty_window.py +++ b/source/isaaclab/isaaclab/envs/ui/empty_window.py @@ -48,7 +48,7 @@ def __init__(self, env: ManagerBasedEnv, window_name: str): # keep a dictionary of stacks so that child environments can add their own UI elements # this can be done by using the `with` context manager - self.ui_window_elements = dict() + self.ui_window_elements = {} # create main frame self.ui_window_elements["main_frame"] = self.ui_window.frame with self.ui_window_elements["main_frame"]: diff --git a/source/isaaclab/isaaclab/managers/action_manager.py b/source/isaaclab/isaaclab/managers/action_manager.py index aa2178d3a3dd..49416995678f 100644 --- a/source/isaaclab/isaaclab/managers/action_manager.py +++ b/source/isaaclab/isaaclab/managers/action_manager.py @@ -434,8 +434,8 @@ def serialize(self) -> dict: def _prepare_terms(self): # create buffers to parse and store terms - self._term_names: list[str] = list() - self._terms: dict[str, ActionTerm] = dict() + self._term_names: list[str] = [] + self._terms: dict[str, ActionTerm] = {} # check if config is dict already if isinstance(self.cfg, dict): diff --git a/source/isaaclab/isaaclab/managers/command_manager.py b/source/isaaclab/isaaclab/managers/command_manager.py index db8e7100fd67..35bc51c515e3 100644 --- a/source/isaaclab/isaaclab/managers/command_manager.py +++ b/source/isaaclab/isaaclab/managers/command_manager.py @@ -46,7 +46,7 @@ def __init__(self, cfg: CommandTermCfg, env: ManagerBasedRLEnv): # create buffers to store the command # -- metrics that can be used for logging - self.metrics = dict() + self.metrics = {} # -- time left before resampling self.time_left = torch.zeros(self.num_envs, device=self.device) # -- counter for the number of times the command has been resampled within the current episode @@ -241,7 +241,7 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): env: The environment instance. """ # create buffers to parse and store terms - self._terms: dict[str, CommandTerm] = dict() + self._terms: dict[str, CommandTerm] = {} # call the base class constructor (this prepares the terms) super().__init__(cfg, env) diff --git a/source/isaaclab/isaaclab/managers/curriculum_manager.py b/source/isaaclab/isaaclab/managers/curriculum_manager.py index 056db32b6dee..7ba550273384 100644 --- a/source/isaaclab/isaaclab/managers/curriculum_manager.py +++ b/source/isaaclab/isaaclab/managers/curriculum_manager.py @@ -46,9 +46,9 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): ValueError: If curriculum term configuration does not satisfy its function signature. """ # create buffers to parse and store terms - self._term_names: list[str] = list() - self._term_cfgs: list[CurriculumTermCfg] = list() - self._class_term_cfgs: list[CurriculumTermCfg] = list() + self._term_names: list[str] = [] + self._term_cfgs: list[CurriculumTermCfg] = [] + self._class_term_cfgs: list[CurriculumTermCfg] = [] # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) @@ -119,7 +119,6 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: # reset all the curriculum terms for term_cfg in self._class_term_cfgs: term_cfg.func.reset(env_ids=env_ids) - # return logged information return extras def compute(self, env_ids: Sequence[int] | None = None): diff --git a/source/isaaclab/isaaclab/managers/event_manager.py b/source/isaaclab/isaaclab/managers/event_manager.py index f4829aeca98d..e9103a9d73e8 100644 --- a/source/isaaclab/isaaclab/managers/event_manager.py +++ b/source/isaaclab/isaaclab/managers/event_manager.py @@ -21,7 +21,6 @@ if TYPE_CHECKING: from ..envs import ManagerBasedEnv -# import logger logger = logging.getLogger(__name__) @@ -69,9 +68,9 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): env: An environment object. """ # create buffers to parse and store terms - self._mode_term_names: dict[str, list[str]] = dict() - self._mode_term_cfgs: dict[str, list[EventTermCfg]] = dict() - self._mode_class_term_cfgs: dict[str, list[EventTermCfg]] = dict() + self._mode_term_names: dict[str, list[str]] = {} + self._mode_term_cfgs: dict[str, list[EventTermCfg]] = {} + self._mode_class_term_cfgs: dict[str, list[EventTermCfg]] = {} # call the base class (this will parse the terms config) super().__init__(cfg, env) @@ -200,7 +199,6 @@ def apply( if mode != "prestartup" and not self._is_scene_entities_resolved: self._resolve_terms_callback(None) - # check if mode is interval and dt is not provided if mode == "interval" and dt is None: raise ValueError(f"Event mode '{mode}' requires the time-step of the environment.") if mode == "interval" and env_ids is not None: @@ -208,11 +206,9 @@ def apply( f"Event mode '{mode}' does not require environment indices. This is an undefined behavior" " as the environment indices are computed based on the time left for each environment." ) - # check if mode is reset and env step count is not provided if mode == "reset" and global_env_step_count is None: raise ValueError(f"Event mode '{mode}' requires the total number of environment steps to be provided.") - # iterate over all the event terms for index, term_cfg in enumerate(self._mode_term_cfgs[mode]): # initialize class-based terms if not already initialized (for non-prestartup modes) if inspect.isclass(term_cfg.func): @@ -221,9 +217,7 @@ def apply( ) term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env) if mode == "interval": - # extract time left for this term time_left = self._interval_term_time_left[index] - # update the time left for each environment time_left -= dt # check if the interval has passed and sample a new interval @@ -242,11 +236,8 @@ def apply( lower, upper = term_cfg.interval_range_s sampled_time = torch.rand(len(valid_env_ids), device=self.device) * (upper - lower) + lower self._interval_term_time_left[index][valid_env_ids] = sampled_time - - # call the event term term_cfg.func(self._env, valid_env_ids, **term_cfg.params) elif mode == "reset": - # obtain the minimum step count between resets min_step_count = term_cfg.min_step_count_between_reset # resolve the environment indices if env_ids is None: @@ -257,11 +248,8 @@ def apply( if min_step_count == 0: self._reset_term_last_triggered_step_id[index][env_ids] = global_env_step_count self._reset_term_last_triggered_once[index][env_ids] = True - - # call the event term with the environment indices term_cfg.func(self._env, env_ids, **term_cfg.params) else: - # extract last reset step for this term last_triggered_step = self._reset_term_last_triggered_step_id[index][env_ids] triggered_at_least_once = self._reset_term_last_triggered_once[index][env_ids] # compute the steps since last reset @@ -287,7 +275,6 @@ def apply( # call the event term term_cfg.func(self._env, valid_env_ids, **term_cfg.params) else: - # call the event term term_cfg.func(self._env, env_ids, **term_cfg.params) """ @@ -412,8 +399,6 @@ def _prepare_terms(self): raise ValueError( f"Event term '{term_name}' has mode 'interval' but 'interval_range_s' is not specified." ) - - # sample the time left for global if term_cfg.is_global_time: lower, upper = term_cfg.interval_range_s time_left = torch.rand(1) * (upper - lower) + lower diff --git a/source/isaaclab/isaaclab/managers/manager_base.py b/source/isaaclab/isaaclab/managers/manager_base.py index 2fcf405ae61f..cb08b228ecbd 100644 --- a/source/isaaclab/isaaclab/managers/manager_base.py +++ b/source/isaaclab/isaaclab/managers/manager_base.py @@ -271,7 +271,6 @@ def _resolve_terms_callback(self, event): Please check the :meth:`_process_term_cfg_at_play` method for more information. """ - # check if scene entities have been resolved if self._is_scene_entities_resolved: return # check if config is dict already @@ -288,8 +287,6 @@ def _resolve_terms_callback(self, event): # process attributes at runtime # these properties are only resolvable once the simulation starts playing self._process_term_cfg_at_play(term_name, term_cfg) - - # set the flag self._is_scene_entities_resolved = True """ diff --git a/source/isaaclab/isaaclab/managers/manager_term_cfg.py b/source/isaaclab/isaaclab/managers/manager_term_cfg.py index bd0e003cf92e..c5f672ff887c 100644 --- a/source/isaaclab/isaaclab/managers/manager_term_cfg.py +++ b/source/isaaclab/isaaclab/managers/manager_term_cfg.py @@ -42,7 +42,7 @@ class ManagerTermBaseCfg: .. _`callable classes`: https://docs.python.org/3/reference/datamodel.html#object.__call__ """ - params: dict[str, Any | SceneEntityCfg] = dict() + params: dict[str, Any | SceneEntityCfg] = {} """The parameters to be passed to the function as keyword arguments. Defaults to an empty dict. .. note:: diff --git a/source/isaaclab/isaaclab/managers/observation_manager.py b/source/isaaclab/isaaclab/managers/observation_manager.py index 9fcc0fc79512..daea47a4ee7e 100644 --- a/source/isaaclab/isaaclab/managers/observation_manager.py +++ b/source/isaaclab/isaaclab/managers/observation_manager.py @@ -83,7 +83,7 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): super().__init__(cfg, env) # compute combined vector for obs group - self._group_obs_dim: dict[str, tuple[int, ...] | list[tuple[int, ...]]] = dict() + self._group_obs_dim: dict[str, tuple[int, ...] | list[tuple[int, ...]]] = {} for group_name, group_term_dims in self._group_obs_term_dim.items(): # if terms are concatenated, compute the combined shape into a single tuple # otherwise, keep the list of shapes as is @@ -259,7 +259,6 @@ def _collect_io_descriptors(self, group_names_to_export: list[str] = ["policy"]) group_term_names = self._group_obs_term_names[group_name] # read attributes for each term obs_terms = zip(group_term_names, self._group_obs_term_cfgs[group_name]) - for term_name, term_cfg in obs_terms: # Call to the observation function to get the IO descriptor with the inspect flag set to True try: @@ -383,22 +382,17 @@ def compute_group(self, group_name: str, update_history: bool = False) -> torch. Raises: ValueError: If input ``group_name`` is not a valid group handled by the manager. """ - # check ig group name is valid if group_name not in self._group_obs_term_names: raise ValueError( f"Unable to find the group '{group_name}' in the observation manager." f" Available groups are: {list(self._group_obs_term_names.keys())}" ) - # iterate over all the terms in each group group_term_names = self._group_obs_term_names[group_name] - # buffer to store obs per group group_obs = dict.fromkeys(group_term_names, None) - # read attributes for each term obs_terms = zip(group_term_names, self._group_obs_term_cfgs[group_name]) # evaluate terms: compute, add noise, clip, scale, custom modifiers for term_name, term_cfg in obs_terms: - # compute term's value obs: torch.Tensor = term_cfg.func(self._env, **term_cfg.params).clone() # apply post-processing if term_cfg.modifiers is not None: @@ -475,17 +469,17 @@ def _prepare_terms(self): """Prepares a list of observation terms functions.""" # create buffers to store information for each observation group # TODO: Make this more convenient by using data structures. - self._group_obs_term_names: dict[str, list[str]] = dict() - self._group_obs_term_dim: dict[str, list[tuple[int, ...]]] = dict() - self._group_obs_term_cfgs: dict[str, list[ObservationTermCfg]] = dict() - self._group_obs_class_term_cfgs: dict[str, list[ObservationTermCfg]] = dict() - self._group_obs_concatenate: dict[str, bool] = dict() - self._group_obs_concatenate_dim: dict[str, int] = dict() - - self._group_obs_term_history_buffer: dict[str, dict] = dict() + self._group_obs_term_names: dict[str, list[str]] = {} + self._group_obs_term_dim: dict[str, list[tuple[int, ...]]] = {} + self._group_obs_term_cfgs: dict[str, list[ObservationTermCfg]] = {} + self._group_obs_class_term_cfgs: dict[str, list[ObservationTermCfg]] = {} + self._group_obs_concatenate: dict[str, bool] = {} + self._group_obs_concatenate_dim: dict[str, int] = {} + + self._group_obs_term_history_buffer: dict[str, dict] = {} # create a list to store classes instances, e.g., for modifiers and noise models # we store it as a separate list to only call reset on them and prevent unnecessary calls - self._group_obs_class_instances: list[modifiers.ModifierBase | noise.NoiseModel] = list() + self._group_obs_class_instances: list[modifiers.ModifierBase | noise.NoiseModel] = [] # make sure the simulation is playing since we compute obs dims which needs asset quantities if not self._env.sim.is_playing(): @@ -512,13 +506,13 @@ def _prepare_terms(self): f" Received: '{type(group_cfg)}'." ) # initialize list for the group settings - self._group_obs_term_names[group_name] = list() - self._group_obs_term_dim[group_name] = list() - self._group_obs_term_cfgs[group_name] = list() - self._group_obs_class_term_cfgs[group_name] = list() + self._group_obs_term_names[group_name] = [] + self._group_obs_term_dim[group_name] = [] + self._group_obs_term_cfgs[group_name] = [] + self._group_obs_class_term_cfgs[group_name] = [] # history buffers - group_entry_history_buffer: dict[str, CircularBuffer] = dict() + group_entry_history_buffer: dict[str, CircularBuffer] = {} # read common config for the group self._group_obs_concatenate[group_name] = group_cfg.concatenate_terms diff --git a/source/isaaclab/isaaclab/managers/recorder_manager.py b/source/isaaclab/isaaclab/managers/recorder_manager.py index efbf1c15bbf7..a29e326deb5c 100644 --- a/source/isaaclab/isaaclab/managers/recorder_manager.py +++ b/source/isaaclab/isaaclab/managers/recorder_manager.py @@ -161,8 +161,8 @@ def __init__(self, cfg: object, env: ManagerBasedEnv): cfg: The configuration object or dictionary (``dict[str, RecorderTermCfg]``). env: The environment instance. """ - self._term_names: list[str] = list() - self._terms: dict[str, RecorderTerm] = dict() + self._term_names: list[str] = [] + self._terms: dict[str, RecorderTerm] = {} # Do nothing if cfg is None or an empty dict if not cfg: @@ -280,7 +280,6 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor] # Do nothing if no active recorder terms are provided if len(self.active_terms) == 0: return {} - # resolve environment ids if env_ids is None: env_ids = list(range(self._env.num_envs)) @@ -578,7 +577,6 @@ def _prepare_terms(self): # check if term config is None if term_cfg is None: continue - # check valid type if not isinstance(term_cfg, RecorderTermCfg): raise TypeError( f"Configuration for the term '{term_name}' is not of type RecorderTermCfg." diff --git a/source/isaaclab/isaaclab/managers/reward_manager.py b/source/isaaclab/isaaclab/managers/reward_manager.py index d822feb415f0..33e142e0e00c 100644 --- a/source/isaaclab/isaaclab/managers/reward_manager.py +++ b/source/isaaclab/isaaclab/managers/reward_manager.py @@ -49,9 +49,9 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): env: The environment instance. """ # create buffers to parse and store terms - self._term_names: list[str] = list() - self._term_cfgs: list[RewardTermCfg] = list() - self._class_term_cfgs: list[RewardTermCfg] = list() + self._term_names: list[str] = [] + self._term_cfgs: list[RewardTermCfg] = [] + self._class_term_cfgs: list[RewardTermCfg] = [] # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) @@ -108,10 +108,8 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor] Returns: Dictionary of episodic sum of individual reward terms. """ - # resolve environment ids if env_ids is None: env_ids = slice(None) - # store information extras = {} for key in self._episode_sums.keys(): # store information @@ -123,7 +121,6 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor] # reset all the reward terms for term_cfg in self._class_term_cfgs: term_cfg.func.reset(env_ids=env_ids) - # return logged information return extras def compute(self, dt: float) -> torch.Tensor: diff --git a/source/isaaclab/isaaclab/managers/scene_entity_cfg.py b/source/isaaclab/isaaclab/managers/scene_entity_cfg.py index 9ccfcea0b5d4..e10a504f72c5 100644 --- a/source/isaaclab/isaaclab/managers/scene_entity_cfg.py +++ b/source/isaaclab/isaaclab/managers/scene_entity_cfg.py @@ -133,7 +133,6 @@ def resolve(self, scene: InteractiveScene): ValueError: If both ``object_collection_names`` and ``object_collection_ids`` are specified and are not consistent. """ - # check if the entity is valid if self.name not in scene.keys(): raise ValueError(f"The scene entity '{self.name}' does not exist. Available entities: {scene.keys()}.") diff --git a/source/isaaclab/isaaclab/managers/termination_manager.py b/source/isaaclab/isaaclab/managers/termination_manager.py index 3410a3ed43cb..a94f17b32ff0 100644 --- a/source/isaaclab/isaaclab/managers/termination_manager.py +++ b/source/isaaclab/isaaclab/managers/termination_manager.py @@ -55,9 +55,9 @@ def __init__(self, cfg: object, env: ManagerBasedRLEnv): env: An environment object. """ # create buffers to parse and store terms - self._term_names: list[str] = list() - self._term_cfgs: list[TerminationTermCfg] = list() - self._class_term_cfgs: list[TerminationTermCfg] = list() + self._term_names: list[str] = [] + self._term_cfgs: list[TerminationTermCfg] = [] + self._class_term_cfgs: list[TerminationTermCfg] = [] # call the base class constructor (this will parse the terms config) super().__init__(cfg, env) @@ -136,7 +136,6 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor] Returns: Dictionary mapping each termination term to its mean activation. """ - # resolve environment ids if env_ids is None: env_ids = slice(None) # add to episode dict @@ -149,7 +148,6 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor] # reset all the termination terms for term_cfg in self._class_term_cfgs: term_cfg.func.reset(env_ids=env_ids) - # return logged information return extras def compute(self) -> torch.Tensor: diff --git a/source/isaaclab/isaaclab/scene/interactive_scene.py b/source/isaaclab/isaaclab/scene/interactive_scene.py index 23b52d5ad3ab..40099db6b875 100644 --- a/source/isaaclab/isaaclab/scene/interactive_scene.py +++ b/source/isaaclab/isaaclab/scene/interactive_scene.py @@ -51,7 +51,6 @@ if TYPE_CHECKING: from pxr import Sdf # noqa: F401 -# import logger logger = logging.getLogger(__name__) @@ -123,29 +122,23 @@ def __init__(self, cfg: InteractiveSceneCfg): Args: cfg: The configuration class for the scene. """ - # check that the config is valid cfg.validate() - # store inputs self.cfg = cfg - # initialize scene elements self._terrain = None - self._articulations = dict() - self._cable_objects = dict() - self._deformable_objects = dict() - self._rigid_objects = dict() - self._rigid_object_collections = dict() - self._sensors = dict() - self._surface_grippers = dict() - self._visual_materials = dict() + self._articulations = {} + self._cable_objects = {} + self._deformable_objects = {} + self._rigid_objects = {} + self._rigid_object_collections = {} + self._sensors = {} + self._surface_grippers = {} + self._visual_materials = {} self._extras: dict[str, Asset | VisualizationMarkers] = {} - # get stage handle self.sim = SimulationContext.instance() self.stage = get_current_stage() self.stage_id = get_current_stage_id() self.physics_backend = self.sim.physics_manager.__name__.lower() - # physics scene path self._physics_scene_path = None - # prepare cloner for environment replication self.cloner_cfg = copy.deepcopy(self.cfg.clone_cfg) self.cloner_cfg.replicate_physics = self.cfg.replicate_physics # the template is authoritative; the regex form is the same namespace spelled for matching @@ -153,7 +146,7 @@ def __init__(self, cfg: InteractiveSceneCfg): self.env_prim_paths = [self._env_fmt.format(i) for i in range(self.cfg.num_envs)] self._ALL_INDICES = torch.arange(self.cfg.num_envs, dtype=torch.long, device=self.device) - self._global_prim_paths = list() + self._global_prim_paths = [] asset_cfgs, global_paths, valid_set = self._collect_asset_cfgs() scene_from_cfg = any( name not in InteractiveSceneCfg.__dataclass_fields__ and cfg is not None @@ -662,11 +655,11 @@ def get_state(self, is_relative: bool = False) -> dict[str, dict[str, dict[str, Returns: A dictionary of the state of the scene entities. """ - state = dict() + state = {} # articulations - state["articulation"] = dict() + state["articulation"] = {} for asset_name, articulation in self._articulations.items(): - asset_state = dict() + asset_state = {} asset_state["root_pose"] = articulation.data.root_pose_w.torch.clone() if is_relative: asset_state["root_pose"][:, :3] -= self.env_origins @@ -675,34 +668,34 @@ def get_state(self, is_relative: bool = False) -> dict[str, dict[str, dict[str, asset_state["joint_velocity"] = articulation.data.joint_vel.torch.clone() state["articulation"][asset_name] = asset_state # cable objects - state["cable_object"] = dict() + state["cable_object"] = {} for asset_name, cable_object in self._cable_objects.items(): - asset_state = dict() + asset_state = {} asset_state["segment_pose"] = cable_object.data.segment_pose_w.torch.clone() if is_relative: asset_state["segment_pose"][..., :3] -= self.env_origins[:, None, :] asset_state["segment_velocity"] = cable_object.data.segment_velocity_w.torch.clone() state["cable_object"][asset_name] = asset_state # deformable objects - state["deformable_object"] = dict() + state["deformable_object"] = {} for asset_name, deformable_object in self._deformable_objects.items(): - asset_state = dict() + asset_state = {} asset_state["nodal_position"] = deformable_object.data.nodal_pos_w.torch.clone() if is_relative: asset_state["nodal_position"] -= self.env_origins[:, None, :] asset_state["nodal_velocity"] = deformable_object.data.nodal_vel_w.torch.clone() state["deformable_object"][asset_name] = asset_state # rigid objects - state["rigid_object"] = dict() + state["rigid_object"] = {} for asset_name, rigid_object in self._rigid_objects.items(): - asset_state = dict() + asset_state = {} asset_state["root_pose"] = rigid_object.data.root_pose_w.torch.clone() if is_relative: asset_state["root_pose"][:, :3] -= self.env_origins asset_state["root_velocity"] = rigid_object.data.root_vel_w.torch.clone() state["rigid_object"][asset_name] = asset_state # surface grippers - state["gripper"] = dict() + state["gripper"] = {} for asset_name, gripper in self._surface_grippers.items(): state["gripper"][asset_name] = wp.to_torch(gripper.state).clone() return state @@ -741,7 +734,6 @@ def __getitem__(self, key: str) -> Any: Returns: The scene entity. """ - # check if it is a terrain if key == "terrain": return self._terrain @@ -841,7 +833,6 @@ def _add_entities_from_cfg(self): # noqa: C901 asset_paths = sim_utils.find_matching_prim_paths(rigid_object_cfg.prim_path) self._global_prim_paths += asset_paths elif isinstance(asset_cfg, SurfaceGripperCfg): - # add surface grippers to scene self._surface_grippers[asset_name] = asset_cfg.class_type(asset_cfg) elif isinstance(asset_cfg, SensorBaseCfg): # Update target frame path(s)' regex name space for FrameTransformer diff --git a/source/isaaclab/isaaclab/sensors/camera/camera.py b/source/isaaclab/isaaclab/sensors/camera/camera.py index f817a82640d4..68b24a8cf77c 100644 --- a/source/isaaclab/isaaclab/sensors/camera/camera.py +++ b/source/isaaclab/isaaclab/sensors/camera/camera.py @@ -32,7 +32,6 @@ if TYPE_CHECKING: from .camera_cfg import CameraCfg -# import logger logger = logging.getLogger(__name__) @@ -276,7 +275,7 @@ def __init__(self, cfg: CameraCfg): ) # UsdGeom Camera prim for the sensor - self._sensor_prims: list[UsdGeom.Camera] = list() + self._sensor_prims: list[UsdGeom.Camera] = [] # Allocated in :meth:`_create_buffers` once the renderer's output contract is known. self._data: CameraData | None = None # The backend's ``__init__`` is its pre-physics phase, so it has to exist before diff --git a/source/isaaclab/isaaclab/sensors/contact_sensor/base_contact_sensor.py b/source/isaaclab/isaaclab/sensors/contact_sensor/base_contact_sensor.py index 99f671fcf185..410f9fbd93c4 100644 --- a/source/isaaclab/isaaclab/sensors/contact_sensor/base_contact_sensor.py +++ b/source/isaaclab/isaaclab/sensors/contact_sensor/base_contact_sensor.py @@ -54,10 +54,7 @@ def __init__(self, cfg: ContactSensorCfg): Args: cfg: The configuration parameters. """ - # initialize base class super().__init__(cfg) - - # check that config is valid if cfg.history_length < 0: raise ValueError(f"History length must be greater than 0! Received: {cfg.history_length}") diff --git a/source/isaaclab/isaaclab/sensors/kernels.py b/source/isaaclab/isaaclab/sensors/kernels.py index db6570323fec..8488ec863655 100644 --- a/source/isaaclab/isaaclab/sensors/kernels.py +++ b/source/isaaclab/isaaclab/sensors/kernels.py @@ -64,7 +64,6 @@ def reset_envs_kernel( timestamp: Current timestamp per env. Will be set to 0.0 for reset envs. timestamp_last_update: Last update timestamp per env. Will be set to 0.0 for reset envs. """ - env = wp.tid() if not reset_mask[env]: return diff --git a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py index 83fa99716d8f..003b323158e0 100644 --- a/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py +++ b/source/isaaclab/isaaclab/sensors/ray_caster/multi_mesh_ray_caster_camera_cfg.py @@ -15,7 +15,6 @@ if TYPE_CHECKING: from .multi_mesh_ray_caster_camera import MultiMeshRayCasterCamera -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/sensors/sensor_base.py b/source/isaaclab/isaaclab/sensors/sensor_base.py index 55e6444632a7..274b7089309a 100644 --- a/source/isaaclab/isaaclab/sensors/sensor_base.py +++ b/source/isaaclab/isaaclab/sensors/sensor_base.py @@ -54,25 +54,19 @@ def __init__(self, cfg: SensorBaseCfg): Args: cfg: The configuration parameters for the sensor. """ - # check that the config is valid cfg.validate() cfg.prim_path = expand_env_regex_ns(cfg.prim_path) - # store inputs self.cfg = cfg.copy() - # flag for whether the sensor is initialized self._is_initialized = False - # flag for whether the sensor is in visualization mode self._is_visualizing = False # clone plan used for this sensor's latest initialization self._clone_plan: ClonePlan | None = None self.stage = sim_utils.get_current_stage() - # register various callback functions self._register_callbacks() # add handle for debug visualization (this is set to a valid handle inside set_debug_vis) self._debug_vis_handle = None - # set initial state of debug visualization self.set_debug_vis(self.cfg.debug_vis) def __del__(self, _sys=sys): diff --git a/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py b/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py index 39a3cf4ec29f..10a094bb4811 100644 --- a/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py +++ b/source/isaaclab/isaaclab/sim/converters/asset_converter_base.py @@ -86,15 +86,11 @@ def __init__(self, cfg: AssetConverterBaseCfg): else: self._usd_file_name = usd_file_name - # create the USD directory os.makedirs(self.usd_dir, exist_ok=True) - # check if usd files exist self._usd_file_exists = os.path.isfile(self.usd_path) # path to read/write asset hash file self._dest_hash_path = os.path.join(self.usd_dir, ".asset_hash") - # create asset hash to check if the asset has changed self._asset_hash = self._config_to_hash(cfg) - # read the saved hash try: with open(self._dest_hash_path) as f: existing_asset_hash = f.readline() diff --git a/source/isaaclab/isaaclab/sim/schemas/schemas.py b/source/isaaclab/isaaclab/sim/schemas/schemas.py index f7de0d306333..b13fd91ac9ac 100644 --- a/source/isaaclab/isaaclab/sim/schemas/schemas.py +++ b/source/isaaclab/isaaclab/sim/schemas/schemas.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause -# needed to import for allowing type-hinting: Usd.Stage | None from __future__ import annotations import dataclasses @@ -32,7 +31,6 @@ from . import schemas_cfg from ._backend_hooks import _skip_joint_drive -# import logger logger = logging.getLogger(__name__) @@ -405,16 +403,11 @@ def define_articulation_root_properties( Use :func:`apply_articulation_root_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get articulation USD prim prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") - # check if prim has articulation applied on it if not UsdPhysics.ArticulationRootAPI(prim): UsdPhysics.ArticulationRootAPI.Apply(prim) # set articulation root properties @@ -524,13 +517,9 @@ def modify_articulation_root_properties( Use :func:`apply_articulation_root_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get articulation USD prim articulation_prim = stage.GetPrimAtPath(prim_path) - # check if prim has articulation applied on it if not UsdPhysics.ArticulationRootAPI(articulation_prim): return False @@ -538,8 +527,6 @@ def modify_articulation_root_properties( cfg_dict = {f.name: getattr(cfg, f.name) for f in dataclasses.fields(cfg)} # extract writer-side (non-USD) properties fix_root_link = cfg_dict.pop("fix_root_link", None) - - # apply per-field exceptions + main-namespace writes _apply_namespaced_schemas(articulation_prim, cfg, cfg_dict) # fix root link based on input @@ -568,7 +555,6 @@ def modify_articulation_root_properties( " the articulation tree. However, this is not implemented yet." ) - # create a fixed joint between the root link and the world frame create_world_fixed_joint(articulation_prim, stage) # Having a fixed joint on a rigid body is not treated as "fixed base articulation". @@ -576,7 +562,6 @@ def modify_articulation_root_properties( # Moving the articulation root to the parent solves this issue. This is a limitation of the PhysX parser. # get parent prim parent_prim = articulation_prim.GetParent() - # apply api to parent UsdPhysics.ArticulationRootAPI.Apply(parent_prim) parent_applied = parent_prim.GetAppliedSchemas() if "PhysxArticulationAPI" not in parent_applied: @@ -620,7 +605,6 @@ def modify_articulation_root_properties( if not articulation_prim.RemoveAppliedSchema(newton_root_schema): raise RuntimeError(f"Failed to remove '{newton_root_schema}' from '{articulation_prim.GetPath()}'.") - # remove api from root articulation_prim.RemoveAppliedSchema("PhysxArticulationAPI") articulation_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI) articulation_prim = parent_prim @@ -634,8 +618,6 @@ def modify_articulation_root_properties( safe_set_attribute_on_usd_prim( articulation_prim, "newton:selfCollisionEnabled", enabled_self_collisions, camel_case=False ) - - # success return True @@ -873,19 +855,13 @@ def define_rigid_body_properties(prim_path: str, cfg: schemas_cfg.RigidBodyBaseC Use :func:`apply_rigid_body_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") - # check if prim has rigid body applied on it if not UsdPhysics.RigidBodyAPI(prim): UsdPhysics.RigidBodyAPI.Apply(prim) - # set rigid body properties modify_rigid_body_properties.__wrapped__(prim_path, cfg, stage) @@ -932,13 +908,9 @@ def modify_rigid_body_properties( Use :func:`apply_rigid_body_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get rigid-body USD prim rigid_body_prim = stage.GetPrimAtPath(prim_path) - # check if prim has rigid-body applied on it if not UsdPhysics.RigidBodyAPI(rigid_body_prim): return False # convert to dict, filtering out class metadata (underscore-prefixed keys) @@ -1038,19 +1010,13 @@ def define_collision_properties( Use :func:`apply_collision_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") - # check if prim has collision applied on it if not UsdPhysics.CollisionAPI(prim): UsdPhysics.CollisionAPI.Apply(prim) - # set collision properties modify_collision_properties.__wrapped__(prim_path, cfg, stage) @@ -1091,13 +1057,9 @@ def modify_collision_properties( Use :func:`apply_collision_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim collider_prim = stage.GetPrimAtPath(prim_path) - # check if prim has collision applied on it if not UsdPhysics.CollisionAPI(collider_prim): return False # dispatch nested mesh-collision cfg if present (preserve legacy behavior) @@ -1115,7 +1077,6 @@ def modify_collision_properties( # ``rest_offset`` via field exceptions; PhysX-subclass fields under # ``physxCollision:*``. _apply_namespaced_schemas(collider_prim, cfg, cfg_dict) - # success return True @@ -1201,19 +1162,13 @@ def define_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, s Use :func:`apply_mass_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") - # check if prim has mass applied on it if not UsdPhysics.MassAPI(prim): UsdPhysics.MassAPI.Apply(prim) - # set mass properties modify_mass_properties.__wrapped__(prim_path, cfg, stage) @@ -1257,13 +1212,9 @@ def modify_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, s Use :func:`apply_mass_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim rigid_prim = stage.GetPrimAtPath(prim_path) - # check if prim has mass API applied on it if not UsdPhysics.MassAPI(rigid_prim): return False @@ -1296,7 +1247,6 @@ def activate_contact_sensors(prim_path: str, threshold: float = 0.0, stage: Usd. ValueError: If the input prim path is not valid. ValueError: If there are no rigid bodies under the prim path. """ - # get stage handle if stage is None: stage = get_current_stage() @@ -1446,7 +1396,6 @@ def apply_drive(cfg, prim_path: str, stage: Usd.Stage | None = None) -> bool: continue usd_attr_name = "type" if field_name == "drive_type" else field_name safe_set_attribute_on_usd_schema(usd_drive_api, usd_attr_name, value, camel_case=True) - return True @@ -1624,13 +1573,9 @@ def modify_joint_drive_properties( Use :func:`apply_joint_drive_properties` with schema fragments instead. This function will be removed in 3.2. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") @@ -1954,12 +1899,9 @@ def define_mesh_collision_properties( Use :func:`apply_mesh_collision_properties` with schema fragments instead. This function will be removed in 3.2. """ - # obtain stage if stage is None: stage = get_current_stage() - # get USD prim prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") @@ -2011,10 +1953,8 @@ def modify_mesh_collision_properties( Use :func:`apply_mesh_collision_properties` with schema fragments instead. This function will be removed in 3.2. """ - # obtain stage if stage is None: stage = get_current_stage() - # get USD prim prim = stage.GetPrimAtPath(prim_path) # we need MeshCollisionAPI to set mesh collision approximation attribute @@ -2044,8 +1984,6 @@ def modify_mesh_collision_properties( # gates ``Physx*CollisionAPI`` application on at least one non-None tuning field, so # Newton-targeted prims stay free of PhysX cooking schemas they did not opt in to. _apply_namespaced_schemas(prim, cfg, cfg_dict) - - # success return True @@ -2153,13 +2091,9 @@ def define_deformable_body_properties( without its optional dependencies. RuntimeError: When setting the deformable body properties fails. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get USD prim root_prim = stage.GetPrimAtPath(prim_path) - # check if prim path is valid if not root_prim.IsValid(): raise ValueError(f"Prim path '{prim_path}' is not valid.") @@ -2184,7 +2118,6 @@ def define_deformable_body_properties( # Search for a visual surface mesh for both surface and volume deformables matching_prims = get_all_matching_child_prims(prim_path, lambda p: p.GetTypeName() == "Mesh") - # check if the visual surface mesh is valid if len(matching_prims) == 0: # in case a TetMesh is found but no Mesh is found, we use the TetMesh surface as visual. if sim_mesh_prim is not None: @@ -2195,7 +2128,6 @@ def define_deformable_body_properties( f"Deformable body at '{prim_path}' has no surface indices on its TetMesh prim; " "cannot sync to visual mesh." ) - # create visual mesh vis_mesh_prim = create_prim( prim_path + "/vis_mesh", prim_type="Mesh", @@ -2210,15 +2142,12 @@ def define_deformable_body_properties( else: raise ValueError(f"Could not find any visual mesh in '{prim_path}'. Please check asset.") if len(matching_prims) > 1: - # get list of all meshes found mesh_paths = [p.GetPrimPath() for p in matching_prims] raise ValueError( f"Found multiple visual meshes in '{prim_path}': {mesh_paths}." " Deformable body schema can only be applied to one mesh for now." ) vis_mesh_prim = matching_prims[0] - - # check if the prim is valid if not vis_mesh_prim.IsValid(): raise ValueError(f"Mesh prim path '{vis_mesh_prim.GetPrimPath()}' is not valid.") @@ -2321,14 +2250,12 @@ def define_deformable_body_properties( sim_mesh_prim.GetAttribute("omniphysics:restTetVtxIndices").Set( sim_mesh_prim.GetAttribute("tetVertexIndices").Get() ) - else: raise ValueError( f"""Unsupported deformable type: '{deformable_type}'. Only surface and volume deformables are supported.""" ) - # apply collision API if not sim_mesh_prim.ApplyAPI(UsdPhysics.CollisionAPI): raise RuntimeError(f"Failed to set {deformable_type} deformable collision API on prim '{sim_mesh_prim_path}'.") @@ -2351,8 +2278,6 @@ def define_deformable_body_properties( sim_mesh_prim.CreateAttribute("deformablePose:default:omniphysics:purposes", Sdf.ValueTypeNames.TokenArray).Set( purposes ) - - # apply deformable body api if not root_prim.ApplyAPI("OmniPhysicsDeformableBodyAPI"): raise RuntimeError(f"Failed to set deformable body API on prim '{prim_path}'.") else: @@ -2375,12 +2300,9 @@ def define_deformable_body_properties( sim_mesh = UsdGeom.Mesh(sim_mesh_prim) vis_mesh.GetFaceVertexIndicesAttr().Set(sim_mesh.GetFaceVertexIndicesAttr().Get()) vis_mesh.GetFaceVertexCountsAttr().Set(sim_mesh.GetFaceVertexCountsAttr().Get()) - - # apply deformable body api if not root_prim.AddAppliedSchema("PhysicsDeformableBodyAPI"): raise RuntimeError(f"Failed to set deformable body API on prim '{prim_path}'.") - # set deformable body properties modify_deformable_body_properties(prim_path, cfg, stage) @@ -2428,11 +2350,8 @@ def modify_deformable_body_properties( Returns: True if the properties were successfully set, False otherwise. """ - # get stage handle if stage is None: stage = get_current_stage() - - # get deformable-body USD prim deformable_body_prim = stage.GetPrimAtPath(prim_path) # check if the prim is valid if not deformable_body_prim.IsValid(): @@ -2452,5 +2371,4 @@ def modify_deformable_body_properties( ) _apply_namespaced_schemas(deformable_body_prim, cfg, cfg_dict) - # success return True diff --git a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py index 72957ed0890d..2b67c2e44b4d 100644 --- a/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py +++ b/source/isaaclab/isaaclab/sim/spawners/from_files/from_files.py @@ -40,7 +40,6 @@ from . import from_files_cfg -# import logger logger = logging.getLogger(__name__) @@ -204,7 +203,6 @@ def spawn_ground_plane( Raises: ValueError: If the prim path already exists. """ - # Obtain current stage stage = get_current_stage() # Spawn Ground-plane @@ -291,8 +289,6 @@ def spawn_ground_plane( # Apply visibility set_prim_visibility(prim, cfg.visible) - - # return the prim return prim @@ -614,10 +610,7 @@ def _spawn_from_usd_file( ) # create material (accepts a legacy material cfg or rigid-body fragment(s)) spawn_physics_material(material_path, cfg.physics_material, stage=stage) - # apply material bind_physics_material(prim_path, material_path, stage=stage) - - # return the prim return stage.GetPrimAtPath(prim_path) @@ -681,8 +674,6 @@ def spawn_from_usd_with_compliant_contact_material( rigid_body_prim_path = path material_path = f"{rigid_body_prim_path}/compliant_material" - - # spawn physics material material_cfg.func(material_path, material_cfg) bind_physics_material( diff --git a/source/isaaclab/isaaclab/sim/spawners/lights/lights.py b/source/isaaclab/isaaclab/sim/spawners/lights/lights.py index ba78b6671d7e..4be1b81f5840 100644 --- a/source/isaaclab/isaaclab/sim/spawners/lights/lights.py +++ b/source/isaaclab/isaaclab/sim/spawners/lights/lights.py @@ -62,7 +62,6 @@ def spawn_light( non_usd_cfg_param_names = ["func", "copy_from_source", "visible", "semantic_tags", "spawn_path"] for param_name in non_usd_cfg_param_names: del cfg[param_name] - # set into USD API for attr_name, value in cfg.items(): # special operation for texture properties # note: this is only used for dome light @@ -81,5 +80,4 @@ def spawn_light( prim_prop_name = f"inputs:{attr_name}" # set the attribute safe_set_attribute_on_usd_prim(prim, prim_prop_name, value, camel_case=True) - # return the prim return prim diff --git a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py index 5861241e8345..536793e79788 100644 --- a/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py +++ b/source/isaaclab/isaaclab/sim/spawners/meshes/meshes.py @@ -59,7 +59,6 @@ def spawn_mesh_sphere( Raises: ValueError: If a prim already exists at the given path. """ - # create a trimesh sphere sphere = trimesh.creation.uv_sphere(radius=cfg.radius) # obtain stage handle @@ -101,7 +100,6 @@ def spawn_mesh_cuboid( Raises: ValueError: If a prim already exists at the given path. """ - # create a trimesh box box = trimesh.creation.box(cfg.size) # obtain stage handle @@ -300,8 +298,6 @@ def spawn_mesh_rectangle( dtype=np.float32, ) rectangle = trimesh.Trimesh(vertices=vertices, faces=((0, 1, 2), (0, 2, 3)), process=False) - - # obtain stage handle stage = get_current_stage() # spawn the rectangle as a mesh _spawn_mesh_geom_from_mesh(prim_path, cfg, rectangle, translation, orientation, None, stage=stage) @@ -439,11 +435,8 @@ def _spawn_mesh_geom_from_mesh( if not is_rigid_material: raise ValueError("Rigid properties require a rigid physics material.") - # create all the paths we need for clarity geom_prim_path = prim_path + "/geometry" mesh_prim_path = geom_prim_path + "/mesh" - - # create the mesh prim mesh_prim = create_prim( mesh_prim_path, prim_type="Mesh", @@ -505,7 +498,6 @@ def _spawn_mesh_geom_from_mesh( else: schemas.define_collision_properties(mesh_prim_path, cfg.collision_props, stage=stage) - # apply visual material if cfg.visual_material is not None: if not cfg.visual_material_path.startswith("/"): material_path = f"{geom_prim_path}/{cfg.visual_material_path}" @@ -513,10 +505,7 @@ def _spawn_mesh_geom_from_mesh( material_path = cfg.visual_material_path # create material cfg.visual_material.func(material_path, cfg.visual_material) - # apply material bind_visual_material(mesh_prim_path, material_path, stage=stage) - - # apply physics material if cfg.physics_material is not None: if not cfg.physics_material_path.startswith("/"): material_path = f"{geom_prim_path}/{cfg.physics_material_path}" @@ -524,14 +513,12 @@ def _spawn_mesh_geom_from_mesh( material_path = cfg.physics_material_path # create material (accepts a legacy material cfg or rigid-body fragment(s)) spawn_physics_material(material_path, cfg.physics_material, stage=stage) - # apply material bind_physics_material(prim_path, material_path, stage=stage) # note: we apply the rigid properties to the parent prim in case of rigid objects. # fragment path: mapping entries anchor at the container prim, so ``""`` preserves the legacy # placement; entries apply in insertion order. Otherwise a legacy cfg routes to the legacy writer. if cfg.rigid_props is not None: - # apply mass properties if cfg.mass_props is not None: mass_props_mapping = fragment_mapping(cfg.mass_props) if mass_props_mapping is not None: diff --git a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py index 9cb03787eb0b..5b0a56d07a2e 100644 --- a/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py +++ b/source/isaaclab/isaaclab/sim/spawners/sensors/sensors.py @@ -17,7 +17,6 @@ if TYPE_CHECKING: from . import sensors_cfg -# import logger logger = logging.getLogger(__name__) CUSTOM_PINHOLE_CAMERA_ATTRIBUTES = { @@ -142,7 +141,6 @@ def spawn_camera( Raises: ValueError: If a prim already exists at the given path. """ - # obtain stage handle stage = get_current_stage() # spawn camera if it doesn't exist. @@ -186,28 +184,19 @@ def spawn_camera( # create attributes for the fisheye camera model # note: for pinhole those are already part of the USD camera prim for attr_name, attr_type in attribute_types.values(): - # check if attribute does not exist if prim.GetAttribute(attr_name).Get() is None: - # create attribute based on type prim.CreateAttribute(attr_name, attr_type) - # set attribute values for param_name, param_value in cfg.__dict__.items(): - # check if value is valid if param_value is None or param_name in non_usd_cfg_param_names: continue - # obtain prim property name if param_name in attribute_types: - # check custom attributes prim_prop_name = attribute_types[param_name][0] else: - # convert attribute name in prim to cfg name prim_prop_name = to_camel_case(param_name, to="cC") - # get attribute from the class prim.GetAttribute(prim_prop_name).Set(param_value) # author the OpenCV lens-distortion model (renderer-agnostic; RTX/OVRTX honors it natively) if cfg.distortion is not None: _author_opencv_distortion(prim, cfg.distortion) - # return the prim return prim diff --git a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py index 656011724bc9..fefcf1090c53 100644 --- a/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py +++ b/source/isaaclab/isaaclab/sim/spawners/shapes/shapes.py @@ -338,7 +338,6 @@ def _spawn_geom_from_prim_type( Raises: ValueError: If a prim already exists at the given path. """ - # obtain stage handle stage = stage if stage is not None else get_current_stage() # spawn geometry if it doesn't exist. @@ -350,8 +349,6 @@ def _spawn_geom_from_prim_type( # create all the paths we need for clarity geom_prim_path = prim_path + "/geometry" mesh_prim_path = geom_prim_path + "/mesh" - - # create the geometry prim create_prim(mesh_prim_path, prim_type, scale=scale, attributes=attributes, stage=stage) if geometry_schema_func is not None: geometry_schema_func(mesh_prim_path, stage=stage) @@ -376,9 +373,7 @@ def _spawn_geom_from_prim_type( material_path = cfg.visual_material_path # create material cfg.visual_material.func(material_path, cfg.visual_material) - # apply material bind_visual_material(mesh_prim_path, material_path, stage=stage) - # apply physics material if cfg.physics_material is not None: if not cfg.physics_material_path.startswith("/"): material_path = f"{geom_prim_path}/{cfg.physics_material_path}" @@ -386,7 +381,6 @@ def _spawn_geom_from_prim_type( material_path = cfg.physics_material_path # create material (accepts a legacy material cfg or rigid-body fragment(s)) spawn_physics_material(material_path, cfg.physics_material, stage=stage) - # apply material bind_physics_material(mesh_prim_path, material_path, stage=stage) # note: we apply rigid properties in the end to later make the instanceable prim diff --git a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py index 7ef40b4933f1..dbd28ea32894 100644 --- a/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py +++ b/source/isaaclab/isaaclab/sim/spawners/wrappers/wrappers.py @@ -76,7 +76,6 @@ def spawn_multi_asset( for asset_prim_path, asset_cfg in zip(asset_prim_paths, cfg.assets_cfg): if asset_prim_path is None: continue - # append semantic tags if specified if cfg.semantic_tags is not None: if asset_cfg.semantic_tags is None: asset_cfg.semantic_tags = cfg.semantic_tags diff --git a/source/isaaclab/isaaclab/sim/utils/legacy.py b/source/isaaclab/isaaclab/sim/utils/legacy.py index 0e3aef861733..c6ceb19dbb70 100644 --- a/source/isaaclab/isaaclab/sim/utils/legacy.py +++ b/source/isaaclab/isaaclab/sim/utils/legacy.py @@ -23,7 +23,6 @@ from .queries import get_next_free_prim_path from .stage import get_current_stage -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/sim/utils/prims.py b/source/isaaclab/isaaclab/sim/utils/prims.py index 4fcd4cc7471e..185ef9f6529d 100644 --- a/source/isaaclab/isaaclab/sim/utils/prims.py +++ b/source/isaaclab/isaaclab/sim/utils/prims.py @@ -34,7 +34,6 @@ from ..spawners.spawner_cfg import SpawnerCfg -# import logger logger = logging.getLogger(__name__) @@ -137,33 +136,24 @@ def create_prim( """ from pxr import UsdGeom # noqa: PLC0415 - # Ensure that user doesn't provide both position and translation if position is not None and translation is not None: raise ValueError("Cannot provide both position and translation. Please provide only one.") - # obtain stage handle stage = get_current_stage() if stage is None else stage - - # check if prim already exists if stage.GetPrimAtPath(prim_path).IsValid(): raise ValueError(f"A prim already exists at path: '{prim_path}'.") - # create prim in stage prim = stage.DefinePrim(prim_path, prim_type) if not prim.IsValid(): raise ValueError(f"Failed to create prim at path: '{prim_path}' of type: '{prim_type}'.") - # apply attributes into prim if attributes is not None: for k, v in attributes.items(): prim.GetAttribute(k).Set(v) - # add reference to USD file if usd_path is not None: add_usd_reference(prim_path=prim_path, usd_path=usd_path, stage=stage) - # add semantic label to prim if semantic_label is not None: add_labels(prim, labels=[semantic_label], instance_name=semantic_type) - # check if prim type is Xformable if not prim.IsA(UsdGeom.Xformable): logger.debug( f"Prim at path '{prim.GetPath().pathString}' is of type '{prim.GetTypeName()}', " @@ -172,7 +162,6 @@ def create_prim( ) return prim - # convert input arguments to tuples position = _to_tuple(position) if position is not None else None translation = _to_tuple(translation) if translation is not None else None orientation = _to_tuple(orientation) if orientation is not None else None @@ -183,8 +172,6 @@ def create_prim( if position is not None: # this means that user provided pose in the world frame translation, orientation = convert_world_pose_to_local(position, orientation, ref_prim=prim.GetParent()) - - # standardize the xform ops standardize_xform_ops(prim, translation, orientation, scale) return prim @@ -211,7 +198,6 @@ def delete_prim(prim_path: str | Sequence[str], stage: Usd.Stage | None = None) # convert prim_path to list if it is a string if isinstance(prim_path, str): prim_path = [prim_path] - # get stage handle stage = get_current_stage() if stage is None else stage # FIXME: We should not need to cache the stage here. It should # happen at the creation of the stage. @@ -323,10 +309,8 @@ def safe_set_attribute_on_usd_schema(schema_api: Usd.APISchemaBase, name: str, v Raises: TypeError: When the input attribute name does not exist on the provided schema API. """ - # if value is None, do nothing if value is None: return - # convert attribute name to camel case if camel_case: attr_name = to_camel_case(name, to="CC") else: @@ -359,13 +343,11 @@ def safe_set_attribute_on_usd_prim(prim: Usd.Prim, attr_name: str, value: Any, c """ from pxr import Sdf # noqa: PLC0415 - # if value is None, do nothing if value is None: return # convert attribute name to camel case if camel_case: attr_name = to_camel_case(attr_name, to="cC") - # resolve sdf type based on value if isinstance(value, bool): sdf_type = Sdf.ValueTypeNames.Bool elif isinstance(value, int): @@ -383,7 +365,6 @@ def safe_set_attribute_on_usd_prim(prim: Usd.Prim, attr_name: str, value: Any, c f"Cannot set attribute '{attr_name}' with value '{value}'. Please modify the code to support this type." ) - # change property using the change_prim_property function change_prim_property( prop_path=f"{prim.GetPath()}.{attr_name}", value=value, @@ -447,7 +428,6 @@ def change_prim_property( """ from pxr import Sdf, Usd # noqa: PLC0415 - # get stage handle stage = get_current_stage() if stage is None else stage # convert to Sdf.Path if needed @@ -459,7 +439,6 @@ def change_prim_property( if not prim or not prim.IsValid(): raise ValueError(f"Prim does not exist at path: '{prim_path}'") - # get or create the property prop = stage.GetPropertyAtPath(prop_path) if not prop: @@ -738,7 +717,6 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs): prim_spawn_path = f"{source_prim_paths[0]}/{asset_path.replace('.*', '0')}" # spawn single instance prim = func(prim_spawn_path, cfg, *args, **kwargs) - # set the prim visibility if hasattr(cfg, "visible"): imageable = UsdGeom.Imageable(prim) if cfg.visible: @@ -810,11 +788,9 @@ def bind_visual_material( """ from pxr import UsdShade # noqa: PLC0415 - # get stage handle if stage is None: stage = get_current_stage() - # check if prim and material exists prim = stage.GetPrimAtPath(prim_path) if not prim.IsValid(): raise ValueError(f"Target prim '{prim_path}' does not exist.") @@ -864,7 +840,6 @@ def bind_physics_material( """ from pxr import UsdPhysics, UsdShade # noqa: PLC0415 - # get stage handle if stage is None: stage = get_current_stage() @@ -948,9 +923,7 @@ def add_usd_reference( except Exception as e: raise FileNotFoundError(f"Failed to retrieve USD file from {usd_path}") from e - # get current stage stage = get_current_stage() if stage is None else stage - # get prim at path prim = stage.GetPrimAtPath(prim_path) if not prim.IsValid(): prim = stage.DefinePrim(prim_path, prim_type) @@ -980,9 +953,7 @@ def get_usd_references(prim_path: str, stage: Usd.Stage | None = None) -> list[s Raises: ValueError: If the prim at the specified path is not valid. """ - # get stage handle stage = get_current_stage() if stage is None else stage - # get prim at path prim = stage.GetPrimAtPath(prim_path) if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") @@ -1133,7 +1104,6 @@ def _to_tuple(value: Any) -> tuple[float, ...]: # This is common when batched operations produce single-item batches if value.ndim != 1: value = value.squeeze() - # Validate that the result is one-dimensional if value.ndim != 1: raise ValueError(f"Input value is not one dimensional: {value.shape}") diff --git a/source/isaaclab/isaaclab/sim/utils/queries.py b/source/isaaclab/isaaclab/sim/utils/queries.py index 1a90ac8e4aec..079fa509ccf1 100644 --- a/source/isaaclab/isaaclab/sim/utils/queries.py +++ b/source/isaaclab/isaaclab/sim/utils/queries.py @@ -19,7 +19,6 @@ if TYPE_CHECKING: from pxr import Sdf, Usd, UsdPhysics # noqa: F401 -# import logger logger = logging.getLogger(__name__) _CHARACTER_CLASS = re.compile(r"\[\^?[^]]*\]") @@ -127,31 +126,22 @@ def get_first_matching_ancestor_prim( Raises: ValueError: If the prim path is not global (i.e: does not start with '/'). """ - # get stage handle if stage is None: stage = get_current_stage() - # make paths str type if they aren't already prim_path = str(prim_path) - # check if prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - # get prim prim = stage.GetPrimAtPath(prim_path) - # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") # walk up to find the first matching ancestor prim ancestor_prim = prim while ancestor_prim and ancestor_prim.IsValid(): - # check if prim passes predicate if predicate(ancestor_prim): return ancestor_prim - # get parent prim ancestor_prim = ancestor_prim.GetParent() - - # If no ancestor prim passes the predicate, return None return None @@ -191,18 +181,13 @@ def get_first_matching_child_prim( """ from pxr import Usd # noqa: PLC0415 - # get stage handle if stage is None: stage = get_current_stage() - # make paths str type if they aren't already prim_path = str(prim_path) - # check if prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - # get prim prim = stage.GetPrimAtPath(prim_path) - # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") # iterate over all prims under prim-path @@ -210,10 +195,8 @@ def get_first_matching_child_prim( while len(all_prims) > 0: # get current prim child_prim = all_prims.pop(0) - # check if prim passes predicate if predicate(child_prim): return child_prim - # add children to list if traverse_instance_prims: all_prims += child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) else: @@ -265,21 +248,15 @@ def get_all_matching_child_prims( """ from pxr import Usd # noqa: PLC0415 - # get stage handle if stage is None: stage = get_current_stage() - # make paths str type if they aren't already prim_path = str(prim_path) - # check if prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - # get prim prim = stage.GetPrimAtPath(prim_path) - # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") - # check if depth is valid if depth is not None and depth <= 0: raise ValueError(f"Depth must be bigger than zero, got {depth}.") if expected_num_matches is not None and expected_num_matches < 0: @@ -292,17 +269,13 @@ def get_all_matching_child_prims( while len(all_prims_queue) > 0: # get current prim child_prim, current_depth = all_prims_queue.pop(0) - # check if prim passes predicate if predicate(child_prim): output_prims.append(child_prim) - # add children to list if depth is None or current_depth < depth: - # resolve prims under the current prim if traverse_instance_prims: children = child_prim.GetFilteredChildren(Usd.TraverseInstanceProxies()) else: children = child_prim.GetChildren() - # add children to list all_prims_queue += [(child, current_depth + 1) for child in children] if expected_num_matches is not None and len(output_prims) != expected_num_matches: @@ -543,15 +516,12 @@ def find_global_fixed_joint_prim( """ from pxr import Usd, UsdPhysics # noqa: PLC0415 - # get stage handle if stage is None: stage = get_current_stage() # check prim path is global if not prim_path.startswith("/"): raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") - - # check if prim exists prim = stage.GetPrimAtPath(prim_path) if not prim.IsValid(): raise ValueError(f"Prim at path '{prim_path}' is not valid.") diff --git a/source/isaaclab/isaaclab/sim/utils/semantics.py b/source/isaaclab/isaaclab/sim/utils/semantics.py index 04601e515149..68e91dec1377 100644 --- a/source/isaaclab/isaaclab/sim/utils/semantics.py +++ b/source/isaaclab/isaaclab/sim/utils/semantics.py @@ -120,10 +120,7 @@ def check_missing_labels(prim_path: str | None = None, stage: Usd.Stage | None = """ from pxr import Usd, UsdGeom # noqa: PLC0415 - # check if stage is valid stage = stage if stage else get_current_stage() - - # check if inspect path is valid start_prim = stage.GetPrimAtPath(prim_path) if prim_path else stage.GetPseudoRoot() if not start_prim: # Allow None prim_path for whole stage check, warn if path specified but not found diff --git a/source/isaaclab/isaaclab/sim/utils/stage.py b/source/isaaclab/isaaclab/sim/utils/stage.py index 6519770ceaa6..b0a0f3df8394 100644 --- a/source/isaaclab/isaaclab/sim/utils/stage.py +++ b/source/isaaclab/isaaclab/sim/utils/stage.py @@ -19,7 +19,6 @@ if TYPE_CHECKING: from pxr import Sdf, Usd, UsdUtils # noqa: F401 -# import logger logger = logging.getLogger(__name__) _context = threading.local() # thread-local storage to handle nested contexts and concurrent access @@ -385,32 +384,24 @@ def save_stage(usd_path: str, save_and_reload_in_place: bool = True) -> bool: """ from pxr import Sdf, Usd # noqa: PLC0415 - # check if USD file is supported if not Usd.Stage.IsSupportedFile(usd_path): raise ValueError(f"The USD file at path '{usd_path}' is not supported.") - # create new layer layer = Sdf.Layer.CreateNew(usd_path) if layer is None: raise RuntimeError(f"Failed to create new USD layer at path '{usd_path}'.") - # get root layer root_layer = get_current_stage().GetRootLayer() - # transfer content from root layer to new layer layer.TransferContent(root_layer) # resolve paths so asset references remain valid from the new location resolve_paths(root_layer.identifier, layer.identifier) - # save layer result = layer.Save() if not result: logger.error(f"Failed to save USD layer to path '{usd_path}'.") - - # if requested, open the saved USD file in place if save_and_reload_in_place and result: open_stage(usd_path) - return result @@ -517,7 +508,6 @@ def _predicate_from_path(prim: Usd.Prim) -> bool: # Custom predicate must also pass the deletable check return predicate(prim) and _is_prim_deletable(prim) - # get all prims to delete prims = get_all_matching_child_prims("/", _predicate_from_path) # convert prims to prim paths prim_paths_to_delete = [prim.GetPath().pathString for prim in prims] @@ -574,7 +564,6 @@ def get_current_stage_id() -> int: """ from pxr import UsdUtils # noqa: PLC0415 - # get current stage stage = get_current_stage() if stage is None: raise RuntimeError("No current stage available. Did you create a stage?") @@ -588,7 +577,6 @@ def get_current_stage_id() -> int: if not stage.GetRootLayer(): raise RuntimeError("Stage has no root layer - cannot cache an incomplete stage.") stage_id = stage_cache.Insert(stage).ToLongInt() - # return stage ID return stage_id diff --git a/source/isaaclab/isaaclab/sim/utils/transforms.py b/source/isaaclab/isaaclab/sim/utils/transforms.py index 3bcffa9ba257..3cf9bd36837d 100644 --- a/source/isaaclab/isaaclab/sim/utils/transforms.py +++ b/source/isaaclab/isaaclab/sim/utils/transforms.py @@ -20,7 +20,6 @@ if TYPE_CHECKING: from pxr import Gf, Sdf, Usd, UsdGeom # noqa: F401 -# import logger logger = logging.getLogger(__name__) @@ -129,11 +128,8 @@ def standardize_xform_ops( """ from pxr import Gf, Sdf, UsdGeom # noqa: PLC0415 - # Validate prim if not prim.IsValid(): raise ValueError(f"Prim at path '{prim.GetPath()}' is not valid.") - - # Check if prim is an Xformable if not prim.IsA(UsdGeom.Xformable): logger.error( f"Prim at path '{prim.GetPath().pathString}' is of type '{prim.GetTypeName()}', " @@ -142,9 +138,7 @@ def standardize_xform_ops( ) return False - # Create xformable interface xformable = UsdGeom.Xformable(prim) - # Get current property names prop_names = prim.GetPropertyNames() # Obtain current local transformations @@ -159,7 +153,6 @@ def standardize_xform_ops( # orientation is (x, y, z, w), Gf.Quatd expects (w, x, y, z) xform_quat = Gf.Quatd(orientation[3], orientation[0], orientation[1], orientation[2]) - # Handle scale resolution if scale is not None: # User provided scale xform_scale = Gf.Vec3d(scale) @@ -175,7 +168,6 @@ def standardize_xform_ops( # No scale exists, use default uniform scale xform_scale = Gf.Vec3d(1.0, 1.0, 1.0) - # Verify if xform stack is reset has_reset = xformable.GetResetXformStack() # Ensure the prim has an "over" spec on the edit target layer. Prims from @@ -192,7 +184,6 @@ def standardize_xform_ops( # Batch the operations with Sdf.ChangeBlock(): - # Clear the existing transform operation order for prop_name in prop_names: if prop_name in _INVALID_XFORM_OPS: prim.RemoveProperty(prop_name) @@ -247,11 +238,9 @@ def validate_standard_xform_ops(prim: Usd.Prim) -> bool: """ from pxr import UsdGeom # noqa: PLC0415 - # check if prim is valid if not prim.IsValid(): logger.error(f"Prim at path '{prim.GetPath().pathString}' is not valid.") return False - # check if prim is an xformable if not prim.IsA(UsdGeom.Xformable): logger.error(f"Prim at path '{prim.GetPath().pathString}' is not an xformable.") return False @@ -319,7 +308,6 @@ def resolve_prim_pose( """ from pxr import Sdf, Usd, UsdGeom # noqa: PLC0415 - # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim.GetPath().pathString}' is not valid.") # get prim xform @@ -382,7 +370,6 @@ def resolve_prim_scale(prim: Usd.Prim) -> tuple[float, float, float]: """ from pxr import Usd, UsdGeom # noqa: PLC0415 - # check if prim is valid if not prim.IsValid(): raise ValueError(f"Prim at path '{prim.GetPath().pathString}' is not valid.") # compute local to world transform @@ -444,7 +431,6 @@ def convert_world_pose_to_local( """ from pxr import Gf, Sdf, Usd, UsdGeom # noqa: PLC0415 - # Check if prim is valid if not ref_prim.IsValid(): raise ValueError(f"Reference prim at path '{ref_prim.GetPath().pathString}' is not valid.") @@ -457,7 +443,6 @@ def convert_world_pose_to_local( # Get reference prim's world transform ref_world_tf = ref_xformable.ComputeLocalToWorldTransform(Usd.TimeCode.Default()) - # Create world transform for the desired position and orientation desired_world_tf = Gf.Matrix4d() desired_world_tf.SetTranslateOnly(Gf.Vec3d(*position)) diff --git a/source/isaaclab/isaaclab/terrains/height_field/utils.py b/source/isaaclab/isaaclab/terrains/height_field/utils.py index 256e8129fe34..16f327fef294 100644 --- a/source/isaaclab/isaaclab/terrains/height_field/utils.py +++ b/source/isaaclab/isaaclab/terrains/height_field/utils.py @@ -71,7 +71,6 @@ def wrapper(difficulty: float, cfg: HfTerrainBaseCfg): y2 = int((cfg.size[1] * 0.5 + 1) / cfg.horizontal_scale) origin_z = np.max(heights[x1:x2, y1:y2]) * cfg.vertical_scale origin = np.array([0.5 * cfg.size[0], 0.5 * cfg.size[1], origin_z]) - # return mesh and origin return [mesh], origin return wrapper @@ -117,7 +116,6 @@ def convert_height_field_to_mesh( - **triangles** (np.ndarray(int)): Array of shape (num_triangles, 3). Each row represents the indices of the 3 vertices connected by this triangle. """ - # read height field num_rows, num_cols = height_field.shape # create a mesh grid of the height field y = np.linspace(0, (num_cols - 1) * horizontal_scale, num_cols) diff --git a/source/isaaclab/isaaclab/terrains/terrain_generator.py b/source/isaaclab/isaaclab/terrains/terrain_generator.py index 0ad26bb830af..dc2cd386a83f 100644 --- a/source/isaaclab/isaaclab/terrains/terrain_generator.py +++ b/source/isaaclab/isaaclab/terrains/terrain_generator.py @@ -24,7 +24,6 @@ from .sub_terrain_cfg import FlatPatchSamplingCfg, SubTerrainBaseCfg from .terrain_generator_cfg import TerrainGeneratorCfg -# import logger logger = logging.getLogger(__name__) @@ -149,7 +148,7 @@ def __init__(self, cfg: TerrainGeneratorCfg, device: str = "cpu"): # buffer for storing valid patches self.flat_patches = {} # create a list of all sub-terrains - self.terrain_meshes = list() + self.terrain_meshes = [] self.terrain_origins = np.zeros((self.cfg.num_rows, self.cfg.num_cols, 3)) # parse configuration and add sub-terrains diff --git a/source/isaaclab/isaaclab/terrains/terrain_importer.py b/source/isaaclab/isaaclab/terrains/terrain_importer.py index 94a37f44ee96..9629c9c48825 100644 --- a/source/isaaclab/isaaclab/terrains/terrain_importer.py +++ b/source/isaaclab/isaaclab/terrains/terrain_importer.py @@ -21,7 +21,6 @@ from .terrain_generator_cfg import TerrainGeneratorCfg from .terrain_importer_cfg import TerrainImporterCfg -# import logger logger = logging.getLogger(__name__) @@ -74,11 +73,11 @@ def __init__(self, cfg: TerrainImporterCfg): self.device = sim_utils.SimulationContext.instance().device # type: ignore # create buffers for the terrains - self.terrain_prim_paths = list() + self.terrain_prim_paths = [] self.terrain_origins = None self.env_origins = None # assigned later when `configure_env_origins` is called # private variables - self._terrain_flat_patches = dict() + self._terrain_flat_patches = {} # auto-import the terrain based on the config if self.cfg.terrain_type == "generator": @@ -166,7 +165,6 @@ def set_debug_vis(self, debug_vis: bool) -> bool: Raises: RuntimeError: If terrain origins are not configured. """ - # create a marker if necessary if debug_vis: if not hasattr(self, "origin_visualizer"): self.origin_visualizer = VisualizationMarkers( @@ -178,7 +176,6 @@ def set_debug_vis(self, debug_vis: bool) -> bool: self.origin_visualizer.visualize(self.env_origins.reshape(-1, 3)) else: raise RuntimeError("Terrain origins are not configured.") - # set visibility self.origin_visualizer.set_visibility(True) else: if hasattr(self, "origin_visualizer"): @@ -227,7 +224,6 @@ def import_ground_plane(self, name: str, size: tuple[float, float] | None = None " Preserving the ground plane's authored material." ) - # get the mesh ground_plane_cfg = sim_utils.GroundPlaneCfg(physics_material=self.cfg.physics_material, size=size, color=color) ground_plane_cfg.func(prim_path, ground_plane_cfg) @@ -252,7 +248,6 @@ def import_mesh(self, name: str, mesh: trimesh.Trimesh): raise ValueError( f"A terrain with the name '{name}' already exists. Existing terrains: {', '.join(self.terrain_names)}." ) - # store the mesh name self.terrain_prim_paths.append(prim_path) # import the mesh @@ -333,7 +328,6 @@ def import_usd(self, name: str, usd_path: str): # store the mesh name self.terrain_prim_paths.append(prim_path) - # add the prim path cfg = sim_utils.UsdFileCfg(usd_path=usd_path) cfg.func(prim_path, cfg) diff --git a/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py b/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py index 52d8956f9121..1398708c18ff 100644 --- a/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py +++ b/source/isaaclab/isaaclab/terrains/trimesh/mesh_terrains.py @@ -41,11 +41,8 @@ def flat_terrain( Returns: A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). """ - # compute the position of the terrain origin = (cfg.size[0] / 2.0, cfg.size[1] / 2.0, 0.0) - # compute the vertices of the terrain plane_mesh = make_plane(cfg.size, 0.0, center_zero=False) - # return the tri-mesh and the position return [plane_mesh], np.array(origin) @@ -105,11 +102,8 @@ def pyramid_stairs_terrain( box_size = (cfg.platform_width, cfg.platform_width) else: box_size = (terrain_size[0] - 2 * k * cfg.step_width, terrain_size[1] - 2 * k * cfg.step_width) - # compute the quantities of the box - # -- location box_z = terrain_center[2] + k * step_height / 2.0 box_offset = (k + 0.5) * cfg.step_width - # -- dimensions box_height = (k + 2) * step_height # generate the boxes # top/bottom @@ -206,11 +200,8 @@ def inverted_pyramid_stairs_terrain( box_size = (cfg.platform_width, cfg.platform_width) else: box_size = (terrain_size[0] - 2 * k * cfg.step_width, terrain_size[1] - 2 * k * cfg.step_width) - # compute the quantities of the box - # -- location box_z = terrain_center[2] - total_height / 2 - (k + 1) * step_height / 2.0 box_offset = (k + 0.5) * cfg.step_width - # -- dimensions box_height = total_height - (k + 1) * step_height # generate the boxes # top/bottom diff --git a/source/isaaclab/isaaclab/terrains/trimesh/utils.py b/source/isaaclab/isaaclab/terrains/trimesh/utils.py index aede42f3b7da..ebf846f608a2 100644 --- a/source/isaaclab/isaaclab/terrains/trimesh/utils.py +++ b/source/isaaclab/isaaclab/terrains/trimesh/utils.py @@ -29,7 +29,6 @@ def make_plane(size: tuple[float, float], height: float, center_zero: bool = Tru Returns: A trimesh.Trimesh objects for the plane. """ - # compute the vertices of the terrain x0 = [size[0], size[1], height] x1 = [size[0], 0.0, height] x2 = [0.0, size[1], height] @@ -38,10 +37,8 @@ def make_plane(size: tuple[float, float], height: float, center_zero: bool = Tru vertices = np.array([x0, x1, x2, x3]) faces = np.array([[1, 0, 2], [2, 3, 1]]) plane_mesh = trimesh.Trimesh(vertices=vertices, faces=faces) - # center the plane at the origin if center_zero: plane_mesh.apply_translation(-np.array([size[0] / 2.0, size[1] / 2.0, 0.0])) - # return the tri-mesh and the position return plane_mesh @@ -73,10 +70,8 @@ def make_border( Returns: A list of trimesh.Trimesh objects that represent the border. """ - # compute thickness of the border thickness_x = (size[0] - inner_size[0]) / 2.0 thickness_y = (size[1] - inner_size[1]) / 2.0 - # generate tri-meshes for the border # top/bottom border box_dims = (size[0], thickness_y, height) # -- top @@ -93,7 +88,6 @@ def make_border( # -- right box_pos = (position[0] + inner_size[0] / 2.0 + thickness_x / 2.0, position[1], position[2]) box_mesh_right = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) - # return the tri-meshes return [box_mesh_left, box_mesh_right, box_mesh_top, box_mesh_bottom] diff --git a/source/isaaclab/isaaclab/terrains/utils.py b/source/isaaclab/isaaclab/terrains/utils.py index f93e6e18cbab..9b7038c96cfd 100644 --- a/source/isaaclab/isaaclab/terrains/utils.py +++ b/source/isaaclab/isaaclab/terrains/utils.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause -# needed to import for allowing type-hinting: np.ndarray | torch.Tensor | None from __future__ import annotations import numpy as np @@ -36,9 +35,7 @@ def color_meshes_by_height(meshes: list[trimesh.Trimesh], **kwargs) -> trimesh.T Returns: A trimesh object with the vertices colored based on the z-coordinate (height) of each vertex. """ - # Combine all meshes into a single mesh mesh = trimesh.util.concatenate(meshes) - # Get the z-coordinates of each vertex heights = mesh.vertices[:, 2] # Check if the z-coordinates are all the same if np.max(heights) == np.min(heights): @@ -174,7 +171,6 @@ def find_flat_patches( RuntimeError: If the function fails to find valid patches. This can happen if the input parameters are not suitable for finding valid patches and maximum number of iterations is reached. """ - # set device to warp mesh device device = wp.device_to_torch(wp_mesh.device) # resolve inputs to consistent type diff --git a/source/isaaclab/isaaclab/ui/widgets/image_plot.py b/source/isaaclab/isaaclab/ui/widgets/image_plot.py index 2a212c6da098..9b48d1eed772 100644 --- a/source/isaaclab/isaaclab/ui/widgets/image_plot.py +++ b/source/isaaclab/isaaclab/ui/widgets/image_plot.py @@ -24,7 +24,6 @@ import isaacsim.gui.components import omni.ui -# import logger logger = logging.getLogger(__name__) @@ -74,7 +73,6 @@ def __init__( min_value: Minimum value for manual normalization/colorization. Defaults to 0.0. max_value: Maximum value for manual normalization/colorization. Defaults to 1.0. """ - self._curr_mode = "None" self._has_built = False diff --git a/source/isaaclab/isaaclab/ui/widgets/line_plot.py b/source/isaaclab/isaaclab/ui/widgets/line_plot.py index 4854cbe37d04..06a3c7eb1745 100644 --- a/source/isaaclab/isaaclab/ui/widgets/line_plot.py +++ b/source/isaaclab/isaaclab/ui/widgets/line_plot.py @@ -154,7 +154,6 @@ def add_datapoint(self, y_coords: list[float]): Args: y_coords: A list of floats containing the y coordinates of the new data points. """ - for idx, y_coord in enumerate(y_coords): if len(self._y_data[idx]) > self._max_data_points: self._y_data[idx] = self._y_data[idx][1:] diff --git a/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py b/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py index 3795bd839c85..7a84134d7526 100644 --- a/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py +++ b/source/isaaclab/isaaclab/ui/widgets/manager_live_visualizer.py @@ -22,7 +22,6 @@ if TYPE_CHECKING: import omni.ui -# import logger logger = logging.getLogger(__name__) @@ -62,7 +61,6 @@ def __init__(self, manager: ManagerBase, cfg: ManagerLiveVisualizerCfg = Manager :meth:`~isaaclab.managers.manager_base.ManagerBase.get_active_iterable_terms` method. cfg: The configuration file used to select desired manager terms to be plotted. """ - self._manager = manager self.debug_vis = cfg.debug_vis self._env_idx: int = 0 @@ -171,7 +169,6 @@ def _set_vis_frame_impl(self, frame: omni.ui.Frame): def _debug_vis_callback(self, event): """Callback for the debug visualization event.""" - if not SimulationContext.instance().is_playing(): # Visualizers have not been created yet. return @@ -302,7 +299,7 @@ def __init__(self, cfg: object, managers: dict[str, ManagerBase]): self._prepare_terms() def _prepare_terms(self): - self._manager_visualizers: dict[str, ManagerLiveVisualizer] = dict() + self._manager_visualizers: dict[str, ManagerLiveVisualizer] = {} # check if config is dict already if isinstance(self.cfg, dict): diff --git a/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py b/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py index 61a32119f300..4ba62d7f8e99 100644 --- a/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py +++ b/source/isaaclab/isaaclab/ui/widgets/ui_visualizer_base.py @@ -75,10 +75,8 @@ def set_env_selection(self, env_selection: int) -> bool: Whether the environment selection was successfully set. False if the component does not support environment selection. """ - # check if environment selection is supported if not self.has_env_selection_implementation: return False - # set environment selection self._set_env_selection_impl(env_selection) return True @@ -95,10 +93,8 @@ def set_window(self, window: omni.ui.Window) -> bool: Whether the window was successfully set. False if the component does not support this functionality. """ - # check if window is supported if not self.has_window_implementation: return False - # set window self._set_window_impl(window) return True @@ -115,10 +111,8 @@ def set_vis_frame(self, vis_frame: omni.ui.Frame) -> bool: Whether the debug visualization frame was successfully set. False if the component does not support debug visualization. """ - # check if debug visualization is supported if not self.has_vis_frame_implementation: return False - # set debug visualization frame self._set_vis_frame_impl(vis_frame) return True diff --git a/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py b/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py index 1221d1de1223..96e43377976c 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/instruction_widget.py @@ -157,7 +157,6 @@ def show_instruction( Returns: UiContainer | None: The container that owns the instruction widget, or ``None`` if creation failed. """ - try: import carb from omni.kit.scene_view.xr import XRSceneView @@ -287,7 +286,6 @@ def hide_instruction(target_prim_path: str = "/newPrim") -> None: Returns: None: This function does not return a value. """ - global camera_facing_widget_container, camera_facing_widget_timers if target_prim_path in camera_facing_widget_container: diff --git a/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py b/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py index b9c87a4c59c4..32868dfc7548 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/scene_visualization.py @@ -525,7 +525,6 @@ def _register(self) -> bool: def _initialize(self, manager: type[VisualizationManager]) -> None: """Initialize the singleton instance with data collector and visualization manager.""" - self._data_collector = DataCollector() self._visualization_manager = manager(self._data_collector) @@ -576,7 +575,6 @@ def set_attrs(cls, attributes: dict[str, Any]) -> None: Args: attributes: Dictionary containing configuration keys and values """ - instance = cls.__get_instance() for name, data in attributes.items(): instance._visualization_manager.set_attr(name, data) diff --git a/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py b/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py index de5beb75bea7..ee2ab3b07661 100644 --- a/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py +++ b/source/isaaclab/isaaclab/ui/xr_widgets/teleop_visualization_manager.py @@ -61,7 +61,6 @@ def _hide_ik_error_widget(self, mgr: VisualizationManager, data_collector: DataC Args: data_collector: DataCollector instance (unused in this handler) """ - hide_instruction(mgr.ik_error_widget_id) mgr.cancel_rule(TriggerType.TRIGGER_ON_PERIOD, mgr._ik_error_widget_timer) delattr(mgr, "_ik_error_widget_timer") diff --git a/source/isaaclab/isaaclab/utils/backend_utils.py b/source/isaaclab/isaaclab/utils/backend_utils.py index 6ea8a8c0943c..1f15ca1b851a 100644 --- a/source/isaaclab/isaaclab/utils/backend_utils.py +++ b/source/isaaclab/isaaclab/utils/backend_utils.py @@ -119,13 +119,11 @@ def resolve_class(cls, *args, **kwargs) -> type: # If backend is not in registry, try to import it and register the class. # This is done to only import the module once. if backend not in cls._registry: - # Construct the module name from the backend and the determined subpath. module_name = cls._get_module_name(backend) try: module = importlib.import_module(module_name) class_name = getattr(cls, "_backend_class_names", {}).get(backend, cls.__name__) module_class = getattr(module, class_name) - # Manually register the class cls.register(backend, module_class) except ImportError as e: @@ -150,7 +148,6 @@ def resolve_class(cls, *args, **kwargs) -> type: def __new__(cls, *args, **kwargs): """Create a new instance of an implementation based on the backend.""" impl = cls.resolve_class(*args, **kwargs) - # Return an instance of the chosen class. return impl(*args, **kwargs) @classmethod diff --git a/source/isaaclab/isaaclab/utils/datasets/episode_data.py b/source/isaaclab/isaaclab/utils/datasets/episode_data.py index dcb4791457f5..40c830a001f1 100644 --- a/source/isaaclab/isaaclab/utils/datasets/episode_data.py +++ b/source/isaaclab/isaaclab/utils/datasets/episode_data.py @@ -13,7 +13,7 @@ class EpisodeData: def __init__(self) -> None: """Initializes episode data class.""" - self._data = dict() + self._data = {} self._next_action_index = 0 self._next_state_index = 0 self._next_joint_target_index = 0 @@ -96,7 +96,6 @@ def add(self, key: str, value: torch.Tensor | dict, clone: bool = True): value: The corresponding value of tensor type or of dict type. clone: Whether to clone the tensor value before storing it in the episode data. """ - # check datatype if isinstance(value, dict): for sub_key, sub_value in value.items(): self.add(f"{key}/{sub_key}", sub_value, clone=clone) diff --git a/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py b/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py index fcb4bda716a3..d2963c94c593 100644 --- a/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py +++ b/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py @@ -260,12 +260,10 @@ def create_dataset_helper(group, key, value): for key, value in episode.data.items(): create_dataset_helper(h5_episode_group, key, value) - # increment total step counts self._hdf5_data_group.attrs["total"] += h5_episode_group.attrs["num_samples"] # Only increment demo count if using default indexing if demo_id is None: - # increment total demo counts self._demo_count += 1 def flush(self): diff --git a/source/isaaclab/isaaclab/utils/dict.py b/source/isaaclab/isaaclab/utils/dict.py index 62126be9538a..8f2ecb032797 100644 --- a/source/isaaclab/isaaclab/utils/dict.py +++ b/source/isaaclab/isaaclab/utils/dict.py @@ -60,7 +60,7 @@ def class_to_dict(obj: object) -> dict[str, Any]: return obj # convert to dictionary - data = dict() + data = {} for key, value in obj_dict.items(): # disregard builtin attributes if key.startswith("__"): @@ -69,7 +69,6 @@ def class_to_dict(obj: object) -> dict[str, Any]: if isinstance(value, ResolvableString): data[key] = str(value) # check if attribute is callable -- function - # check if attribute is callable -- function elif callable(value): data[key] = callable_to_string(value) # check if attribute is a dictionary @@ -263,7 +262,7 @@ def convert_dict_to_backend( tensor_type_conversions = TENSOR_TYPE_CONVERSIONS[backend] # Parse the array types and convert them to the corresponding types: "numpy" -> np.ndarray, etc. - parsed_types = list() + parsed_types = [] for t in array_types: # Check type is valid. if t not in TENSOR_TYPES: @@ -275,7 +274,7 @@ def convert_dict_to_backend( parsed_types.append(TENSOR_TYPES[t]) # Convert the data to the desired backend. - output_dict = dict() + output_dict = {} for key, value in data.items(): # Obtain the data type of the current value. data_type = type(value) diff --git a/source/isaaclab/isaaclab/utils/leapp/leapp_semantics.py b/source/isaaclab/isaaclab/utils/leapp/leapp_semantics.py index 5fc88addc0e0..f2183989369b 100644 --- a/source/isaaclab/isaaclab/utils/leapp/leapp_semantics.py +++ b/source/isaaclab/isaaclab/utils/leapp/leapp_semantics.py @@ -81,7 +81,6 @@ def leapp_tensor_semantics( const: bool = False, ) -> Callable: """Attach LEAPP semantic metadata to a raw tensor-producing function.""" - semantics = LeappTensorSemantics( kind=kind, element_names=element_names, diff --git a/source/isaaclab/isaaclab/utils/math.py b/source/isaaclab/isaaclab/utils/math.py index 9f2ac7bf4dc3..851bde397c76 100644 --- a/source/isaaclab/isaaclab/utils/math.py +++ b/source/isaaclab/isaaclab/utils/math.py @@ -16,7 +16,6 @@ import torch import torch.nn.functional -# import logger logger = logging.getLogger(__name__) """ @@ -436,7 +435,6 @@ def matrix_from_euler(euler_angles: torch.Tensor, convention: str) -> torch.Tens if letter not in ("X", "Y", "Z"): raise ValueError(f"Invalid letter {letter} in convention string.") matrices = [_axis_angle_rotation(c, e) for c, e in zip(convention, torch.unbind(euler_angles, -1))] - # return functools.reduce(torch.matmul, matrices) return torch.matmul(torch.matmul(matrices[0], matrices[1]), matrices[2]) @@ -877,7 +875,6 @@ def rigid_body_twist_transform( return v1, w1 -# @torch.jit.script def subtract_frame_transforms( t01: torch.Tensor, q01: torch.Tensor, t02: torch.Tensor | None = None, q02: torch.Tensor | None = None ) -> tuple[torch.Tensor, torch.Tensor]: @@ -912,7 +909,6 @@ def subtract_frame_transforms( return t12, q12 -# @torch.jit.script def compute_pose_error( t01: torch.Tensor, q01: torch.Tensor, @@ -1006,7 +1002,6 @@ def apply_delta_pose( return target_pos, target_rot -# @torch.jit.script def transform_points( points: torch.Tensor, pos: torch.Tensor | None = None, quat: torch.Tensor | None = None ) -> torch.Tensor: diff --git a/source/isaaclab/isaaclab/utils/mesh.py b/source/isaaclab/isaaclab/utils/mesh.py index 9e6315cc83c7..3200cab1c2e4 100644 --- a/source/isaaclab/isaaclab/utils/mesh.py +++ b/source/isaaclab/isaaclab/utils/mesh.py @@ -59,7 +59,6 @@ def create_trimesh_from_geom_shape(prim: Usd.Prim) -> trimesh.Trimesh: Raises: ValueError: If the prim is not a supported primitive. Check PRIMITIVE_MESH_TYPES for supported primitives. """ - if prim.GetTypeName() not in PRIMITIVE_MESH_TYPES: raise ValueError(f"Prim at path '{prim.GetPath()}' is not a primitive mesh. Cannot convert to trimesh.") diff --git a/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py b/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py index 955b9d5bed9b..d279fc62f2ab 100644 --- a/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py +++ b/source/isaaclab/isaaclab/utils/modifiers/modifier_cfg.py @@ -29,7 +29,7 @@ class ModifierCfg: observation manager constructs them with the configuration, observation dimensions, and device. """ - params: dict[str, Any] = dict() + params: dict[str, Any] = {} """Parameters used by the modifier. Defaults to an empty dictionary. Function modifiers receive them as keyword arguments on each call. Class modifiers access them through this diff --git a/source/isaaclab/isaaclab/utils/sensors.py b/source/isaaclab/isaaclab/utils/sensors.py index d9016c2f885a..ce5d193dde7e 100644 --- a/source/isaaclab/isaaclab/utils/sensors.py +++ b/source/isaaclab/isaaclab/utils/sensors.py @@ -5,7 +5,6 @@ import logging -# import logger logger = logging.getLogger(__name__) diff --git a/source/isaaclab/isaaclab/utils/string.py b/source/isaaclab/isaaclab/utils/string.py index c0419bd9f8dd..f2a1d3632891 100644 --- a/source/isaaclab/isaaclab/utils/string.py +++ b/source/isaaclab/isaaclab/utils/string.py @@ -289,18 +289,15 @@ def _resolve_matching_names_impl( for target_index, potential_match_string in enumerate(list_of_strings): for key_index, re_key in enumerate(keys): if re.fullmatch(re_key, potential_match_string): - # check if match already found if target_strings_match_found[target_index]: raise ValueError( f"Multiple matches for '{potential_match_string}':" f" '{target_strings_match_found[target_index]}' and '{re_key}'!" ) - # add to list target_strings_match_found[target_index] = re_key index_list.append(target_index) names_list.append(potential_match_string) key_idx_list.append(key_index) - # add for regex key keys_match_found[key_index].append(potential_match_string) # reorder keys if they should be returned in order of the query keys if preserve_order: @@ -329,7 +326,6 @@ def _resolve_matching_names_impl( for key, value in zip(keys, keys_match_found): msg += f"\t{key}: {value}\n" msg += f"Available strings: {list_of_strings}\n" - # raise error raise ValueError( f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}" ) @@ -436,7 +432,6 @@ def resolve_matching_names_values( ValueError: When multiple matches are found for a string in the dictionary. ValueError: When not all regular expressions in the data keys are matched (if strict is True). """ - # check valid input if not isinstance(data, dict): raise TypeError(f"Input argument `data` should be a dictionary. Received: {data}") # find matching patterns diff --git a/source/isaaclab/isaaclab/utils/timer.py b/source/isaaclab/isaaclab/utils/timer.py index 00b9f52cc01e..0af1ba488de5 100644 --- a/source/isaaclab/isaaclab/utils/timer.py +++ b/source/isaaclab/isaaclab/utils/timer.py @@ -70,7 +70,7 @@ class Timer(ContextDecorator): Reference: https://gist.github.com/sumeet/1123871 """ - timing_info: ClassVar[dict[str, dict[str, float]]] = dict() + timing_info: ClassVar[dict[str, dict[str, float]]] = {} """Dictionary for storing the elapsed time per timer instances globally. This dictionary logs the timer information. The keys are the names given to the timer class @@ -78,7 +78,7 @@ class Timer(ContextDecorator): is recorded in the dictionary. """ - _welford_state: ClassVar[dict[str, float]] = dict() + _welford_state: ClassVar[dict[str, float]] = {} """Internal accumulator (m2) for Welford's online algorithm, keyed by timer name.""" enable: ClassVar[bool] = True diff --git a/source/isaaclab/isaaclab/utils/warp/fabric.py b/source/isaaclab/isaaclab/utils/warp/fabric.py index c2681552e1a5..40882c904668 100644 --- a/source/isaaclab/isaaclab/utils/warp/fabric.py +++ b/source/isaaclab/isaaclab/utils/warp/fabric.py @@ -4,10 +4,6 @@ # SPDX-License-Identifier: BSD-3-Clause # pyright: ignore -# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). # noqa: E501 -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause """Warp kernels for GPU-accelerated Fabric operations.""" diff --git a/source/isaaclab/isaaclab/utils/warp/kernels.py b/source/isaaclab/isaaclab/utils/warp/kernels.py index dfc0cc808dfb..793ef910f036 100644 --- a/source/isaaclab/isaaclab/utils/warp/kernels.py +++ b/source/isaaclab/isaaclab/utils/warp/kernels.py @@ -59,7 +59,6 @@ def raycast_mesh_kernel( return_normal: Whether to return the ray hit normals. Defaults to False. return_face_id: Whether to return the ray hit face ids. Defaults to False. """ - # get the thread id tid = wp.tid() t = float(0.0) # hit distance along ray @@ -199,7 +198,6 @@ def raycast_static_meshes_kernel( return_face_id: Whether to return the ray hit face ids. Defaults to False. return_mesh_id: Whether to return the mesh id. Defaults to False. """ - # get the thread id tid_mesh_id, tid_env, tid_ray = wp.tid() direction = ray_directions[tid_env, tid_ray] @@ -294,7 +292,6 @@ def raycast_dynamic_meshes_kernel( return_face_id: Whether to return the ray hit face ids. Defaults to False. return_mesh_id: Whether to return the mesh id. Defaults to False. """ - # get the thread id tid_mesh_id, tid_env, tid_ray = wp.tid() if not env_mask[tid_env]: return @@ -358,7 +355,6 @@ def reshape_tiled_image( num_channels: The number of channels in the image. num_tiles_x: The number of tiles in x-direction. """ - # get the thread id camera_id, height_id, width_id = wp.tid() # resolve the tile indices diff --git a/source/isaaclab/isaaclab/utils/warp/ops.py b/source/isaaclab/isaaclab/utils/warp/ops.py index 3ecc875ae047..dda48a5def15 100644 --- a/source/isaaclab/isaaclab/utils/warp/ops.py +++ b/source/isaaclab/isaaclab/utils/warp/ops.py @@ -5,7 +5,6 @@ """Wrapping around warp kernels for compatibility with torch tensors.""" -# needed to import for allowing type-hinting: torch.Tensor | None from __future__ import annotations import numpy as np @@ -114,19 +113,16 @@ def raycast_mesh( Will only return if :attr:`return_face_id` is True else returns None. The returned tensor contains :obj:`int(-1)` for missed hits. """ - # extract device and shape information shape = ray_starts.shape device = ray_starts.device # device of the mesh torch_device = wp.device_to_torch(mesh.device) - # reshape the tensors ray_starts = ray_starts.to(torch_device).view(-1, 3).contiguous() ray_directions = ray_directions.to(torch_device).view(-1, 3).contiguous() num_rays = ray_starts.shape[0] # create output tensor for the ray hits ray_hits = torch.full((num_rays, 3), float("inf"), device=torch_device).contiguous() - # map the memory to warp arrays ray_starts_wp = wp.from_torch(ray_starts, dtype=wp.vec3) ray_directions_wp = wp.from_torch(ray_directions, dtype=wp.vec3) ray_hits_wp = wp.from_torch(ray_hits, dtype=wp.vec3) @@ -152,7 +148,6 @@ def raycast_mesh( ray_face_id = None ray_face_id_wp = wp.empty((1,), dtype=wp.int32, device=torch_device) - # launch the warp kernel wp.launch( kernel=kernels.raycast_mesh_kernel, dim=num_rays, @@ -286,7 +281,6 @@ def raycast_dynamic_meshes( Will only return if :attr:`return_mesh_id` is True else returns None. The returned tensor contains :obj:`-1` for missed hits. """ - # extract device and shape information shape = ray_starts.shape device = ray_starts.device @@ -352,7 +346,6 @@ def raycast_dynamic_meshes( ### if mesh_positions_w is None and mesh_orientations_w is None: # Static mesh case, no need to pass in positions and rotations. - # launch the warp kernel wp.launch( kernel=kernels.raycast_static_meshes_kernel, dim=[n_meshes, n_envs, n_rays_per_env], From 67dcf3e942304b8fd5e379323c1da70610cfab75 Mon Sep 17 00:00:00 2001 From: r-schmitt <139814266+r-schmitt@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:38:25 -0400 Subject: [PATCH 2/3] fix camera cloning for duo_camera (#7981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description camera cloning on ovrtx for the duo-camera preset was failing, this PR addresses the issue and properly clones them Fixes # (issue) OVRTX scenes with more than one camera failing at startup with ``Layout-compatible non-array tensor shape[0] (N) must equal binding prim count (1)``. Cameras registered after the first one bound the camera prims authored on the USD stage, which is one prototype per spawn variant rather than one per environment whenever USD replication does not run, as in kitless runs on OvPhysx and Newton. Every camera now binds one prim per environment, for both its transform and its calibration columns. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Release backport - [x] Backport this pull request to the active release branch after it merges into `develop` ## Checklist Docker and GPU tests run on demand. Push the commits you want tested, then comment `run-ci` on the pull request. - [x] I have read and understood the [contribution guidelines](https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html) - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source//changelog.d/` for every touched package (do **not** edit `CHANGELOG.rst` or bump `extension.toml` — CI handles that) - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --- .../ovrtx-multi-camera-env-paths.rst | 9 +++++ .../isaaclab_ov/renderers/ovrtx_renderer.py | 40 +++++++++++++++---- .../test/test_ovrtx_renderer_contract.py | 39 ++++++++++++++++++ .../kuka-allegro-wrist-camera-pose.rst | 6 +++ .../lift/config/kuka_allegro/camera_cfg.py | 2 + 5 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 source/isaaclab_ov/changelog.d/ovrtx-multi-camera-env-paths.rst create mode 100644 source/isaaclab_tasks/changelog.d/kuka-allegro-wrist-camera-pose.rst diff --git a/source/isaaclab_ov/changelog.d/ovrtx-multi-camera-env-paths.rst b/source/isaaclab_ov/changelog.d/ovrtx-multi-camera-env-paths.rst new file mode 100644 index 000000000000..e60e6133be4c --- /dev/null +++ b/source/isaaclab_ov/changelog.d/ovrtx-multi-camera-env-paths.rst @@ -0,0 +1,9 @@ +Fixed +^^^^^ + +* Fixed OVRTX scenes with more than one camera failing at startup with + ``Layout-compatible non-array tensor shape[0] (N) must equal binding prim count (1)``. Cameras + registered after the first one bound the camera prims authored on the USD stage, which is one + prototype per spawn variant rather than one per environment whenever USD replication does not + run, as in kitless runs on OvPhysx and Newton. Every camera now binds one prim per environment, + for both its transform and its calibration columns. diff --git a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py index e5c36845f96e..8c026265a9ed 100644 --- a/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py +++ b/source/isaaclab_ov/isaaclab_ov/renderers/ovrtx_renderer.py @@ -229,6 +229,31 @@ def _write_file(output_dir: Path, file_name: str, content: str) -> None: logger.info("Wrote USD file: %s", output_path) +def _env_camera_prim_paths(camera_path_relative_to_env_0: str | None, num_instances: int) -> list[str]: + """Per-env camera prim paths derived from the env 0 prototype. + + ``CameraRenderSpec.camera_prim_paths`` names only the camera prims authored on the USD stage, + which is one prototype per spawn variant whenever USD replication does not run. That is the + kitless case: the clone plan routes ``UsdReplicateContext`` only under Kit, so OvPhysx, Newton + and OVRTX each replicate the prototype themselves. OVRTX still needs one path per environment, + which is safe to synthesize because :meth:`OVRTXRenderer.prepare_stage` requires env ids + ordered from zero. + + Args: + camera_path_relative_to_env_0: Camera prim path with the ``/World/envs/env_0/`` prefix stripped. + num_instances: Number of environments the camera is replicated into. + + Returns: + One absolute camera prim path per environment, in env id order. + + Raises: + ValueError: If the camera prototype does not live under ``/World/envs/env_0/``. + """ + if not camera_path_relative_to_env_0: + raise ValueError("OVRTX cameras must be under /World/envs/env_0/.") + return [f"/World/envs/env_{i}/{camera_path_relative_to_env_0}" for i in range(num_instances)] + + def _write_combined_stage(output_dir: Path, scene_usd: str, render_product_usd: str) -> None: """Write the scene and render product prims in one debug layer, preserving scene metadata.""" from pxr import Sdf @@ -591,7 +616,7 @@ def _initialize_camera_render_data_from_spec_legacy( render_data.resources.callback(self.backend.renderer.remove_usd, reference) logger.info("OVRTX loaded USD from string successfully") - camera_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)] + camera_paths = _env_camera_prim_paths(self._camera_rel_path, num_envs) if num_envs > 1: self._clone_sources_in_ovrtx() self._update_scene_partitions_after_clone(num_envs) @@ -672,7 +697,7 @@ def _update_scene_partitions_after_clone(self, num_envs: int): logger.info("Writing scene partitions for %d environments...", num_envs) partition_tokens = [f"env_{i}" for i in range(num_envs)] env_prim_paths = [f"/World/envs/env_{i}" for i in range(num_envs)] - camera_prim_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)] + camera_prim_paths = _env_camera_prim_paths(self._camera_rel_path, num_envs) self.backend.renderer.write_attribute( env_prim_paths, @@ -957,9 +982,10 @@ def create_render_data(self, spec: CameraRenderSpec) -> OVRTXCameraRenderData: else: self._register_camera(spec, render_data) if not self._use_ovstage: + intrinsic_prim_paths = _env_camera_prim_paths(spec.camera_path_relative_to_env_0, spec.num_instances) for name in _CAMERA_INTRINSIC_ATTRIBUTES: binding = self.backend.renderer.bind_attribute( - prim_paths=list(spec.camera_prim_paths), + prim_paths=intrinsic_prim_paths, attribute_name=name, dtype="float32", prim_mode=PrimMode.EXISTING_ONLY, @@ -976,9 +1002,7 @@ def create_render_data(self, spec: CameraRenderSpec) -> OVRTXCameraRenderData: def _register_camera(self, spec: CameraRenderSpec, render_data: OVRTXCameraRenderData) -> None: """Add another tiled product and camera binding without reloading the shared scene.""" - camera_paths = list(spec.camera_prim_paths) - if not camera_paths or not camera_paths[0].startswith("/World/envs/env_0/"): - raise ValueError("OVRTX cameras must be under /World/envs/env_0/.") + camera_paths = _env_camera_prim_paths(spec.camera_path_relative_to_env_0, spec.num_instances) scope = render_data.render_scope_name product_path = render_data.render_product_path usd = build_render_product_as_string( @@ -1963,7 +1987,7 @@ def _initialize_camera_render_data_from_spec_ovstage( self._initialized_scene = True - camera_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)] + camera_paths = _env_camera_prim_paths(self._camera_rel_path, num_envs) # Re-author the RenderProduct's camera relationship after clone. ``stage.clone`` recreates the per-env # cameras, so the RenderProduct must be pointed at the freshly-interned camera path ids to discover every @@ -2064,7 +2088,7 @@ def _update_scene_partitions_after_clone_ovstage(self, num_envs: int): """Update scene partition attributes on cloned environments and cameras (ovstage path).""" logger.info("Writing scene partitions for %d environments...", num_envs) env_prim_paths = [f"/World/envs/env_{i}" for i in range(num_envs)] - camera_prim_paths = [f"/World/envs/env_{i}/{self._camera_rel_path}" for i in range(num_envs)] + camera_prim_paths = _env_camera_prim_paths(self._camera_rel_path, num_envs) # TOKEN_ID semantic tells ovstage the uint64 values are interned string tokens, not raw integers; # the renderer resolves them back to the original "env_N" strings for scene-partition lookup. token_ids = np.array([self.backend.paths.intern_token(f"env_{i}") for i in range(num_envs)], dtype=np.uint64) diff --git a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py index dfcc61351ac6..b28cb582a637 100644 --- a/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py +++ b/source/isaaclab_ov/test/test_ovrtx_renderer_contract.py @@ -843,6 +843,45 @@ def test_intrinsic_updates_target_the_given_camera(monkeypatch, use_ovstage): assert all(not binding.unbind.called for binding in cameras[0].intrinsic_bindings) +@pytest.mark.parametrize("use_ovstage", [False, True]) +def test_registered_camera_expands_env_0_prototype_to_every_env(monkeypatch, use_ovstage): + """Kitless runs author one prototype prim, so a later camera must still bind one prim per env.""" + renderer = _make_ovrtx_renderer_without_backend() + renderer._initialized_scene = True + renderer._device = "cpu" + renderer._next_camera_id = 0 + renderer._render_product_paths = [] + renderer._use_ovstage = use_ovstage + renderer._current_ordinal = 1 + renderer.backend.renderer = MagicMock() + renderer.backend.renderer.bind_attribute.side_effect = lambda **kwargs: MagicMock() + renderer.backend.stage = MagicMock() + renderer.backend.paths = MagicMock() + renderer.backend.paths.create_path_list_from_strings.side_effect = tuple + renderer.backend.stage.query_from_path_list.side_effect = lambda paths: contextlib.nullcontext(object()) + for name in ("add_usd_reference_from_string", "apply_usd_changes", "remove_usd"): + monkeypatch.setattr(ovrtx_renderer_module.ovstage.population, name, MagicMock()) + # A wrist-mounted camera nests several segments below the env root. + relative_path = "Robot/ee_link/palm_link/Camera" + camera = renderer.create_render_data( + types.SimpleNamespace( + cfg=_make_camera_cfg(["depth"]), + device="cpu", + num_instances=3, + camera_prim_paths=(f"/World/envs/env_0/{relative_path}",), + camera_path_relative_to_env_0=relative_path, + ) + ) + expected_paths = [f"/World/envs/env_{i}/{relative_path}" for i in range(3)] + if use_ovstage: + bound_paths = [call.args[0] for call in renderer.backend.paths.create_path_list_from_strings.call_args_list] + assert bound_paths == [[camera.render_product_path], expected_paths] + else: + bound_paths = [call.kwargs["prim_paths"] for call in renderer.backend.renderer.bind_attribute.call_args_list] + # One transform binding plus one binding per calibration column, each covering every env. + assert bound_paths == [expected_paths] * (1 + len(ovrtx_renderer_module._CAMERA_INTRINSIC_ATTRIBUTES)) + + class _RecordingBinding: def __init__(self, events: list[str], name: str): self._events = events diff --git a/source/isaaclab_tasks/changelog.d/kuka-allegro-wrist-camera-pose.rst b/source/isaaclab_tasks/changelog.d/kuka-allegro-wrist-camera-pose.rst new file mode 100644 index 000000000000..7948f5fa999b --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/kuka-allegro-wrist-camera-pose.rst @@ -0,0 +1,6 @@ +Fixed +^^^^^ + +* Fixed the Kuka Allegro wrist camera rendering from its reset pose for the whole episode in the + ``duo_camera`` presets. The camera is mounted on the palm, so ``update_latest_camera_pose`` is now + enabled and its rendered view follows the arm. diff --git a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py index c32cb427ae75..fb75d01bc661 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/core/lift/config/kuka_allegro/camera_cfg.py @@ -44,6 +44,8 @@ WRIST_CAMERA_CFG = CameraCfg( prim_path="{ENV_REGEX_NS}/Robot/ee_link/palm_link/Camera", + # The camera rides on the palm, so the renderer needs its pose refreshed every capture. + update_latest_camera_pose=True, offset=CameraCfg.OffsetCfg( pos=(0.038, -0.38, -0.18), rot=(0.641, 0.641, -0.299, 0.299), From 371490734c2ca87b8eed0a8c563ca60ead61f88e Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Wed, 23 Sep 2026 16:43:37 -0400 Subject: [PATCH 3/3] Add a physics-plus-render mode to the canonical render benchmark (#7797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Follow-up to #7702. Added direct-pose and actuator-driven workloads to the canonical Franka cabinet renderer benchmark, with physics and render timings available through the benchmark API and persisted reports. - `BENCHMARK_MODE=render` (default) writes the analytic joint pose after physics and before the camera read. `BENCHMARK_MODE=physics_render` sets actuator targets before physics. Both modes still step physics. - Direct posing requires lazy sensor updates. With Isaac RTX, it also rejects visualizers that pump the Kit app before the pose write; use `--visualizer none` or `physics_render`. - Runtime benchmarks collect synchronized scopes after warmup when the task has a non-`None` `benchmark_mode` and the corresponding `ISAACLAB_PHYSICS_PROFILE` / `ISAACLAB_RENDER_PROFILE` flag is enabled. Temporary wrappers restore the original methods on success or failure, so subsequent runs do not inherit profiling overhead or duplicate samples. - Schema **1.4 remains unchanged**. `BenchmarkResult.bundle.extra`, schema JSON, and OmniPerf output include per-call mean, standard deviation, maximum [ms], and call count under `physics_*` and `render_*`. Disabled scopes add no keys, and distributed metadata is preserved. - Ordered raw samples remain in local `profile_timings.json`, outside `output_paths`. The renderer sweep groups physics calls by rendered frame and reports render, physics, and combined statistics without scraping logs. Invalid profiling output errors propagate. ```bash uv run python scripts/benchmarks/benchmark_renderer.py 'newton_*' BENCHMARK_MODE=physics_render uv run python scripts/benchmarks/benchmark_renderer.py 'newton_*' ``` Scope timing synchronizes the device and changes execution overlap, so profiled runs are diagnostics. API/OmniPerf summaries cover all captured calls; the sweep additionally removes padding frames and reports per-frame statistics. Migration: collect scope timings through the runtime benchmark; normal simulations no longer install render timers from the profiling flag. Read scalar summaries from `bundle.extra` or raw `timings_ms` pairs from the local profiling file. The old `isaaclab.renderers.render_context.RENDER_PROFILE_SCOPE` import remains available with a deprecation warning; use `isaaclab.benchmark.stepping.RENDER_PROFILE_SCOPE`. ## Validation - Benchmark, renderer orchestration, task configuration, sweep, and runtime smoke tests: 398 passed, 1 skipped. - Regression tests failed before the fixes and passed afterward for profiling restoration, the deprecated scope import, and invalid direct-pose rendering configurations. - Real Newton GPU sweeps passed in both modes with 1 environment, 3 measured frames, and 64×64 resolution. Reported statistics were independently checked against raw samples. - Formatting and changelog checks passed against the current `develop` base. - Sphinx documentation built without warnings or errors using `uv run --isolated --extra dev -- make -C docs current-docs` with `SPHINXOPTS='-D viewcode_follow_imported_members=0'` to omit imported source-code links. ## Type of change - New feature - Bug fix - Breaking change: render profiling moved from normal simulation runs into the runtime benchmark - Documentation update ## Release backport - [x] Backport this pull request to the active release branch after it merges into `develop` ## Checklist - [x] I have read and understood the contribution guidelines - [x] I have run the pre-commit checks with `uv run isaaclab -f` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source//changelog.d/` for every touched package - [x] My name already exists in `CONTRIBUTORS.md` --- docs/source/api/lab/isaaclab.benchmark.rst | 4 + .../benchmarking/benchmark_api.rst | 9 + .../benchmarking/run_benchmarks.rst | 28 ++ scripts/benchmarks/benchmark_renderer.py | 306 +++++++++++------- .../test/test_benchmark_renderer.py | 169 ++++++++-- scripts/benchmarks/test/test_runtime_smoke.py | 66 +++- .../benchmark-scope-profiling.major.rst | 33 ++ source/isaaclab/isaaclab/benchmark/api.py | 10 +- .../isaaclab/benchmark/benchmark_core.py | 15 +- .../isaaclab/benchmark/entrypoints/runtime.py | 50 ++- .../isaaclab/isaaclab/benchmark/stepping.py | 134 +++++++- .../isaaclab/renderers/render_context.py | 41 +-- .../test/benchmark/test_benchmark_core.py | 47 ++- .../test/benchmark/test_formatters.py | 9 +- .../isaaclab/test/benchmark/test_stepping.py | 116 +++++++ .../test_simulation_render_context.py | 26 +- .../render-benchmark-physics-mode.minor.rst | 10 + .../benchmark/render_benchmark/__init__.py | 2 +- .../render_benchmark/render_benchmark_env.py | 58 +++- .../render_benchmark_env_cfg.py | 42 +++ .../benchmark/test_render_benchmark_cfg.py | 93 ++++++ 21 files changed, 1058 insertions(+), 210 deletions(-) create mode 100644 source/isaaclab/changelog.d/benchmark-scope-profiling.major.rst create mode 100644 source/isaaclab_tasks/changelog.d/render-benchmark-physics-mode.minor.rst diff --git a/docs/source/api/lab/isaaclab.benchmark.rst b/docs/source/api/lab/isaaclab.benchmark.rst index 8b49b95eaf21..4d60a7ab1c14 100644 --- a/docs/source/api/lab/isaaclab.benchmark.rst +++ b/docs/source/api/lab/isaaclab.benchmark.rst @@ -150,6 +150,10 @@ Workflow Functions .. autofunction:: run_runtime_benchmark +.. autofunction:: isaaclab.benchmark.stepping.profile_physics_steps + +.. autofunction:: isaaclab.benchmark.stepping.profile_renderers + .. autofunction:: run_startup_benchmark .. autofunction:: run_training_benchmark diff --git a/docs/source/developer-tools/benchmarking/benchmark_api.rst b/docs/source/developer-tools/benchmarking/benchmark_api.rst index 22ec0644934b..25e1774bf1c7 100644 --- a/docs/source/developer-tools/benchmarking/benchmark_api.rst +++ b/docs/source/developer-tools/benchmarking/benchmark_api.rst @@ -58,6 +58,15 @@ The command prints the summary report. The paths in ``result.output_paths`` identify the schema and summary JSON files that were written. Use these paths in automation instead of reconstructing the timestamped names. +With a non-``None`` task ``benchmark_mode`` and ``ISAACLAB_RENDER_PROFILE=1`` or +``ISAACLAB_PHYSICS_PROFILE=1``, the runtime workflow also includes scalar profiling +summaries in ``result.bundle.extra``: ``physics_mean_ms``, ``physics_std_ms``, +``physics_max_ms``, ``physics_calls``, and the corresponding ``render_*`` keys. +These report mean, standard deviation, maximum time per call [ms], and call count +over the captured samples. Disabled scopes contribute no keys. The summaries are +included in schema and OmniPerf output without changing schema version 1.4. +Raw ordered samples remain in ``/profile_timings.json`` for local analysis. + Choose a request ---------------- diff --git a/docs/source/developer-tools/benchmarking/run_benchmarks.rst b/docs/source/developer-tools/benchmarking/run_benchmarks.rst index ec9296c2a079..7dbd87ec9d52 100644 --- a/docs/source/developer-tools/benchmarking/run_benchmarks.rst +++ b/docs/source/developer-tools/benchmarking/run_benchmarks.rst @@ -78,6 +78,34 @@ environment-step rate. Runtime samples random actions before starting the runtime run, ``runtime.collection_fps`` and ``runtime.total_fps`` describe the same random-action stepping workload. +Render and physics scope profiling requires a non-``None`` ``benchmark_mode`` in the +task configuration. If the field is absent or ``None``, both scopes remain disabled +even when their profiling flags are set; standard runtime reports are still produced. + +For ``Isaac-RenderBenchmark-Franka-Cabinet``, ``BENCHMARK_MODE=render`` (the default) +writes analytic joint poses after physics, while ``BENCHMARK_MODE=physics_render`` +sets actuator targets before physics. Both modes still step physics. Direct posing +requires ``scene.lazy_sensor_update=True``. With Isaac RTX, use ``--visualizer none`` +in this mode: a Kit visualizer would render before the pose write. + +Set ``ISAACLAB_PHYSICS_PROFILE=1`` to collect synchronized physics-step timings during +the runtime measurement loop. The benchmark wraps the selected physics +manager's ``step`` through :func:`~isaaclab.benchmark.stepping.profile_physics_steps` +during the measurement loop. Similarly, ``ISAACLAB_RENDER_PROFILE=1`` wraps registered +renderers through :func:`~isaaclab.benchmark.stepping.profile_renderers`, timing only +``render()`` and excluding scene updates and output readback. Both context managers install +wrappers after warmup and restore the original methods when measurement ends, including on +failure, so subsequent benchmark runs are unaffected. The render sweep +enables both flags automatically. Ordered ``[scope, elapsed_ms]`` samples are written under +``timings_ms`` in the local ``/profile_timings.json`` file. The benchmark bundle's +``extra`` dictionary holds scalar ``physics_mean_ms``, ``physics_std_ms``, ``physics_max_ms``, +``physics_calls``, and corresponding ``render_*`` summaries. Disabled scopes contribute no keys. +Schema version 1.4 remains unchanged, and OmniPerf output includes the same summaries. +These statistics describe individual calls after warmup; the render sweep instead groups physics +calls by rendered frame and discards its padding frames. The sweep reads the local profiling file +from a separate directory for each profile. Device synchronization changes execution overlap, +so these profiled runs are diagnostics rather than throughput measurements. + .. dropdown:: Canonical workstation output and provenance Headless runtime summary (output abbreviated): diff --git a/scripts/benchmarks/benchmark_renderer.py b/scripts/benchmarks/benchmark_renderer.py index 4cb93e3d032c..8c801ea49f56 100644 --- a/scripts/benchmarks/benchmark_renderer.py +++ b/scripts/benchmarks/benchmark_renderer.py @@ -11,8 +11,8 @@ import argparse import fnmatch import json +import math import os -import re import shutil import site import statistics @@ -20,65 +20,61 @@ import sys from pathlib import Path +OVRTX_RENDERER = "ovrtx_renderer" +NEWTON_RENDERER = "newton_renderer" + PROFILES = [ { - "name": "ovrtx_constant_diffuse_oldpipe", - "preset": "ovrtx_renderer,simple_shading_constant_diffuse", - "settings": {"min-pipe": False}, - }, - { - "name": "ovrtx_constant_diffuse_newpipe", - "preset": "ovrtx_renderer,simple_shading_constant_diffuse", - "settings": {"min-pipe": True}, - }, - { - "name": "ovrtx_diffuse_mdl_oldpipe", - "preset": "ovrtx_renderer,simple_shading_diffuse_mdl", - "settings": {"min-pipe": False}, - }, - { - "name": "ovrtx_diffuse_mdl_newpipe", - "preset": "ovrtx_renderer,simple_shading_diffuse_mdl", - "settings": {"min-pipe": True}, - }, - { - "name": "ovrtx_full_mdl_oldpipe", - "preset": "ovrtx_renderer,simple_shading_full_mdl", - "settings": {"min-pipe": False}, - }, + "name": f"ovrtx_{shading}_{pipeline}", + "preset": f"{OVRTX_RENDERER},simple_shading_{shading}", + "settings": {"min-pipe": minimal}, + } + for shading in ("constant_diffuse", "diffuse_mdl", "full_mdl") + for pipeline, minimal in (("oldpipe", False), ("newpipe", True)) +] + [ { - "name": "ovrtx_full_mdl_newpipe", - "preset": "ovrtx_renderer,simple_shading_full_mdl", - "settings": {"min-pipe": True}, - }, - {"name": "newton_lbvh_lbvh", "preset": "newton_renderer,rgb", "settings": {"tlas": "lbvh", "blas": "lbvh"}}, - {"name": "newton_lbvh_sah", "preset": "newton_renderer,rgb", "settings": {"tlas": "lbvh", "blas": "sah"}}, - {"name": "newton_lbvh_cubql", "preset": "newton_renderer,rgb", "settings": {"tlas": "lbvh", "blas": "cubql"}}, - {"name": "newton_sah_lbvh", "preset": "newton_renderer,rgb", "settings": {"tlas": "sah", "blas": "lbvh"}}, - {"name": "newton_sah_sah", "preset": "newton_renderer,rgb", "settings": {"tlas": "sah", "blas": "sah"}}, - {"name": "newton_sah_cubql", "preset": "newton_renderer,rgb", "settings": {"tlas": "sah", "blas": "cubql"}}, + "name": f"newton_{tlas}_{blas}", + "preset": f"{NEWTON_RENDERER},rgb", + "settings": {"tlas": tlas, "blas": blas}, + } + for tlas in ("lbvh", "sah") + for blas in ("lbvh", "sah", "cubql") ] TASK_NAME = "Isaac-RenderBenchmark-Franka-Cabinet" FRAME_PADDING = 5 +STAT_KEYS = ("median", "mean", "min", "max", "stdev") # Resolved from this file rather than the working directory, so the script runs from anywhere. SCRIPT_DIR = Path(__file__).resolve().parent RUNTIME_SCRIPT = SCRIPT_DIR / "runtime.py" OUTPUT_PATH = str(SCRIPT_DIR.parent.parent / "benchmarks") -OVRTX_RENDERER = "ovrtx_renderer" -NEWTON_RENDERER = "newton_renderer" - RENDER_SCOPE = "IsaacLab::Renderer::render" -"""Backend-agnostic timer name around ``BaseRenderer.render``, enabled by ``ISAACLAB_RENDER_PROFILE``. - -See :data:`isaaclab.renderers.render_context.RENDER_PROFILE_SCOPE`. It brackets the render alone, -excluding the scene-state sync before it and the output readback after it. ``wp.ScopedTimer`` prints -one ``" took X.XX ms"`` line per call, which :func:`parse_log` regexes out of the run's log. -""" - -RENDER_SCOPE_PATTERN = re.compile(rf"{re.escape(RENDER_SCOPE)} took ([\d.]+) ms") +"""Timer around ``BaseRenderer.render``, excluding scene updates and output readback.""" + +PHYSICS_SCOPE = "IsaacLab::Physics::step" +"""Timer around one physics step, matching :data:`isaaclab.benchmark.stepping.PHYSICS_PROFILE_SCOPE`.""" + +FRAME_SCOPE = "render" +"""Key of the :data:`PROFILE_SCOPES` entry whose timing closes a frame.""" + +PROFILE_SCOPES = { + FRAME_SCOPE: RENDER_SCOPE, + "physics": PHYSICS_SCOPE, +} +"""Report scope names mapped to timer names; :data:`FRAME_SCOPE` closes each frame.""" + +TABLE_COLUMNS = [ + ("RENDER", "median_ms"), + ("MEAN", "mean_ms"), + ("MIN", "min_ms"), + ("MAX", "max_ms"), + ("STDEV", "stdev_ms"), + ("PHYSICS", "physics_median_ms"), + ("TOTAL", "total_median_ms"), +] +"""``(heading, record key)`` pairs for the report's timing columns, in display order.""" log_stream = sys.stdout """Destination for progress and diagnostics. ``--json`` points it at stderr so stdout holds only JSON.""" @@ -108,77 +104,175 @@ def build_record(profile: dict, results: dict | None, num_envs: int, resolution: Args: profile: Profile entry from :data:`PROFILES`. - results: Timing statistics from :func:`parse_log`, or ``None`` if the run failed. + results: Timing statistics from :func:`parse_profile`, or ``None`` if the run failed. num_envs: Number of environments the profile rendered. resolution: Width and height of each environment's tile [px]. Returns: A record carrying the profile's identity plus either its timings or the log to inspect. + The unqualified ``*_ms`` keys are the render ones; every other scope is prefixed with its + name. ``pixels_per_second`` stays derived from the render time alone, so it remains + comparable against a run whose physics cost differed. """ record = {"name": profile["name"], "preset": profile["preset"], "settings": profile["settings"]} if not results: return record | {"status": "failed", "log": os.path.join(OUTPUT_PATH, profile["name"] + ".log")} - return ( - record - | { - "status": "ok", - "size": results["size"], - "pixels_per_second": pixels_per_second(results["median"], num_envs, resolution), - } - | {f"{key}_ms": results[key] for key in ("median", "mean", "min", "max", "stdev")} + + record |= { + "status": "ok", + "size": results["size"], + "pixels_per_second": pixels_per_second(results["median"], num_envs, resolution), + } + record |= {f"{key}_ms": results[key] for key in STAT_KEYS} + for group, stats in results.items(): + if isinstance(stats, dict): + record |= {f"{group}_{key}_ms": stats[key] for key in STAT_KEYS} + return record + + +def format_table(records: list[dict]) -> list[str]: + """Render the results table as lines of text. + + Widths follow the longest profile name rather than a fixed column, so a row stays aligned + whichever profiles were selected. + + Args: + records: Records from :func:`build_record`, in the order they should appear. + + Returns: + The heading, separators, and one row per record. + """ + name_width = max([len("PROFILE")] + [len(record["name"]) for record in records]) + separator = ( + "|" + "-" * (name_width + 2) + "|------|--------------|" + "|".join(["-" * 14] * len(TABLE_COLUMNS)) + "|" ) + headings = "|".join(f"{heading:^14}" for heading, _ in TABLE_COLUMNS) + + lines = ["| " + "PROFILE".ljust(name_width) + " | SIZE | PIXEL / SEC |" + headings + "|", separator] + for record in records: + if record["status"] == "ok": + cells = "|".join(f" {record[key]:>10.2f}ms " for _, key in TABLE_COLUMNS) + gpxs = record["pixels_per_second"] / 1e9 + lines.append(f"| {record['name']:<{name_width}} | {record['size']:>4} | {gpxs:>6.2f} Gpx/s |{cells}|") + else: + lines.append(f"| {record['name']:<{name_width}} | FAILED {record['log']} |") + lines.append(separator) + return lines + +def summarize(samples: list[float]) -> dict: + """Reduce a list of per-frame times [ms] to the statistics the report shows. -def parse_log(filename: str, num_frames: int): - """Summarize per-frame render times [ms] from a run's captured log. + Args: + samples: One time per frame [ms]. Must be non-empty. + + Returns: + Median, mean, min, max, and standard deviation [ms]. + """ + return { + "median": statistics.median(samples), + "mean": statistics.mean(samples), + "min": min(samples), + "max": max(samples), + "stdev": statistics.stdev(samples) if len(samples) > 1 else 0, + } + + +def parse_frames(filename: str, scopes: dict[str, str] | None = None) -> list[dict[str, float]]: + """Read per-frame scope times [ms] from a run's structured profiling file, in order. + + A rendered frame is preceded by however many physics steps the task's decimation implies, so + non-frame scopes are accumulated until the frame scope's timing closes them out rather than + assumed to be one per frame. A scope absent from the file reads as zero for every frame. + + Args: + filename: Path to the profiling JSON file written by the runtime benchmark. + scopes: Report scope name to timer name, defaulting to :data:`PROFILE_SCOPES`. Must contain + :data:`FRAME_SCOPE`. + + Returns: + One ``{scope: time_ms}`` dict per frame, each carrying every key in ``scopes``. + + Raises: + TypeError: A timing entry has an invalid type. + ValueError: The profiling file does not contain valid ordered scope timings. + """ + scopes = PROFILE_SCOPES if scopes is None else scopes + frames: list[dict[str, float]] = [] + pending = dict.fromkeys(scopes, 0.0) - Every backend is measured the same way: the wall time of :data:`RENDER_SCOPE`, printed once per + with open(filename) as file: + payload = json.load(file) + if not isinstance(payload, dict) or not isinstance(payload.get("timings_ms"), list): + raise ValueError("Expected a 'timings_ms' list of [scope, elapsed_ms] pairs.") + + scope_names = {timer: name for name, timer in scopes.items()} + for timer, elapsed_ms in payload["timings_ms"]: + if type(elapsed_ms) not in (int, float) or not math.isfinite(elapsed_ms) or elapsed_ms < 0: + raise ValueError("Expected a finite nonnegative time [ms].") + if (name := scope_names.get(timer)) is None: + continue + if name == FRAME_SCOPE: + frames.append(pending | {name: elapsed_ms}) + pending = dict.fromkeys(scopes, 0.0) + else: + pending[name] += elapsed_ms + + return frames + + +def parse_profile(filename: str, num_frames: int, scopes: dict[str, str] | None = None) -> dict | None: + """Summarize per-frame times [ms] from a profiling JSON file, one entry per scope. + + Every backend is measured the same way: the wall time of :data:`RENDER_SCOPE`, collected once per render by ``wp.ScopedTimer`` when ``ISAACLAB_RENDER_PROFILE`` is set. The timer synchronizes the device on both ends, so it covers completed rather than merely submitted work — including for the RTX backends, whose Vulkan render is consumed by warp extraction kernels inside the scope. + ``ISAACLAB_PHYSICS_PROFILE`` times :data:`PHYSICS_SCOPE` the same way, so a frame also carries + what its physics steps cost and the two summed. + Args: - filename: Path to the captured run log. + filename: Path to the profiling JSON file written by the runtime benchmark. num_frames: Number of frames to measure, after skipping :data:`FRAME_PADDING` warm-up frames. + scopes: Report scope name to timer name, defaulting to :data:`PROFILE_SCOPES`. Returns: - Timing statistics, or ``None`` if the log holds no usable frames. + The :data:`FRAME_SCOPE` statistics flat, a sub-dict per remaining scope, and a ``total`` + sub-dict summing all of them. ``None`` if the file holds no usable frames. """ - with open(filename) as file: - frames = [float(match.group(1)) for line in file if (match := RENDER_SCOPE_PATTERN.search(line))] + scopes = PROFILE_SCOPES if scopes is None else scopes + frames = parse_frames(filename, scopes) if not frames: log(f"No '{RENDER_SCOPE}' timings in {filename}; was ISAACLAB_RENDER_PROFILE set for this run?") return None out = frames[FRAME_PADDING : FRAME_PADDING + num_frames] - if out: - return { - "size": len(out), - "median": statistics.median(out), - "mean": statistics.mean(out), - "min": min(out), - "max": max(out), - "stdev": statistics.stdev(out) if len(out) > 1 else 0, - } - return None + if not out: + return None + + results = {"size": len(out)} | summarize([frame[FRAME_SCOPE] for frame in out]) + results |= {name: summarize([frame[name] for frame in out]) for name in scopes if name != FRAME_SCOPE} + return results | {"total": summarize([sum(frame.values()) for frame in out])} def run_profile(profile: dict, args: argparse.Namespace): - """Run one entry of :data:`PROFILES` and summarize its render times from the captured log. + """Run one entry of :data:`PROFILES` and summarize its structured profiling results. Args: profile: Profile entry naming the preset and its backend settings. args: Parsed command-line arguments. Returns: - Timing statistics from :func:`parse_log`, or ``False`` if the run failed. + Timing statistics from :func:`parse_profile`, or a false value if the run failed. """ warp_cache_path = os.path.join(OUTPUT_PATH, "warp-cache") env = { "NEWTON_USE_CUDA_GRAPH": "0", "ISAACLAB_RENDER_PROFILE": "1", + "ISAACLAB_PHYSICS_PROFILE": "1", "BENCHMARK_SAVE_IMAGE": "1" if args.save_image else "0", "BENCHMARK_RENDER_RESOLUTION": f"{args.resolution}", "WARP_CACHE_PATH": warp_cache_path, @@ -206,8 +300,12 @@ def run_profile(profile: dict, args: argparse.Namespace): env["NEWTON_BVH_SCENE"] = profile["settings"]["tlas"] env["NEWTON_BVH_GEOMETRY"] = profile["settings"]["blas"] - os.makedirs(OUTPUT_PATH, exist_ok=True) + output_path = os.path.join(OUTPUT_PATH, profile_name) + os.makedirs(output_path, exist_ok=True) log_filename = os.path.join(OUTPUT_PATH, profile_name + ".log") + profile_filename = os.path.join(output_path, "profile_timings.json") + # A successful subprocess must produce its own measurements, never reuse a previous run's. + Path(profile_filename).unlink(missing_ok=True) cmd = [ sys.executable, @@ -221,7 +319,7 @@ def run_profile(profile: dict, args: argparse.Namespace): "--num_steps", f"{args.num_frames + FRAME_PADDING * 2}", "--output_path", - OUTPUT_PATH, + output_path, f"presets={preset}", ] @@ -241,15 +339,15 @@ def run_profile(profile: dict, args: argparse.Namespace): log(f"Failed with exit code {process.returncode}, see {log_filename} for details.") return False - return parse_log(log_filename, args.num_frames) + return parse_profile(profile_filename, args.num_frames) def _build_arg_parser() -> argparse.ArgumentParser: """Build the CLI parser, kept separate from module import so tests can import pure helpers.""" parser = argparse.ArgumentParser("IsaacLab Benchmark: Sweep Franka Cabinet") - parser.add_argument("--num_frames", type=int, default="20", help="Number of frames to render") - parser.add_argument("--num_envs", type=int, default="1024", help="Number of environments to render") - parser.add_argument("--resolution", type=int, default="256", help="Render resolution") + parser.add_argument("--num_frames", type=int, default=20, help="Number of frames to render") + parser.add_argument("--num_envs", type=int, default=1024, help="Number of environments to render") + parser.add_argument("--resolution", type=int, default=256, help="Render resolution") parser.add_argument("--task", default=TASK_NAME, help="Gym task id to profile") parser.add_argument( "--keep_warp_cache", @@ -280,20 +378,20 @@ def main() -> None: log("Available profiles:") for profile in PROFILES: log(" " + profile["name"]) - exit(0) + sys.exit(0) matched_names = set() for profile_name in args.profile: matches = [profile["name"] for profile in PROFILES if fnmatch.fnmatch(profile["name"], profile_name)] if not matches: print(f"No profile found matching: {profile_name}", file=sys.stderr) - exit(1) + sys.exit(1) matched_names.update(matches) # Run in declaration order so a given selection always reports in the same order. selected_profiles = [profile for profile in PROFILES if profile["name"] in matched_names] - all_results = {} + records = [] for profile in selected_profiles: log(f"profile: {profile['name']}") log(f" preset: {profile['preset']}") @@ -301,25 +399,19 @@ def main() -> None: log(f" {key}: {value}") try: - all_results[profile["name"]] = run_profile(profile, args) + results = run_profile(profile, args) except KeyboardInterrupt: break - if results := all_results[profile["name"]]: - log(f" size: {results['size']}") - for key, value in results.items(): - if key != "size": - log(f" {key}: {value:.2f}ms") + record = build_record(profile, results, args.num_envs, args.resolution) + records.append(record) + if record["status"] == "ok": + log(f" size: {record['size']}") + for key, value in record.items(): + if key.endswith("_ms"): + log(f" {key.removesuffix('_ms')}: {value:.2f}ms") log("") - # A KeyboardInterrupt leaves the remaining profiles unrun; report only what completed. - records = [ - build_record(profile, all_results[profile["name"]], args.num_envs, args.resolution) - for profile in selected_profiles - if profile["name"] in all_results - ] - benchmark_failed = any(record["status"] == "failed" for record in records) - if args.json: print( json.dumps( @@ -334,26 +426,12 @@ def main() -> None: ) ) else: - separator = "|------------------------------------------|------|--------------|--------------|--------------|--------------|--------------|--------------|" # noqa: E501 log("") - log( - "| PROFILE | SIZE | PIXEL / SEC | MEDIAN | MEAN | MIN | MAX | STDEV |" # noqa: E501 - ) - log(separator) - for record in records: - if record["status"] == "ok": - gpxs = record["pixels_per_second"] / 1e9 - log( - f"| {record['name']:<40} | {record['size']:>4} | {gpxs:>6.2f} Gpx/s | {record['median_ms']:>10.2f}ms | {record['mean_ms']:>10.2f}ms | {record['min_ms']:>10.2f}ms | {record['max_ms']:>10.2f}ms | {record['stdev_ms']:>10.2f}ms |" # noqa: E501 - ) - else: - log(f"| {record['name']:<40} | FAILED {record['log']:<87} |") - log(separator) + for line in format_table(records): + log(line) log("") - if benchmark_failed: - exit(1) - exit(0) + sys.exit(1 if any(record["status"] == "failed" for record in records) else 0) if __name__ == "__main__": diff --git a/scripts/benchmarks/test/test_benchmark_renderer.py b/scripts/benchmarks/test/test_benchmark_renderer.py index 05c57d63ff5b..b681efe19bfe 100644 --- a/scripts/benchmarks/test/test_benchmark_renderer.py +++ b/scripts/benchmarks/test/test_benchmark_renderer.py @@ -6,14 +6,16 @@ """Unit tests for the pure helpers in ``scripts/benchmarks/benchmark_renderer.py``. The script drives a rendering backend end-to-end, so that path is not covered here. These tests -exercise the record-building, log-parsing, and CLI-parsing logic that does not require a GPU. +exercise the record-building, structured timing, and CLI-parsing logic that does not require a GPU. """ import importlib.util import json import subprocess import sys +from contextlib import nullcontext from pathlib import Path +from types import SimpleNamespace import pytest @@ -34,11 +36,12 @@ def benchmark_renderer(): return _load_module() -def test_render_scope_matches_render_context(benchmark_renderer): - """The script's timer name must match the one the renderer prints, or nothing is parsed.""" - from isaaclab.renderers.render_context import RENDER_PROFILE_SCOPE +def test_profile_scopes_match_benchmark_wrappers(benchmark_renderer): + """The script's timer names must match the ones collected by the profiling shims.""" + from isaaclab.benchmark.stepping import PHYSICS_PROFILE_SCOPE, RENDER_PROFILE_SCOPE assert benchmark_renderer.RENDER_SCOPE == RENDER_PROFILE_SCOPE + assert benchmark_renderer.PHYSICS_SCOPE == PHYSICS_PROFILE_SCOPE def test_pixels_per_second(benchmark_renderer): @@ -75,42 +78,130 @@ def test_build_record_failed(benchmark_renderer): _RENDER_SCOPE = "IsaacLab::Renderer::render" +_PHYSICS_SCOPE = "IsaacLab::Physics::step" -def _write_log(path: Path, timings_ms: list[float]) -> None: - """Write a synthetic run log with one ``wp.ScopedTimer`` print line per timing. +def _write_profile(path: Path, timings_ms: list[tuple[str, float]]) -> None: + """Write ordered scope timings [ms] in the runtime benchmark's structured format.""" + path.write_text(json.dumps({"timings_ms": timings_ms})) - Interleaves unrelated lines to mimic real subprocess output (warp init banner, other timers), - so the parser is exercised against noise rather than a file with only matching lines. - """ - lines = ["Warp 1.17.0 initialized:", "SomeOtherScope took 0.10 ms"] - for value in timings_ms: - lines.append(f"{_RENDER_SCOPE} took {value:.2f} ms") - path.write_text("\n".join(lines) + "\n") - -def test_parse_log_skips_padding_and_keeps_num_frames(benchmark_renderer, tmp_path): - """Only the ``num_frames`` timings after :data:`FRAME_PADDING` warm-up frames are summarized.""" - log_path = tmp_path / "profile.log" +def test_parse_profile_skips_padding_and_keeps_num_frames(benchmark_renderer, tmp_path): + """Summarize only the requested frames after warm-up, preserving unrounded timings.""" + profile_path = tmp_path / "profile.json" padding = benchmark_renderer.FRAME_PADDING - # padding warm-up frames of 1ms each, then 4 measured frames of 2ms each (only 3 are kept). - timings = [1.0] * padding + [2.0] * 4 - _write_log(log_path, timings) + measured_ms = 2.123456789 + timings = [1.0] * padding + [measured_ms] * 3 + [99.0] + _write_profile(profile_path, [(_RENDER_SCOPE, value) for value in timings]) + + results = benchmark_renderer.parse_profile(str(profile_path), num_frames=3) - results = benchmark_renderer.parse_log(str(log_path), num_frames=3) + assert results["size"] == 3 + assert results["median"] == measured_ms + assert results["min"] == measured_ms + assert results["max"] == measured_ms + assert results["physics"]["median"] == pytest.approx(0.0) + assert results["total"]["median"] == measured_ms + + +@pytest.mark.parametrize( + ("contents", "error"), + [ + (None, FileNotFoundError), + ("{", json.JSONDecodeError), + ("{}", ValueError), + (json.dumps({"timings_ms": [(_PHYSICS_SCOPE, 1.0)]}), None), + (json.dumps({"timings_ms": [(_RENDER_SCOPE, 1.0)]}), None), + ], +) +def test_parse_profile_handles_unusable_timings(benchmark_renderer, tmp_path, contents, error): + """Invalid files raise; valid files without measured frames return no result.""" + profile_path = tmp_path / "profile.json" + if contents is not None: + profile_path.write_text(contents) + + with pytest.raises(error) if error else nullcontext(): + assert benchmark_renderer.parse_profile(str(profile_path), num_frames=3) is None + + +def test_parse_profile_sums_physics_steps_within_one_frame(benchmark_renderer, tmp_path): + """Frame boundaries group a variable number of steps and ignore an unfinished frame.""" + profile_path = tmp_path / "profile.json" + render_ms = [2.1, 3.2, 4.3] + physics_ms = [[0.123456789, 0.234567891, 0.345678912], [], [0.456789123]] + timings = [(_RENDER_SCOPE, 99.0)] * benchmark_renderer.FRAME_PADDING + for render, steps in zip(render_ms, physics_ms): + timings.extend((_PHYSICS_SCOPE, value) for value in steps) + timings.append(("Unreported::scope", 100.0)) + timings.append((_RENDER_SCOPE, render)) + timings.append((_PHYSICS_SCOPE, 999.0)) + _write_profile(profile_path, timings) + + frames = benchmark_renderer.parse_frames(str(profile_path))[benchmark_renderer.FRAME_PADDING :] + results = benchmark_renderer.parse_profile(str(profile_path), num_frames=3) assert results["size"] == 3 - assert results["median"] == pytest.approx(2.0) - assert results["min"] == pytest.approx(2.0) - assert results["max"] == pytest.approx(2.0) + assert [frame["render"] for frame in frames] == render_ms + assert [frame["physics"] for frame in frames] == [sum(steps) for steps in physics_ms] + assert results["median"] == render_ms[1] + assert results["physics"]["median"] == sum(physics_ms[2]) + assert results["total"]["mean"] == pytest.approx( + sum(render + sum(steps) for render, steps in zip(render_ms, physics_ms)) / len(render_ms) + ) -def test_parse_log_returns_none_without_matching_lines(benchmark_renderer, tmp_path): - """A log with no ``RENDER_SCOPE`` timings means profiling was never enabled for that run.""" - log_path = tmp_path / "profile.log" - _write_log(log_path, []) +def test_parse_profile_takes_an_arbitrary_scope_mapping(benchmark_renderer, tmp_path): + """Scopes are data, so a caller can summarize a timer the script does not know about.""" + profile_path = tmp_path / "profile.json" + timings = [("Custom::scope", 4.0), (_RENDER_SCOPE, 2.0)] * (benchmark_renderer.FRAME_PADDING + 3) + _write_profile(profile_path, timings) - assert benchmark_renderer.parse_log(str(log_path), num_frames=3) is None + results = benchmark_renderer.parse_profile( + str(profile_path), + num_frames=3, + scopes={ + benchmark_renderer.FRAME_SCOPE: _RENDER_SCOPE, + "custom": "Custom::scope", + }, + ) + + assert results["custom"]["median"] == pytest.approx(4.0) + assert "physics" not in results + + +@pytest.mark.parametrize("write_timings", [False, True]) +def test_run_profile_reads_fresh_structured_output(benchmark_renderer, tmp_path, monkeypatch, write_timings): + """The run consumes its own profiling artifact; stale artifacts and timing logs cannot satisfy it.""" + profile = {"name": "p", "preset": "newton_renderer,rgb", "settings": {"tlas": "sah", "blas": "lbvh"}} + profile_path = tmp_path / "p" / "profile_timings.json" + profile_path.parent.mkdir() + frames = benchmark_renderer.FRAME_PADDING + 2 + _write_profile(profile_path, [(_RENDER_SCOPE, 99.0)] * frames) + monkeypatch.setattr(benchmark_renderer, "OUTPUT_PATH", str(tmp_path)) + + def launch(cmd, **kwargs): + assert cmd[cmd.index("--output_path") + 1] == str(profile_path.parent) + assert "--profile_output_path" not in cmd + assert kwargs["env"]["ISAACLAB_RENDER_PROFILE"] == "1" + assert kwargs["env"]["ISAACLAB_PHYSICS_PROFILE"] == "1" + assert not profile_path.exists() + if write_timings: + _write_profile(profile_path, [(_PHYSICS_SCOPE, 1.234567), (_RENDER_SCOPE, 2.345678)] * frames) + return SimpleNamespace(stdout=iter([f"{_RENDER_SCOPE} took 50.00 ms\n"] * frames), returncode=0, wait=lambda: 0) + + monkeypatch.setattr(benchmark_renderer.subprocess, "Popen", launch) + args = benchmark_renderer._build_arg_parser().parse_args(["--num_frames", "2"]) + + with nullcontext() if write_timings else pytest.raises(FileNotFoundError): + results = benchmark_renderer.run_profile(profile, args) + + assert (tmp_path / "p.log").read_text() == f"{_RENDER_SCOPE} took 50.00 ms\n" * frames + if write_timings: + record = benchmark_renderer.build_record(profile, results, args.num_envs, args.resolution) + assert record["median_ms"] == 2.345678 + assert record["physics_median_ms"] == 1.234567 + assert record["total_median_ms"] == 2.345678 + 1.234567 + assert "p" in "\n".join(benchmark_renderer.format_table([record])) def _run_cli(args: list[str]) -> subprocess.CompletedProcess: @@ -128,6 +219,26 @@ def test_cli_lists_available_profiles_as_json(benchmark_renderer): assert payload["available_profiles"] == [profile["name"] for profile in benchmark_renderer.PROFILES] +def test_cli_reports_grouped_timings_as_json(benchmark_renderer, monkeypatch, capsys): + """Grouped physics and total statistics are reported without contaminating JSON stdout.""" + profile = benchmark_renderer.PROFILES[0] + stats = {key: 2.0 for key in benchmark_renderer.STAT_KEYS} + results = {"size": 1, **stats, "physics": stats, "total": stats} + monkeypatch.setattr(benchmark_renderer, "run_profile", lambda profile, args: results) + monkeypatch.setattr(sys, "argv", [str(SCRIPT_PATH), "--json", profile["name"]]) + monkeypatch.setattr(benchmark_renderer, "log_stream", sys.stdout) + + with pytest.raises(SystemExit) as error: + benchmark_renderer.main() + + assert error.value.code == 0 + captured = capsys.readouterr() + record = json.loads(captured.out)["profiles"][0] + assert record["physics_median_ms"] == results["physics"]["median"] + assert record["total_median_ms"] == results["total"]["median"] + assert "physics_median:" in captured.err + + def test_cli_rejects_unmatched_profile_glob(): """An unmatched profile pattern fails fast, before any profiling subprocess is launched.""" result = _run_cli(["does-not-exist-*"]) diff --git a/scripts/benchmarks/test/test_runtime_smoke.py b/scripts/benchmarks/test/test_runtime_smoke.py index 7f333fa6e7d8..df139c60d795 100644 --- a/scripts/benchmarks/test/test_runtime_smoke.py +++ b/scripts/benchmarks/test/test_runtime_smoke.py @@ -7,6 +7,7 @@ import json import subprocess +import sys from pathlib import Path import pytest @@ -17,11 +18,12 @@ @pytest.mark.parametrize("measure_sync_step", [False, True], ids=["default", "synchronized_breakdown"]) -def test_runtime_writes_all_requested_formats(tmp_path, measure_sync_step: bool): - """The runtime entry point writes schema and OmniPerf data in one run.""" +def test_runtime_writes_all_requested_formats(tmp_path, monkeypatch, measure_sync_step: bool): + """Tasks without a benchmark mode write reports without collecting profiling scopes.""" + monkeypatch.setenv("ISAACLAB_RENDER_PROFILE", "1") + monkeypatch.setenv("ISAACLAB_PHYSICS_PROFILE", "1") cmd = [ - str(ROOT / "isaaclab.sh"), - "-p", + sys.executable, "scripts/benchmarks/runtime.py", "--task", _TASK, @@ -52,6 +54,7 @@ def test_runtime_writes_all_requested_formats(tmp_path, measure_sync_step: bool) assert device_lines and device_lines[-1].endswith(": cpu"), f"unexpected device output: {device_lines}" files = sorted(tmp_path.glob("*.json")) + assert not (tmp_path / "profile_timings.json").exists() schema_files = [path for path in files if path.name.endswith("_schema.json")] omniperf_files = [path for path in files if path.name.endswith("_omniperf.json")] assert len(schema_files) == len(omniperf_files) == 1 @@ -59,6 +62,7 @@ def test_runtime_writes_all_requested_formats(tmp_path, measure_sync_step: bool) schema_data = json.loads(schema_files[0].read_text()) assert schema_data["run"]["config"]["physics_backend"] == "newton_mjwarp" assert schema_data["runtime"]["iterations_completed"] == 20 + assert "scope_timings" not in schema_data["runtime"] assert schema_data["extra"] is None assert schema_data["runtime"]["startup_time_s"]["first_step"] > 0.0 timing = schema_data["runtime"]["environment_step_timing"] @@ -87,3 +91,57 @@ def test_runtime_writes_all_requested_formats(tmp_path, measure_sync_step: bool) else: assert "Mean Total FPS" in omniperf_data["runtime"] assert "Mean Serialized Diagnostic Total FPS" not in omniperf_data["runtime"] + + +def test_runtime_api_returns_profile_summary(tmp_path, monkeypatch): + """The API and saved reports carry scalar summaries while raw samples stay local.""" + monkeypatch.setenv("ISAACLAB_RENDER_PROFILE", "1") + monkeypatch.setenv("ISAACLAB_PHYSICS_PROFILE", "1") + monkeypatch.setenv("BENCHMARK_RENDER_RESOLUTION", "64") + script = tmp_path / "run_profile.py" + script.write_text(""" +import json +import statistics +import sys +from dataclasses import asdict +from pathlib import Path + +from isaaclab.benchmark import BenchmarkOutputConfig, BenchmarkRuntimeRequest, run_runtime_benchmark +from isaaclab.benchmark.stepping import PHYSICS_PROFILE_SCOPE, RENDER_PROFILE_SCOPE + +result = run_runtime_benchmark(BenchmarkRuntimeRequest( + task="Isaac-RenderBenchmark-Franka-Cabinet", + num_envs=1, + num_steps=2, + warmup_steps=1, + presets=("newton_renderer", "rgb"), + hydra_args=("env.benchmark_mode=physics_render",), + output=BenchmarkOutputConfig(path=Path(sys.argv[1]), formatters=("schema", "omniperf")), +)) +assert result.bundle.extra is not None +timings = json.loads((Path(sys.argv[1]) / "profile_timings.json").read_text())["timings_ms"] +assert len(result.output_paths) == 2 +schema_path = next(path for path in result.output_paths if path.name.endswith("_schema.json")) +schema = json.loads(schema_path.read_text()) +assert schema == json.loads(json.dumps(asdict(result.bundle))) +assert schema["schema_version"] == "1.4" +assert "scope_timings" not in schema["runtime"] +omniperf_path = next(path for path in result.output_paths if path.name.endswith("_omniperf.json")) +metrics = json.loads(omniperf_path.read_text())["runtime"] +expected = {} +for prefix, scope in (("physics", PHYSICS_PROFILE_SCOPE), ("render", RENDER_PROFILE_SCOPE)): + samples = [elapsed_ms for name, elapsed_ms in timings if name == scope] + expected.update({ + f"{prefix}_mean_ms": statistics.mean(samples), + f"{prefix}_std_ms": statistics.stdev(samples), + f"{prefix}_max_ms": max(samples), + f"{prefix}_calls": len(samples), + }) + assert metrics[f"{scope} Calls"] == expected[f"{prefix}_calls"] + assert metrics[f"Mean {scope} Time per Call"] == expected[f"{prefix}_mean_ms"] +assert result.bundle.extra == expected +""") + res = subprocess.run( + [sys.executable, str(script), str(tmp_path)], cwd=ROOT, capture_output=True, text=True, timeout=900 + ) + assert res.returncode == 0, f"STDOUT:\n{res.stdout[-2000:]}\nSTDERR:\n{res.stderr[-2000:]}" diff --git a/source/isaaclab/changelog.d/benchmark-scope-profiling.major.rst b/source/isaaclab/changelog.d/benchmark-scope-profiling.major.rst new file mode 100644 index 000000000000..f4c2ed705348 --- /dev/null +++ b/source/isaaclab/changelog.d/benchmark-scope-profiling.major.rst @@ -0,0 +1,33 @@ +Added +^^^^^ + +* Added :func:`~isaaclab.benchmark.stepping.profile_physics_steps` and + :func:`~isaaclab.benchmark.stepping.profile_renderers` context managers for synchronized + runtime benchmark timings. Wrappers recorded complete calls after warmup and restored + the original methods when measurement ended, including on failure. + +Changed +^^^^^^^ + +* Restricted scope capture to tasks with a non-``None`` ``benchmark_mode``. To collect timings, + enable ``ISAACLAB_PHYSICS_PROFILE=1`` or ``ISAACLAB_RENDER_PROFILE=1`` for such a task. + Other tasks continued to produce standard runtime reports without scope profiling. + +* Included scalar physics and render profiling summaries in ``BenchmarkResult.bundle.extra`` + without changing schema version 1.4. Schema and OmniPerf output included each scope's mean, + standard deviation, maximum time per call [ms], and call count. Consumers should read + ``physics_mean_ms``, ``physics_std_ms``, ``physics_max_ms``, ``physics_calls``, and the + corresponding ``render_*`` keys for these summaries. Raw ordered samples remained in + ``/profile_timings.json`` as ``timings_ms`` pairs for local analysis instead + of parsing printed timer lines; use ``--output_path`` to select the output directory. + +* **Breaking:** Moved render profiling into the runtime benchmark through + :func:`~isaaclab.benchmark.stepping.profile_renderers`. To collect render timings with + ``ISAACLAB_RENDER_PROFILE=1``, use the runtime benchmark; normal simulation runs no longer + allocate render timers. Scene updates and output readback remained outside the timed scope. + +Deprecated +^^^^^^^^^^ + +* Deprecated ``isaaclab.renderers.render_context.RENDER_PROFILE_SCOPE``; use + :data:`~isaaclab.benchmark.stepping.RENDER_PROFILE_SCOPE` instead. diff --git a/source/isaaclab/isaaclab/benchmark/api.py b/source/isaaclab/isaaclab/benchmark/api.py index ceed5dcccdf5..0a1e44ed64d3 100644 --- a/source/isaaclab/isaaclab/benchmark/api.py +++ b/source/isaaclab/isaaclab/benchmark/api.py @@ -264,7 +264,8 @@ class BenchmarkResult(Generic[_BenchmarkBundleT]): """Completed benchmark result. Args: - bundle: Typed benchmark result bundle. + bundle: Typed benchmark result bundle. Runtime profiling summaries, when enabled, + are included as scalar metrics in ``bundle.extra``. output_paths: Files written by the selected formatters. """ @@ -303,6 +304,13 @@ def run_benchmark(request: BenchmarkRequest) -> BenchmarkResult: def run_runtime_benchmark(request: BenchmarkRuntimeRequest) -> BenchmarkResult[RuntimeBundle]: """Run an environment runtime benchmark. + A task with a non-``None`` ``benchmark_mode`` can collect synchronized scope + timings through ``ISAACLAB_RENDER_PROFILE`` and ``ISAACLAB_PHYSICS_PROFILE``. + The returned bundle includes per-call mean, standard deviation, maximum [ms], + and call count in ``extra`` under ``physics_*`` and ``render_*`` keys. Raw + samples remain in the local ``profile_timings.json`` file. Disabled scopes + contribute no summary keys. + Args: request: Runtime benchmark request. diff --git a/source/isaaclab/isaaclab/benchmark/benchmark_core.py b/source/isaaclab/isaaclab/benchmark/benchmark_core.py index c9b3a01031cd..6e1e4ebc5b10 100644 --- a/source/isaaclab/isaaclab/benchmark/benchmark_core.py +++ b/source/isaaclab/isaaclab/benchmark/benchmark_core.py @@ -27,6 +27,7 @@ TestPhase, ) from .recorders import CPUInfoRecorder, GPUInfoRecorder, MemoryInfoRecorder, VersionInfoRecorder +from .stepping import PHYSICS_PROFILE_SCOPE, RENDER_PROFILE_SCOPE if TYPE_CHECKING: from .schema import ( @@ -164,7 +165,10 @@ def _curve_measurements(label: str, curve: "LearningCurve", ema_alpha: float) -> def _measurements_from_bundle( bundle: "RuntimeBundle | TrainingBundle | StartupBundle | PlayBundle", ) -> dict[str, list[Measurement]]: - """Project a typed bundle into flat phases for non-schema formatters.""" + """Project a typed bundle into flat phases for non-schema formatters. + + Profiling summaries in ``extra`` describe individual physics or render calls. + """ from .schema import PlayBundle, StartupBundle, TrainingBundle if isinstance(bundle, StartupBundle): @@ -185,6 +189,15 @@ def _measurements_from_bundle( return projected projected = _runtime_measurements(bundle.runtime) + extra = bundle.extra or {} + for prefix, scope in (("physics", PHYSICS_PROFILE_SCOPE), ("render", RENDER_PROFILE_SCOPE)): + for statistic in ("mean", "std", "max"): + if (key := f"{prefix}_{statistic}_ms") in extra: + projected["runtime"].append( + SingleMeasurement(name=f"{statistic.title()} {scope} Time per Call", value=extra[key], unit="ms") + ) + if (key := f"{prefix}_calls") in extra: + projected["runtime"].append(SingleMeasurement(name=f"{scope} Calls", value=extra[key], unit="count")) if isinstance(bundle, TrainingBundle): train = _curve_measurements("Reward", bundle.learning.reward, bundle.learning.ema_alpha) train.extend(_curve_measurements("Episode Length", bundle.learning.ep_length, bundle.learning.ema_alpha)) diff --git a/source/isaaclab/isaaclab/benchmark/entrypoints/runtime.py b/source/isaaclab/isaaclab/benchmark/entrypoints/runtime.py index 829c39b4bb2d..77127c3296db 100644 --- a/source/isaaclab/isaaclab/benchmark/entrypoints/runtime.py +++ b/source/isaaclab/isaaclab/benchmark/entrypoints/runtime.py @@ -30,7 +30,9 @@ from isaaclab.benchmark import BenchmarkResult import argparse +import os import sys +from pathlib import Path def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]: @@ -113,7 +115,9 @@ def run(argv: list[str]) -> BenchmarkResult | None: stepping, ) from isaaclab.benchmark.distributed import DistributedContext + from isaaclab.benchmark.metrics import mean_std from isaaclab.benchmark.schema import StartupTime + from isaaclab.benchmark.serialize import write_bundle_file # Importing the task packages registers their gym environments so the # requested ``--task`` can be resolved. @@ -183,7 +187,20 @@ def run(argv: list[str]) -> BenchmarkResult | None: environment_step_timer = stepping.EnvironmentStepTimingRecorder( env, measure_synchronized_step_breakdown=args.measure_sync_step ) - with environment_step_timer, BenchmarkMonitor(benchmark, interval=1.0): + benchmark_mode = getattr(env_cfg, "benchmark_mode", None) + profile_render = benchmark_mode is not None and os.environ.get("ISAACLAB_RENDER_PROFILE", "0") != "0" + profile_physics = benchmark_mode is not None and os.environ.get("ISAACLAB_PHYSICS_PROFILE", "0") != "0" + profile_timings: list[tuple[str, float]] = [] + with ( + stepping.profile_renderers( + env.unwrapped.sim.render_context, active=profile_render, timings=profile_timings + ), + stepping.profile_physics_steps( + env.unwrapped.sim.physics_manager, active=profile_physics, timings=profile_timings + ), + environment_step_timer, + BenchmarkMonitor(benchmark, interval=1.0), + ): step_times_s = stepping.run_runtime_loop(env, args.num_steps, reset=False) first_step_s = warmup_step_times_s[0] if warmup_step_times_s else step_times_s[0] @@ -239,22 +256,43 @@ def run(argv: list[str]) -> BenchmarkResult | None: num_envs=num_envs, ) + # Ranks step independently, so summaries and throughput describe rank 0's workload. + extra = ( + distributed.bundle_metadata(workload_scope="rank0", num_envs_per_rank=num_envs) + if distributed.enabled + else {} + ) + for prefix, scope in ( + ("physics", stepping.PHYSICS_PROFILE_SCOPE), + ("render", stepping.RENDER_PROFILE_SCOPE), + ): + samples = [elapsed_ms for name, elapsed_ms in profile_timings if name == scope] + if samples: + stats = mean_std(samples) + extra.update( + { + f"{prefix}_mean_ms": stats.mean, + f"{prefix}_std_ms": stats.std, + f"{prefix}_max_ms": max(samples), + f"{prefix}_calls": len(samples), + } + ) + bundle = builders.build_runtime_bundle( run=run, versions=versions, hardware=hardware, runtime=runtime, resources=resources, - # Ranks step independently rather than in lockstep, so rank 0's throughput is - # reported as measured instead of being multiplied out to a global rate. - extra=distributed.bundle_metadata(workload_scope="rank0", num_envs_per_rank=num_envs) - if distributed.enabled - else None, + extra=extra or None, ) benchmark.attach_bundle(bundle) output_paths = benchmark.finalize() + if profile_render or profile_physics: + profile_path = Path(args.output_path) / "profile_timings.json" + write_bundle_file({"timings_ms": profile_timings}, str(profile_path)) result = BenchmarkResult(bundle=bundle, output_paths=output_paths) console.print_runtime_report(bundle, output_paths) diff --git a/source/isaaclab/isaaclab/benchmark/stepping.py b/source/isaaclab/isaaclab/benchmark/stepping.py index abc2e8ea7fc9..34f3ead2dc52 100644 --- a/source/isaaclab/isaaclab/benchmark/stepping.py +++ b/source/isaaclab/isaaclab/benchmark/stepping.py @@ -13,15 +13,131 @@ from __future__ import annotations import time -from contextlib import AbstractContextManager -from typing import TYPE_CHECKING +from collections.abc import Iterator +from contextlib import AbstractContextManager, contextmanager +from functools import wraps +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: import torch + from ..physics import PhysicsManager + from ..renderers.render_context import RenderContext + from ..utils.string import ResolvableString from .schema import MeanStd +PHYSICS_PROFILE_SCOPE = "IsaacLab::Physics::step" +"""Scope name for benchmark physics-step timings [ms].""" + +RENDER_PROFILE_SCOPE = "IsaacLab::Renderer::render" +"""Scope name for benchmark render timings [ms], excluding scene updates and output readback.""" + + +@contextmanager +def profile_renderers( + render_context: RenderContext, *, active: bool = True, timings: list[tuple[str, float]] | None = None +) -> Iterator[list[tuple[str, float]]]: + """Temporarily time the benchmark's currently registered renderers. + + Original methods are restored when the context exits, including on failure. + Enabled timings synchronize device work on entry and exit and perturb throughput. + + Args: + render_context: Simulation rendering context whose renderers will be timed. + active: Whether to install the timing wrappers. + timings: Shared list of scope names and elapsed times [ms], in call order. + A new list is created if omitted. Timings are collected without printing. + + Yields: + The list populated by the wrappers, including timings of calls that raise. + """ + if timings is None: + timings = [] + if not active: + yield timings + return + + import warp as wp # noqa: PLC0415 + + scope_timings = {RENDER_PROFILE_SCOPE: _ProfileScopeTimings(RENDER_PROFILE_SCOPE, timings)} + missing = object() + originals = [] + try: + for _, renderer in render_context._renderer_entries: + render = renderer.render + original = vars(renderer).get("render", missing) + + @wraps(render) + def timed_render(render_data: Any, _render=render) -> None: + with wp.ScopedTimer(RENDER_PROFILE_SCOPE, dict=scope_timings, print=False, synchronize=True): + return _render(render_data) + + renderer.render = timed_render + originals.append((renderer, original)) + + yield timings + finally: + for renderer, original in reversed(originals): + if original is missing: + del renderer.render + else: + renderer.render = original + + +@contextmanager +def profile_physics_steps( + physics_manager: type[PhysicsManager] | ResolvableString, + *, + active: bool = True, + timings: list[tuple[str, float]] | None = None, +) -> Iterator[list[tuple[str, float]]]: + """Temporarily time the benchmark's selected physics manager. + + Only the selected manager is wrapped, so inherited ``super().step()`` calls + are included in one timing record. The original class method is restored when + the context exits, including on failure. + Enabled timings synchronize device work on entry and exit and perturb throughput. + + Args: + physics_manager: Concrete physics manager selected by the environment, or its lazy class reference. + active: Whether to install the timing wrapper. + timings: Shared list of scope names and elapsed times [ms], in call order. + A new list is created if omitted. Timings are collected without printing. + + Yields: + The list populated by the wrapper, including timings of calls that raise. + """ + if timings is None: + timings = [] + if not active: + yield timings + return + + import warp as wp # noqa: PLC0415 + + # The bound method identifies the concrete class even through a lazy class reference. + physics_manager = physics_manager.step.__self__ + step = physics_manager.step.__func__ + missing = object() + original = vars(physics_manager).get("step", missing) + scope_timings = {PHYSICS_PROFILE_SCOPE: _ProfileScopeTimings(PHYSICS_PROFILE_SCOPE, timings)} + + @wraps(step) + def timed_step(cls: type[PhysicsManager]) -> None: + with wp.ScopedTimer(PHYSICS_PROFILE_SCOPE, dict=scope_timings, print=False, synchronize=True): + return step(cls) + + physics_manager.step = classmethod(timed_step) + try: + yield timings + finally: + if original is missing: + del physics_manager.step + else: + physics_manager.step = original + + def sample_random_actions(env) -> torch.Tensor | dict[str, torch.Tensor]: """Sample random actions for a single-agent or multi-agent environment. @@ -409,3 +525,17 @@ def run_play_loop(env, policy, num_steps: int) -> tuple[list[float], MeanStd | N success_rate = round(sum(successes) / len(successes), 4) if successes else None return step_times, reward_agg, ep_length_agg, success_rate + + +class _ProfileScopeTimings(list[float]): + """Keep Warp's per-scope timings [ms] in a shared sequence for frame grouping.""" + + def __init__(self, scope: str, timings: list[tuple[str, float]]): + super().__init__() + self._scope = scope + self._timings = timings + + def append(self, elapsed_ms: float) -> None: + """Record one scope timing [ms] in completion order.""" + super().append(elapsed_ms) + self._timings.append((self._scope, elapsed_ms)) diff --git a/source/isaaclab/isaaclab/renderers/render_context.py b/source/isaaclab/isaaclab/renderers/render_context.py index 3b544122f52a..9ca243c1a671 100644 --- a/source/isaaclab/isaaclab/renderers/render_context.py +++ b/source/isaaclab/isaaclab/renderers/render_context.py @@ -8,7 +8,7 @@ from __future__ import annotations import logging -import os +import warnings from typing import TYPE_CHECKING, Any import torch @@ -23,21 +23,19 @@ logger = logging.getLogger(__name__) -RENDER_PROFILE_SCOPE = "IsaacLab::Renderer::render" -"""Name of the timed scope bracketing :meth:`BaseRenderer.render`, emitted when render profiling is on. -Every backend renders through the same call, so a profile can compare them under one scope name -instead of one internal name per backend. ``wp.ScopedTimer`` prints one ``" took X.XX ms"`` -line per call, which ``scripts/benchmarks/benchmark_renderer.py`` parses back out of the run log. -""" +def __getattr__(name: str) -> Any: + if name == "RENDER_PROFILE_SCOPE": + from ..benchmark.stepping import RENDER_PROFILE_SCOPE -_RENDER_PROFILE_ENABLED = os.environ.get("ISAACLAB_RENDER_PROFILE", "0") != "0" -"""Whether to time and print :data:`RENDER_PROFILE_SCOPE`, read once from ``ISAACLAB_RENDER_PROFILE``. - -Off by default because the timer synchronizes the device on entry and exit. That is what lets it -measure completed device work rather than submitted work, but it also removes CPU/GPU overlap, so -an enabled run is a profiling aid and not a throughput measurement. -""" + warnings.warn( + "isaaclab.renderers.render_context.RENDER_PROFILE_SCOPE is deprecated; " + "use isaaclab.benchmark.stepping.RENDER_PROFILE_SCOPE instead.", + DeprecationWarning, + stacklevel=2, + ) + return RENDER_PROFILE_SCOPE + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @wp.kernel(enable_backward=False) @@ -340,20 +338,9 @@ def render_into_camera( camera_data: CameraData, physics_step_count: int, ) -> None: - """Sync scene state, render, and read outputs into ``camera_data``. - - Only the render itself is bracketed by :data:`RENDER_PROFILE_SCOPE`, so a profile - attributes neither the scene-state sync before it nor the output readback after it to - rendering. See :data:`_RENDER_PROFILE_ENABLED` for how to turn the timer on. - """ + """Sync scene state, render, and read outputs into ``camera_data``.""" self.update_scene_state(physics_step_count) - with wp.ScopedTimer( - RENDER_PROFILE_SCOPE, - active=_RENDER_PROFILE_ENABLED, - print=True, - synchronize=True, - ): - renderer.render(render_data) + renderer.render(render_data) renderer.read_output(render_data, camera_data) def reset_stage_prepare_flag(self) -> None: diff --git a/source/isaaclab/test/benchmark/test_benchmark_core.py b/source/isaaclab/test/benchmark/test_benchmark_core.py index 4a29d6105a28..5e8061cb310d 100644 --- a/source/isaaclab/test/benchmark/test_benchmark_core.py +++ b/source/isaaclab/test/benchmark/test_benchmark_core.py @@ -303,14 +303,47 @@ def test_formatter_selection_and_output_filenames(tmp_path): def test_attached_bundles_are_projected_to_flat_formatters(tmp_path): + bundle = _minimal_runtime_bundle() + physics_scope = "IsaacLab::Physics::step" + render_scope = "IsaacLab::Renderer::render" + render_ms = 2.123456789 + profile_metrics = { + "physics_mean_ms": 2.0, + "physics_std_ms": 0.5, + "physics_max_ms": 3.0, + "physics_calls": 2, + "render_mean_ms": render_ms, + "render_std_ms": 0.0, + "render_max_ms": render_ms, + "render_calls": 1, + } cases = [ - (_minimal_runtime_bundle(), "runtime", "Mean Total FPS", 100.0), - (_minimal_training_bundle(), "train", "Last Reward", 3.0), - (_minimal_play_bundle(), "play", "Mean Reward", 4.0), - (_minimal_startup_bundle(), "python_imports", "Wall Clock Time", 0.25), + (bundle, "runtime", {"Mean Total FPS": 100.0}), + ( + replace(bundle, extra={"distributed": True, "world_size": 2, "workload_scope": "rank0"}), + "runtime", + {"Mean Total FPS": 100.0}, + ), + ( + replace(bundle, extra=profile_metrics), + "runtime", + { + f"Mean {physics_scope} Time per Call": 2.0, + f"Std {physics_scope} Time per Call": 0.5, + f"Max {physics_scope} Time per Call": 3.0, + f"{physics_scope} Calls": 2, + f"Mean {render_scope} Time per Call": render_ms, + f"Std {render_scope} Time per Call": 0.0, + f"Max {render_scope} Time per Call": render_ms, + f"{render_scope} Calls": 1, + }, + ), + (_minimal_training_bundle(), "train", {"Last Reward": 3.0}), + (_minimal_play_bundle(), "play", {"Mean Reward": 4.0}), + (_minimal_startup_bundle(), "python_imports", {"Wall Clock Time": 0.25}), ] - for index, (bundle, phase, metric, expected) in enumerate(cases): + for index, (bundle, phase, expected) in enumerate(cases): benchmark = BaseIsaacLabBenchmark( f"bundle_{index}", formatter_type="omniperf", @@ -323,7 +356,9 @@ def test_attached_bundles_are_projected_to_flat_formatters(tmp_path): with open(benchmark.output_file_path) as f: data = json.load(f) - assert data[phase][metric] == expected + assert {metric: data[phase][metric] for metric in expected} == expected + if bundle.extra != profile_metrics: + assert not any(physics_scope in metric or render_scope in metric for metric in data.get("runtime", {})) def test_environment_step_timing_flat_labels_describe_measurement_mode(): diff --git a/source/isaaclab/test/benchmark/test_formatters.py b/source/isaaclab/test/benchmark/test_formatters.py index 6561ad31dce1..8f8198f6e1d4 100644 --- a/source/isaaclab/test/benchmark/test_formatters.py +++ b/source/isaaclab/test/benchmark/test_formatters.py @@ -8,6 +8,7 @@ import json import os import re +from dataclasses import replace from datetime import datetime import pytest @@ -111,7 +112,9 @@ def test_schema_bundle_file_serializes_bundle_and_rejects_missing_bundle(tmp_pat phase = TestPhase(phase_name="runtime") phase.measurements.append(SingleMeasurement(name="Test FPS", value=60.0, unit="FPS")) formatter.add_metrics(phase) - formatter.finalize(str(tmp_path), "runtime", bundle=_minimal_runtime_bundle()) + profile_metrics = {"physics_mean_ms": 1.123456789, "render_mean_ms": 2.234567891, "physics_calls": 2} + bundle = replace(_minimal_runtime_bundle(), extra=profile_metrics) + formatter.finalize(str(tmp_path), "runtime", bundle=bundle) with open(os.path.join(str(tmp_path), "runtime.json")) as f: data = json.load(f) @@ -119,8 +122,10 @@ def test_schema_bundle_file_serializes_bundle_and_rejects_missing_bundle(tmp_pat assert data["run"]["task"] == "Isaac-Ant-Direct-v0" assert data["run"]["framework"] is None assert data["runtime"]["total_fps"]["mean"] == pytest.approx(100.0) + assert data["extra"] == profile_metrics + assert "scope_timings" not in data["runtime"] assert data["resources"]["gpu_mem_gb"]["peak"] == pytest.approx(12.0) - assert data["schema_version"] + assert data["schema_version"] == "1.4" assert "Test FPS" not in json.dumps(data) with pytest.raises(RuntimeError, match="requires a benchmark bundle"): diff --git a/source/isaaclab/test/benchmark/test_stepping.py b/source/isaaclab/test/benchmark/test_stepping.py index 337e2657e16f..1bcacd3bac89 100644 --- a/source/isaaclab/test/benchmark/test_stepping.py +++ b/source/isaaclab/test/benchmark/test_stepping.py @@ -6,19 +6,135 @@ """Tests for the runtime stepping helpers.""" import time +from contextlib import nullcontext +from types import SimpleNamespace +from unittest.mock import Mock import numpy as np import pytest import torch from isaaclab.benchmark.stepping import ( + PHYSICS_PROFILE_SCOPE, + RENDER_PROFILE_SCOPE, EnvironmentStepTimingRecorder, + profile_physics_steps, + profile_renderers, run_runtime_loop, run_runtime_warmup, sample_random_actions, ) +@pytest.mark.parametrize( + ("active", "inherited", "fail"), + [ + pytest.param(False, False, False, id="disabled"), + pytest.param(True, False, False, id="enabled"), + pytest.param(True, True, True, id="inherited-failure"), + ], +) +def test_profile_physics_steps_times_complete_step_once(monkeypatch, capsys, active, inherited, fail): + """Profile complete steps once and restore the class across failures and repeated runs.""" + import warp as wp + + from isaaclab.physics import PhysicsManager + + calls = [] + + class BaseManager(PhysicsManager): + @classmethod + def step(cls): + assert cls is manager + calls.append("base") + + class CoupledManager(BaseManager): + @classmethod + def step(cls): + calls.append("before") + super().step() + calls.append("after") + if fail: + raise ValueError("step failed") + + class InheritedManager(CoupledManager): + pass + + manager = InheritedManager if inherited else CoupledManager + synchronize = Mock(wraps=wp.synchronize) + monkeypatch.setattr(wp, "synchronize", synchronize) + + timings = [] + original_step = manager.step + with pytest.raises(ValueError, match="step failed") if fail else nullcontext(): + with profile_physics_steps(manager, active=active, timings=timings) as collected: + assert collected is timings + manager.step() + + assert calls == ["before", "base", "after"] + assert synchronize.call_count == (2 if active else 0) + assert len(timings) == int(active) + assert all(scope == PHYSICS_PROFILE_SCOPE and elapsed >= 0.0 for scope, elapsed in timings) + assert PHYSICS_PROFILE_SCOPE not in capsys.readouterr().out + assert manager.step == original_step + assert ("step" in vars(manager)) is not inherited + + fail = False + manager.step() + for enabled in (True, False): + with profile_physics_steps(manager, active=enabled) as later_timings: + manager.step() + assert len(later_timings) == int(enabled) + assert len(timings) == int(active) + assert manager.step == original_step + assert synchronize.call_count == 2 * (int(active) + 1) + + +def test_profile_renderers_wraps_each_renderer(monkeypatch, capsys): + """Renderer methods and instance overrides are restored after failures and repeated runs.""" + import warp as wp + + second_render = Mock(side_effect=ValueError("render failed")) + + class Renderer: + def render(self, render_data): + second_render(render_data) + + first, second = SimpleNamespace(render=Mock()), Renderer() + originals = [first.render, second.render] + context = SimpleNamespace(_renderer_entries=[(None, first), (None, second)]) + synchronize = Mock() + monkeypatch.setattr(wp, "synchronize", synchronize) + + timings = [] + with pytest.raises(ValueError, match="render failed"): + with profile_renderers(context, timings=timings) as collected: + assert collected is timings + first.render("first") + second.render("second") + + originals[0].assert_called_once_with("first") + second_render.assert_called_once_with("second") + assert synchronize.call_count == 4 + assert len(timings) == 2 + assert all(scope == RENDER_PROFILE_SCOPE and elapsed >= 0.0 for scope, elapsed in timings) + assert RENDER_PROFILE_SCOPE not in capsys.readouterr().out + assert [first.render, second.render] == originals + assert "render" not in vars(second) + + second_render.side_effect = None + first.render("unprofiled") + for enabled in (True, False): + with profile_renderers(context, active=enabled) as later_timings: + first.render("first") + second.render("second") + assert len(later_timings) == 2 * int(enabled) + assert len(timings) == 2 + assert [first.render, second.render] == originals + assert "render" not in vars(second) + assert synchronize.call_count == 8 + + class _Space: def __init__(self, n): self.shape = (n,) diff --git a/source/isaaclab/test/renderers/test_simulation_render_context.py b/source/isaaclab/test/renderers/test_simulation_render_context.py index f940b15af34f..84a60c13dfda 100644 --- a/source/isaaclab/test/renderers/test_simulation_render_context.py +++ b/source/isaaclab/test/renderers/test_simulation_render_context.py @@ -7,14 +7,13 @@ from __future__ import annotations -import re from types import SimpleNamespace from unittest.mock import Mock, call import pytest import torch -from isaaclab.renderers import render_context +from isaaclab.benchmark.stepping import RENDER_PROFILE_SCOPE, profile_renderers from isaaclab.renderers.base_renderer import BaseRenderer from isaaclab.renderers.render_context import RenderContext from isaaclab.renderers.renderer_cfg import RendererCfg @@ -173,14 +172,14 @@ def test_scene_state_updates_once_per_step_until_cadence_reset(sim): @pytest.mark.parametrize("profile", [False, True]) -def test_render_into_camera_call_order_and_profile_output(sim, monkeypatch, capsys, profile): - """Profiling preserves call order and prints the renderer benchmark's timing format.""" - monkeypatch.setattr(render_context, "_RENDER_PROFILE_ENABLED", profile) +def test_render_into_camera_call_order_and_profile_output(sim, capsys, profile): + """Profiling preserves call order and collects timings without printing.""" renderer = sim.get_or_create_backend(RendererCfg(class_type=_renderer)) data, camera = object(), CameraData() - sim.render_context.render_into_camera(renderer, data, camera, physics_step_count=1) - sim.render_context.render_into_camera(renderer, data, camera, physics_step_count=1) + with profile_renderers(sim.render_context, active=profile) as timings: + sim.render_context.render_into_camera(renderer, data, camera, physics_step_count=1) + sim.render_context.render_into_camera(renderer, data, camera, physics_step_count=1) assert renderer.mock_calls == [ call.update_transforms(), @@ -190,8 +189,17 @@ def test_render_into_camera_call_order_and_profile_output(sim, monkeypatch, caps call.render(data), call.read_output(data, camera), ] - timing = rf"{re.escape(render_context.RENDER_PROFILE_SCOPE)} took [\d.]+ ms" - assert len(re.findall(timing, capsys.readouterr().out)) == (2 if profile else 0) + assert len(timings) == (2 if profile else 0) + assert all(scope == RENDER_PROFILE_SCOPE and elapsed >= 0.0 for scope, elapsed in timings) + assert RENDER_PROFILE_SCOPE not in capsys.readouterr().out + + +def test_legacy_render_profile_scope_warns_and_preserves_import(): + """The old scope import remains available during its deprecation period.""" + with pytest.warns(DeprecationWarning, match="isaaclab.benchmark.stepping.RENDER_PROFILE_SCOPE"): + from isaaclab.renderers.render_context import RENDER_PROFILE_SCOPE as legacy_scope + + assert legacy_scope == RENDER_PROFILE_SCOPE @pytest.mark.parametrize("fail_writer", [False, True]) diff --git a/source/isaaclab_tasks/changelog.d/render-benchmark-physics-mode.minor.rst b/source/isaaclab_tasks/changelog.d/render-benchmark-physics-mode.minor.rst new file mode 100644 index 000000000000..91a908815f8b --- /dev/null +++ b/source/isaaclab_tasks/changelog.d/render-benchmark-physics-mode.minor.rst @@ -0,0 +1,10 @@ +Added +^^^^^ + +* Added a ``benchmark_mode`` option to the ``Isaac-RenderBenchmark-Franka-Cabinet`` task, read from the + ``BENCHMARK_MODE`` environment variable. The default ``"render"`` mode wrote analytic joint poses after + physics and required ``scene.lazy_sensor_update=True`` so rendering followed the pose write. + Isaac RTX direct posing also rejected visualizers that pumped the Kit app; use ``--visualizer none``. + Set ``BENCHMARK_MODE=physics_render`` to preserve actuator-driven animation. Both modes still stepped physics. + The renderer sweep enabled physics and render timers and reported per-frame and combined timings + from the profiling JSON file. diff --git a/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/__init__.py b/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/__init__.py index 7ab3de636b7a..0877507cd640 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/__init__.py +++ b/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Render-only benchmark scene for OVRTX / Newton-Warp comparison.""" +"""Renderer and physics benchmark scene for OVRTX / Newton-Warp comparison.""" import gymnasium as gym diff --git a/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env.py b/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env.py index 84c4f03b5f87..b4bf6d2f41f2 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env.py +++ b/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env.py @@ -3,11 +3,12 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Direct environment for render-only benchmarking.""" +"""Direct environment for renderer and physics-plus-renderer benchmarking.""" from __future__ import annotations import math +from collections.abc import Iterator from typing import TYPE_CHECKING import torch @@ -16,6 +17,8 @@ from isaaclab.sensors import save_images_to_file if TYPE_CHECKING: + from isaaclab.assets import Articulation + from .render_benchmark_env_cfg import RenderBenchmarkFrankaCabinetEnvCfg @@ -23,19 +26,34 @@ class RenderBenchmarkEnv(DirectRLEnv): """Environment that animates its articulations so a renderer can be profiled on them. Every articulation in the configured scene is driven by a sinusoid around its default joint - positions. Actions are ignored and rewards are zero: the only output that matters is the camera - image, and the only cost that matters is the time spent producing it. + positions. Actions are ignored and rewards are zero. + + ``"physics_render"`` mode tracks the sinusoid through actuator targets. ``"render"`` mode + writes joint poses after physics and requires lazy sensor updates so rendering sees those poses. + Isaac RTX direct posing also requires visualizers that do not pump the Kit app loop. """ cfg: RenderBenchmarkFrankaCabinetEnvCfg def __init__(self, cfg: RenderBenchmarkFrankaCabinetEnvCfg, render_mode: str | None = None, **kwargs): + if cfg.benchmark_mode == "render" and not cfg.scene.lazy_sensor_update: + raise ValueError("Render benchmark mode requires scene.lazy_sensor_update=True to render the direct pose.") # Per-(env, joint) sinusoid phases [rad] and the elapsed animation time [s]. The phases # are filled on the first step; see :meth:`_sample_animation_phases`. self._anim_phases: dict[str, torch.Tensor] | None = None self._anim_time: float = 0.0 super().__init__(cfg, render_mode, **kwargs) self._tiled_camera = self.scene["tiled_camera"] + if ( + cfg.benchmark_mode == "render" + and "isaac_rtx" in self.sim.render_context.renderer_types + and any(visualizer.pumps_app_update() for visualizer in self.sim.visualizers) + ): + self.close() + raise ValueError( + "Isaac RTX direct posing requires --visualizer none or a visualizer that does not pump the Kit app" + " loop. Alternatively, use benchmark_mode=physics_render." + ) # --- joint animation ----------------------------------------------------- @@ -44,7 +62,10 @@ def _pre_physics_step(self, actions: torch.Tensor) -> None: return if self._anim_phases is None: self._anim_phases = self._sample_animation_phases() - self._animate_joints() + self._anim_time += self.cfg.sim.dt * self.cfg.decimation + # Direct poses are applied after physics in _get_observations. + if self.cfg.benchmark_mode == "physics_render": + self._request_joint_targets() def _apply_action(self) -> None: pass @@ -70,20 +91,41 @@ def _sample_animation_phases(self) -> dict[str, torch.Tensor]: phases[name] = ((env_idx * 7919.0 + joint_idx * 6553.0) % 10007.0) * (2.0 * math.pi / 10007.0) return phases - def _animate_joints(self) -> None: - """Advance the animation clock and drive every joint to its sinusoidal target.""" - self._anim_time += self.cfg.sim.dt * self.cfg.decimation + def _animation_targets(self) -> Iterator[tuple[Articulation, torch.Tensor]]: + """Yield every articulation with its joint pose for the current animation time. + + Yields: + Each articulation and its per-joint target [m or rad, depending on joint type], + clamped to the joint's soft limits, shape ``[num_envs, num_joints]``. + """ omega = 2.0 * math.pi * self.cfg.joint_animation_freq_hz for name, articulation in self.scene.articulations.items(): default_pos = articulation.data.default_joint_pos.torch offset = self.cfg.joint_animation_amplitude * torch.sin(omega * self._anim_time + self._anim_phases[name]) soft_limits = articulation.data.soft_joint_pos_limits.torch - target = torch.clamp(default_pos + offset, soft_limits[..., 0], soft_limits[..., 1]) + yield articulation, torch.clamp(default_pos + offset, soft_limits[..., 0], soft_limits[..., 1]) + + def _request_joint_targets(self) -> None: + """Set actuator targets for the current animation pose.""" + for articulation, target in self._animation_targets(): articulation.actuators.target_command.set_position_index(value=target) + def _pose_joints_directly(self) -> None: + """Write the current pose and clear momentum from the preceding physics step.""" + for articulation, target in self._animation_targets(): + articulation.write_joint_position_to_sim_index(position=target) + articulation.write_joint_velocity_to_sim_index(velocity=torch.zeros_like(target)) + # --- DirectRLEnv plumbing ------------------------------------------------ def _get_observations(self) -> dict: + # Apply direct poses after physics and propagate them to the renderer's body transforms. + if self.cfg.benchmark_mode == "render" and self._anim_phases is not None: + self._pose_joints_directly() + self.sim.forward() + # forward() changes transforms without advancing the renderer's physics-step key. + self.sim.render_context.reset_scene_state_cadence() + # Sensor buffers update lazily, so reading the camera's data is what drives the render. # This access is the work the benchmark measures: keep it unconditional even when no # image is written, or the profile records a scene that was never rendered. diff --git a/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env_cfg.py b/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env_cfg.py index 229d194716b1..45b655f35c0c 100644 --- a/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env_cfg.py +++ b/source/isaaclab_tasks/isaaclab_tasks/benchmark/render_benchmark/render_benchmark_env_cfg.py @@ -6,6 +6,7 @@ from __future__ import annotations import os +from typing import Literal, cast from isaaclab_newton.physics import MJWarpSolverCfg, NewtonCfg from isaaclab_newton.renderers import NewtonWarpRendererCfg @@ -28,6 +29,37 @@ from isaaclab_assets.robots.franka import FRANKA_PANDA_HIGH_PD_CFG +BenchmarkMode = Literal["render", "physics_render"] +"""Animation mode used by the render benchmark. + +``"render"`` writes analytic joint poses after physics and requires ``scene.lazy_sensor_update=True``. +Isaac RTX direct posing also requires no Kit app-pumping visualizer (for example, ``--visualizer none``). +``"physics_render"`` sends actuator targets before physics and renders the resulting state. +Both modes still step physics; the renderer sweep reports physics and rendering timings separately. +""" + +BENCHMARK_MODES: tuple[BenchmarkMode, ...] = ("render", "physics_render") +"""Every value :attr:`RenderBenchmarkFrankaCabinetEnvCfg.benchmark_mode` accepts.""" + + +def _read_benchmark_mode() -> BenchmarkMode: + """Read the default benchmark mode from ``BENCHMARK_MODE``, rejecting unknown values. + + Returns: + The configured mode, or ``"render"`` when the variable is unset. + + Raises: + ValueError: If ``BENCHMARK_MODE`` is set to a value outside :data:`BENCHMARK_MODES`. + """ + mode = os.getenv("BENCHMARK_MODE", "render") + if mode not in BENCHMARK_MODES: + raise ValueError(f"Unknown BENCHMARK_MODE '{mode}'. Expected one of {list(BENCHMARK_MODES)}.") + return cast(BenchmarkMode, mode) + + +BENCHMARK_MODE: BenchmarkMode = _read_benchmark_mode() +"""Default :attr:`RenderBenchmarkFrankaCabinetEnvCfg.benchmark_mode`, read once at import.""" + @configclass class RenderBenchmarkPhysicsCfg(PresetCfg): @@ -171,6 +203,8 @@ class RenderBenchmarkFrankaCabinetEnvCfg(DirectRLEnvCfg): Franka's seven, and a sinusoidal animation drives all of them so every rendered frame has moving articulated geometry rather than a static scene. There is no policy: actions are ignored, rewards are zero, and the episode only ends on time-out. + + :attr:`benchmark_mode` selects direct posing or actuator tracking. See :data:`BenchmarkMode`. """ decimation: int = 2 @@ -193,5 +227,13 @@ class RenderBenchmarkFrankaCabinetEnvCfg(DirectRLEnvCfg): joint_animation_freq_hz: float = 0.35 """Frequency of the joint animation [Hz].""" + benchmark_mode: BenchmarkMode = BENCHMARK_MODE + """Whether animation uses direct joint poses or actuator targets. + + See :data:`BenchmarkMode`. Defaults to the ``BENCHMARK_MODE`` environment variable, or + ``"render"`` when it is unset. Render mode requires ``scene.lazy_sensor_update=True``. + With Isaac RTX, use ``--visualizer none`` or a visualizer that does not pump the Kit app loop. + """ + write_image_to_file: bool = os.getenv("BENCHMARK_SAVE_IMAGE", "0") == "1" """Whether to dump each rendered frame to a PNG, for eyeballing renderer output.""" diff --git a/source/isaaclab_tasks/test/benchmark/test_render_benchmark_cfg.py b/source/isaaclab_tasks/test/benchmark/test_render_benchmark_cfg.py index cb99824f25c6..cc332b357f84 100644 --- a/source/isaaclab_tasks/test/benchmark/test_render_benchmark_cfg.py +++ b/source/isaaclab_tasks/test/benchmark/test_render_benchmark_cfg.py @@ -9,17 +9,24 @@ :mod:`scripts.benchmarks.benchmark_renderer` selects a preset before launching a run. """ +from functools import partial +from types import SimpleNamespace +from unittest.mock import Mock, PropertyMock + import gymnasium as gym import pytest +import torch from isaaclab_newton.physics import NewtonCfg from isaaclab_newton.renderers import NewtonWarpRendererCfg from isaaclab_physx.physics import PhysxCfg from isaaclab_physx.sim.schemas import PhysxRigidBodyCfg +from isaaclab.envs import DirectRLEnv from isaaclab.physics import PhysxAutoCfg from isaaclab.sim.schemas import MassCfg, UsdPhysicsCollisionCfg, UsdPhysicsRigidBodyCfg import isaaclab_tasks # noqa: F401 +from isaaclab_tasks.benchmark.render_benchmark.render_benchmark_env import RenderBenchmarkEnv from isaaclab_tasks.utils.hydra import collect_presets, resolve_presets from isaaclab_tasks.utils.parse_cfg import load_cfg_from_registry @@ -48,6 +55,92 @@ def test_default_scene_and_articulations(): assert cfg.scene.robot.prim_path == "{ENV_REGEX_NS}/Robot" assert cfg.scene.cabinet.prim_path == "{ENV_REGEX_NS}/Cabinet" assert cfg.joint_animation_amplitude == pytest.approx(0.4) + assert cfg.benchmark_mode == "render" + + +@pytest.mark.parametrize( + ("mode", "lazy_sensor_update", "renderer_type", "pumps_app_update", "error"), + [ + ("render", False, "newton_warp", False, "lazy_sensor_update=True"), + ("physics_render", False, "newton_warp", False, None), + ("render", True, "isaac_rtx", True, "--visualizer none"), + ("physics_render", True, "isaac_rtx", True, None), + ("render", True, "ovrtx", True, None), + ("render", True, "isaac_rtx", False, None), + ], +) +def test_render_mode_rejects_rendering_before_direct_pose( + monkeypatch, mode, lazy_sensor_update, renderer_type, pumps_app_update, error +): + """Direct posing requires camera rendering to follow the joint writes.""" + cfg = _load_cfg().replace(benchmark_mode=mode) + cfg.scene.lazy_sensor_update = lazy_sensor_update + + def initialize(self, *args, **kwargs): + self.scene = {"tiled_camera": None} + self.sim = SimpleNamespace( + render_context=SimpleNamespace(renderer_types=(renderer_type,)), + visualizers=[SimpleNamespace(pumps_app_update=lambda: pumps_app_update)], + ) + + close = Mock() + monkeypatch.setattr(DirectRLEnv, "__init__", initialize) + monkeypatch.setattr(DirectRLEnv, "close", close) + + if error: + with pytest.raises(ValueError, match=error): + RenderBenchmarkEnv(cfg) + else: + RenderBenchmarkEnv(cfg) + if error == "--visualizer none": + close.assert_called_once() + else: + close.assert_not_called() + + +@pytest.mark.parametrize("mode", ["render", "physics_render"]) +def test_benchmark_mode_orders_joint_updates_and_rendering(mode): + """Direct poses follow physics; actuator targets precede it, and both render last.""" + events = Mock() + articulation = events.articulation + articulation.data.default_joint_pos.torch = torch.zeros(1, 2) + articulation.data.soft_joint_pos_limits.torch = torch.tensor([[[-1.0, 1.0], [-1.0, 1.0]]]) + camera_data = Mock() + type(camera_data).output = PropertyMock(side_effect=lambda: events.render()) + env = SimpleNamespace( + cfg=_load_cfg().replace(benchmark_mode=mode, write_image_to_file=False), + sim=events.sim, + num_envs=1, + device="cpu", + _anim_time=0.0, + _anim_phases={"robot": torch.zeros(1, 2)}, + scene=SimpleNamespace(articulations={"robot": articulation}), + _tiled_camera=SimpleNamespace(data=camera_data), + ) + for name in ("_animation_targets", "_request_joint_targets", "_pose_joints_directly"): + setattr(env, name, partial(getattr(RenderBenchmarkEnv, name), env)) + + RenderBenchmarkEnv._pre_physics_step(env, actions=None) + events.physics() + RenderBenchmarkEnv._get_observations(env) + + if mode == "render": + assert [entry[0] for entry in events.mock_calls] == [ + "physics", + "articulation.write_joint_position_to_sim_index", + "articulation.write_joint_velocity_to_sim_index", + "sim.forward", + "sim.render_context.reset_scene_state_cadence", + "render", + ] + velocity = articulation.write_joint_velocity_to_sim_index.call_args.kwargs["velocity"] + assert torch.equal(velocity, torch.zeros_like(velocity)) + else: + assert [entry[0] for entry in events.mock_calls] == [ + "articulation.actuators.target_command.set_position_index", + "physics", + "render", + ] @pytest.mark.parametrize(