From 71b89c457ac2721b0bf9252c1cc221606529a9e9 Mon Sep 17 00:00:00 2001 From: Mayank Mittal <12863862+Mayankm96@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:06:47 +0100 Subject: [PATCH] Updates the docs to latest sphinx version (#190) # Description This MR does the following: * Addresses some of the build issues related to docs and migrates to the latest sphinx * Also updates the API references ## Type of change - New feature (non-breaking change which adds functionality) ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./orbit.sh --format` - [x] 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 - [ ] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file --- .github/workflows/docs.yaml | 2 +- .gitignore | 4 + CONTRIBUTORS.md | 2 +- README.md | 15 +- docs/conf.py | 98 ++-- docs/index.rst | 38 +- docs/requirements.txt | 16 +- docs/source/_static/NVIDIA-logo-black.png | Bin 0 -> 39261 bytes docs/source/_static/NVIDIA-logo-white.png | Bin 0 -> 130540 bytes docs/source/_static/css/custom.css | 74 +++ docs/source/_static/css/nvidia.css | 352 -------------- docs/source/_static/nv-logo.png | Bin 53324 -> 0 bytes docs/source/api/index.rst | 69 ++- docs/source/api/orbit.actuators.group.rst | 174 ------- docs/source/api/orbit.actuators.model.rst | 123 ----- docs/source/api/orbit.app.rst | 5 - docs/source/api/orbit.asset_loader.rst | 7 - docs/source/api/orbit.command_generators.rst | 6 - docs/source/api/orbit.compat.rst | 114 ----- docs/source/api/orbit.devices.rst | 7 - docs/source/api/orbit.markers.rst | 7 - docs/source/api/orbit.objects.articulated.rst | 7 - docs/source/api/orbit.objects.rigid.rst | 7 - docs/source/api/orbit.robots.rst | 45 -- docs/source/api/orbit.sensors.rst | 43 -- docs/source/api/orbit.utils.assets.rst | 7 - docs/source/api/orbit.utils.math.rst | 7 - docs/source/api/orbit.utils.mdp.rst | 7 - docs/source/api/orbit.utils.rst | 52 -- .../api/orbit/omni.isaac.orbit.actuators.rst | 104 ++++ .../source/api/orbit/omni.isaac.orbit.app.rst | 16 + .../api/orbit/omni.isaac.orbit.assets.rst | 69 +++ .../omni.isaac.orbit.command_generators.rst | 103 ++++ .../orbit/omni.isaac.orbit.controllers.rst | 25 + .../api/orbit/omni.isaac.orbit.devices.rst | 61 +++ .../api/orbit/omni.isaac.orbit.envs.mdp.rst | 44 ++ .../api/orbit/omni.isaac.orbit.envs.rst | 49 ++ .../api/orbit/omni.isaac.orbit.envs.ui.rst | 24 + .../api/orbit/omni.isaac.orbit.managers.rst | 127 +++++ .../api/orbit/omni.isaac.orbit.markers.rst | 23 + .../api/orbit/omni.isaac.orbit.scene.rst | 23 + .../omni.isaac.orbit.sensors.patterns.rst | 51 ++ .../api/orbit/omni.isaac.orbit.sensors.rst | 137 ++++++ .../orbit/omni.isaac.orbit.sim.converters.rst | 54 +++ .../source/api/orbit/omni.isaac.orbit.sim.rst | 49 ++ .../orbit/omni.isaac.orbit.sim.schemas.rst | 68 +++ .../orbit/omni.isaac.orbit.sim.spawners.rst | 236 +++++++++ .../omni.isaac.orbit.terrains.rst} | 66 ++- .../api/orbit/omni.isaac.orbit.utils.rst | 92 ++++ docs/source/api/orbit_tasks.isaac_env.rst | 63 --- .../api/orbit_tasks.utils.data_collector.rst | 79 --- docs/source/api/orbit_tasks.utils.rst | 7 - .../source/api/orbit_tasks.utils.wrappers.rst | 54 --- ...isaac.orbit_tasks.utils.data_collector.rst | 17 + .../omni.isaac.orbit_tasks.utils.rst | 13 + .../omni.isaac.orbit_tasks.utils.wrappers.rst | 33 ++ docs/source/features/actuators.rst | 31 +- docs/source/features/environments.rst | 2 +- docs/source/refs/faq.rst | 26 +- docs/source/refs/license.rst | 6 +- docs/source/refs/roadmap.rst | 114 ----- docs/source/setup/developer.rst | 27 +- docs/source/setup/docker.rst | 20 +- docs/source/setup/installation.rst | 100 ++-- docs/source/setup/sample.rst | 2 +- docs/source/tutorials/00_empty.rst | 74 ++- docs/source/tutorials_envs/00_gym_env.rst | 2 +- docs/source/tutorials_envs/01_create_env.rst | 2 +- docs/source/tutorials_envs/02_wrappers.rst | 70 +-- orbit.sh | 3 +- .../omni.isaac.orbit/docs/CHANGELOG.rst | 2 +- .../omni/isaac/orbit/__init__.py | 4 +- .../omni/isaac/orbit/actuators/__init__.py | 40 +- .../isaac/orbit/actuators/actuator_base.py | 32 +- .../isaac/orbit/actuators/actuator_cfg.py | 20 +- .../omni.isaac.orbit/omni/isaac/orbit/app.py | 96 ++-- .../omni/isaac/orbit/assets/__init__.py | 33 +- .../orbit/assets/articulation/__init__.py | 4 +- .../orbit/assets/articulation/articulation.py | 78 ++- .../assets/articulation/articulation_data.py | 34 +- .../omni/isaac/orbit/assets/asset_base.py | 28 +- .../omni/isaac/orbit/assets/asset_base_cfg.py | 25 +- .../isaac/orbit/assets/config/__init__.py | 9 +- .../omni/isaac/orbit/assets/config/anymal.py | 5 +- .../omni/isaac/orbit/assets/config/franka.py | 2 - .../orbit/assets/config/ridgeback_franka.py | 2 - .../omni/isaac/orbit/assets/config/unitree.py | 2 - .../orbit/assets/config/universal_robots.py | 2 - .../orbit/assets/rigid_object/__init__.py | 4 +- .../orbit/assets/rigid_object/rigid_object.py | 46 +- .../assets/rigid_object/rigid_object_cfg.py | 4 +- .../assets/rigid_object/rigid_object_data.py | 44 +- .../orbit/command_generators/__init__.py | 22 +- .../omni/isaac/orbit/compat/__init__.py | 7 +- .../orbit/compat/actuators/group/__init__.py | 26 - .../actuators/group/actuator_control_cfg.py | 56 --- .../compat/actuators/group/actuator_group.py | 458 ------------------ .../actuators/group/actuator_group_cfg.py | 77 --- .../compat/actuators/group/gripper_group.py | 152 ------ .../actuators/group/non_holonomic_group.py | 103 ---- .../isaac/orbit/compat/markers/__init__.py | 4 - .../orbit/compat/markers/point_marker.py | 8 +- .../orbit/compat/markers/static_marker.py | 10 +- .../isaac/orbit/compat/sensors/__init__.py | 2 - .../orbit/compat/sensors/camera/__init__.py | 4 - .../orbit/compat/sensors/camera/camera.py | 6 +- .../orbit/compat/sensors/camera/camera_cfg.py | 4 +- .../compat/sensors/height_scanner/__init__.py | 4 - .../sensors/height_scanner/height_scanner.py | 10 +- .../height_scanner/height_scanner_marker.py | 8 +- .../compat/sensors/height_scanner/utils.py | 2 - .../omni/isaac/orbit/compat/utils/__init__.py | 9 - .../isaac/orbit/compat/utils/configclass.py | 226 --------- .../omni/isaac/orbit/controllers/__init__.py | 10 +- .../orbit/controllers/differential_ik.py | 3 + .../orbit/controllers/joint_impedance.py | 4 +- .../orbit/controllers/operational_space.py | 2 +- .../omni/isaac/orbit/devices/__init__.py | 70 +-- .../omni/isaac/orbit/devices/device_base.py | 2 +- .../isaac/orbit/devices/gamepad/__init__.py | 4 - .../isaac/orbit/devices/keyboard/__init__.py | 4 - .../orbit/devices/spacemouse/__init__.py | 4 - .../isaac/orbit/devices/spacemouse/utils.py | 4 - .../omni/isaac/orbit/envs/__init__.py | 36 +- .../omni/isaac/orbit/envs/base_env.py | 18 +- .../omni/isaac/orbit/envs/base_env_cfg.py | 2 - .../omni/isaac/orbit/envs/mdp/__init__.py | 15 +- .../isaac/orbit/envs/mdp/actions/__init__.py | 6 +- .../omni/isaac/orbit/envs/mdp/curriculums.py | 2 +- .../omni/isaac/orbit/envs/mdp/observations.py | 2 +- .../isaac/orbit/envs/mdp/randomizations.py | 4 +- .../omni/isaac/orbit/envs/mdp/rewards.py | 2 +- .../omni/isaac/orbit/envs/mdp/terminations.py | 2 +- .../omni/isaac/orbit/envs/rl_task_env.py | 2 +- .../omni/isaac/orbit/envs/ui/__init__.py | 9 +- .../omni/isaac/orbit/managers/__init__.py | 33 +- .../isaac/orbit/managers/action_manager.py | 4 +- .../omni/isaac/orbit/managers/manager_base.py | 2 +- .../isaac/orbit/managers/manager_term_cfg.py | 6 +- .../orbit/managers/observation_manager.py | 4 +- .../orbit/managers/termination_manager.py | 10 +- .../omni/isaac/orbit/markers/__init__.py | 7 +- .../orbit/markers/visualization_markers.py | 10 +- .../omni/isaac/orbit/scene/__init__.py | 5 +- .../isaac/orbit/scene/interactive_scene.py | 2 +- .../orbit/scene/interactive_scene_cfg.py | 5 +- .../omni/isaac/orbit/sensors/__init__.py | 39 +- .../isaac/orbit/sensors/camera/__init__.py | 6 +- .../isaac/orbit/sensors/camera/camera_cfg.py | 4 +- .../isaac/orbit/sensors/camera/camera_data.py | 10 +- .../omni/isaac/orbit/sensors/camera/utils.py | 17 +- .../orbit/sensors/contact_sensor/__init__.py | 8 +- .../contact_sensor/contact_sensor_data.py | 18 +- .../sensors/frame_transformer/__init__.py | 8 +- .../frame_transformer_cfg.py | 2 +- .../frame_transformer_data.py | 6 +- .../orbit/sensors/ray_caster/__init__.py | 18 +- .../sensors/ray_caster/patterns/__init__.py | 19 +- .../ray_caster/patterns/patterns_cfg.py | 6 +- .../sensors/ray_caster/ray_caster_camera.py | 2 +- .../ray_caster/ray_caster_camera_cfg.py | 2 +- .../sensors/ray_caster/ray_caster_cfg.py | 2 +- .../sensors/ray_caster/ray_caster_data.py | 8 +- .../isaac/orbit/sensors/sensor_base_cfg.py | 5 +- .../omni/isaac/orbit/sim/__init__.py | 2 +- .../isaac/orbit/sim/converters/__init__.py | 13 +- .../converters/asset_converter_base_cfg.py | 8 +- .../omni/isaac/orbit/sim/schemas/__init__.py | 22 - .../omni/isaac/orbit/sim/schemas/schemas.py | 18 +- .../isaac/orbit/sim/schemas/schemas_cfg.py | 16 +- .../omni/isaac/orbit/sim/simulation_cfg.py | 8 +- .../omni/isaac/orbit/sim/spawners/__init__.py | 56 ++- .../orbit/sim/spawners/from_files/__init__.py | 17 +- .../orbit/sim/spawners/lights/__init__.py | 17 +- .../isaac/orbit/sim/spawners/lights/lights.py | 2 +- .../orbit/sim/spawners/materials/__init__.py | 17 +- .../spawners/materials/visual_materials.py | 4 +- .../materials/visual_materials_cfg.py | 2 +- .../orbit/sim/spawners/sensors/__init__.py | 12 +- .../orbit/sim/spawners/sensors/sensors_cfg.py | 8 +- .../orbit/sim/spawners/shapes/__init__.py | 34 +- .../omni/isaac/orbit/sim/utils.py | 2 + .../omni/isaac/orbit/terrains/__init__.py | 13 +- .../orbit/terrains/height_field/__init__.py | 20 - .../terrains/height_field/hf_terrains.py | 16 +- .../terrains/height_field/hf_terrains_cfg.py | 6 +- .../orbit/terrains/height_field/utils.py | 2 +- .../orbit/terrains/terrain_generator_cfg.py | 8 +- .../isaac/orbit/terrains/terrain_importer.py | 2 +- .../orbit/terrains/terrain_importer_cfg.py | 2 +- .../isaac/orbit/terrains/trimesh/__init__.py | 18 - .../orbit/terrains/trimesh/mesh_terrains.py | 36 +- .../omni/isaac/orbit/utils/__init__.py | 51 +- .../omni/isaac/orbit/utils/array.py | 6 +- .../omni/isaac/orbit/utils/assets.py | 2 +- .../omni/isaac/orbit/utils/configclass.py | 80 +-- .../omni/isaac/orbit/utils/dict.py | 11 +- .../omni/isaac/orbit/utils/io/__init__.py | 2 - .../omni/isaac/orbit/utils/math.py | 442 +++++++++++------ .../omni/isaac/orbit/utils/noise/__init__.py | 25 +- .../omni/isaac/orbit/utils/noise/noise_cfg.py | 2 +- .../omni/isaac/orbit/utils/string.py | 13 +- .../omni/isaac/orbit/utils/timer.py | 2 +- .../omni/isaac/orbit/utils/warp/__init__.py | 9 +- .../omni/isaac/orbit/utils/warp/ops.py | 1 - .../test/managers/test_observation_manager.py | 2 +- .../test/managers/test_reward_manager.py | 2 +- .../omni/isaac/orbit_tasks/__init__.py | 26 +- .../omni/isaac/orbit_tasks/utils/__init__.py | 4 +- .../utils/data_collector/__init__.py | 80 ++- .../omni/isaac/orbit_tasks/utils/importer.py | 4 +- .../omni/isaac/orbit_tasks/utils/parse_cfg.py | 17 +- .../orbit_tasks/utils/wrappers/__init__.py | 31 +- .../orbit_tasks/utils/wrappers/rl_games.py | 3 - .../utils/wrappers/rsl_rl/__init__.py | 16 +- .../utils/wrappers/rsl_rl/exporter.py | 2 - .../utils/wrappers/rsl_rl/rl_cfg.py | 4 +- .../utils/wrappers/rsl_rl/vecenv_wrapper.py | 3 +- .../isaac/orbit_tasks/utils/wrappers/sb3.py | 8 +- .../isaac/orbit_tasks/utils/wrappers/skrl.py | 2 - source/standalone/demo/play_empty.py | 23 +- .../environments/state_machine/play_lift.py | 4 +- source/tools/convert_mesh.py | 14 +- source/tools/convert_urdf.py | 12 +- 224 files changed, 3022 insertions(+), 3905 deletions(-) create mode 100644 docs/source/_static/NVIDIA-logo-black.png create mode 100644 docs/source/_static/NVIDIA-logo-white.png create mode 100644 docs/source/_static/css/custom.css delete mode 100644 docs/source/_static/css/nvidia.css delete mode 100644 docs/source/_static/nv-logo.png delete mode 100644 docs/source/api/orbit.actuators.group.rst delete mode 100644 docs/source/api/orbit.actuators.model.rst delete mode 100644 docs/source/api/orbit.app.rst delete mode 100644 docs/source/api/orbit.asset_loader.rst delete mode 100644 docs/source/api/orbit.command_generators.rst delete mode 100644 docs/source/api/orbit.compat.rst delete mode 100644 docs/source/api/orbit.devices.rst delete mode 100644 docs/source/api/orbit.markers.rst delete mode 100644 docs/source/api/orbit.objects.articulated.rst delete mode 100644 docs/source/api/orbit.objects.rigid.rst delete mode 100644 docs/source/api/orbit.robots.rst delete mode 100644 docs/source/api/orbit.sensors.rst delete mode 100644 docs/source/api/orbit.utils.assets.rst delete mode 100644 docs/source/api/orbit.utils.math.rst delete mode 100644 docs/source/api/orbit.utils.mdp.rst delete mode 100644 docs/source/api/orbit.utils.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.actuators.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.app.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.assets.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.command_generators.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.controllers.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.devices.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.envs.mdp.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.envs.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.envs.ui.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.managers.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.markers.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.scene.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.sensors.patterns.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.sensors.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.sim.converters.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.sim.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.sim.schemas.rst create mode 100644 docs/source/api/orbit/omni.isaac.orbit.sim.spawners.rst rename docs/source/api/{orbit.terrains.rst => orbit/omni.isaac.orbit.terrains.rst} (77%) create mode 100644 docs/source/api/orbit/omni.isaac.orbit.utils.rst delete mode 100644 docs/source/api/orbit_tasks.isaac_env.rst delete mode 100644 docs/source/api/orbit_tasks.utils.data_collector.rst delete mode 100644 docs/source/api/orbit_tasks.utils.rst delete mode 100644 docs/source/api/orbit_tasks.utils.wrappers.rst create mode 100644 docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.data_collector.rst create mode 100644 docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.rst create mode 100644 docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.wrappers.rst delete mode 100644 docs/source/refs/roadmap.rst delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/__init__.py delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_control_cfg.py delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group.py delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group_cfg.py delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/gripper_group.py delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/non_holonomic_group.py delete mode 100644 source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/configclass.py diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 9391c7bc35..4db83a9394 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -15,7 +15,7 @@ jobs: - name: Setup python uses: actions/setup-python@v2 with: - python-version: "3.8" + python-version: "3.10" architecture: x64 - name: Install dev requirements diff --git a/.gitignore b/.gitignore index 39dd07eefd..46e0c17a4a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,10 @@ docker/artifacts/ *.tmp +# Doc Outputs +**/docs/_build/* +**/generated/* + # Isaac-Sim packman _isaac_sim* _repo diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 6175e9f617..1fb51d7165 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -25,6 +25,7 @@ Guidelines for modifications: * James Smith * **Mayank Mittal** (maintainer) * Nikita Rudin +* Pascal Roth ## Contributors @@ -37,7 +38,6 @@ Guidelines for modifications: * Jingzhou Liu * Kourosh Darvish * Qinxi Yu -* Pascal Roth * René Zurbrügg * Ritvik Singh * Rosario Scalise diff --git a/README.md b/README.md index 178bcc03e2..3988b6437f 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,19 @@ # Orbit -[![IsaacSim](https://img.shields.io/badge/Isaac%20Sim-2022.2.0-orange.svg)](https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html) -[![Python](https://img.shields.io/badge/python-3.7-blue.svg)](https://docs.python.org/3/whatsnew/3.7.html) -[![Linux platform](https://img.shields.io/badge/platform-linux--64-lightgrey.svg)](https://releases.ubuntu.com/20.04/) +[![IsaacSim](https://img.shields.io/badge/IsaacSim-2023.1.0--hotfix.1-silver.svg)](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html) +[![Python](https://img.shields.io/badge/python-3.10-blue.svg)](https://docs.python.org/3/whatsnew/3.10.html) +[![Linux platform](https://img.shields.io/badge/platform-linux--64-orange.svg)](https://releases.ubuntu.com/20.04/) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://pre-commit.com/) [![Docs status](https://img.shields.io/badge/docs-passing-brightgreen.svg)](https://isaac-orbit.github.io/orbit) [![License](https://img.shields.io/badge/license-BSD--3-yellow.svg)](https://opensource.org/licenses/BSD-3-Clause) -Orbit is a unified and modular framework for robot learning powered by [NVIDIA Isaac Sim](https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/overview.html). It offers a modular design to easily and efficiently create robotic environments with photo-realistic scenes, and fast and accurate simulation. +**Orbit** is a unified and modular framework for robot learning that aims to simplify common workflows +in robotics research (such as RL, learning from demonstrations, and motion planning). It is built upon +[NVIDIA Isaac Sim](https://docs.omniverse.nvidia.com/isaacsim/latest/overview.html) to leverage the latest +simulation capabilities for photo-realistic scenes, and fast and accurate simulation. Please refer our [documentation page](https://isaac-orbit.github.io/orbit) to learn more about the installation steps, features and tutorials. @@ -37,13 +40,13 @@ For issues related to Isaac Sim, we recommend checking its [documentation](https NVIDIA Isaac Sim is available freely under [individual license](https://www.nvidia.com/en-us/omniverse/download/). For more information about its license terms, please check [here](https://docs.omniverse.nvidia.com/app_isaacsim/common/NVIDIA_Omniverse_License_Agreement.html#software-support-supplement). -ORBIT framework is released under [BSD-3 License](LICENSE). The license files of its dependencies and assets are present in the [`docs/licenses`](docs/licenses) directory. +Orbit framework is released under [BSD-3 License](LICENSE). The license files of its dependencies and assets are present in the [`docs/licenses`](docs/licenses) directory. ## Citation Please cite [this paper](https://arxiv.org/abs/2301.04195) if you use this framework in your work: -``` +```text @article{mittal2023orbit, author={Mittal, Mayank and Yu, Calvin and Yu, Qinxi and Liu, Jingzhou and Rudin, Nikita and Hoeller, David and Yuan, Jia Lin and Singh, Ritvik and Guo, Yunrong and Mazhar, Hammad and Mandlekar, Ajay and Babich, Buck and State, Gavriel and Hutter, Marco and Garg, Animesh}, journal={IEEE Robotics and Automation Letters}, diff --git a/docs/conf.py b/docs/conf.py index 8ea04b7098..ae4ac588da 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,10 +26,10 @@ # -- Project information ----------------------------------------------------- project = "orbit" -copyright = "2022, NVIDIA, ETH Zurich and University of Toronto" -author = "NVIDIA, ETH Zurich and University of Toronto" +copyright = "2022-2023, The ORBIT Project Developers." +author = "The ORBIT Project Developers." -version = "0.1.0" +version = "0.2.0" # -- General configuration --------------------------------------------------- @@ -37,18 +37,20 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx.ext.autodoc", + "autodocsumm", + "myst_parser", "sphinx.ext.napoleon", - "sphinx.ext.mathjax", - "sphinx.ext.intersphinx", + "sphinxemoji.sphinxemoji", + "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.githubpages", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", "sphinx.ext.todo", + "sphinx.ext.viewcode", "sphinxcontrib.bibtex", - "myst_parser", - "autodocsumm", "sphinx_copybutton", - "sphinx_panels", + "sphinx_design", ] # mathjax hacks @@ -69,17 +71,22 @@ ".md": "markdown", } -# put type hints inside the description instead of the signature (easier to read) -autodoc_typehints = "description" -autodoc_typehints_description_target = "documented" +# put type hints inside the signature instead of the description (easier to maintain) +autodoc_typehints = "signature" +# autodoc_typehints_format = "fully-qualified" # document class *and* __init__ methods autoclass_content = "class" # # separate class docstring from __init__ docstring autodoc_class_signature = "separated" # sort members by source order -autodoc_member_order = "groupwise" +autodoc_member_order = "bysource" +# inherit docstrings from base classes +autodoc_inherit_docstrings = True # BibTeX configuration bibtex_bibfiles = ["source/_static/refs.bib"] +# generate autosummary even if no references +autosummary_generate = True +autosummary_generate_overwrite = False # default autodoc settings autodoc_default_options = { "autosummary": True, @@ -88,8 +95,10 @@ # generate links to the documentation of objects in external projects intersphinx_mapping = { "python": ("https://docs.python.org/3", None), - "numpy": ("http://docs.scipy.org/doc/numpy", None), + "numpy": ("https://numpy.org/doc/stable/", None), "torch": ("https://pytorch.org/docs/stable/", None), + "isaac": ("https://docs.omniverse.nvidia.com/py/isaacsim", None), + "gymnasium": ("https://gymnasium.farama.org/", None), } # Add any paths that contain templates here, relative to this directory. @@ -112,6 +121,8 @@ "omni.kit", "omni.usd", "omni.client", + "omni.physx", + "omni.physics", "pxr.PhysxSchema", "pxr.PhysicsSchemaTools", "omni.replicator", @@ -120,6 +131,10 @@ "omni.isaac.cloner", "omni.isaac.urdf", "omni.isaac.version", + "omni.isaac.motion_generation", + "omni.isaac.ui", + "omni.timeline", + "omni.ui", "gym", "skrl", "stable_baselines3", @@ -156,23 +171,28 @@ "ref.python", ] +# -- Internationalization ---------------------------------------------------- + +# specifying the natural language populates some key tags +language = "en" + # -- Options for HTML output ------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# import sphinx_book_theme +html_title = "orbit documentation" html_theme_path = [sphinx_book_theme.get_html_theme_path()] html_theme = "sphinx_book_theme" -html_logo = "source/_static/nv-logo.png" html_favicon = "source/_static/favicon.ico" +html_show_copyright = True +html_show_sphinx = False +html_last_updated_fmt = "" # to reveal the build date in the pages meta # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["source/_static"] -html_css_files = ["css/nvidia.css"] +html_static_path = ["source/_static/css"] +html_css_files = ["custom.css"] html_theme_options = { "collapse_navigation": True, @@ -180,23 +200,45 @@ "use_repository_button": True, "use_issues_button": True, "use_edit_page_button": True, - "show_toc_level": 2, + "show_toc_level": 1, "use_sidenotes": True, - "announcement": ( - "⚠️This is a pre-release version of Orbit. Please report any issues on GitHub." - ), + "logo": { + "text": f"orbit documentation", + "image_light": "source/_static/NVIDIA-logo-white.png", + "image_dark": "source/_static/NVIDIA-logo-black.png", + }, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/NVIDIA-Omniverse/Orbit", + "icon": "fa-brands fa-square-github", + "type": "fontawesome", + }, + { + "name": "Isaac Sim", + "url": "https://developer.nvidia.com/isaac-sim", + "icon": "https://img.shields.io/badge/IsaacSim-2023.1.0-silver.svg", + "type": "url", + }, + { + "name": "Stars", + "url": "https://img.shields.io/github/stars/NVIDIA-Omniverse/Orbit?color=fedcba", + "icon": "https://img.shields.io/github/stars/NVIDIA-Omniverse/Orbit?color=fedcba", + "type": "url", + }, + ], + "icon_links_label": "Quick Links", } -html_show_copyright = True -html_show_sphinx = False +html_sidebars = {"**": ["navbar-logo.html", "icon-links.html", "search-field.html", "sbt-sidebar-nav.html"]} # -- Advanced configuration ------------------------------------------------- def skip_member(app, what, name, obj, skip, options): - exclusions = ["from_dict", "to_dict", "replace"] # List the names of the functions you want to skip here + # List the names of the functions you want to skip here + exclusions = ["from_dict", "to_dict", "replace", "copy", "__post_init__"] if name in exclusions: return True return None diff --git a/docs/index.rst b/docs/index.rst index 31a3cb3faa..1aaf3c866f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,34 +1,25 @@ Overview ======== -**Isaac Orbit** (or *orbit* in short) is a unified and modular framework, built on top of `NVIDIA -Omniverse `__ and `Isaac -Sim `__, -for robot learning. It offers a modular design to easily and efficiently -create robot learning environments with photo-realistic scenes, and fast -and efficient simulation. +**Orbit** is a unified and modular framework for robot learning that aims to simplify common workflows +in robotics research (such as RL, learning from demonstrations, and motion planning). It is built upon +`NVIDIA Isaac Sim` to leverage the latest simulation capabilities for photo-realistic scenes, and fast +and efficient simulation. The core objectives of the framework are: + +- **Modularity**: Easily customize and add new environments, robots, and sensors. +- **Agility**: Adapt to the changing needs of the community. +- **Openness**: Remain open-sourced to allow the community to contribute and extend the framework. +- **Battery-included**: Include a number of environments, sensors, and tasks that are ready to use. + +For more information about the framework, please refer to the `paper `_ +:cite:`mittal2023orbit`. For clarifications on NVIDIA Isaac ecosystem, please check out the +:doc:`/source/refs/faq` section. .. figure:: source/_static/tasks.jpg :width: 100% :alt: Example tasks created using orbit -If you use ``orbit`` in your work, please cite the `paper `_ -:cite:`mittal2023orbit` using the following BibTeX entry: - -.. code-block:: bibtex - - @article{mittal2023orbit, - author={Mittal, Mayank and Yu, Calvin and Yu, Qinxi and Liu, Jingzhou and Rudin, Nikita and Hoeller, David and Yuan, Jia Lin and Singh, Ritvik and Guo, Yunrong and Mazhar, Hammad and Mandlekar, Ajay and Babich, Buck and State, Gavriel and Hutter, Marco and Garg, Animesh}, - journal={IEEE Robotics and Automation Letters}, - title={Orbit: A Unified Simulation Framework for Interactive Robot Learning Environments}, - year={2023}, - volume={8}, - number={6}, - pages={3740-3747}, - doi={10.1109/LRA.2023.3270034} - } - .. toctree:: :maxdepth: 2 :caption: Getting Started @@ -80,7 +71,6 @@ If you use ``orbit`` in your work, please cite the `paper 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 literal 0 HcmV?d00001 diff --git a/docs/source/_static/NVIDIA-logo-white.png b/docs/source/_static/NVIDIA-logo-white.png new file mode 100644 index 0000000000000000000000000000000000000000..1143326b85f66ec9d1c84f38656a926ceb45dfc7 GIT binary patch literal 130540 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 L2jmgq%v`dZdt4DSq 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 div.version { - color: black; -} - - -.wy-nav-content-wrap { - background: inherit; -} - -.wy-side-nav-search input[type="text"] { - border: none; - box-shadow: none; - background: white; - border-radius: 0; - font-size: 100%; -} - -.wy-menu-vertical li.current a, -.wy-menu-vertical li.toctree-l1.current > a { - border: none; -} - -.ethical-rtd > div.ethical-sidebar, -.ethical-rtd > div.ethical-footer { - display: none !important; -} - -h1 { - /* text-transform: uppercase; */ - font-family: inherit; - font-weight: 200; -} - -h2, -.rst-content .toctree-wrapper p.caption { - font-family: inherit; - font-weight: 200; -} - -.rst-content a:visited { - color: #76b900; -} - -/* Begin code */ -.rst-content pre.literal-block, -.rst-content div[class^="highlight"] { - border: none; -} - -.rst-content pre.literal-block, -.rst-content div[class^="highlight"] pre, -.rst-content .linenodiv pre { - font-size: 80%; -} - -.highlight { - background: #f6f8fa; - border-radius: 6px; -} - -.highlight .kn, -.highlight .k { - color: #76b900; -} - -.highlight .nn { - color: inherit; - font-weight: inherit; -} - -.highlight .nc { - color: #76b900; - font-weight: inherit; -} - -.highlight .fm, -.highlight .nd, -.highlight .nf, -.highlight .nb { - color: #76b900; -} - -.highlight .bp, -.highlight .n { - color: inherit; -} - -.highlight .kc, -.highlight .s1, -.highlight .s2, -.highlight .mi, -.highlight .mf, -.highlight .bp, -.highlight .bn, -.highlight .ow { - color: #76b900; - font-weight: inherit; -} - -.highlight .c1 { - color: #6a737d; -} - -.rst-content code.xref { - padding: .2em .4em; - background: rgba(27,31,35,.05); - border-radius: 6px; - border: none; -} -/* End code */ - -/* This ensures that multiple constructors will remain in separate lines. */ -.rst-content dl:not(.docutils) dt, -.rst-content dl:not(.docutils) dl dt { - display: table; - background: rgb(243,244,247); - color: rgba(83, 150, 0, 0.9); - background-color: rgba(118, 185, 0, 0.15); - border-top-color: rgba(83, 150, 0, 0.9); -} - -.rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal { - color: #76b900 !important; -} - -.rst-content dl:not(.docutils) dt.field-odd, -.rst-content dl:not(.docutils) dt.field-odd { - text-transform: uppercase; - background: inherit; - border: none; - padding: 6px 0; -} - -.rst-content dl:not(.docutils) .property { - text-transform: uppercase; - font-style: normal; - padding-right: 12px; -} - -em.sig-param span.n:first-child, em.sig-param span.n:nth-child(2) { - color: black; - font-style: normal; -} - -em.sig-param span.n:nth-child(3), -em.sig-param span.n:nth-child(3) a { - color: inherit; - font-weight: normal; - font-style: normal; -} - -em.sig-param span.default_value { - font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace; - font-style: normal; - font-size: 90%; -} - -.sig-paren { - padding: 0 4px; -} - -.wy-table-responsive table td, -.wy-table-responsive table th { - white-space: normal; -} - -.wy-table-bordered-all, -.rst-content table.docutils { - border: none; -} - -.wy-table-bordered-all td, -.rst-content table.docutils td { - border: none; -} - -.wy-table-odd td, -.wy-table-striped tr:nth-child(2n-1) td, -.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td { - background: rgb(243,244,247); -} - -.wy-table td, -.rst-content table.docutils td, -.rst-content table.field-list td, -.wy-table th, -.rst-content table.docutils th, -.rst-content table.field-list th { - padding: 16px; -} -/* -.admonition { - content: '\f12a'; - font-family: FontAwesome; -} */ - -.admonition.note, div.admonition.note { - border-color: rgba(var(--pst-color-admonition-note),1); -} - -.admonition.note>.admonition-title:before, div.admonition.note>.admonition-title:before { - color: rgba(var(--pst-color-admonition-note),1); - content: '\f12a'!important; - /* content: var(--pst-icon-admonition-note); */ -} - -.admonition.question>.admonition-title:before, div.admonition.question>.admonition-title:before { - color: rgba(var(--pst-color-admonition-note),1); - content: '\003f'!important; - /* content: var(--pst-icon-admonition-note); */ -} - -.admonition.explanation>.admonition-title:before, div.admonition.explanation>.admonition-title:before { - color: rgba(var(--pst-color-admonition-note),1); - content: '\f02d'!important; - /* content: var(--pst-icon-admonition-note); */ -} - -.card { - /* Add shadows to create the "card" effect */ - box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); - transition: 0.3s; - border-radius: 5px; /* 5px rounded corners */ - width: 100%; - padding-bottom: 10px; -} - -/* On mouse-over, add a deeper shadow */ -.card:hover { - box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); - -} - -/* Add some padding inside the card container */ -.container { - padding: 2px 16px; -} - -.row:after { - content: ""; - display: table; - clear: both; -} - -.column { - float: left; - width: 50%; - padding: 20px 10px; -} - -.box{ - display: none; - width: 100%; -} - -/* Use NVIDIA Colors */ - -a { - color: #76b900; -} - -a:hover { - color: #76b900; -} - -a:visited { - color: #76b900; -} - -a:hover + .box,.box:hover{ - display: block; - position: absolute; - z-index: 100; - border-radius: 50px!important; - margin-left: 60px; - margin-top: 0px; - color: #76b900; -} - -a:hover + .card:hover{ - display: block; - position: absolute; - z-index: 100; - border-radius: 50px!important; - margin-left: 60px; - margin-top: 0px; - color: #76b900; -} - -a.reference.external { - color: #76b900!important; -} - -#p1 a { - color: #76b900!important; -} - - -#frame { zoom: 0.75; -moz-transform: scale(0.75); -moz-transform-origin: 0 0; } - -/* Global */ - -#typewriter body{ - height: calc(100vh - 8em); - padding: 4em; - color: rgba(255,255,255,1.0); - font-family: "Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; - background-color: rgb(25,25,25); -} - -#typewriter .line-1{ - position: relative; - top: 50%; - width: 24em; - margin: 0 auto; - border-right: 2px solid rgba(255,255,255,.75); - font-size: 180%; - text-align: center; - white-space: nowrap; - overflow: hidden; - transform: translateY(-50%); -} - -/* Animation */ -.anim-typewriter{ - animation: typewriter 4s steps(44) 1s 1 normal both, - blinkTextCursor 500ms steps(44) infinite normal; -} -@keyframes typewriter{ - from{width: 0;} - to{width: 24em;} -} -@keyframes blinkTextCursor{ - from{border-right-color: rgba(255,255,255,.75);} - to{border-right-color: transparent;} -} - - -.trimmed-cover { - object-fit: cover; - width: 120%; - height: 177px; - object-position: center 40%; - margin-right: -100px; -} diff --git a/docs/source/_static/nv-logo.png b/docs/source/_static/nv-logo.png deleted file mode 100644 index 3cd1028ba0758d1e8fc4277e1c325d091acf13fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53324 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 diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index ba53cc3af8..8654dfeba7 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -1,37 +1,58 @@ API Reference ============= +This page gives an overview of all the modules and classes in the Orbit extensions. + omni.isaac.orbit extension -------------------------- +The following modules are available in the ``omni.isaac.orbit`` extension: + +.. currentmodule:: omni.isaac.orbit + +.. autosummary:: + :toctree: orbit + + app + actuators + assets + command_generators + controllers + devices + envs + managers + markers + scene + sensors + sim + terrains + utils + .. toctree:: - :maxdepth: 1 - - orbit.actuators.group - orbit.actuators.model - orbit.app - orbit.asset_loader - orbit.devices - orbit.markers - orbit.objects.articulated - orbit.objects.rigid - orbit.robots - orbit.sensors - orbit.terrains - orbit.command_generators - orbit.utils - orbit.utils.assets - orbit.utils.math - orbit.utils.mdp - orbit.compat + :hidden: + + orbit/omni.isaac.orbit.envs.mdp + orbit/omni.isaac.orbit.envs.ui + orbit/omni.isaac.orbit.sensors.patterns + orbit/omni.isaac.orbit.sim.converters + orbit/omni.isaac.orbit.sim.schemas + orbit/omni.isaac.orbit.sim.spawners omni.isaac.orbit_tasks extension -------------------------------- +The following modules are available in the ``omni.isaac.orbit_tasks`` extension: + +.. currentmodule:: omni.isaac.orbit_tasks + +.. autosummary:: + :toctree: orbit_tasks + + utils + + .. toctree:: - :maxdepth: 1 + :hidden: - orbit_tasks.isaac_env - orbit_tasks.utils - orbit_tasks.utils.data_collector - orbit_tasks.utils.wrappers + orbit_tasks/omni.isaac.orbit_tasks.utils.wrappers + orbit_tasks/omni.isaac.orbit_tasks.utils.data_collector diff --git a/docs/source/api/orbit.actuators.group.rst b/docs/source/api/orbit.actuators.group.rst deleted file mode 100644 index de0e2d783b..0000000000 --- a/docs/source/api/orbit.actuators.group.rst +++ /dev/null @@ -1,174 +0,0 @@ -omni.isaac.orbit.actuators.group -================================ - -Actuator groups apply the same actuator model over a collection of actuated joints. -It deals with both explicit and implicit models, and processes the input joint -configuration and actions accordingly. The joint names, that are a part of a given -actuator group, are configured using regex expressions. These expressions are matched -with the joint names in the robot's kinematic tree. For instance, in the Franka Panda -Emika arm, the first four joints and last three joints can be captured using the regex -expressions ``"panda_joint[1-4]"`` and ``"panda_joint[5-7]"``, - -For a given actuator group, it is possible to provide multiple joint-level command types -(e.g. position, velocity, torque, etc.). The command types are processed as a list of strings. -Each string has two sub-strings joined by underscore: - -- **type of command mode:** "p" (position), "v" (velocity), "t" (torque) -- **type of command resolving:** "abs" (absolute), "rel" (relative) - -For instance, the command type ``"p_abs"`` defines a position command in absolute mode, while ``"v_rel"`` -defines a velocity command in relative mode. - -To facilitate easier composability, the actuator group handles the offsets and scalings applied to -the input commands. These are set through the :class:`ActuatorControlCfg` and by default are set to zero -and one, respectively. Based on these, the input commands are processed as follows: - -.. math:: - - \text{command} = \text{offset} + \text{scaling} \times \text{input command} - -where :math:`\text{command}` is the command that is sent to the actuator model, :math:`\text{offset}` -is the offset applied to the input command, :math:`\text{scaling}` is the scaling applied to the input -command, and :math:`\text{input command}` is the input command from the user. - -1. **Relative command:** The offset is based on the current joint state. For instance, if the - command type is "p_rel", the offset is the current joint position. -2. **Absolute command:** The offset is based on the values set in the :class:`ActuatorControlCfg`. - For instance, if the command type is "p_abs", the offset is the value for :attr:`dof_pos_offset` - in the :class:`ActuatorControlCfg` instance. - -.. note:: - - Currently, the following joint command types are supported: "p_abs", "p_rel", "v_abs", "v_rel", "t_abs". - - -On initialization, the actuator group performs the following: - -1. **Sets the control mode into simulation:** The control mode is set into the simulator for each joint - based on the command types and actuator models. For implicit actuator models, this is interpreted - from the first entry in the input list of command types. For explicit actuator models, the control - mode is always set to torque. -2. **Sets the joint stiffness and damping gains:** In case of implicit actuators, these are set into - simulator directly, while for explicit actuators, the gains are set into the actuator model. -3. **Sets the joint torque limits:** In case of implicit actuators, these are set into simulator directly - based on the parsed configuration. For explicit actuators, the torque limits are set high since the - actuator model itself is responsible for enforcing the torque limits. - -While computing the joint actions, the actuator group takes the following steps: - -1. **Formats the input actions:** It formats the input actions to account for additional constraints over - the joints. These include mimicking the input command over the joints, or considering non-holonomic steering - constraint for a wheel base. -2. **Pre-process joint commands:** It splits the formatted commands based on number of command types. For - instance, if the input command types are "p_abs" and "v_abs", the input command is split into two tensors. - Over each tensor, the scaling and offset are applied. -3. **Computes the joint actions:** The joint actions are computed based on the actuator model. For implicit - actuator models, the joint actions are returned directly. For explicit actuator models, the joint actions - are computed by calling the :meth:`IdealActuator.compute` and :meth:`IdealActuator.clip_torques` method. - - -Consider a scene with multiple Franka Emika Panda robots present in the stage at the USD paths *"/World/Robot_{n}"* -where *n* is the number of the instance. The following is an example of using the default actuator group to control -the robot with position and velocity commands: - -.. code-block:: python - - import torch - from omni.isaac.core.articulations import ArticulationView - from omni.isaac.orbit.actuator.model import IdealActuatorCfg - from omni.isaac.orbit.actuator.group import ActuatorControlCfg, ActuatorGroupCfg, ActuatorGroup - - # Note: We assume the stage is created and simulation is playing. - - # create an articulation view for the arms - # Reference: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/ext_omni_isaac_core.html - articulation_view = ArticulationView( - "/World/Robot_.*", "panda_arm", reset_xform_properties=False - ) - articulation_view.initialize() - - # create a configuration instance - # -- model - model_cfg = IdealActuatorCfg(motor_torque_limit=40, gear_ratio=1) - # -- control - control_cfg = ActuatorControlCfg( - command_types=["p_abs", "v_abs"], - stiffness={".*": 1000}, - damping={".*": 10} - ) - # -- group - group_cfg = ActuatorGroupCfg( - dof_names=["panda_joint[1-7]"], - model_cfg=model_cfg, - control_cfg=control_cfg, - ) - # create the actuator group - group = ActuatorGroup(group_cfg, articulation_view) - # clear history in the actuator model (if any) - group.reset() - - # create random commands - shape = (articulation_view.count, 7) - dof_pos_des, dof_vel_des = torch.rand(*shape), torch.rand(*shape) - # concatenate the commands into a single tensor - group_actions = torch.cat([dof_pos_des, dof_vel_des], dim=1) - # check that commands are of the correct shape - assert group_actions.shape == (group.num_articulation, group.control_dim) - - # read current joint state - dof_pos = articulation_view.get_joint_positions(joint_indices=group.dof_indices) - dof_vel = articulation_view.get_joint_velocities(joint_indices=group.dof_indices) - - # compute the joint actions - joint_actions = group.compute(group_actions, dof_pos, dof_vel) - # set actions into simulator - articulation_view.apply_actions(joint_actions) - - -Actuator Control Configuration ------------------------------- - -.. autoclass:: omni.isaac.orbit.actuators.group.ActuatorControlCfg - :members: - :undoc-members: - :show-inheritance: - -Default Actuator Group ----------------------- - -.. autoclass:: omni.isaac.orbit.actuators.group.ActuatorGroup - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.group.ActuatorGroupCfg - :members: - :undoc-members: - :show-inheritance: - - -Gripper Actuator Group ------------------------ - -.. autoclass:: omni.isaac.orbit.actuators.group.GripperActuatorGroup - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.group.GripperActuatorGroupCfg - :members: - :undoc-members: - :show-inheritance: - -Non-Holonomic Kinematics Group ------------------------------- - -.. autoclass:: omni.isaac.orbit.actuators.group.NonHolonomicKinematicsGroup - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.group.NonHolonomicKinematicsGroupCfg - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.actuators.model.rst b/docs/source/api/orbit.actuators.model.rst deleted file mode 100644 index 6f5e49637a..0000000000 --- a/docs/source/api/orbit.actuators.model.rst +++ /dev/null @@ -1,123 +0,0 @@ -omni.isaac.orbit.actuators.model -================================ - -There are two main categories of actuator models that are supported: - -- **Implicit**: Motor model with ideal PD from the physics engine. -- **Explicit**: Motor models based on physical drive models. - - - **Physics-based**: Derives the motor models based on first-principles. - - **Neural Network-based**: Learned motor models from actuator data. - -Currently, all the explicit motor models convert input joint commands into joint efforts to -apply into the simulation. This process comprise of three main steps: - -1. :func:`set_command`: Set the desired command to the model. These include joint positions, velocities, feedforward torque, stiffness and damping gain. -2. :func:`compute_torque`: Compute the joint efforts using the actuator model. -3. :func:`clip_torques`: Clip the desired torques epxlicitly using an actuator saturation model. - -It is up to the model how the input values from step (1) are processed and dealt with in step (2). -The steps (2) and (3) are segregrated explicitly, since many times in learning, we need both the -computed (desired) or clipped (applied) joint efforts. For instance, to penalize the difference -between the computed and clipped joint efforts, so that the learned policy does not keep outputting -arbitrarily large commands. - -All explicit models inherit from the base actuator model, :class:`IdealActuator`, which implements a -PD controller with feed-forward effort, and simple clipping based on the configured maximum effort. - -The following is a simple example of using the actuator model: - -.. code-block:: python - - import torch - from omni.isaac.orbit.actuators.model import IdealActuator, IdealActuatorCfg - - # joint information from the articulation - num_actuators, num_envs = 5, 32 - device ="cpu" - # create a configuration instance - cfg = IdealActuatorCfg(motor_torque_limit=20, gear_ratio=1) - # create the actuator model instance - model = IdealActuator(cfg, num_actuators, num_envs, device) - # clear history in the actuator model (if any) - model.reset() - - # creat random commands - dof_pos_des, dof_vel_des = torch.rand(32, 5), torch.rand(32, 5) - # create random state - dof_pos, dof_vel = torch.rand(32, 5), torch.rand(32, 5) - - # set desired joint state - model.set_command(dof_pos_des, dof_vel_des) - # compute the torques - computed_torques = model.compute_torques(dof_pos, dof_vel) - applied_torques = model.clip_torques(computed_torques) - - -Implicit Actuator ------------------ - -.. autoclass:: omni.isaac.orbit.actuators.model.ImplicitActuatorCfg - :members: - :show-inheritance: - -Ideal Actuator ---------------- - -.. autoclass:: omni.isaac.orbit.actuators.model.IdealActuator - :members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.model.IdealActuatorCfg - :members: - :show-inheritance: - -Direct Control (DC) Actuator ----------------------------- - -Fixed Gear Ratio -~~~~~~~~~~~~~~~~ - -.. autoclass:: omni.isaac.orbit.actuators.model.DCMotor - :members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.model.DCMotorCfg - :members: - :show-inheritance: - -Variable Gear Ratio -~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: omni.isaac.orbit.actuators.model.VariableGearRatioDCMotor - :members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.model.VariableGearRatioDCMotorCfg - :members: - :show-inheritance: - -Actuator Networks ------------------ - -Multi-layer Perceptron (MLP) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: omni.isaac.orbit.actuators.model.ActuatorNetMLP - :members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.model.ActuatorNetMLPCfg - :members: - :show-inheritance: - -Long Short-term Memory (LSTM) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autoclass:: omni.isaac.orbit.actuators.model.ActuatorNetLSTM - :members: - :show-inheritance: - -.. autoclass:: omni.isaac.orbit.actuators.model.ActuatorNetLSTMCfg - :members: - :show-inheritance: diff --git a/docs/source/api/orbit.app.rst b/docs/source/api/orbit.app.rst deleted file mode 100644 index 51ae5826f7..0000000000 --- a/docs/source/api/orbit.app.rst +++ /dev/null @@ -1,5 +0,0 @@ -omni.isaac.orbit.app -~~~~~~~~~~~~~~~~~~~~ -.. automodule:: omni.isaac.orbit.app - :members: - :undoc-members: diff --git a/docs/source/api/orbit.asset_loader.rst b/docs/source/api/orbit.asset_loader.rst deleted file mode 100644 index d439f6318a..0000000000 --- a/docs/source/api/orbit.asset_loader.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.asset_loader -============================= - -.. automodule:: omni.isaac.orbit.asset_loader - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.command_generators.rst b/docs/source/api/orbit.command_generators.rst deleted file mode 100644 index b82598baee..0000000000 --- a/docs/source/api/orbit.command_generators.rst +++ /dev/null @@ -1,6 +0,0 @@ -omni.isaac.orbit.command_generators -==================================== - -.. automodule:: omni.isaac.orbit.command_generators - :members: - :show-inheritance: diff --git a/docs/source/api/orbit.compat.rst b/docs/source/api/orbit.compat.rst deleted file mode 100644 index 7cac8039d9..0000000000 --- a/docs/source/api/orbit.compat.rst +++ /dev/null @@ -1,114 +0,0 @@ -omni.isaac.orbit.compat -======================= - -.. warning:: - - This module is deprecated and will be removed in a future release. Please use - the following modules instead: - - * :mod:`omni.isaac.orbit.markers` - * :mod:`omni.isaac.orbit.sensors` - * :mod:`omni.isaac.orbit.managers` - * :mod:`omni.isaac.orbit.sim` - -Markers -------- - -.. automodule:: omni.isaac.orbit.compat.markers - :members: - :undoc-members: - :show-inheritance: - -Sensor Base ------------ - -.. automodule:: omni.isaac.orbit.compat.sensors.sensor_base - :members: - :undoc-members: - :show-inheritance: - -Camera ------- - -.. autoclass:: omni.isaac.orbit.compat.sensors.camera.camera.Camera - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Data -^^^^ - -.. autoclass:: omni.isaac.orbit.compat.sensors.camera.camera.CameraData - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Configuration -^^^^^^^^^^^^^ - -.. automodule:: omni.isaac.orbit.sensors.camera.camera_cfg - :members: - :undoc-members: - :show-inheritance: - :noindex: - -Utilities -^^^^^^^^^ - -.. automodule:: omni.isaac.orbit.sensors.camera.utils - :members: - :undoc-members: - :show-inheritance: - :noindex: - - -Height Scanner --------------- - -.. autoclass:: omni.isaac.orbit.compat.sensors.height_scanner.height_scanner.HeightScanner - :members: - :undoc-members: - :show-inheritance: - -Data -^^^^ - -.. autoclass:: omni.isaac.orbit.compat.sensors.height_scanner.height_scanner.HeightScannerData - :members: - :undoc-members: - :show-inheritance: - -Configuration -^^^^^^^^^^^^^ - -.. automodule:: omni.isaac.orbit.compat.sensors.height_scanner.height_scanner_cfg - :members: - :undoc-members: - :show-inheritance: - -Utilities -^^^^^^^^^ - -.. automodule:: omni.isaac.orbit.compat.sensors.height_scanner.utils - :members: - :undoc-members: - :show-inheritance: - - -MDP Managers ------------- - -.. automodule:: omni.isaac.orbit.compat.utils.mdp - :members: - :undoc-members: - :show-inheritance: - -Kit Utilities -------------- - -.. automodule:: omni.isaac.orbit.compat.utils.kit - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.devices.rst b/docs/source/api/orbit.devices.rst deleted file mode 100644 index eff66385c7..0000000000 --- a/docs/source/api/orbit.devices.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.devices -======================== - -.. automodule:: omni.isaac.orbit.devices - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.markers.rst b/docs/source/api/orbit.markers.rst deleted file mode 100644 index ac1aa7ecaf..0000000000 --- a/docs/source/api/orbit.markers.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.markers -======================== - -.. automodule:: omni.isaac.orbit.markers - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.objects.articulated.rst b/docs/source/api/orbit.objects.articulated.rst deleted file mode 100644 index 7d7912ec07..0000000000 --- a/docs/source/api/orbit.objects.articulated.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.objects.articulated -==================================== - -.. automodule:: omni.isaac.orbit.objects.articulated - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.objects.rigid.rst b/docs/source/api/orbit.objects.rigid.rst deleted file mode 100644 index d773ee00ca..0000000000 --- a/docs/source/api/orbit.objects.rigid.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.objects.rigid -============================== - -.. automodule:: omni.isaac.orbit.objects.rigid - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.robots.rst b/docs/source/api/orbit.robots.rst deleted file mode 100644 index 4d6ad14c59..0000000000 --- a/docs/source/api/orbit.robots.rst +++ /dev/null @@ -1,45 +0,0 @@ -omni.isaac.orbit.robots -======================== - -Robot Base ----------- - -.. automodule:: omni.isaac.orbit.robots.robot_base - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: omni.isaac.orbit.robots.robot_base_cfg - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: omni.isaac.orbit.robots.robot_base_data - :members: - :undoc-members: - :show-inheritance: - -Legged Robot ------------- - -.. automodule:: omni.isaac.orbit.robots.legged_robot - :members: - :undoc-members: - :show-inheritance: - - -Single Arm Manipulator ----------------------- - -.. automodule:: omni.isaac.orbit.robots.single_arm - :members: - :undoc-members: - :show-inheritance: - -Mobile Manipulator ------------------- - -.. automodule:: omni.isaac.orbit.robots.mobile_manipulator - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.sensors.rst b/docs/source/api/orbit.sensors.rst deleted file mode 100644 index 216acfc84f..0000000000 --- a/docs/source/api/orbit.sensors.rst +++ /dev/null @@ -1,43 +0,0 @@ -omni.isaac.orbit.sensors -======================== - - -Sensor Base ------------ - -.. automodule:: omni.isaac.orbit.sensors.sensor_base - :members: - :undoc-members: - :show-inheritance: - -Camera ------- - -.. autoclass:: omni.isaac.orbit.sensors.camera.camera.Camera - :members: - :undoc-members: - :show-inheritance: - -Data -^^^^ - -.. autoclass:: omni.isaac.orbit.sensors.camera.camera.CameraData - :members: - :undoc-members: - :show-inheritance: - -Configuration -^^^^^^^^^^^^^ - -.. automodule:: omni.isaac.orbit.sensors.camera.camera_cfg - :members: - :undoc-members: - :show-inheritance: - -Utilities -^^^^^^^^^ - -.. automodule:: omni.isaac.orbit.sensors.camera.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.utils.assets.rst b/docs/source/api/orbit.utils.assets.rst deleted file mode 100644 index 79a13d7b0f..0000000000 --- a/docs/source/api/orbit.utils.assets.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.utils.assets -============================= - -.. automodule:: omni.isaac.orbit.utils.assets - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.utils.math.rst b/docs/source/api/orbit.utils.math.rst deleted file mode 100644 index f28244356f..0000000000 --- a/docs/source/api/orbit.utils.math.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.utils.math -=========================== - -.. automodule:: omni.isaac.orbit.utils.math - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.utils.mdp.rst b/docs/source/api/orbit.utils.mdp.rst deleted file mode 100644 index c56959644a..0000000000 --- a/docs/source/api/orbit.utils.mdp.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit.utils.mdp -========================== - -.. automodule:: omni.isaac.orbit.utils.mdp - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit.utils.rst b/docs/source/api/orbit.utils.rst deleted file mode 100644 index d6cb5d4dea..0000000000 --- a/docs/source/api/orbit.utils.rst +++ /dev/null @@ -1,52 +0,0 @@ -omni.isaac.orbit.utils -====================== - -Sub-module containing utilities for the Orbit framework. - -Configuration operations -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: omni.isaac.orbit.utils.configclass - :members: - :undoc-members: - :show-inheritance: - -Loading and saving data -~~~~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: omni.isaac.orbit.utils.io - :members: - :undoc-members: - :show-inheritance: - -Array operations -~~~~~~~~~~~~~~~~ - -.. automodule:: omni.isaac.orbit.utils.array - :members: - :undoc-members: - :show-inheritance: - -Dictionary operations -~~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: omni.isaac.orbit.utils.dict - :members: - :undoc-members: - :show-inheritance: - -String operations -~~~~~~~~~~~~~~~~~ - -.. automodule:: omni.isaac.orbit.utils.string - :members: - :undoc-members: - :show-inheritance: - -Timer operations -~~~~~~~~~~~~~~~~ - -.. automodule:: omni.isaac.orbit.utils.timer - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit/omni.isaac.orbit.actuators.rst b/docs/source/api/orbit/omni.isaac.orbit.actuators.rst new file mode 100644 index 0000000000..03a8e28fa0 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.actuators.rst @@ -0,0 +1,104 @@ +orbit.actuators +=============== + +.. automodule:: omni.isaac.orbit.actuators + + .. rubric:: Classes + + .. autosummary:: + + ActuatorBase + ActuatorBaseCfg + ImplicitActuator + ImplicitActuatorCfg + IdealPDActuator + IdealPDActuatorCfg + DCMotor + DCMotorCfg + ActuatorNetMLP + ActuatorNetMLPCfg + ActuatorNetLSTM + ActuatorNetLSTMCfg + +Actuator Base +------------- + +.. autoclass:: ActuatorBase + :members: + :inherited-members: + +.. autoclass:: ActuatorBaseCfg + :members: + :inherited-members: + :exclude-members: __init__, class_type + +Implicit Actuator +----------------- + +.. autoclass:: ImplicitActuator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ImplicitActuatorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Ideal PD Actuator +----------------- + +.. autoclass:: IdealPDActuator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: IdealPDActuatorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +DC Motor Actuator +----------------- + +.. autoclass:: DCMotor + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DCMotorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +MLP Network Actuator +--------------------- + +.. autoclass:: ActuatorNetMLP + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ActuatorNetMLPCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + + +LSTM Network Actuator +--------------------- + +.. autoclass:: ActuatorNetLSTM + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ActuatorNetLSTMCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/orbit/omni.isaac.orbit.app.rst b/docs/source/api/orbit/omni.isaac.orbit.app.rst new file mode 100644 index 0000000000..469ebf1808 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.app.rst @@ -0,0 +1,16 @@ +orbit.app +========= + +.. automodule:: omni.isaac.orbit.app + + .. rubric:: Classes + + .. autosummary:: + + AppLauncher + +Simulation App Launcher +----------------------- + +.. autoclass:: AppLauncher + :members: diff --git a/docs/source/api/orbit/omni.isaac.orbit.assets.rst b/docs/source/api/orbit/omni.isaac.orbit.assets.rst new file mode 100644 index 0000000000..9f89cc638c --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.assets.rst @@ -0,0 +1,69 @@ +orbit.assets +============ + +.. automodule:: omni.isaac.orbit.assets + + .. rubric:: Classes + + .. autosummary:: + + AssetBase + AssetBaseCfg + RigidObject + RigidObjectData + RigidObjectCfg + Articulation + ArticulationData + ArticulationCfg + +.. currentmodule:: omni.isaac.orbit.assets + +Asset Base +---------- + +.. autoclass:: AssetBase + :members: + +.. autoclass:: AssetBaseCfg + :members: + :exclude-members: __init__, class_type + +Rigid Object +------------ + +.. autoclass:: RigidObject + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RigidObjectData + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: RigidObjectCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Articulation +------------ + +.. autoclass:: Articulation + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ArticulationData + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: ArticulationCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/orbit/omni.isaac.orbit.command_generators.rst b/docs/source/api/orbit/omni.isaac.orbit.command_generators.rst new file mode 100644 index 0000000000..191d5ef722 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.command_generators.rst @@ -0,0 +1,103 @@ +orbit.command\_generators +========================= + +.. automodule:: omni.isaac.orbit.command_generators + + .. rubric:: Classes + + .. autosummary:: + + CommandGeneratorBase + CommandGeneratorBaseCfg + NullCommandGenerator + NullCommandGeneratorCfg + UniformVelocityCommandGenerator + UniformVelocityCommandGeneratorCfg + NormalVelocityCommandGenerator + NormalVelocityCommandGeneratorCfg + TerrainBasedPositionCommandGenerator + TerrainBasedPositionCommandGeneratorCfg + +Command Generator Base +---------------------- + +.. autoclass:: CommandGeneratorBase + :members: + +.. autoclass:: CommandGeneratorBaseCfg + :members: + :exclude-members: __init__, class_type + +Null Command Generator +---------------------- + +.. autoclass:: NullCommandGenerator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: NullCommandGeneratorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type, resampling_time_range + +Uniform SE(2) Velocity Command Generator +---------------------------------------- + +.. autoclass:: UniformVelocityCommandGenerator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: UniformVelocityCommandGeneratorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Normal SE(2) Velocity Command Generator +--------------------------------------- + +.. autoclass:: NormalVelocityCommandGenerator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: NormalVelocityCommandGeneratorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Uniform SE(3) Pose Command Generator +------------------------------------ + +.. autoclass:: UniformPoseCommandGenerator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: UniformPoseCommandGeneratorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + + +Terrain-based SE(2) Position Command Generator +---------------------------------------------- + +.. note:: + This command generator is currently not tested. It may not work as expected. + +.. autoclass:: TerrainBasedPositionCommandGenerator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: TerrainBasedPositionCommandGeneratorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/orbit/omni.isaac.orbit.controllers.rst b/docs/source/api/orbit/omni.isaac.orbit.controllers.rst new file mode 100644 index 0000000000..897e4815ac --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.controllers.rst @@ -0,0 +1,25 @@ +orbit.controllers +================= + +.. automodule:: omni.isaac.orbit.controllers + + .. rubric:: Classes + + .. autosummary:: + + DifferentialIKController + DifferentialIKControllerCfg + +Differential Inverse Kinematics +------------------------------- + +.. autoclass:: DifferentialIKController + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DifferentialIKControllerCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/orbit/omni.isaac.orbit.devices.rst b/docs/source/api/orbit/omni.isaac.orbit.devices.rst new file mode 100644 index 0000000000..700fe71628 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.devices.rst @@ -0,0 +1,61 @@ +orbit.devices +============= + +.. automodule:: omni.isaac.orbit.devices + + .. rubric:: Classes + + .. autosummary:: + + DeviceBase + Se2Gamepad + Se3Gamepad + Se2Keyboard + Se3Keyboard + Se3SpaceMouse + Se3SpaceMouse + +Device Base +----------- + +.. autoclass:: DeviceBase + :members: + +Game Pad +-------- + +.. autoclass:: Se2Gamepad + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: Se3Gamepad + :members: + :inherited-members: + :show-inheritance: + +Keyboard +-------- + +.. autoclass:: Se2Keyboard + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: Se3Keyboard + :members: + :inherited-members: + :show-inheritance: + +Space Mouse +----------- + +.. autoclass:: Se2SpaceMouse + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: Se3SpaceMouse + :members: + :inherited-members: + :show-inheritance: diff --git a/docs/source/api/orbit/omni.isaac.orbit.envs.mdp.rst b/docs/source/api/orbit/omni.isaac.orbit.envs.mdp.rst new file mode 100644 index 0000000000..b04587cf75 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.envs.mdp.rst @@ -0,0 +1,44 @@ +orbit.envs.mdp +============== + +.. automodule:: omni.isaac.orbit.envs.mdp + +Observations +------------ + +.. automodule:: omni.isaac.orbit.envs.mdp.observations + :members: + +Actions +------- + +.. automodule:: omni.isaac.orbit.envs.mdp.actions + +.. automodule:: omni.isaac.orbit.envs.mdp.actions.actions_cfg + :members: + :show-inheritance: + :exclude-members: __init__, class_type + +Randomization +------------- + +.. automodule:: omni.isaac.orbit.envs.mdp.randomizations + :members: + +Rewards +------- + +.. automodule:: omni.isaac.orbit.envs.mdp.rewards + :members: + +Terminations +------------ + +.. automodule:: omni.isaac.orbit.envs.mdp.terminations + :members: + +Curriculum +---------- + +.. automodule:: omni.isaac.orbit.envs.mdp.curriculums + :members: diff --git a/docs/source/api/orbit/omni.isaac.orbit.envs.rst b/docs/source/api/orbit/omni.isaac.orbit.envs.rst new file mode 100644 index 0000000000..17b4c9affa --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.envs.rst @@ -0,0 +1,49 @@ +orbit.envs +========== + +.. automodule:: omni.isaac.orbit.envs + + .. rubric:: Submodules + + .. autosummary:: + + mdp + ui + + .. rubric:: Classes + + .. autosummary:: + + BaseEnv + BaseEnvCfg + ViewerCfg + RLTaskEnv + RLTaskEnvCfg + +Base Environment +---------------- + +.. autoclass:: BaseEnv + :members: + +.. autoclass:: BaseEnvCfg + :members: + :exclude-members: __init__, class_type + +.. autoclass:: ViewerCfg + :members: + :exclude-members: __init__ + +RL Task Environment +------------------- + +.. autoclass:: RLTaskEnv + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RLTaskEnvCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/orbit/omni.isaac.orbit.envs.ui.rst b/docs/source/api/orbit/omni.isaac.orbit.envs.ui.rst new file mode 100644 index 0000000000..6946758eb3 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.envs.ui.rst @@ -0,0 +1,24 @@ +orbit.envs.ui +============= + +.. automodule:: omni.isaac.orbit.envs.ui + + .. rubric:: Classes + + .. autosummary:: + + BaseEnvWindow + RLTaskEnvWindow + +Base Environment UI +------------------- + +.. autoclass:: BaseEnvWindow + :members: + +RL Task Environment UI +---------------------- + +.. autoclass:: RLTaskEnvWindow + :members: + :show-inheritance: diff --git a/docs/source/api/orbit/omni.isaac.orbit.managers.rst b/docs/source/api/orbit/omni.isaac.orbit.managers.rst new file mode 100644 index 0000000000..92a0bac329 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.managers.rst @@ -0,0 +1,127 @@ +orbit.managers +============== + +.. automodule:: omni.isaac.orbit.managers + + .. rubric:: Classes + + .. autosummary:: + + SceneEntityCfg + ManagerBase + ManagerTermBase + ManagerTermBaseCfg + ObservationManager + ObservationGroupCfg + ObservationTermCfg + ActionManager + ActionTerm + ActionTermCfg + RandomizationManager + RandomizationTermCfg + RewardManager + RewardTermCfg + TerminationManager + TerminationTermCfg + CurriculumManager + CurriculumTermCfg + +Scene Entity +------------ + +.. autoclass:: SceneEntityCfg + :members: + :exclude-members: __init__ + +Manager Base +------------ + +.. autoclass:: ManagerBase + :members: + +.. autoclass:: ManagerTermBase + :members: + +.. autoclass:: ManagerTermBaseCfg + :members: + :exclude-members: __init__ + +Observation Manager +------------------- + +.. autoclass:: ObservationManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ObservationGroupCfg + :members: + :exclude-members: __init__ + +.. autoclass:: ObservationTermCfg + :members: + :exclude-members: __init__ + +Action Manager +-------------- + +.. autoclass:: ActionManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ActionTerm + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ActionTermCfg + :members: + :exclude-members: __init__ + +Randomization Manager +--------------------- + +.. autoclass:: RandomizationManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RandomizationTermCfg + :members: + :exclude-members: __init__ + +Reward Manager +-------------- + +.. autoclass:: RewardManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RewardTermCfg + :exclude-members: __init__ + +Termination Manager +------------------- + +.. autoclass:: TerminationManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: TerminationTermCfg + :members: + :exclude-members: __init__ + +Curriculum Manager +------------------ + +.. autoclass:: CurriculumManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: CurriculumTermCfg + :members: + :exclude-members: __init__ diff --git a/docs/source/api/orbit/omni.isaac.orbit.markers.rst b/docs/source/api/orbit/omni.isaac.orbit.markers.rst new file mode 100644 index 0000000000..f1b33c17ac --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.markers.rst @@ -0,0 +1,23 @@ +orbit.markers +============= + +.. automodule:: omni.isaac.orbit.markers + + .. rubric:: Classes + + .. autosummary:: + + VisualizationMarkers + VisualizationMarkersCfg + +Visualization Markers +--------------------- + +.. autoclass:: VisualizationMarkers + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: VisualizationMarkersCfg + :members: + :exclude-members: __init__ diff --git a/docs/source/api/orbit/omni.isaac.orbit.scene.rst b/docs/source/api/orbit/omni.isaac.orbit.scene.rst new file mode 100644 index 0000000000..fb6aca9c9b --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.scene.rst @@ -0,0 +1,23 @@ +orbit.scene +=========== + +.. automodule:: omni.isaac.orbit.scene + + .. rubric:: Classes + + .. autosummary:: + + InteractiveScene + InteractiveSceneCfg + +interactive Scene +----------------- + +.. autoclass:: InteractiveScene + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: InteractiveSceneCfg + :members: + :exclude-members: __init__ diff --git a/docs/source/api/orbit/omni.isaac.orbit.sensors.patterns.rst b/docs/source/api/orbit/omni.isaac.orbit.sensors.patterns.rst new file mode 100644 index 0000000000..a3aa9bd9f7 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.sensors.patterns.rst @@ -0,0 +1,51 @@ +orbit.sensors.patterns +====================== + +.. automodule:: omni.isaac.orbit.sensors.patterns + + .. rubric:: Classes + + .. autosummary:: + + PatternBaseCfg + GridPatternCfg + PinholeCameraPatternCfg + BpearlPatternCfg + +Pattern Base +------------ + +.. autoclass:: PatternBaseCfg + :members: + :inherited-members: + :exclude-members: __init__ + +Grid Pattern +------------ + +.. autofunction:: omni.isaac.orbit.sensors.patterns.grid_pattern + +.. autoclass:: GridPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func + +Pinhole Camera Pattern +---------------------- + +.. autofunction:: omni.isaac.orbit.sensors.patterns.pinhole_camera_pattern + +.. autoclass:: PinholeCameraPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func + +RS-Bpearl Pattern +----------------- + +.. autofunction:: omni.isaac.orbit.sensors.patterns.bpearl_pattern + +.. autoclass:: BpearlPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func diff --git a/docs/source/api/orbit/omni.isaac.orbit.sensors.rst b/docs/source/api/orbit/omni.isaac.orbit.sensors.rst new file mode 100644 index 0000000000..ef23dceb09 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.sensors.rst @@ -0,0 +1,137 @@ +orbit.sensors +============= + +.. automodule:: omni.isaac.orbit.sensors + + .. rubric:: Submodules + + .. autosummary:: + + patterns + + .. rubric:: Classes + + .. autosummary:: + + SensorBase + SensorBaseCfg + Camera + CameraData + CameraCfg + ContactSensor + ContactSensorData + ContactSensorCfg + FrameTransformer + FrameTransformerData + FrameTransformerCfg + RayCaster + RayCasterData + RayCasterCfg + RayCasterCamera + RayCasterCameraCfg + +Sensor Base +----------- + +.. autoclass:: SensorBase + :members: + +.. autoclass:: SensorBaseCfg + :members: + :exclude-members: __init__, class_type + +USD Camera +---------- + +.. autoclass:: Camera + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: CameraData + :members: + :inherited-members: + :exclude-members: __init__ + +.. autoclass:: CameraCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Contact Sensor +-------------- + +.. autoclass:: ContactSensor + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ContactSensorData + :members: + :inherited-members: + :exclude-members: __init__ + +.. autoclass:: ContactSensorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + + +Frame Transformer +----------------- + +.. autoclass:: FrameTransformer + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: FrameTransformerData + :members: + :inherited-members: + :exclude-members: __init__ + +.. autoclass:: FrameTransformerCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +.. autoclass:: OffsetCfg + :members: + :inherited-members: + :exclude-members: __init__ + +Ray-Cast Sensor +--------------- + +.. autoclass:: RayCaster + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RayCasterData + :members: + :inherited-members: + :exclude-members: __init__ + +.. autoclass:: RayCasterCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Ray-Cast Camera +--------------- + +.. autoclass:: RayCasterCamera + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RayCasterCameraCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/docs/source/api/orbit/omni.isaac.orbit.sim.converters.rst b/docs/source/api/orbit/omni.isaac.orbit.sim.converters.rst new file mode 100644 index 0000000000..d12c77076a --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.sim.converters.rst @@ -0,0 +1,54 @@ +orbit.sim.converters +==================== + +.. automodule:: omni.isaac.orbit.sim.converters + + .. rubric:: Classes + + .. autosummary:: + + AssetConverterBase + AssetConverterBaseCfg + MeshConverter + MeshConverterCfg + UrdfConverter + UrdfConverterCfg + +Asset Converter Base +-------------------- + +.. autoclass:: AssetConverterBase + :members: + +.. autoclass:: AssetConverterBaseCfg + :members: + :exclude-members: __init__ + +Mesh Converter +-------------- + +.. autoclass:: MeshConverter + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: MeshConverterCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + + +URDF Converter +-------------- + +.. autoclass:: UrdfConverter + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: UrdfConverterCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ diff --git a/docs/source/api/orbit/omni.isaac.orbit.sim.rst b/docs/source/api/orbit/omni.isaac.orbit.sim.rst new file mode 100644 index 0000000000..53ad0b4721 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.sim.rst @@ -0,0 +1,49 @@ +orbit.sim +========= + +.. automodule:: omni.isaac.orbit.sim + + .. rubric:: Submodules + + .. autosummary:: + + converters + schemas + spawners + utils + + .. rubric:: Classes + + .. autosummary:: + + SimulationContext + SimulationCfg + PhysxCfg + +Simulation Context +------------------ + +.. autoclass:: SimulationContext + :members: + :show-inheritance: + +Simulation Configuration +------------------------ + +.. autoclass:: SimulationCfg + :members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: PhysxCfg + :members: + :show-inheritance: + :exclude-members: __init__ + + +Utilities +--------- + +.. automodule:: omni.isaac.orbit.sim.utils + :members: + :show-inheritance: diff --git a/docs/source/api/orbit/omni.isaac.orbit.sim.schemas.rst b/docs/source/api/orbit/omni.isaac.orbit.sim.schemas.rst new file mode 100644 index 0000000000..d19808c1a1 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.sim.schemas.rst @@ -0,0 +1,68 @@ +orbit.sim.schemas +================= + +.. automodule:: omni.isaac.orbit.sim.schemas + + .. rubric:: Classes + + .. autosummary:: + + ArticulationRootPropertiesCfg + RigidBodyPropertiesCfg + CollisionPropertiesCfg + MassPropertiesCfg + + .. rubric:: Functions + + .. autosummary:: + + define_articulation_root_properties + modify_articulation_root_properties + define_rigid_body_properties + modify_rigid_body_properties + activate_contact_sensors + define_collision_properties + modify_collision_properties + define_mass_properties + modify_mass_properties + +Articulation Root +----------------- + +.. autoclass:: ArticulationRootPropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: define_articulation_root_properties +.. autofunction:: modify_articulation_root_properties + +Rigid Body +---------- + +.. autoclass:: RigidBodyPropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: define_rigid_body_properties +.. autofunction:: modify_rigid_body_properties +.. autofunction:: activate_contact_sensors + +Collision +--------- + +.. autoclass:: CollisionPropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: define_collision_properties +.. autofunction:: modify_collision_properties + +Mass +---- + +.. autoclass:: MassPropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: define_mass_properties +.. autofunction:: modify_mass_properties diff --git a/docs/source/api/orbit/omni.isaac.orbit.sim.spawners.rst b/docs/source/api/orbit/omni.isaac.orbit.sim.spawners.rst new file mode 100644 index 0000000000..b5c3cb26c5 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.sim.spawners.rst @@ -0,0 +1,236 @@ +orbit.sim.spawners +================== + +.. automodule:: omni.isaac.orbit.sim.spawners + + .. rubric:: Submodules + + .. autosummary:: + + shapes + lights + sensors + from_files + materials + + .. rubric:: Classes + + .. autosummary:: + + SpawnerCfg + RigidObjectSpawnerCfg + +Spawners +-------- + +.. autoclass:: SpawnerCfg + :members: + :exclude-members: __init__ + +.. autoclass:: RigidObjectSpawnerCfg + :members: + :show-inheritance: + :exclude-members: __init__ + +Shapes +------ + +.. automodule:: omni.isaac.orbit.sim.spawners.shapes + + .. rubric:: Classes + + .. autosummary:: + + ShapeCfg + CapsuleCfg + ConeCfg + CuboidCfg + CylinderCfg + SphereCfg + +.. autoclass:: ShapeCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_capsule + +.. autoclass:: CapsuleCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_cone + +.. autoclass:: ConeCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_cuboid + +.. autoclass:: CuboidCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_cylinder + +.. autoclass:: CylinderCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_sphere + +.. autoclass:: SphereCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + + +Lights +------ + +.. automodule:: omni.isaac.orbit.sim.spawners.lights + + .. rubric:: Classes + + .. autosummary:: + + LightCfg + CylinderLightCfg + DiskLightCfg + DistantLightCfg + DomeLightCfg + SphereLightCfg + +.. autofunction:: spawn_light + +.. autoclass:: LightCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: CylinderLightCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: DiskLightCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: DistantLightCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: DomeLightCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: SphereLightCfg + :members: + :exclude-members: __init__, func + +Sensors +------- + +.. automodule:: omni.isaac.orbit.sim.spawners.sensors + + .. rubric:: Classes + + .. autosummary:: + + PinholeCameraCfg + FisheyeCameraCfg + +.. autofunction:: spawn_camera + +.. autoclass:: PinholeCameraCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: FisheyeCameraCfg + :members: + :exclude-members: __init__, func + +From Files +---------- + +.. automodule:: omni.isaac.orbit.sim.spawners.from_files + + .. rubric:: Classes + + .. autosummary:: + + UrdfFileCfg + UsdFileCfg + GroundPlaneCfg + +.. autofunction:: spawn_from_urdf + +.. autoclass:: UrdfFileCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_from_usd + +.. autoclass:: UsdFileCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_ground_plane + +.. autoclass:: GroundPlaneCfg + :members: + :exclude-members: __init__, func + +Materials +--------- + +.. automodule:: omni.isaac.orbit.sim.spawners.materials + + .. rubric:: Classes + + .. autosummary:: + + VisualMaterialCfg + PreviewSurfaceCfg + MdlFileCfg + GlassMdlCfg + PhysicsMaterialCfg + RigidBodyMaterialCfg + +Visual Materials +~~~~~~~~~~~~~~~~ + +.. autoclass:: VisualMaterialCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_preview_surface + +.. autoclass:: PreviewSurfaceCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_from_mdl_file + +.. autoclass:: MdlFileCfg + :members: + :exclude-members: __init__, func + +.. autoclass:: GlassMdlCfg + :members: + :exclude-members: __init__, func + +Physical Materials +~~~~~~~~~~~~~~~~~~ + +.. autoclass:: PhysicsMaterialCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_rigid_body_material + +.. autoclass:: RigidBodyMaterialCfg + :members: + :exclude-members: __init__, func diff --git a/docs/source/api/orbit.terrains.rst b/docs/source/api/orbit/omni.isaac.orbit.terrains.rst similarity index 77% rename from docs/source/api/orbit.terrains.rst rename to docs/source/api/orbit/omni.isaac.orbit.terrains.rst index 951a92acb8..77bf3d81db 100644 --- a/docs/source/api/orbit.terrains.rst +++ b/docs/source/api/orbit/omni.isaac.orbit.terrains.rst @@ -1,42 +1,57 @@ -omni.isaac.orbit.terrains -========================= +orbit.terrains +============== .. automodule:: omni.isaac.orbit.terrains - :members: - :undoc-members: - :show-inheritance: + + .. rubric:: Classes + + .. autosummary:: + + TerrainImporter + TerrainImporterCfg + TerrainGenerator + TerrainGeneratorCfg + SubTerrainBaseCfg + Terrain importer ---------------- -.. autoclass:: omni.isaac.orbit.terrains.terrain_cfg.TerrainImporterCfg +.. autoclass:: TerrainImporter :members: :show-inheritance: -.. autoclass:: omni.isaac.orbit.terrains.terrain_importer.TerrainImporter +.. autoclass:: TerrainImporterCfg :members: - :show-inheritance: + :exclude-members: __init__, class_type Terrain generator ----------------- -.. autoclass:: omni.isaac.orbit.terrains.terrain_cfg.SubTerrainBaseCfg +.. autoclass:: TerrainGenerator :members: - :show-inheritance: -.. autoclass:: omni.isaac.orbit.terrains.terrain_cfg.TerrainGeneratorCfg +.. autoclass:: TerrainGeneratorCfg :members: - :show-inheritance: + :exclude-members: __init__ -.. autoclass:: omni.isaac.orbit.terrains.terrain_generator.TerrainGenerator +.. autoclass:: SubTerrainBaseCfg :members: - :show-inheritance: + :exclude-members: __init__ Height fields ------------- .. automodule:: omni.isaac.orbit.terrains.height_field +All sub-terrains must inherit from the :class:`HfTerrainBaseCfg` class which contains the common +parameters for all terrains generated from height fields. + +.. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfTerrainBaseCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + Random Uniform Terrain ^^^^^^^^^^^^^^^^^^^^^^ @@ -45,6 +60,7 @@ Random Uniform Terrain .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfRandomUniformTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Pyramid Sloped Terrain ^^^^^^^^^^^^^^^^^^^^^^ @@ -54,10 +70,12 @@ Pyramid Sloped Terrain .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfPyramidSlopedTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfInvertedPyramidSlopedTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Pyramid Stairs Terrain ^^^^^^^^^^^^^^^^^^^^^^ @@ -67,10 +85,12 @@ Pyramid Stairs Terrain .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfPyramidStairsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfInvertedPyramidStairsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Discrete Obstacles Terrain ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -80,6 +100,7 @@ Discrete Obstacles Terrain .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfDiscreteObstaclesTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Wave Terrain ^^^^^^^^^^^^ @@ -89,6 +110,7 @@ Wave Terrain .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfWaveTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Stepping Stones Terrain ^^^^^^^^^^^^^^^^^^^^^^^ @@ -98,6 +120,7 @@ Stepping Stones Terrain .. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfSteppingStonesTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Trimesh terrains ---------------- @@ -113,6 +136,7 @@ Flat terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshPlaneTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Pyramid terrain ^^^^^^^^^^^^^^^ @@ -122,6 +146,7 @@ Pyramid terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshPyramidStairsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Inverted pyramid terrain ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -131,6 +156,7 @@ Inverted pyramid terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshInvertedPyramidStairsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Random grid terrain ^^^^^^^^^^^^^^^^^^^ @@ -140,6 +166,7 @@ Random grid terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshRandomGridTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Rails terrain ^^^^^^^^^^^^^ @@ -149,6 +176,7 @@ Rails terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshRailsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Pit terrain ^^^^^^^^^^^ @@ -158,6 +186,7 @@ Pit terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshPitTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Box terrain ^^^^^^^^^^^^^ @@ -167,6 +196,7 @@ Box terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshBoxTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Gap terrain ^^^^^^^^^^^ @@ -176,6 +206,7 @@ Gap terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshGapTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Floating ring terrain ^^^^^^^^^^^^^^^^^^^^^ @@ -185,6 +216,7 @@ Floating ring terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshFloatingRingTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Star terrain ^^^^^^^^^^^^ @@ -194,7 +226,7 @@ Star terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshStarTerrainCfg :members: :show-inheritance: - + :exclude-members: __init__, function Repeated Objects Terrain ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -204,18 +236,22 @@ Repeated Objects Terrain .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedObjectsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedPyramidsTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedBoxesTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function .. autoclass:: omni.isaac.orbit.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedCylindersTerrainCfg :members: :show-inheritance: + :exclude-members: __init__, function Utilities --------- diff --git a/docs/source/api/orbit/omni.isaac.orbit.utils.rst b/docs/source/api/orbit/omni.isaac.orbit.utils.rst new file mode 100644 index 0000000000..8c7031ced4 --- /dev/null +++ b/docs/source/api/orbit/omni.isaac.orbit.utils.rst @@ -0,0 +1,92 @@ +orbit.utils +=========== + +.. automodule:: omni.isaac.orbit.utils + + .. Rubric:: Submodules + + .. autosummary:: + + io + array + dict + math + noise + string + timer + warp + + .. Rubric:: Functions + + .. autosummary:: + + configclass + +Configuration class +~~~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.configclass + :members: + :show-inheritance: + +IO operations +~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.io + :members: + :imported-members: + :show-inheritance: + +Array operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.array + :members: + :show-inheritance: + +Dictionary operations +~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.dict + :members: + :show-inheritance: + +Math operations +~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.math + :members: + :inherited-members: + :show-inheritance: + +Noise operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.noise + :members: + :imported-members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, func + +String operations +~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.string + :members: + :show-inheritance: + +Timer operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.timer + :members: + :show-inheritance: + +Warp operations +~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.orbit.utils.warp + :members: + :imported-members: + :show-inheritance: diff --git a/docs/source/api/orbit_tasks.isaac_env.rst b/docs/source/api/orbit_tasks.isaac_env.rst deleted file mode 100644 index 8688b5564b..0000000000 --- a/docs/source/api/orbit_tasks.isaac_env.rst +++ /dev/null @@ -1,63 +0,0 @@ -omni.isaac.orbit_tasks.isaac_env -================================ - -We use OpenAI Gym registry to register the environment and their default configuration file. -The default configuration file is passed to the argument "kwargs" in the Gym specification registry. -The string is parsed into respective configuration container which needs to be passed to the environment -class. This is done using the function :meth:`load_cfg_from_registry` in the sub-module -:mod:`omni.isaac.orbit.utils.parse_cfg`. - - -.. attention:: - - There is a slight abuse of kwargs since they are meant to be directly passed into the environment class. - Instead, we remove the key :obj:`cfg_file` from the "kwargs" dictionary and the user needs to provide - the kwarg argument :obj:`cfg` while creating the environment. - - -.. code-block:: python - - import gymnasium as gym - import omni.isaac.orbit_tasks - from omni.isaac.orbit_tasks.utils.parse_cfg import load_cfg_from_registry - - task_name = "Isaac-Cartpole-v0" - cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point") - env = gym.make(task_name, cfg=cfg) - - -All environments must inherit from :class:`IsaacEnv` class which is defined in the sub-module -:mod:`omni.isaac.orbit_tasks.isaac_env`. -The main methods that needs to be implemented by an inherited environment class: - -* :meth:`_design_scene`: Design the template environment for cloning. -* :meth:`_reset_idx`: Environment reset function based on environment indices. -* :meth:`_step_impl`: Apply actions into simulation and compute MDP signals. -* :meth:`_get_observations`: Get observations from the environment. - -The following attributes need to be set by the inherited class: - -* :attr:`action_space`: The Space object corresponding to valid actions -* :attr:`observation_space`: The Space object corresponding to valid observations -* :attr:`reward_range`: A tuple corresponding to the min and max possible rewards. A default reward range set to [-inf, +inf] already exists. - -The action and observation space correspond to single environment (and not vectorized). - - -Base Environment ----------------- - -.. automodule:: omni.isaac.orbit_tasks.isaac_env - :members: - :undoc-members: - :show-inheritance: - :private-members: - :exclude-members: _configure_simulation_flags, _create_viewport_render_product, _last_obs_buf - -Base Configuration ---------------------- - -.. automodule:: omni.isaac.orbit_tasks.isaac_env_cfg - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit_tasks.utils.data_collector.rst b/docs/source/api/orbit_tasks.utils.data_collector.rst deleted file mode 100644 index 613314c519..0000000000 --- a/docs/source/api/orbit_tasks.utils.data_collector.rst +++ /dev/null @@ -1,79 +0,0 @@ -omni.isaac.orbit_tasks.utils.data_collector -========================================== - -All post-processed robomimic compatible datasets share the same data structure. A single dataset is a -single HDF5 file. The stored data follows the structure provided -`here `_. - -The collector takes input data in its batched format and stores them as different demonstrations, each corresponding -to a given environment index. The demonstrations are flushed to disk when the :meth:`flush()` is called for the -respective environments. All the data is saved when the :meth:`close()` is called. - -The following sample shows how to use the ``robomimic`` data collector: - -.. code-block:: python - - import os - import torch - - from omni.isaac.orbit_tasks.utils.data_collector import RobomimicDataCollector - - # name of the environment (needed by robomimic) - task_name = "Isaac-Franka-Lift-v0" - # specify directory for logging experiments - test_dir = os.path.dirname(os.path.abspath(__file__)) - log_dir = os.path.join(test_dir, "logs", "demos") - # name of the file to save data - filename = "hdf_dataset.hdf5" - # number of episodes to collect - num_demos = 10 - # number of environments to simulate - num_envs = 4 - - # create data-collector - collector_interface = RobomimicDataCollector(task_name, log_dir, filename, num_demos) - - # reset the collector - collector_interface.reset() - - while not collector_interface.is_stopped(): - # generate random data to store - # -- obs - obs = { - "joint_pos": torch.randn(num_envs, 10), - "joint_vel": torch.randn(num_envs, 10) - } - # -- actions - actions = torch.randn(num_envs, 10) - # -- rewards - rewards = torch.randn(num_envs) - # -- dones - dones = torch.rand(num_envs) > 0.5 - - # store signals - # -- obs - for key, value in obs.items(): - collector_interface.add(f"obs/{key}", value) - # -- actions - collector_interface.add("actions", actions) - # -- next_obs - for key, value in obs.items(): - collector_interface.add(f"next_obs/{key}", value.cpu().numpy()) - # -- rewards - collector_interface.add("rewards", rewards) - # -- dones - collector_interface.add("dones", dones) - - # flush data from collector for successful environments - # note: in this case we flush all the time - reset_env_ids = dones.nonzero(as_tuple=False).squeeze(-1) - collector_interface.flush(reset_env_ids) - - # close collector - collector_interface.close() - - -.. automodule:: omni.isaac.orbit_tasks.utils.data_collector - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit_tasks.utils.rst b/docs/source/api/orbit_tasks.utils.rst deleted file mode 100644 index 60927f754a..0000000000 --- a/docs/source/api/orbit_tasks.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -omni.isaac.orbit_tasks.utils -=============================== - -.. automodule:: omni.isaac.orbit_tasks.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/orbit_tasks.utils.wrappers.rst b/docs/source/api/orbit_tasks.utils.wrappers.rst deleted file mode 100644 index 8cabe984df..0000000000 --- a/docs/source/api/orbit_tasks.utils.wrappers.rst +++ /dev/null @@ -1,54 +0,0 @@ -omni.isaac.orbit_tasks.utils.wrappers -==================================== - -Wrappers allow you to modify the behavior of an environment without modifying the environment itself. -This is useful for modifying the observation space, action space, or reward function. Additionally, -they can be used to cast a given environment into the respective environment class definition used by -different learning frameworks. This operation may include handling of asymmetric actor-critic observations, -casting the data between different backends such `numpy` and `pytorch`, or organizing the returned data -into the expected data structure by the learning framework. - -All wrappers derive from the ``gym.Wrapper``` class. Using a wrapper is as simple as passing the initialized -environment instance to the wrapper constructor. For instance, to wrap an environment in the -`Stable-Baselines3`_ wrapper, you can do the following: - -.. code-block:: python - - from omni.isaac.orbit_tasks.utils.wrappers.sb3 import Sb3VecEnvWrapper - - env = Sb3VecEnvWrapper(env) - - -.. _RL-Games: https://github.com/Denys88/rl_games -.. _RSL-RL: https://github.com/leggedrobotics/rsl_rl -.. _skrl: https://github.com/Toni-SM/skrl -.. _Stable-Baselines3: https://github.com/DLR-RM/stable-baselines3 - - -RL-Games Wrapper ----------------- - -.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.rl_games - :members: - :show-inheritance: - -RSL-RL Wrapper --------------- - -.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.rsl_rl - :members: - :show-inheritance: - -SKRL Wrapper ------------- - -.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.skrl - :members: - :show-inheritance: - -Stable-Baselines3 Wrapper -------------------------- - -.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.sb3 - :members: - :show-inheritance: diff --git a/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.data_collector.rst b/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.data_collector.rst new file mode 100644 index 0000000000..9de72ffd54 --- /dev/null +++ b/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.data_collector.rst @@ -0,0 +1,17 @@ +orbit\_tasks.utils.data\_collector +================================== + +.. automodule:: omni.isaac.orbit_tasks.utils.data_collector + + .. Rubric:: Classes + + .. autosummary:: + + RobomimicDataCollector + +Robomimic Data Collector +------------------------ + +.. autoclass:: RobomimicDataCollector + :members: + :show-inheritance: diff --git a/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.rst b/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.rst new file mode 100644 index 0000000000..8174920cf7 --- /dev/null +++ b/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.rst @@ -0,0 +1,13 @@ +orbit\_tasks.utils +================== + +.. automodule:: omni.isaac.orbit_tasks.utils + :members: + :imported-members: + + .. rubric:: Submodules + + .. autosummary:: + + data_collector + wrappers diff --git a/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.wrappers.rst b/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.wrappers.rst new file mode 100644 index 0000000000..529bf29921 --- /dev/null +++ b/docs/source/api/orbit_tasks/omni.isaac.orbit_tasks.utils.wrappers.rst @@ -0,0 +1,33 @@ +orbit\_tasks.utils.wrappers +=========================== + +.. automodule:: omni.isaac.orbit_tasks.utils.wrappers + +RL-Games Wrapper +---------------- + +.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.rl_games + :members: + :show-inheritance: + +RSL-RL Wrapper +-------------- + +.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.rsl_rl + :members: + :imported-members: + :show-inheritance: + +SKRL Wrapper +------------ + +.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.skrl + :members: + :show-inheritance: + +Stable-Baselines3 Wrapper +------------------------- + +.. automodule:: omni.isaac.orbit_tasks.utils.wrappers.sb3 + :members: + :show-inheritance: diff --git a/docs/source/features/actuators.rst b/docs/source/features/actuators.rst index 742951129b..e4e4ec5660 100644 --- a/docs/source/features/actuators.rst +++ b/docs/source/features/actuators.rst @@ -25,8 +25,9 @@ The explicit actuator model performs two steps: 1) it computes the desired joint the input commands, and 2) it clips the desired torques based on the motor capabilities. The clipped torques are the desired actuation efforts that are set into the simulation. -All explicit models inherit from the base actuator model, :class:`IdealActuator`, which implements a -PD controller with feed-forward effort, and simple clipping based on the configured maximum effort: +As an example of an ideal explicit actuator model, we provide the :class:`omni.isaac.orbit.actuators.IdealPDActuator` +class, which implements a PD controller with feed-forward effort, and simple clipping based on the configured +maximum effort: .. math:: @@ -39,38 +40,28 @@ are the current joint positions and velocities, :math:`q_{des}`, :math:`\dot{q}_ are the desired joint positions, velocities and torques commands. The parameters :math:`\gamma` and :math:`\tau_{motor, max}` are the gear box ratio and the maximum motor effort possible. -.. seealso:: - - We provide implementations for various explicit actuator models. These are detailed in - `omni.isaac.orbit.actuators.model <../api/orbit.actuators.model.html>`_ sub-package. - Actuator groups --------------- The actuator models by themselves are computational blocks that take as inputs the desired joint commands and output the the joint commands to apply into the simulator. They do not contain any knowledge about the -joints they are acting on themselves. These are handled by the actuator groups. +joints they are acting on themselves. These are handled by the :class:`omni.isaac.orbit.assets.Articulation` +class, which wraps around the physics engine's articulation class. -Actuator groups collect a set of actuated joints on an articulation that are using the same actuator model. +Actuator are collected as a set of actuated joints on an articulation that are using the same actuator model. For instance, the quadruped, ANYmal-C, uses series elastic actuator, ANYdrive 3.0, for all its joints. This grouping configures the actuator model for those joints, translates the input commands to the joint level -commands, and returns the articulation action to set into the simulator. Through this mechanism, it is also -possible to configure the included joints under some constraints, such as mimicking of gripper commands or -introducing non-holonomic constraints for a wheel base. +commands, and returns the articulation action to set into the simulator. Having an arm with a different +actuator model, such as a DC motor, would require configuring a different actuator group. -An articulated system can be composed of different actuator groups. For instance, a legged mobile manipulator -can be composed of a base group, an arm group, and a gripper group. If the base is the quadruped, ANYmal-C, -the base group can utilize the actuator network to model the series elastic actuation. Similarly, the arm, -a Kinova Jaco2, can use a DC motor model and take joint positions as input commands. Finally, the gripper group -can employ the gripper mimic group which processes binary open/close command into individual gripper joint actions. +The following figure shows the actuator groups for a legged mobile manipulator: .. image:: ../_static/actuator_groups.svg :width: 600 :align: center :alt: Actuator groups for a legged mobile manipulator - .. seealso:: - For more information on the actuator groups, please refer to the documentation of the - `omni.isaac.orbit.actuators.group <../api/orbit.actuators.group.html>`_ subpackage. + We provide implementations for various explicit actuator models. These are detailed in + `omni.isaac.orbit.actuators <../api/orbit.actuators.html>`_ sub-package. diff --git a/docs/source/features/environments.rst b/docs/source/features/environments.rst index e07dad0000..41ff43706d 100644 --- a/docs/source/features/environments.rst +++ b/docs/source/features/environments.rst @@ -4,7 +4,7 @@ Environments .. note:: The environments are currently under development. We will update the documentation as soon as - they are ready. Please check the :ref:`development_roadmap` for more information. + they are ready. Please check the GitHub repository for the latest updates. Rigid-body manipulation ----------------------- diff --git a/docs/source/refs/faq.rst b/docs/source/refs/faq.rst index 75dac9a71e..e4293ab2a9 100644 --- a/docs/source/refs/faq.rst +++ b/docs/source/refs/faq.rst @@ -41,30 +41,16 @@ With the release of above two tools, NVIDIA also released an open-sourced set of These environments have been designed to display the capabilities of the underlying simulators and provide a starting point to understand what is possible with the simulators for robot learning. These environments can be used for benchmarking but are not designed for developing and testing custom environments and algorithms. -This is where ``orbit`` comes in. +This is where Orbit comes in. Orbit :cite:`mittal2023orbit` is built on top of Isaac Sim to provide a unified and flexible framework for robot learning that exploits latest simulation technologies. It is designed to be modular and extensible, and aims to simplify common workflows in robotics research (such as RL, learning from demonstrations, and -motion planning). The main goals of the framework are summarized as follows: - -- **Modularity**: It is possible to easily add new environments, sensors, and tasks. It is also possible - to easily swap out different components to suit the needs of the user. -- **Agility**: While still dependent on Isaac Sim, the framework includes pre-release features that - are not yet available in Isaac Sim. This allows the framework to be more agile and adapt to the - changing needs of the community. -- **Openness**: The framework is designed to be open-sourced. This makes it possible to easily - integrate with other tools and environments from the robotics community with long-term support and - maintenance. -- **Battery-included**: The framework includes a number of environments, sensors, and tasks that are - ready to use. This allows researchers to get started quickly and focus on their research. It also - includes a number of examples to showcase the capabilities of the framework. - -While it includes some pre-built environments, sensors, and tasks, its main goal is to provide an open-sourced, -unified and easy-to-use interface for developing and testing custom environments and robot learning algorithms. -While it inherits the capabilities of Isaac Sim, it also adds a number of new features that pertain to robot -learning research. For example, including actuator dynamics in the simulation and support to collect data from -human demonstrations. +motion planning). While it includes some pre-built environments, sensors, and tasks, its main goal is to +provide an open-sourced, unified, and easy-to-use interface for developing and testing custom environments +and robot learning algorithms. It not only inherits the capabilities of Isaac Sim, but also adds a number +of new features that pertain to robot learning research. For example, including actuator dynamics in the +simulation, procedural terrain generation, and support to collect data from human demonstrations. Where does the name come from? diff --git a/docs/source/refs/license.rst b/docs/source/refs/license.rst index 5e062627aa..4ec3eadbe4 100644 --- a/docs/source/refs/license.rst +++ b/docs/source/refs/license.rst @@ -14,11 +14,11 @@ Orbit framework is open-sourced under the .. code-block:: text - Copyright (c) 2022, ETH Zurich - Copyright (c) 2022, University of Toronto - Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES + Copyright (c) 2022-2023, The ORBIT Project Developers. All rights reserved. + SPDX-License-Identifier: BSD-3-Clause + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/docs/source/refs/roadmap.rst b/docs/source/refs/roadmap.rst deleted file mode 100644 index fbd192c3c7..0000000000 --- a/docs/source/refs/roadmap.rst +++ /dev/null @@ -1,114 +0,0 @@ -.. _development_roadmap: - -Development Roadmap -=================== - -Following is a loosely defined roadmap for the development of the codebase. The roadmap is subject to -change and is not a commitment to deliver specific features by specific dates or in the specified order. - -Some of the features listed are already implemented in the codebase, but are not yet documented -and/or tested. We will be working on improving the documentation and testing of these features in the -coming months. - -If you have any questions or suggestions, let us know on -`GitHub discussions `_. - -**January 2023** - -* |check_| Experimental functional API -* Supported motion generators - - * |check_| Joint-space control - * |check_| Differential inverse kinematics control - * |check_| Riemannian Motion Policies (RMPs) - -* Supported robots - - * |check_| Quardupeds: ANYmal-B, ANYmal-C, Unitree A1 - * |check_| Arms: Franka Emika Panda, UR10 - * |check_| Mobile manipulators: Franka Emika Panda and UR10 on Clearpath Ridgeback - -* Supported sensors - - * |check_| Camera (non-parallelized) - * |check_| Height scanner (non-parallelized) - -* Included environments - - * |check_| classic: MuJoCo-style environments (ant, humanoid, cartpole) - * |check_| locomotion: flat terrain for legged robots - * |check_| rigid-object manipulation: end-effector tracking, object lifting - -**February 2023** - -* |check_| Bug fixes and improvements to the functional API -* |check_| Support for `skrl `_ (a library for reinforcement learning) - -**March 2023** - -* |check_| Support for conda virtual environment -* |check_| Example of using warp-based state machine for task-space manipulation - -.. attention:: - - Unfortunately, due to various deadlines, the development of Orbit has been paused for the months of - April and May. One of the many reasons for this is that we are working on a new version of Isaac Sim - (2023.1.0) which brings in a lot of new features and improvements. We will resume the development of - Orbit in June. - -**June 2023** - -* |uncheck| Example on using the APIs in an Omniverse extension -* |uncheck| Add APIs for rough terrain generation -* |uncheck| Extend MDP manager classes to use sensor observations - -* Supported motion generators - - * |uncheck| Operational-space control - * |uncheck| Model predictive control (OCS2) - -* Supported sensors - - * |uncheck| Height scanner (parallelized for terrains) - -* Supported robots - - * |uncheck| Quardupeds: Unitree B1, Unitree Go1 - * |uncheck| Arms: Kinova Jaco2, Kinova Gen3, Sawyer, UR10e - * |uncheck| Mobile manipulators: Fetch, PR2 - -* Included environments - - * |uncheck| locomotion: rough terrain for legged robots - * |uncheck| rigid-object manipulation: in-hand manipulation, hockey puck pushing, peg-in-hole, stacking - -**July 2023** - -* |uncheck| Stabilize APIs and release 1.0 - -* Supported sensors (depends on Isaac Sim 2023.1.0 release) - - * |uncheck| Cameras (parallelized) - -**August 2023** - -* Included environments - - * |uncheck| deformable-object manipulation: cloth folding, cloth lifting - * |uncheck| deformable-object manipulation: fluid transfer, fluid pouring, soft object lifting - -.. |check| raw:: html - - - -.. |check_| raw:: html - - - -.. |uncheck| raw:: html - - - -.. |uncheck_| raw:: html - - diff --git a/docs/source/setup/developer.rst b/docs/source/setup/developer.rst index b642d9673c..18b4c3e8d0 100644 --- a/docs/source/setup/developer.rst +++ b/docs/source/setup/developer.rst @@ -18,19 +18,22 @@ and include the following files: .. code-block:: bash .vscode + ├── tools + │   ├── launch.template.json + │   ├── settings.template.json + │   └── setup_vscode.py ├── extensions.json - ├── launch.json - ├── settings.json + ├── launch.json # <- this is generated by setup_vscode.py + ├── settings.json # <- this is generated by setup_vscode.py └── tasks.json To setup the IDE, please follow these instructions: 1. Open the ``orbit`` directory on Visual Studio Code IDE -2. Run VSCode - `Tasks `__, by - pressing ``Ctrl+Shift+P``, selecting ``Tasks: Run Task`` and - running the ``setup_python_env`` in the drop down menu. +2. Run VSCode `Tasks `__, by + pressing ``Ctrl+Shift+P``, selecting ``Tasks: Run Task`` and running the + ``setup_python_env`` in the drop down menu. .. image:: ../_static/vscode_tasks.png :width: 600px @@ -99,9 +102,9 @@ The ``orbit`` repository is structured as follows: │   └── tools └── VERSION -The ``source`` directory contains the source code for ``orbit`` *extensions* +The ``source`` directory contains the source code for all ``orbit`` *extensions* and *standalone applications*. The two are the different development workflows -supported in `NVIDIA Isaac Sim `__. +supported in `Isaac Sim `__. These are described in the following sections. Extensions @@ -111,7 +114,7 @@ Extensions are the recommended way to develop applications in Isaac Sim. They ar modularized packages that formulate the Omniverse ecosystem. Each extension provides a set of functionalities that can be used by other extensions or standalone applications. A folder is recognized as an extension if it contains -an ``extension.toml`` file. More information on extensions can be found in the +an ``extension.toml`` file in the ``config`` directory. More information on extensions can be found in the `Omniverse documentation `__. Orbit in itself provides extensions for robot learning. These are written into the @@ -124,6 +127,7 @@ follows the following structure: ├── config │   └── extension.toml ├── docs + │   ├── CHANGELOG.md │   └── README.md ├── │ ├── __init__.py @@ -135,12 +139,13 @@ follows the following structure: The ``config/extension.toml`` file contains the metadata of the extension. This includes the name, version, description, dependencies, etc. This information is used by Omniverse to load the extension. The ``docs`` directory contains the documentation -for the extension with more detailed information about the extension. +for the extension with more detailed information about the extension and a CHANGELOG +file that contains the changes made to the extension in each version. The ```` directory contains the main python package for the extension. It may also contains the ``scripts`` directory for keeping python-based applications that are loaded into Omniverse when then extension is enabled using the -`Extension Manager `__. +`Extension Manager `__. More specifically, when an extension is enabled, the python module specified in the ``config/extension.toml`` file is loaded and scripts that contains children of the diff --git a/docs/source/setup/docker.rst b/docs/source/setup/docker.rst index 4fa33fae1b..ab79cb7fa6 100644 --- a/docs/source/setup/docker.rst +++ b/docs/source/setup/docker.rst @@ -141,9 +141,6 @@ To stop the container, you can use the following command: ./docker/container.sh stop -Frequently Asked Questions --------------------------- - Python Interpreter ~~~~~~~~~~~~~~~~~~ @@ -185,6 +182,9 @@ To view the contents of these volumes, you can use the following command: docker volume inspect isaac-cache-kit +Known Issues +------------ + Invalid mount config for type "bind" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -224,21 +224,17 @@ when you have multiple docker versions installed on your machine. To fix this, y * Install the latest version of docker based on the instructions in the setup section. - -Known Issues ------------- - WebRTC and WebSocket Streaming ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When streaming the GUI from Isaac Sim, there are `several options`_ available. There is a `known issue`_ when +When streaming the GUI from Isaac Sim, there are `several streaming clients`_ available. There is a `known issue`_ when attempting to use WebRTC streaming client on Google Chrome and Safari while running Isaac Sim inside a container. To avoid this problem, we suggest using either the Native Streaming Client or WebSocket options, or using the Mozilla Firefox browser on which WebRTC works. -.. _`NVIDIA Omniverse EULA`: https://docs.omniverse.nvidia.com/app_isaacsim/common/NVIDIA_Omniverse_License_Agreement.html -.. _`container installation`: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/install_container.html +.. _`NVIDIA Omniverse EULA`: https://docs.omniverse.nvidia.com/platform/latest/common/NVIDIA_Omniverse_License_Agreement.html +.. _`container installation`: https://docs.omniverse.nvidia.com/isaacsim/latest/installation/install_container.html .. _`Docker website`: https://docs.docker.com/desktop/install/linux-install/ .. _`docker-compose`: https://docs.docker.com/compose/install/linux/#install-using-the-repository .. _`NVIDIA Container Toolkit`: https://github.com/NVIDIA/nvidia-container-toolkit @@ -246,5 +242,5 @@ Mozilla Firefox browser on which WebRTC works. .. _`post-installation steps`: https://docs.docker.com/engine/install/linux-postinstall/ .. _`Isaac Sim container`: https://catalog.ngc.nvidia.com/orgs/nvidia/containers/isaac-sim .. _`NGC API key`: https://docs.nvidia.com/ngc/gpu-cloud/ngc-user-guide/index.html#generating-api-key -.. _several options: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/manual_livestream_clients.html -.. _known issue: https://forums.developer.nvidia.com/t/unable-to-use-webrtc-when-i-run-runheadless-webrtc-sh-in-remote-headless-container/222916 +.. _`several streaming clients`: https://docs.omniverse.nvidia.com/isaacsim/latest/installation/manual_livestream_clients.html +.. _`known issue`: https://forums.developer.nvidia.com/t/unable-to-use-webrtc-when-i-run-runheadless-webrtc-sh-in-remote-headless-container/222916 diff --git a/docs/source/setup/installation.rst b/docs/source/setup/installation.rst index 1005566388..25104436a3 100644 --- a/docs/source/setup/installation.rst +++ b/docs/source/setup/installation.rst @@ -1,15 +1,15 @@ Installation Guide =================== -.. image:: https://img.shields.io/badge/IsaacSim-2022.2.0-brightgreen.svg +.. image:: https://img.shields.io/badge/IsaacSim-2023.1.0--hotfix.1-silver.svg :target: https://developer.nvidia.com/isaac-sim - :alt: IsaacSim 2022.2.0 + :alt: IsaacSim 2023.1.0 -.. image:: https://img.shields.io/badge/python-3.7-blue.svg - :target: https://www.python.org/downloads/release/python-370/ - :alt: Python 3.7 +.. image:: https://img.shields.io/badge/python-3.10-blue.svg + :target: https://www.python.org/downloads/release/python-31013/ + :alt: Python 3.10 -.. image:: https://img.shields.io/badge/platform-linux--64-lightgrey.svg +.. image:: https://img.shields.io/badge/platform-linux--64-orange.svg :target: https://releases.ubuntu.com/20.04/ :alt: Ubuntu 20.04 @@ -20,22 +20,26 @@ Installing Isaac Sim .. caution:: - We have observed a few issues with the Isaac Sim 2022.2.1 release. We recommend using the - Isaac Sim 2022.2.0 release. For more information, please check the :doc:`/source/refs/issues` page. + While the framework contains backwards compatibility for Isaac Sim 2022.2.1, we recommend using + the latest Isaac Sim 2023.1.0-hotfix.1 release. This release contains various improvements on the + simulation side, and is the recommended version to use with Orbit. + + For more information, please refer to the + `Isaac Sim release notes `__. Downloading pre-built binaries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Please follow the Isaac Sim -`documentation `__ +`documentation `__ to install the latest Isaac Sim release. To check the minimum system requirements,refer to the documentation -`here `__. +`here `__. .. note:: - We have tested ORBIT with Isaac Sim 2022.2.0 release on Ubuntu - 20.04LTS with NVIDIA driver 515.76. + We have tested Orbit with Isaac Sim 2023.1.0 release on Ubuntu + 20.04LTS with NVIDIA driver 525.147. Configuring the environment variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -47,34 +51,38 @@ a virtual environment following the instructions `here `__. Please locate the `Python executable in Isaac -Sim `__ +Sim `__ by navigating to Isaac Sim root folder. In the remaining of the documentation, we will refer to its path as ``ISAACSIM_PYTHON_EXE``. .. note:: + On Linux systems, by default, this should be the executable ``python.sh`` in the directory ``${HOME}/.local/share/ov/pkg/isaac_sim-*``, with ``*`` corresponding to the Isaac Sim version. To avoid the overhead of finding and locating the Isaac Sim installation directory every time, we recommend exporting the following environment -variables to your ``~/.bashrc`` or ``~/.zshrc`` files: +variables to your terminal for the remaining of the installation instructions: .. code:: bash # Isaac Sim root directory - export ISAACSIM_PATH="${HOME}/.local/share/ov/pkg/isaac_sim-2022.2.0" + export ISAACSIM_PATH="${HOME}/.local/share/ov/pkg/isaac_sim-2023.1.0-hotfix.1" # Isaac Sim python executable export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh" +For more information on common paths, please check the Isaac Sim +`documentation `__. + Running the simulator ~~~~~~~~~~~~~~~~~~~~~ Once Isaac Sim is installed successfully, make sure that the simulator runs on your system. For this, we encourage the user to try some of the introductory -tutorials on their `website `__. +tutorials on their `website `__. For completeness, we specify the commands here to check that everything is configured correctly. -On a new terminal (**``Ctrl+Alt+T``**), run the following: +On a new terminal (**Ctrl+Alt+T**), run the following: - Check that the simulator runs as expected: @@ -105,9 +113,10 @@ On a new terminal (**``Ctrl+Alt+T``**), run the following: If the simulator does not run or crashes while following the above instructions, it means that something is incorrectly configured. To debug and troubleshoot, please check Isaac Sim -`documentation `__ +`documentation `__ and the -`forums `__. +`forums `__. + Installing Orbit ---------------- @@ -157,8 +166,8 @@ utilities to manage extensions: optional arguments: -h, --help Display the help content. - -i, --install Install the extensions inside Orbit. - -e, --extra Install extra dependencies such as the learning frameworks. + -i, --install Install the extensions inside Isaac Orbit. + -e, --extra [LIB] Install learning frameworks (rl_games, rsl_rl, sb3) as extra dependencies. Default is 'all'. -f, --format Run pre-commit to format the code and check lints. -p, --python Run the python executable (python.sh) provided by Isaac Sim. -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. @@ -180,6 +189,16 @@ running the following on your terminal: # option2: for zshell users echo -e "alias orbit=$(pwd)/orbit.sh" >> ${HOME}/.zshrc +After running the above command, don't forget to source your ``.bashrc`` or ``.zshrc`` file: + +.. code:: bash + + # option1: for bash users + source ${HOME}/.bashrc + # option2: for zshell users + source ${HOME}/.zshrc + + Setting up the environment ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -187,7 +206,7 @@ The executable ``orbit.sh`` automatically fetches the python bundled with Isaac Sim, using ``./orbit.sh -p`` command (unless inside a virtual environment). This executable behaves like a python executable, and can be used to run any python script or module with the simulator. For more information, please refer to the -`documentation `__. +`documentation `__. Although using a virtual environment is optional, we recommend using ``conda``. To install ``conda``, please follow the instructions `here `__. @@ -209,14 +228,10 @@ activate the environment before running any scripts. For example: conda activate orbit # or `conda activate my_env` Once you are in the virtual environment, you do not need to use ``./orbit.sh -p`` -to run python scripts. You can use the default python executable in your environment. - -As an example, you can run the following command to check if the virtual environment is -set up correctly: - -.. code:: bash - - python -c "import omni.isaac.orbit; print('Orbit configuration is now complete.')" +to run python scripts. You can use the default python executable in your environment +by running ``python`` or ``python3``. However, for the rest of the documentation, +we will assume that you are using ``./orbit.sh -p`` to run python scripts. This command +is equivalent to running ``python`` or ``python3`` in your virtual environment. Building extensions ~~~~~~~~~~~~~~~~~~~ @@ -242,4 +257,29 @@ To build all the extensions, run the following commands: .. code:: bash + # Option 1: Install all dependencies ./orbit.sh --extra # or `./orbit.sh -e` + # Option 2: Install only a subset of dependencies + # note: valid options are 'rl_games', 'rsl_rl', 'sb3', 'robomimic', 'all' + ./orbit.sh --extra rsl_rl # or `./orbit.sh -e rsl_rl + + +Verifying the installation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To verify that the installation was successful, run the following command from the +top of the repository: + +.. code:: bash + + # Option 1: Using the orbit.sh executable + # note: this works for both the bundled python and the virtual environment + ./orbit.sh -p source/standalone/demo/play_empty.py + # Option 2: Using python in your virtual environment + python source/standalone/demo/play_empty.py + +The above command should launch the simulator and display a window with a black +ground plane. You can exit the script by pressing ``Ctrl+C`` on your terminal or +by pressing the ``STOP`` button on the simulator window. + +If you see this, then the installation was successful! |:tada:| diff --git a/docs/source/setup/sample.rst b/docs/source/setup/sample.rst index 1cac962682..9a164b8ef8 100644 --- a/docs/source/setup/sample.rst +++ b/docs/source/setup/sample.rst @@ -18,7 +18,7 @@ A few quick demo scripts to run and checkout: ./orbit.sh -p source/standalone/demo/play_quadrupeds.py -- Spawn multiple Franka arms and apply random position commands: +- Spawn multiple Franka arms and apply random joint position commands: .. code:: bash diff --git a/docs/source/tutorials/00_empty.rst b/docs/source/tutorials/00_empty.rst index d907b96ba8..fca68042c7 100644 --- a/docs/source/tutorials/00_empty.rst +++ b/docs/source/tutorials/00_empty.rst @@ -2,12 +2,9 @@ Creating an empty scene ======================= This tutorial introduces how to create a standalone python script to set up a simple empty scene in Orbit. -It introduces the two main classes used in the simulator, :class:`SimulationApp` and :class:`SimulationContext`, +It introduces the two main classes used in the simulator, :class:`AppLauncher` and :class:`SimulationContext`, that help launch and control the simulation timeline respectively. -Additionally, it introduces the :meth:`create_prim()` function that allows users to create objects in the scene. -We will use this function to create two lights in the scene. - Please review `Isaac Sim Interface `_ and `Isaac Sim Workflows `_ prior to beginning this tutorial. @@ -21,7 +18,7 @@ The tutorial corresponds to the ``play_empty.py`` script in the ``orbit/source/s .. literalinclude:: ../../../source/standalone/demo/play_empty.py :language: python - :emphasize-lines: 20-22,41-42,64-65,69-79,85-86 + :emphasize-lines: 11-23,27-31,37-41,44,49-51,64 :linenos: @@ -31,19 +28,23 @@ The Code Explained Launching the simulator ----------------------- -The first step when working with standalone python scripts is to import the ``SimulationApp`` class. This -class is used to launch the simulation app from the python script. It takes in a dictionary of configuration parameters -that can be used to configure the launched application. For more information on the configuration parameters, please +The first step when working with standalone python scripts is to import the :class:`AppLauncher` class. This +class is used to launch the simulation app from the python script. It takes in a :class:`argparse.Namespace` object of configuration parameters +that can be used to configure the launched application. There is a set of standard parameters that can be +added automatically to the parser with the :meth:`AppLauncher.add_app_launcher_args()` method. These parameters include +`headless` (launch app in no-gui mode), `livestream` (determine the streamining option of the app), `ros` +(enables the ROS1 or ROS2 bridge) and `offscreen_render`(enables offscreen-render mode). +For more information on the configuration parameters, please check the `SimulationApp documentation `_. -Here, we launch the simulation app with the default configuration parameters but with the ``headless`` flag -read from the parsed command line arguments. +Here, we only use the arguments defined by :meth:`AppLauncher.add_app_launcher_args()` which can be set from the +command line. If not explicitly set, the default values are used. .. literalinclude:: ../../../source/standalone/demo/play_empty.py :language: python - :lines: 20-22 + :lines: 11-23 :linenos: - :lineno-start: 20 + :lineno-start: 11 Importing python modules ------------------------ @@ -56,7 +57,7 @@ we will be using in the script. :language: python :lines: 27-31 :linenos: - :lineno-start: 27 + :lineno-start: 31 Designing the simulation scene @@ -77,39 +78,25 @@ time-step size, and advanced solver algorithm settings). (such as ``"numpy"`` and ``"torch"``) in which the returned physics tensors are casted in. Currently, ``orbit`` only supports ``"torch"`` backend. -For this tutorial, we set the physics time step to 0.01 seconds and the rendering time step to 0.01 seconds. Rendering, +For this tutorial, we set the physics and rendering time step to 0.01 seconds. Rendering, here, refers to updating a frame of the current simulation state, which includes the viewport, cameras, and other existing UI elements. .. literalinclude:: ../../../source/standalone/demo/play_empty.py :language: python - :lines: 41-44 + :lines: 37-39 :linenos: - :lineno-start: 41 - -Next, we add a ground plane and some lights into the scene. These objects are referred to as primitives or prims in -the USD definition. More concretely, prims are the basic building blocks of a USD scene. They can be considered -similar to an "element" in HTML. For example, a polygonal mesh is a prim, a light is a prim, a material is a prim. -An “xform” prim stores a transform that applies to it's child prims. - -Each prim has a unique name, zero or more *properties*, and zero or more *children*. Prims can be nested to create -a hierarchy of objects that define a simulation *stage*. Using this powerful concept, it is possible to create -complex scenes with a single USD file. For more information on USD, -please refer to the `USD documentation `_. + :lineno-start: 37 - -In this tutorial, we create prims, we use the :meth:`create_prim()` function which takes as inputs the USD prim path, -the type of prim, prim's location and the prim's properties. The prim path is a string that specifies the prim's -unique name in the USD stage. The prim type is the type of prim (such as ``"Xform"``, ``"Sphere"``, or ``"SphereLight"``). -The prim's properties are passed as key-value pairs in a dictionary. For example, in this tutorial, the ``"radius"`` -property of a ``"SphereLight"`` prim is set to ``2.5``. +Next, we set the initial view shown the GUI. The view is defined as the position where the viewpoint is placed (here `[2.5, 2.5, 2.5]`) +and the target to look at which defines the orientation of the viewpoint (here `[0, 0, 0]`). .. literalinclude:: ../../../source/standalone/demo/play_empty.py :language: python - :lines: 49-62 + :lines: 40-41 :linenos: - :lineno-start: 49 + :lineno-start: 40 Running the simulation loop @@ -125,23 +112,23 @@ and other timeline events. The :meth:`sim.step()` method takes in a ``render`` p current simulation state should be rendered or not. This parameter is set to ``False`` when the ``headless`` flag is set to ``True``. -To ensure a safe execution, we wrap the execution loop with checks to ensure that the simulation app is running and -that the simulator is playing. If the simulator is not playing, we simply step the simulator and continue to the next -iteration of the loop. - .. literalinclude:: ../../../source/standalone/demo/play_empty.py :language: python - :lines: 64-79 + :lines: 43-51 :linenos: - :lineno-start: 64 + :lineno-start: 43 +To ensure a safe execution, we wrap the execution loop with checks to ensure that the simulation app is running and +that the simulator is playing. If the simulator is not playing, we simply step the simulator and continue to the next +iteration of the loop. Lastly, we call the :meth:`simulation_app.close()` method to stop the simulation application and close the window. .. literalinclude:: ../../../source/standalone/demo/play_empty.py :language: python - :lines: 85-86 + :lines: 55-64 :linenos: - :lineno-start: 85 + :lineno-start: 55 + The Code Execution ~~~~~~~~~~~~~~~~~~ @@ -153,10 +140,9 @@ Now that we have gone through the code, let's run the script and see the result: ./orbit.sh -p source/standalone/demo/play_empty.py -This should open a stage with a ground plane and lights spawned at the specified locations. The simulation should be playing and the stage should be rendering. To stop the simulation, you can either close the window, or press the ``STOP`` button in the UI, or press ``Ctrl+C`` in the terminal. Now that we have a basic understanding of how to run a simulation, let's move on to the next tutorial -where we will learn how to add a robot to the stage. +where we will learn how to add a assets to the stage. diff --git a/docs/source/tutorials_envs/00_gym_env.rst b/docs/source/tutorials_envs/00_gym_env.rst index 402e3642f0..e7e90b34d0 100644 --- a/docs/source/tutorials_envs/00_gym_env.rst +++ b/docs/source/tutorials_envs/00_gym_env.rst @@ -2,7 +2,7 @@ Running an RL environment ========================= In this tutorial, we will learn how to run existing learning environments provided in the ``omni.isaac.orbit_tasks`` -extension. All the environments included in Orbit follow the ``gym.Env`` interface, which means that they can be used +extension. All the environments included in Orbit follow the :class:`gymnasium.Env` interface, which means that they can be used with any reinforcement learning framework that supports OpenAI Gym. However, since the environments are implemented in a vectorized fashion, they can only be used with frameworks that support vectorized environments. diff --git a/docs/source/tutorials_envs/01_create_env.rst b/docs/source/tutorials_envs/01_create_env.rst index 119e537d49..28b0061e2d 100644 --- a/docs/source/tutorials_envs/01_create_env.rst +++ b/docs/source/tutorials_envs/01_create_env.rst @@ -15,7 +15,7 @@ different steps. The Code ~~~~~~~~ -All environments in Orbit inherit from the base class :py:class:`IsaacEnv`. The base class follows the ``gym.Env`` +All environments in Orbit inherit from the base class :py:class:`IsaacEnv`. The base class follows the :class:`gymnasium.Env` interface and provides the basic functionality for an environment. Similar to `IsaacGym `_, all environments designed in Orbit are *vectorized* implementations. This means that multiple environment instances are packed into the simulation and the user can interact with the environment by passing in a batch of actions. diff --git a/docs/source/tutorials_envs/02_wrappers.rst b/docs/source/tutorials_envs/02_wrappers.rst index fc598ffa0c..07bdcad7b3 100644 --- a/docs/source/tutorials_envs/02_wrappers.rst +++ b/docs/source/tutorials_envs/02_wrappers.rst @@ -3,11 +3,10 @@ Using environment wrappers Environment wrappers are a way to modify the behavior of an environment without modifying the environment itself. This can be used to apply functions to modify observations or rewards, record videos, enforce time limits, etc. -A detailed description of the API is available in the `gym.Wrapper `_ class. +A detailed description of the API is available in the :class:`gymnasium.Wrapper` class. - -At present, all environments inheriting from the :class:`omni.isaac.orbit_tasks.isaac_env.IsaacEnv` class -are compatible with ``gym.Wrapper``, since the base class implements the ``gym.Env`` interface. +At present, all RL environments inheriting from the :class:`omni.isaac.orbit.envs.RLTaskEnv` class +are compatible with :class:`gymnasium.Wrapper`, since the base class implements the :class:`gymnasium.Env` interface. In order to wrap an environment, you need to first initialize the base environment. After that, you can wrap it with as many wrappers as you want by calling `env = wrapper(env, *args, **kwargs)` repeatedly. @@ -41,7 +40,7 @@ For example, here is how you would wrap an environment to enforce that reset is Wrapper for recording videos ---------------------------- -The :class:`gym.wrappers.RecordVideo ` wrapper can be used to record videos of the environment. +The :class:`gymnasium.wrappers.RecordVideo` wrapper can be used to record videos of the environment. The wrapper takes a ``video_dir`` argument, which specifies where to save the videos. The videos are saved in `mp4 `__ format at specified intervals for specified number of environment steps or episodes. @@ -52,13 +51,6 @@ To use the wrapper, you need to first install ``ffmpeg``. On Ubuntu, you can ins sudo apt-get install ffmpeg -The :class:`omni.isaac.orbit.envs.RlEnv` supports the rendering modes: - -* **"human"**: When you want to render the environment to the screen. It does not return any image. -* **"rgb_array"**: When you want to get the rendered image as a numpy array. It returns the image as a numpy array. - This mode is only possible when viewport is enabled, i.e. either the GUI window is open or off-screen rendering flag - is enabled. - .. attention:: By default, when running an environment in headless mode, the Omniverse viewport is disabled. This is done to @@ -74,21 +66,21 @@ The :class:`omni.isaac.orbit.envs.RlEnv` supports the rendering modes: The viewport camera used for rendering is the default camera in the scene called ``"/OmniverseKit_Persp"``. The camera's pose and image resolution can be configured through the -:class:`omni.isaac.orbit.envs.base_env_cfg.ViewerCfg` class. +:class:`omni.isaac.orbit.envs.ViewerCfg` class. -.. dropdown:: :fa:`eye,mr-1` Default parameters of the :class:`ViewerCfg` in the ``base_env_cfg.py`` file: +.. dropdown:: :fa:`eye,mr-1` Default parameters of the :class:`ViewerCfg` class: .. literalinclude:: ../../../source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env_cfg.py :language: python - :lines: 23-38 - :linenos: - :lineno-start: 31 + :pyobject: ViewerCfg After adjusting the parameters, you can record videos by wrapping the environment with the -:class:`gym.wrappers.RecordVideo ` wrapper and enabling the off-screen rendering -flag. As an example, the following code records a video of the ``Isaac-Reach-Franka-v0`` environment +:class:`gymnasium.wrappers.RecordVideo` wrapper and enabling the off-screen rendering +flag. Additionally, you need to specify the render mode of the environment as ``"rgb_array"``. + +As an example, the following code records a video of the ``Isaac-Reach-Franka-v0`` environment for 200 steps, and saves it in the ``videos`` folder at a step interval of 1500 steps. .. code:: python @@ -112,7 +104,8 @@ for 200 steps, and saves it in the ``videos`` folder at a step interval of 1500 env_cfg.viewer.eye = (1.0, 1.0, 1.0) env_cfg.viewer.lookat = (0.0, 0.0, 0.0) # create isaac-env instance - env = gym.make(task_name, cfg=env_cfg) + # set render mode to rgb_array to obtain images on render calls + env = gym.make(task_name, cfg=env_cfg, render_mode="rgb_array") # wrap for video recording video_kwargs = { "video_folder": "videos", @@ -126,22 +119,21 @@ Wrapper for learning frameworks ------------------------------- Every learning framework has its own API for interacting with environments. For example, the -`Stable Baselines3 `__ library uses the -`gym.Env `__ interface to interact with environments. -However, libraries like `RL-Games `__ or -`RSL-RL `__ use their own API for interfacing with a -learning environments. Since there is no one-size-fits-all solution, we do not base the :class:`IsaacEnv` -class on any particular learning framework's environment definition. Instead, we implement -wrappers to make it compatible with the learning framework's environment definition. +`Stable-Baselines3`_ library uses the `gym.Env `_ +interface to interact with environments. However, libraries like `RL-Games`_ or `RSL-RL`_ +use their own API for interfacing with a learning environments. Since there is no one-size-fits-all +solution, we do not base the :class:`RLTaskEnv` class on any particular learning framework's +environment definition. Instead, we implement wrappers to make it compatible with the learning +framework's environment definition. -As an example of how to use the :class:`IsaacEnv` with Stable-Baselines3: +As an example of how to use the RL task environment with Stable-Baselines3: .. code:: python from omni.isaac.orbit_tasks.utils.wrappers.sb3 import Sb3VecEnvWrapper # create isaac-env instance - env = gym.make(task_name, cfg=env_cfg, render=headless) + env = gym.make(task_name, cfg=env_cfg) # wrap around environment for stable baselines env = Sb3VecEnvWrapper(env) @@ -150,11 +142,19 @@ As an example of how to use the :class:`IsaacEnv` with Stable-Baselines3: Wrapping the environment with the respective learning framework's wrapper should happen in the end, i.e. after all other wrappers have been applied. This is because the learning framework's wrapper - modifies the interpretation of environment's APIs which may no longer be compatible with ``gym.Env``. + modifies the interpretation of environment's APIs which may no longer be compatible with :class:`gymnasium.Env`. + + +Adding new wrappers +------------------- + +All new wrappers should be added to the :mod:`omni.isaac.orbit_tasks.utils.wrappers` module. +They should check that the underlying environment is an instance of :class:`omni.isaac.orbit.envs.RLTaskEnv` +before applying the wrapper. This can be done by using the :func:`unwrapped` property. +We include a set of wrappers in this module that can be used as a reference to implement your own wrappers. +If you implement a new wrapper, please consider contributing it to the framework by opening a pull request. -To add support for a new learning framework, you need to implement a wrapper class that -converts the :class:`IsaacEnv` to the learning framework's environment definition. This -wrapper class should typically inherit from the ``gym.Wrapper`` class. We include a -set of these wrappers in the :mod:`omni.isaac.orbit_tasks.utils.wrappers` module. You can -use these wrappers as a reference to implement your own wrapper for a new learning framework. +.. _Stable-Baselines3: https://stable-baselines3.readthedocs.io/en/master/ +.. _RL-Games: https://github.com/Denys88/rl_games +.. _RSL-RL: https://github.com/leggedrobotics/rsl_rl diff --git a/orbit.sh b/orbit.sh index 820743176f..802aad525a 100755 --- a/orbit.sh +++ b/orbit.sh @@ -324,7 +324,8 @@ while [[ $# -gt 0 ]]; do # build the documentation echo "[INFO] Building documentation..." # retrieve the python executable - python_exe=$(extract_python_exe) + # python_exe=$(extract_python_exe) + python_exe=python3 # install pip packages cd ${ORBIT_PATH}/docs ${python_exe} -m pip install -r requirements.txt > /dev/null diff --git a/source/extensions/omni.isaac.orbit/docs/CHANGELOG.rst b/source/extensions/omni.isaac.orbit/docs/CHANGELOG.rst index 275a5cc34e..b7b202cf0b 100644 --- a/source/extensions/omni.isaac.orbit/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.orbit/docs/CHANGELOG.rst @@ -823,7 +823,7 @@ Changed 0.8.10 (2023-08-17) -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~ Added ^^^^^ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/__init__.py index 0c45b792fa..539fea2fc6 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/__init__.py @@ -3,9 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Python module with robotic environments. -""" +"""Package containing the core framework.""" import os import toml diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/__init__.py index d41598fc52..8e1e9524fa 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/__init__.py @@ -3,9 +3,24 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Subpackage for handling actuator models.""" +"""Sub-package for different actuator models. -from __future__ import annotations +Actuator models are used to model the behavior of the actuators in an articulation. These +are usually meant to be used in simulation to model different actuator dynamics and delays. + +There are two main categories of actuator models that are supported: + +- **Implicit**: Motor model with ideal PD from the physics engine. This is similar to having a continuous time + PD controller. The motor model is implicit in the sense that the motor model is not explicitly defined by the user. +- **Explicit**: Motor models based on physical drive models. + + - **Physics-based**: Derives the motor models based on first-principles. + - **Neural Network-based**: Learned motor models from actuator data. + +Every actuator model inherits from the :class:`omni.isaac.orbit.actuators.ActuatorBase` class, +which defines the common interface for all actuator models. The actuator models are handled +and called by the :class:`omni.isaac.orbit.assets.Articulation` class. +""" from .actuator_base import ActuatorBase from .actuator_cfg import ( @@ -18,24 +33,3 @@ ) from .actuator_net import ActuatorNetLSTM, ActuatorNetMLP from .actuator_pd import DCMotor, IdealPDActuator, ImplicitActuator - -__all__ = [ - # base actuator - "ActuatorBase", - "ActuatorBaseCfg", - # implicit actuator - "ImplicitActuatorCfg", - "ImplicitActuator", - # ideal pd actuator - "IdealPDActuatorCfg", - "IdealPDActuator", - # dc motor - "DCMotorCfg", - "DCMotor", - # actuator net -- lstm - "ActuatorNetLSTMCfg", - "ActuatorNetLSTM", - # actuator net -- mlp - "ActuatorNetMLPCfg", - "ActuatorNetMLP", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_base.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_base.py index 52a22f7795..ee94b756df 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_base.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_base.py @@ -37,21 +37,21 @@ class ActuatorBase(ABC): """ computed_effort: torch.Tensor - """The computed effort for the actuator group. Shape is ``(num_envs, num_joints)``.""" + """The computed effort for the actuator group. Shape is (num_envs, num_joints).""" applied_effort: torch.Tensor - """The applied effort for the actuator group. Shape is ``(num_envs, num_joints)``.""" + """The applied effort for the actuator group. Shape is (num_envs, num_joints).""" effort_limit: torch.Tensor - """The effort limit for the actuator group. Shape is ``(num_envs, num_joints)``.""" + """The effort limit for the actuator group. Shape is (num_envs, num_joints).""" velocity_limit: torch.Tensor - """The velocity limit for the actuator group. Shape is ``(num_envs, num_joints)``.""" + """The velocity limit for the actuator group. Shape is (num_envs, num_joints).""" stiffness: torch.Tensor - """The stiffness (P gain) of the PD controller. Shape is ``(num_envs, num_joints)``.""" + """The stiffness (P gain) of the PD controller. Shape is (num_envs, num_joints).""" damping: torch.Tensor - """The damping (D gain) of the PD controller. Shape is ``(num_envs, num_joints)``.""" + """The damping (D gain) of the PD controller. Shape is (num_envs, num_joints).""" armature: torch.Tensor - """The armature of the actuator joints. Shape is ``(num_envs, num_joints)``.""" + """The armature of the actuator joints. Shape is (num_envs, num_joints).""" friction: torch.Tensor - """The joint friction of the actuator joints. Shape is ``(num_envs, num_joints)``.""" + """The joint friction of the actuator joints. Shape is (num_envs, num_joints).""" def __init__( self, @@ -81,17 +81,17 @@ def __init__( num_envs: Number of articulations in the view. device: Device used for processing. stiffness: The default joint stiffness (P gain). Defaults to 0.0. - If a tensor, then the shape is ``(num_envs, num_joints)``. + If a tensor, then the shape is (num_envs, num_joints). damping: The default joint damping (D gain). Defaults to 0.0. - If a tensor, then the shape is ``(num_envs, num_joints)``. + If a tensor, then the shape is (num_envs, num_joints). armature: The default joint armature. Defaults to 0.0. - If a tensor, then the shape is ``(num_envs, num_joints)``. + If a tensor, then the shape is (num_envs, num_joints). friction: The default joint friction. Defaults to 0.0. - If a tensor, then the shape is ``(num_envs, num_joints)``. + If a tensor, then the shape is (num_envs, num_joints). effort_limit: The default effort limit. Defaults to infinity. - If a tensor, then the shape is ``(num_envs, num_joints)``. + If a tensor, then the shape is (num_envs, num_joints). velocity_limit: The default velocity limit. Defaults to infinity. - If a tensor, then the shape is ``(num_envs, num_joints)``. + If a tensor, then the shape is (num_envs, num_joints). """ # save parameters self.cfg = cfg @@ -177,8 +177,8 @@ def compute( Args: control_action: The joint action instance comprising of the desired joint positions, joint velocities and (feed-forward) joint efforts. - joint_pos: The current joint positions of the joints in the group. Shape is ``(num_envs, num_joints)``. - joint_vel: The current joint velocities of the joints in the group. Shape is ``(num_envs, num_joints)``. + joint_pos: The current joint positions of the joints in the group. Shape is (num_envs, num_joints). + joint_vel: The current joint velocities of the joints in the group. Shape is (num_envs, num_joints). Returns: The computed desired joint positions, joint velocities and joint efforts. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_cfg.py index a6c244832f..61b3964717 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/actuators/actuator_cfg.py @@ -32,39 +32,39 @@ class ActuatorBaseCfg: """ effort_limit: float | None = None - """Force/Torque limit of the joints in the group. Defaults to :obj:`None`. + """Force/Torque limit of the joints in the group. Defaults to None. - If :obj:`None`, the limit is set to the value specified in the USD joint prim. + If None, the limit is set to the value specified in the USD joint prim. """ velocity_limit: float | None = None - """Velocity limit of the joints in the group. Defaults to :obj:`None`. + """Velocity limit of the joints in the group. Defaults to None. - If :obj:`None`, the limit is set to infinity. + If None, the limit is set to infinity. """ stiffness: dict[str, float] | float | None = MISSING """Stiffness gains (also known as p-gain) of the joints in the group. - If :obj:`None`, the stiffness is set to the value from the USD joint prim. + If None, the stiffness is set to the value from the USD joint prim. """ damping: dict[str, float] | float | None = MISSING """Damping gains (also known as d-gain) of the joints in the group. - If :obj:`None`, the damping is set to the value from the USD joint prim. + If None, the damping is set to the value from the USD joint prim. """ armature: dict[str, float] | float | None = None - """Armature of the joints in the group. + """Armature of the joints in the group. Defaults to None. - If :obj:`None`, the armature is set to the value from the USD joint prim. + If None, the armature is set to the value from the USD joint prim. """ friction: dict[str, float] | float | None = None - """Joint friction of the joints in the group. + """Joint friction of the joints in the group. Defaults to None. - If :obj:`None`, the joint friction is set to the value from the USD joint prim. + If None, the joint friction is set to the value from the USD joint prim. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/app.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/app.py index 065cb54718..82595c2b20 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/app.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/app.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Utility to configure the ``omni.isaac.kit.SimulationApp`` based on environment variables. +"""Sub-package with the utility class to configure the :class:`omni.isaac.kit.SimulationApp`. Based on the desired functionality, this class parses environment variables and input CLI arguments to launch the simulator in various different modes. This includes with or without GUI, switching between @@ -11,8 +11,8 @@ extensions to be loaded in a specific order, otherwise a segmentation fault occurs. The launched `SimulationApp`_ instance is accessible via the :attr:`AppLauncher.app` property. -Available modes ---------------- +Environment variables +--------------------- The following details the behavior of the class based on the environment variables: @@ -37,6 +37,12 @@ * ``ROS_ENABLED=1``: Enables the ROS1 Noetic bridge in Isaac Sim. * ``ROS_ENABLED=2``: Enables the ROS2 Foxy bridge in Isaac Sim. + .. caution:: + + In Isaac Sim 2022.2.1, loading ``omni.isaac.ros_bridge`` before ``omni.kit.livestream.native`` + causes a segfault. Thus, to work around this issue, we enable the ROS-bridge extensions after the + livestreaming extensions. + * **Offscreen Render**: If the environment variable ``OFFSCREEN_RENDER`` is set to 1, then the offscreen-render pipeline is enabled. This is useful for running the simulator without a GUI but still rendering the viewport and camera images. @@ -44,19 +50,12 @@ * ``OFFSCREEN_RENDER=1``: Enables the offscreen-render pipeline which allows users to render the scene without launching a GUI. - .. note:: - The off-screen rendering pipeline only works when used in conjunction with the - :class:`omni.isaac.orbit.sim.SimulationContext` class. This is because the off-screen rendering - pipeline enables flags that are internally used by the SimulationContext class. - - .. caution:: - Currently, in Isaac Sim 2022.2.1, loading ``omni.isaac.ros_bridge`` before ``omni.kit.livestream.native`` - causes a segfault. Thus, to work around this issue, we enable the ROS-bridge extensions after the - livestreaming extensions. + .. note:: + The off-screen rendering pipeline only works when used in conjunction with the + :class:`omni.isaac.orbit.sim.SimulationContext` class. This is because the off-screen rendering + pipeline enables flags that are internally used by the SimulationContext class. -Usage ------ To set the environment variables, one can use the following command in the terminal: @@ -74,6 +73,44 @@ REMOTE_DEPLOYMENT=3 OFFSCREEN_RENDER=1 ./orbit.sh -p source/standalone/demo/play_quadrupeds.py +Overriding the environment variables +------------------------------------ + +The environment variables can be overridden in the python script itself using the :class:`AppLauncher`. +These can be passed as a dictionary, a :class:`argparse.Namespace` object or as keyword arguments. +When the passed arguments are not the default values, then they override the environment variables. + +The following snippet shows how use the :class:`AppLauncher` in different ways: + +.. code:: python + + import argparser + + from omni.isaac.orbit.app import AppLauncher + + # add argparse arguments + parser = argparse.ArgumentParser() + # add your own arguments + # .... + # add app launcher arguments for cli + AppLauncher.add_app_launcher_args(parser) + # parse arguments + args = parser.parse_args() + + # launch omniverse isaac-sim app + # -- Option 1: Pass the settings as a Namespace object + app_launcher = AppLauncher(args).app + # -- Option 2: Pass the settings as keywords arguments + app_launcher = AppLauncher(headless=args.headless, livestream=args.livestream) + # -- Option 3: Pass the settings as a dictionary + app_launcher = AppLauncher(vars(args)) + # -- Option 4: Pass no settings + app_launcher = AppLauncher() + + # obtain the launched app + simulation_app = app_launcher.app + + .. _SimulationApp: https://docs.omniverse.nvidia.com/py/isaacsim/source/extensions/omni.isaac.kit/docs/index.html .. _livestream: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/manual_livestream_clients.html .. _`Native Livestream`: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/manual_livestream_clients.html#isaac-sim-setup-kit-remote @@ -112,35 +149,6 @@ class AppLauncher: value >-1. In other words, if ``livestream=-1``, then the value from the environment variable ``LIVESTREAM`` is used. - Usage: - - .. code:: python - - import argparser - - from omni.isaac.orbit.app import AppLauncher - - # add argparse arguments - parser = argparse.ArgumentParser() - # add your own arguments - # .... - # add app launcher arguments for cli - AppLauncher.add_app_launcher_args(parser) - # parse arguments - args = parser.parse_args() - - # launch omniverse isaac-sim app - # -- Option 1: Pass the settings as a Namespace object - app_launcher = AppLauncher(args).app - # -- Option 2: Pass the settings as keywords arguments - app_launcher = AppLauncher(headless=args.headless, livestream=args.livestream) - # -- Option 3: Pass the settings as a dictionary - app_launcher = AppLauncher(vars(args)) - # -- Option 4: Pass no settings - app_launcher = AppLauncher() - - # obtain the launched app - simulation_app = app_launcher.app """ def __init__(self, launcher_args: argparse.Namespace | dict = None, **kwargs): @@ -369,7 +377,7 @@ def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: This is used to check against name collisions for arguments passed to the :class:`AppLauncher` class as well as for type checking. It corresponds closely to the :attr:`SimulationApp.DEFAULT_LAUNCHER_CONFIG`, - but specifically denotes where :obj:`None` types are allowed. + but specifically denotes where None types are allowed. """ @staticmethod diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/__init__.py index 74169be11d..d6d4252620 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/__init__.py @@ -3,7 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" +"""Sub-package for different assets, such as rigid objects and articulations. + An asset is a physical object that can be spawned in the simulation. The class handles both the spawning of the asset into the USD stage as well as initialization of necessary physics handles to interact with the asset. @@ -22,13 +23,13 @@ The asset class follows the following naming convention for its methods: -* `set_xxx()`: These are used to only set the buffers into the :attr:`data` instance. However, they - do not write the data into the simulator. The writing of data only happens when the - :meth:`write_data_to_sim` method is called. -* `write_xxx_to_sim()`: These are used to set the buffers into the :attr:`data` instance and write - the corresponding data into the simulator as well. -* `update(dt)`: These are used to update the buffers in the :attr:`data` instance. This should - be called after a simulation step is performed. +* **set_xxx()**: These are used to only set the buffers into the :attr:`data` instance. However, they + do not write the data into the simulator. The writing of data only happens when the + :meth:`write_data_to_sim` method is called. +* **write_xxx_to_sim()**: These are used to set the buffers into the :attr:`data` instance and write + the corresponding data into the simulator as well. +* **update(dt)**: These are used to update the buffers in the :attr:`data` instance. This should + be called after a simulation step is performed. The main reason to separate the ``set`` and ``write`` operations is to provide flexibility to the user when they need to perform a post-processing operation of the buffers before applying them @@ -37,23 +38,7 @@ the corresponding actuator torques. """ -from __future__ import annotations - from .articulation import Articulation, ArticulationCfg, ArticulationData from .asset_base import AssetBase from .asset_base_cfg import AssetBaseCfg from .rigid_object import RigidObject, RigidObjectCfg, RigidObjectData - -__all__ = [ - # asset base - "AssetBaseCfg", - "AssetBase", - # rigid object - "RigidObjectCfg", - "RigidObjectData", - "RigidObject", - # articulation - "ArticulationCfg", - "ArticulationData", - "Articulation", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/__init__.py index 24994307f0..373c978755 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/__init__.py @@ -3,8 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Sub-module for rigid articulated assets.""" + from .articulation import Articulation from .articulation_cfg import ArticulationCfg from .articulation_data import ArticulationData - -__all__ = ["Articulation", "ArticulationCfg", "ArticulationData"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation.py index 7c61ef3bfe..76f65e37c1 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation.py @@ -32,7 +32,60 @@ class Articulation(RigidObject): - """Class for handling articulations.""" + """An articulation asset class. + + An articulation is a collection of rigid bodies connected by joints. The joints can be either + fixed or actuated. The joints can be of different types, such as revolute, prismatic, D-6, etc. + However, the articulation class has currently been tested with revolute and prismatic joints. + The class supports both floating-base and fixed-base articulations. The type of articulation + is determined based on the root joint of the articulation. If the root joint is fixed, then + the articulation is considered a fixed-base system. Otherwise, it is considered a floating-base + system. This can be checked using the :attr:`Articulation.is_fixed_base` attribute. + + For an asset to be considered an articulation, the root prim of the asset must have the + `USD ArticulationRootAPI`_. This API is used to define the sub-tree of the articulation using + the reduced coordinate formulation. On playing the simulation, the physics engine parses the + articulation root prim and creates the corresponding articulation in the physics engine. The + articulation root prim can be specified using the :attr:`AssetBaseCfg.prim_path` attribute. + + The articulation class is a subclass of the :class:`RigidObject` class. Therefore, it inherits + all the functionality of the rigid object class. In case of an articulation, the :attr:`root_view` + attribute corresponds to the articulation root view and can be used to access the articulation + related data. The :attr:`body_view` attribute corresponds to the rigid body view of the articulated + links and can be used to access the rigid body related data. The :attr:`root_physx_view` and + :attr:`body_physx_view` attributes correspond to the underlying physics views of the articulation + root and the articulated links, respectively. + + The articulation class also provides the functionality to augment the simulation of an articulated + system with custom actuator models. These models can either be explicit or implicit, as detailed in + the :mod:`omni.isaac.orbit.actuators` module. The actuator models are specified using the + :attr:`ArticulationCfg.actuators` attribute. These are then parsed and used to initialize the + corresponding actuator models, when the simulation is played. + + During the simulation step, the articulation class first applies the actuator models to compute + the joint commands based on the user-specified targets. These joint commands are then applied + into the simulation. The joint commands can be either position, velocity, or effort commands. + As an example, the following snippet shows how this can be used for position commands: + + .. code-block:: python + + # an example instance of the articulation class + my_articulation = Articulation(cfg) + + # set joint position targets + my_articulation.set_joint_position_target(position) + # propagate the actuator models and apply the computed commands into the simulation + my_articulation.write_data_to_sim() + + # step the simulation using the simulation context + sim_context.step() + + # update the articulation state, where dt is the simulation time step + my_articulation.update(dt) + + .. _`USD ArticulationRootAPI`: https://openusd.org/dev/api/class_usd_physics_articulation_root_a_p_i.html + + """ cfg: ArticulationCfg """Configuration instance for the articulations.""" @@ -143,6 +196,9 @@ def find_joints( ) -> tuple[list[int], list[str]]: """Find joints in the articulation based on the name keys. + Please see the :func:`omni.isaac.orbit.utils.string.resolve_matching_names` function for more information + on the name matching. + Args: name_keys: A regular expression or a list of regular expressions to match the joint names. joint_subset: A subset of joints to search for. Defaults to None, which means all joints @@ -197,8 +253,8 @@ def write_joint_state_to_sim( """Write joint positions and velocities to the simulation. Args: - position: Joint positions. Shape is ``(len(env_ids), len(joint_ids))``. - velocity: Joint velocities. Shape is ``(len(env_ids), len(joint_ids))``. + position: Joint positions. Shape is (len(env_ids), len(joint_ids)). + velocity: Joint velocities. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all environments). """ @@ -227,7 +283,7 @@ def write_joint_stiffness_to_sim( """Write joint stiffness into the simulation. Args: - stiffness: Joint stiffness. Shape is ``(len(env_ids), len(joint_ids))``. + stiffness: Joint stiffness. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the stiffness for. Defaults to None (all joints). env_ids: The environment indices to set the stiffness for. Defaults to None (all environments). """ @@ -253,7 +309,7 @@ def write_joint_damping_to_sim( """Write joint damping into the simulation. Args: - damping: Joint damping. Shape is ``(len(env_ids), len(joint_ids))``. + damping: Joint damping. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the damping for. Defaults to None (all joints). env_ids: The environment indices to set the damping for. @@ -281,7 +337,7 @@ def write_joint_effort_limit_to_sim( """Write joint effort limits into the simulation. Args: - limits: Joint torque limits. Shape is ``(len(env_ids), len(joint_ids))``. + limits: Joint torque limits. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the joint torque limits for. Defaults to None (all joints). env_ids: The environment indices to set the joint torque limits for. Defaults to None (all environments). """ @@ -311,7 +367,7 @@ def write_joint_armature_to_sim( """Write joint armature into the simulation. Args: - armature: Joint armature. Shape is ``(len(env_ids), len(joint_ids))``. + armature: Joint armature. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the joint torque limits for. Defaults to None (all joints). env_ids: The environment indices to set the joint torque limits for. Defaults to None (all environments). """ @@ -336,7 +392,7 @@ def write_joint_friction_to_sim( """Write joint friction into the simulation. Args: - joint_friction: Joint friction. Shape is ``(len(env_ids), len(joint_ids))``. + joint_friction: Joint friction. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the joint torque limits for. Defaults to None (all joints). env_ids: The environment indices to set the joint torque limits for. Defaults to None (all environments). """ @@ -366,7 +422,7 @@ def set_joint_position_target( the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint position targets. Shape is ``(len(env_ids), len(joint_ids))``. + target: Joint position targets. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all environments). """ @@ -388,7 +444,7 @@ def set_joint_velocity_target( the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint velocity targets. Shape is ``(len(env_ids), len(joint_ids))``. + target: Joint velocity targets. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all environments). """ @@ -410,7 +466,7 @@ def set_joint_effort_target( the desired values. To apply the joint targets, call the :meth:`write_data_to_sim` function. Args: - target: Joint effort targets. Shape is ``(len(env_ids), len(joint_ids))``. + target: Joint effort targets. Shape is (len(env_ids), len(joint_ids)). joint_ids: The joint indices to set the targets for. Defaults to None (all joints). env_ids: The environment indices to set the targets for. Defaults to None (all environments). """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation_data.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation_data.py index b58c7edc5f..f98142e57a 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation_data.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/articulation/articulation_data.py @@ -26,30 +26,30 @@ class ArticulationData(RigidObjectData): ## default_joint_pos: torch.Tensor = None - """Default joint positions of all joints. Shape is ``(count, num_joints)``.""" + """Default joint positions of all joints. Shape is (count, num_joints).""" default_joint_vel: torch.Tensor = None - """Default joint velocities of all joints. Shape is ``(count, num_joints)``.""" + """Default joint velocities of all joints. Shape is (count, num_joints).""" ## # Joint states <- From simulation. ## joint_pos: torch.Tensor = None - """Joint positions of all joints. Shape is ``(count, num_joints)``.""" + """Joint positions of all joints. Shape is (count, num_joints).""" joint_vel: torch.Tensor = None - """Joint velocities of all joints. Shape is ``(count, num_joints)``.""" + """Joint velocities of all joints. Shape is (count, num_joints).""" joint_acc: torch.Tensor = None - """Joint acceleration of all joints. Shape is ``(count, num_joints)``.""" + """Joint acceleration of all joints. Shape is (count, num_joints).""" ## # Joint commands -- Set into simulation. ## joint_pos_target: torch.Tensor = None - """Joint position targets commanded by the user. Shape is ``(count, num_joints)``. + """Joint position targets commanded by the user. Shape is (count, num_joints). For an implicit actuator model, the targets are directly set into the simulation. For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), @@ -57,7 +57,7 @@ class ArticulationData(RigidObjectData): """ joint_vel_target: torch.Tensor = None - """Joint velocity targets commanded by the user. Shape is ``(count, num_joints)``. + """Joint velocity targets commanded by the user. Shape is (count, num_joints). For an implicit actuator model, the targets are directly set into the simulation. For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), @@ -65,7 +65,7 @@ class ArticulationData(RigidObjectData): """ joint_effort_target: torch.Tensor = None - """Joint effort targets commanded by the user. Shape is ``(count, num_joints)``. + """Joint effort targets commanded by the user. Shape is (count, num_joints). For an implicit actuator model, the targets are directly set into the simulation. For an explicit actuator model, the targets are used to compute the joint torques (see :attr:`applied_torque`), @@ -73,23 +73,23 @@ class ArticulationData(RigidObjectData): """ joint_stiffness: torch.Tensor = None - """Joint stiffness provided to simulation. Shape is ``(count, num_joints)``.""" + """Joint stiffness provided to simulation. Shape is (count, num_joints).""" joint_damping: torch.Tensor = None - """Joint damping provided to simulation. Shape is ``(count, num_joints)``.""" + """Joint damping provided to simulation. Shape is (count, num_joints).""" joint_armature: torch.Tensor = None - """Joint armature provided to simulation. Shape is ``(count, num_joints)``.""" + """Joint armature provided to simulation. Shape is (count, num_joints).""" joint_friction: torch.Tensor = None - """Joint friction provided to simulation. Shape is ``(count, num_joints)``.""" + """Joint friction provided to simulation. Shape is (count, num_joints).""" ## # Joint commands -- Explicit actuators. ## computed_torque: torch.Tensor = None - """Joint torques computed from the actuator model (before clipping). Shape is ``(count, num_joints)``. + """Joint torques computed from the actuator model (before clipping). Shape is (count, num_joints). This quantity is the raw torque output from the actuator mode, before any clipping is applied. It is exposed for users who want to inspect the computations inside the actuator model. @@ -99,7 +99,7 @@ class ArticulationData(RigidObjectData): """ applied_torque: torch.Tensor = None - """Joint torques applied from the actuator model (after clipping). Shape is ``(count, num_joints)``. + """Joint torques applied from the actuator model (after clipping). Shape is (count, num_joints). These torques are set into the simulation, after clipping the :attr:`computed_torque` based on the actuator model. @@ -112,10 +112,10 @@ class ArticulationData(RigidObjectData): ## soft_joint_pos_limits: torch.Tensor = None - """Joint positions limits for all joints. Shape is ``(count, num_joints, 2)``.""" + """Joint positions limits for all joints. Shape is (count, num_joints, 2).""" soft_joint_vel_limits: torch.Tensor = None - """Joint velocity limits for all joints. Shape is ``(count, num_joints, 2)``.""" + """Joint velocity limits for all joints. Shape is (count, num_joints, 2).""" gear_ratio: torch.Tensor = None - """Gear ratio for relating motor torques to applied Joint torques. Shape is ``(count, num_joints)``.""" + """Gear ratio for relating motor torques to applied Joint torques. Shape is (count, num_joints).""" diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base.py index 704b9757a7..00dc826edc 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base.py @@ -20,7 +20,33 @@ class AssetBase(ABC): - """The interface class for assets.""" + """The base interface class for assets. + + An asset corresponds to any physics-enabled object that can be spawned in the simulation. These include + rigid objects, articulated objects, deformable objects etc. The core functionality of an asset is to + provide a set of buffers that can be used to interact with the simulator. The buffers are updated + by the asset class and can be written into the simulator using the their respective ``write`` methods. + This allows a convenient way to perform post-processing operations on the buffers before writing them + into the simulator and obtaining the corresponding simulation results. + + The class handles both the spawning of the asset into the USD stage as well as initialization of necessary + physics handles to interact with the asset. Upon construction of the asset instance, the prim corresponding + to the asset is spawned into the USD stage if the spawn configuration is not None. The spawn configuration + is defined in the :attr:`AssetBaseCfg.spawn` attribute. In case the configured :attr:`AssetBaseCfg.prim_path` + is an expression, then the prim is spawned at all the matching paths. Otherwise, a single prim is spawned + at the configured path. For more information on the spawn configuration, see the + :mod:`omni.isaac.orbit.sim.spawners` module. + + Unlike Isaac Sim interface, where one usually needs to call the + :meth:`omni.isaac.core.prims.XFormPrimView.initialize` method to initialize the PhysX handles, the asset + class automatically initializes and invalidates the PhysX handles when the stage is played/stopped. This + is done by registering callbacks for the stage play/stop events. + + Additionally, the class registers a callback for debug visualization of the asset if a debug visualization + is implemented in the asset class. This can be enabled by setting the :attr:`AssetBaseCfg.debug_vis` attribute + to True. The debug visualization is implemented through the :meth:`_set_debug_vis_impl` and + :meth:`_debug_vis_callback` methods. + """ def __init__(self, cfg: AssetBaseCfg): """Initialize the asset base. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base_cfg.py index 91808ac651..4bf6bcf44a 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/asset_base_cfg.py @@ -16,17 +16,28 @@ @configclass class AssetBaseCfg: - """Configuration parameters for an asset.""" + """The base configuration class for an asset's parameters. + + Please see the :class:`AssetBase` class for more information on the asset class. + """ @configclass class InitialStateCfg: - """Initial state of the asset.""" + """Initial state of the asset. + + This defines the default initial state of the asset when it is spawned into the simulation, as + well as the default state when the simulation is reset. + + After parsing the initial state, the asset class stores this information in the :attr:`data` + attribute of the asset class. This can then be accessed by the user to modify the state of the asset + during the simulation, for example, at resets. + """ # root position pos: tuple[float, float, float] = (0.0, 0.0, 0.0) """Position of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0).""" rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) - """Quaternion rotation ``(w, x, y, z)`` of the root in simulation world frame. + """Quaternion rotation (w, x, y, z) of the root in simulation world frame. Defaults to (1.0, 0.0, 0.0, 0.0). """ @@ -46,16 +57,16 @@ class InitialStateCfg: Example: ``{ENV_REGEX_NS}/Robot`` will be replaced with ``/World/envs/env_.*/Robot``. """ - init_state: InitialStateCfg = InitialStateCfg() - """Initial state of the rigid object. Defaults to identity pose.""" - spawn: SpawnerCfg | None = MISSING """Spawn configuration for the asset. - If :obj:`None`, then no prims are spawned by the asset class. Instead, it is assumed that the + If None, then no prims are spawned by the asset class. Instead, it is assumed that the asset is already present in the scene. """ + init_state: InitialStateCfg = InitialStateCfg() + """Initial state of the rigid object. Defaults to identity pose.""" + collision_group: Literal[0, -1] = 0 """Collision group of the asset. Defaults to ``0``. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/__init__.py index 07111fe6df..67525b2efa 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/__init__.py @@ -3,11 +3,12 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Sub-module containing configuration instances for different assets. -""" +"""Configuration instances for different assets. -from __future__ import annotations +This sub-module contains the configuration instances for different assets. The configuration +instances are used to spawn and configure the assets in the simulation. They are passed to +their corresponding asset classes during construction. +""" from .anymal import * from .franka import * diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/anymal.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/anymal.py index f405c1dc9d..cfdd7fc4a0 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/anymal.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/anymal.py @@ -6,15 +6,16 @@ """Configuration for the ANYbotics robots. The following configuration parameters are available: + * :obj:`ANYMAL_B_CFG`: The ANYmal-B robot with ANYdrives 3.0 * :obj:`ANYMAL_C_CFG`: The ANYmal-C robot with ANYdrives 3.0 Reference: + * https://github.com/ANYbotics/anymal_b_simple_description * https://github.com/ANYbotics/anymal_c_simple_description -""" -from __future__ import annotations +""" import omni.isaac.orbit.sim as sim_utils from omni.isaac.orbit.actuators import ActuatorNetLSTMCfg, DCMotorCfg diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/franka.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/franka.py index 2e00e3ce0b..a9ec3c87d6 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/franka.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/franka.py @@ -12,8 +12,6 @@ Reference: https://github.com/frankaemika/franka_ros """ -from __future__ import annotations - import omni.isaac.orbit.sim as sim_utils from omni.isaac.orbit.actuators import ImplicitActuatorCfg from omni.isaac.orbit.utils.assets import ISAAC_ORBIT_NUCLEUS_DIR diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/ridgeback_franka.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/ridgeback_franka.py index c3b11d35e7..b1ae202eca 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/ridgeback_franka.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/ridgeback_franka.py @@ -12,8 +12,6 @@ Reference: https://github.com/ridgeback/ridgeback_manipulation """ -from __future__ import annotations - import omni.isaac.orbit.sim as sim_utils from omni.isaac.orbit.actuators import ImplicitActuatorCfg from omni.isaac.orbit.utils.assets import ISAAC_NUCLEUS_DIR diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/unitree.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/unitree.py index 8e7b20a5f0..c9a0a3c361 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/unitree.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/unitree.py @@ -12,8 +12,6 @@ Reference: https://github.com/unitreerobotics/unitree_ros """ -from __future__ import annotations - import omni.isaac.orbit.sim as sim_utils from omni.isaac.orbit.actuators import DCMotorCfg from omni.isaac.orbit.utils.assets import ISAAC_ORBIT_NUCLEUS_DIR diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/universal_robots.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/universal_robots.py index c9a3bcc168..a805914d63 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/universal_robots.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/config/universal_robots.py @@ -12,8 +12,6 @@ Reference: https://github.com/ros-industrial/universal_robot """ -from __future__ import annotations - import omni.isaac.orbit.sim as sim_utils from omni.isaac.orbit.actuators import ImplicitActuatorCfg from omni.isaac.orbit.utils.assets import ISAAC_ORBIT_NUCLEUS_DIR diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/__init__.py index ab8d0b9bc5..eabdcbd82e 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/__init__.py @@ -3,10 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause -from __future__ import annotations +"""Sub-module for rigid object assets.""" from .rigid_object import RigidObject from .rigid_object_cfg import RigidObjectCfg from .rigid_object_data import RigidObjectData - -__all__ = ["RigidObject", "RigidObjectCfg", "RigidObjectData"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object.py index 531b79f737..7871ef53a1 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object.py @@ -25,7 +25,28 @@ class RigidObject(AssetBase): - """Class for handling rigid objects.""" + """A rigid object asset class. + + Rigid objects are assets comprising of rigid bodies. They can be used to represent dynamic objects + such as boxes, spheres, etc. A rigid body is described by its pose, velocity and mass distribution. + + For an asset to be considered a rigid object, the root prim of the asset must have the `USD RigidBodyAPI`_ + applied to it. This API is used to define the simulation properties of the rigid body. On playing the + simulation, the physics engine will automatically register the rigid body and create a corresponding + rigid body handle. This handle can be accessed using the :attr:`root_physx_view` and :attr:`body_physx_view` + attributes. For a single rigid body asset, the :attr:`root_physx_view` and :attr:`body_physx_view` attributes + are the same. However, these are different for articulated assets as explained in the :class:`Articulation` + class. + + .. note:: + + For users familiar with Isaac Sim, they can use the :attr:`root_view` and :attr:`body_view` attributes + to access the rigid body views. These views are wrappers around the PhysX rigid body handles. However, + for advanced users who have a deep understanding of PhysX SDK and TensorAPI, they can use the + :attr:`root_physx_view` and :attr:`body_physx_view` attributes to access the rigid body handles directly. + + .. _`USD RigidBodyAPI`: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html + """ cfg: RigidObjectCfg """Configuration instance for the rigid object.""" @@ -137,9 +158,11 @@ def update(self, dt: float): def find_bodies(self, name_keys: str | Sequence[str]) -> tuple[list[int], list[str]]: """Find bodies in the articulation based on the name keys. + Please check the :meth:`omni.isaac.orbit.utils.string_utils.resolve_matching_names` function for more + information on the name matching. + Args: - name_keys: A regular expression or a list of regular expressions - to match the body names. + name_keys: A regular expression or a list of regular expressions to match the body names. Returns: A tuple of lists containing the body indices and names. @@ -157,8 +180,8 @@ def write_root_state_to_sim(self, root_state: torch.Tensor, env_ids: Sequence[in and angular velocity. All the quantities are in the simulation frame. Args: - root_state: Root state in simulation frame. Shape is ``(len(env_ids), 13)``. - env_ids: Environment indices. If :obj:`None`, then all indices are used. + root_state: Root state in simulation frame. Shape is (len(env_ids), 13). + env_ids: Environment indices. If None, then all indices are used. """ # set into simulation self.write_root_pose_to_sim(root_state[:, :7], env_ids=env_ids) @@ -170,8 +193,8 @@ def write_root_pose_to_sim(self, root_pose: torch.Tensor, env_ids: Sequence[int] The root pose comprises of the cartesian position and quaternion orientation in (w, x, y, z). Args: - root_pose: Root poses in simulation frame. Shape is ``(len(env_ids), 7)``. - env_ids: Environment indices. If :obj:`None`, then all indices are used. + root_pose: Root poses in simulation frame. Shape is (len(env_ids), 7). + env_ids: Environment indices. If None, then all indices are used. """ # resolve all indices physx_env_ids = env_ids @@ -191,8 +214,8 @@ def write_root_velocity_to_sim(self, root_velocity: torch.Tensor, env_ids: Seque """Set the root velocity over selected environment indices into the simulation. Args: - root_velocity: Root velocities in simulation frame. Shape is ``(len(env_ids), 6)``. - env_ids: Environment indices. If :obj:`None`, then all indices are used. + root_velocity: Root velocities in simulation frame. Shape is (len(env_ids), 6). + env_ids: Environment indices. If None, then all indices are used. """ # resolve all indices physx_env_ids = env_ids @@ -227,6 +250,7 @@ def set_external_force_and_torque( of external wrench to the simulation. .. code-block:: python + # example of disabling external wrench asset.set_external_force_and_torque(forces=torch.zeros(0, 3), torques=torch.zeros(0, 3)) @@ -235,8 +259,8 @@ def set_external_force_and_torque( the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function. Args: - forces: External forces in bodies' local frame. Shape is ``(len(env_ids), len(body_ids), 3)``. - torques: External torques in bodies' local frame. Shape is ``(len(env_ids), len(body_ids), 3)``. + forces: External forces in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). + torques: External torques in bodies' local frame. Shape is (len(env_ids), len(body_ids), 3). body_ids: Body indices to apply external wrench to. Defaults to None (all bodies). env_ids: Environment indices to apply external wrench to. Defaults to None (all instances). """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_cfg.py index 100873f8f1..190ce46296 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_cfg.py @@ -15,8 +15,6 @@ class RigidObjectCfg(AssetBaseCfg): """Configuration parameters for a rigid object.""" - class_type: type = RigidObject - @configclass class InitialStateCfg(AssetBaseCfg.InitialStateCfg): """Initial state of the rigid body.""" @@ -30,5 +28,7 @@ class InitialStateCfg(AssetBaseCfg.InitialStateCfg): # Initialize configurations. ## + class_type: type = RigidObject + init_state: InitialStateCfg = InitialStateCfg() """Initial state of the rigid object. Defaults to identity pose with zero velocity.""" diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_data.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_data.py index c45ceb7433..179e00ba78 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_data.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/assets/rigid_object/rigid_object_data.py @@ -11,7 +11,7 @@ @dataclass class RigidObjectData: - """Data container for a robot.""" + """Data container for a rigid object.""" ## # Properties. @@ -25,23 +25,23 @@ class RigidObjectData: ## default_root_state: torch.Tensor = None - """Default root state ``[pos, quat, lin_vel, ang_vel]`` in local environment frame. Shape is ``(count, 13)``.""" + """Default root state ``[pos, quat, lin_vel, ang_vel]`` in local environment frame. Shape is (count, 13).""" ## # Frame states. ## root_state_w: torch.Tensor = None - """Root state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is ``(count, 13)``.""" + """Root state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is (count, 13).""" root_vel_b: torch.Tensor = None - """Root velocity `[lin_vel, ang_vel]` in base frame. Shape is ``(count, 6)``.""" + """Root velocity `[lin_vel, ang_vel]` in base frame. Shape is (count, 6).""" projected_gravity_b: torch.Tensor = None - """Projection of the gravity direction on base frame. Shape is ``(count, 3)``.""" + """Projection of the gravity direction on base frame. Shape is (count, 3).""" heading_w: torch.Tensor = None - """Yaw heading of the base frame (in radians). Shape is ``(count,)``. + """Yaw heading of the base frame (in radians). Shape is (count,). Note: This quantity is computed by assuming that the forward-direction of the base @@ -50,10 +50,10 @@ class RigidObjectData: body_state_w: torch.Tensor = None """State of all bodies `[pos, quat, lin_vel, ang_vel]` in simulation world frame. - Shape is ``(count, num_bodies, 13)``.""" + Shape is (count, num_bodies, 13).""" body_acc_w: torch.Tensor = None - """Acceleration of all bodies. Shape is ``(count, num_bodies, 6)``. + """Acceleration of all bodies. Shape is (count, num_bodies, 6). Note: This quantity is computed based on the rigid body state from the last step. @@ -65,70 +65,70 @@ class RigidObjectData: @property def root_pos_w(self) -> torch.Tensor: - """Root position in simulation world frame. Shape is ``(count, 3)``.""" + """Root position in simulation world frame. Shape is (count, 3).""" return self.root_state_w[:, :3] @property def root_quat_w(self) -> torch.Tensor: - """Root orientation (w, x, y, z) in simulation world frame. Shape is ``(count, 4)``.""" + """Root orientation (w, x, y, z) in simulation world frame. Shape is (count, 4).""" return self.root_state_w[:, 3:7] @property def root_vel_w(self) -> torch.Tensor: - """Root velocity in simulation world frame. Shape is ``(count, 6)``.""" + """Root velocity in simulation world frame. Shape is (count, 6).""" return self.root_state_w[:, 7:13] @property def root_lin_vel_w(self) -> torch.Tensor: - """Root linear velocity in simulation world frame. Shape is ``(count, 3)``.""" + """Root linear velocity in simulation world frame. Shape is (count, 3).""" return self.root_state_w[:, 7:10] @property def root_ang_vel_w(self) -> torch.Tensor: - """Root angular velocity in simulation world frame. Shape is ``(count, 3)``.""" + """Root angular velocity in simulation world frame. Shape is (count, 3).""" return self.root_state_w[:, 10:13] @property def root_lin_vel_b(self) -> torch.Tensor: - """Root linear velocity in base frame. Shape is ``(count, 3)``.""" + """Root linear velocity in base frame. Shape is (count, 3).""" return self.root_vel_b[:, 0:3] @property def root_ang_vel_b(self) -> torch.Tensor: - """Root angular velocity in base world frame. Shape is ``(count, 3)``.""" + """Root angular velocity in base world frame. Shape is (count, 3).""" return self.root_vel_b[:, 3:6] @property def body_pos_w(self) -> torch.Tensor: - """Positions of all bodies in simulation world frame. Shape is ``(count, num_bodies, 3)``.""" + """Positions of all bodies in simulation world frame. Shape is (count, num_bodies, 3).""" return self.body_state_w[..., :3] @property def body_quat_w(self) -> torch.Tensor: - """Orientation (w, x, y, z) of all bodies in simulation world frame. Shape is ``(count, num_bodies, 4)``.""" + """Orientation (w, x, y, z) of all bodies in simulation world frame. Shape is (count, num_bodies, 4).""" return self.body_state_w[..., 3:7] @property def body_vel_w(self) -> torch.Tensor: - """Velocity of all bodies in simulation world frame. Shape is ``(count, num_bodies, 6)``.""" + """Velocity of all bodies in simulation world frame. Shape is (count, num_bodies, 6).""" return self.body_state_w[..., 7:13] @property def body_lin_vel_w(self) -> torch.Tensor: - """Linear velocity of all bodies in simulation world frame. Shape is ``(count, num_bodies, 3)``.""" + """Linear velocity of all bodies in simulation world frame. Shape is (count, num_bodies, 3).""" return self.body_state_w[..., 7:10] @property def body_ang_vel_w(self) -> torch.Tensor: - """Angular velocity of all bodies in simulation world frame. Shape is ``(count, num_bodies, 3)``.""" + """Angular velocity of all bodies in simulation world frame. Shape is (count, num_bodies, 3).""" return self.body_state_w[..., 10:13] @property def body_lin_acc_w(self) -> torch.Tensor: - """Linear acceleration of all bodies in simulation world frame. Shape is ``(count, num_bodies, 3)``.""" + """Linear acceleration of all bodies in simulation world frame. Shape is (count, num_bodies, 3).""" return self.body_acc_w[..., 0:3] @property def body_ang_acc_w(self) -> torch.Tensor: - """Angular acceleration of all bodies in simulation world frame. Shape is ``(count, num_bodies, 3)``.""" + """Angular acceleration of all bodies in simulation world frame. Shape is (count, num_bodies, 3).""" return self.body_acc_w[..., 3:6] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/command_generators/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/command_generators/__init__.py index 88cda6ca2b..6225751997 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/command_generators/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/command_generators/__init__.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This submodule provides command generators for goal-conditioned tasks. +"""Sub-package for different command generators implementations. The command generators are used to generate commands for the agent to execute. The command generators act as utility classes to make it convenient to switch between different command generation strategies within @@ -30,22 +29,3 @@ from .pose_command_generator import UniformPoseCommandGenerator from .position_command_generator import TerrainBasedPositionCommandGenerator from .velocity_command_generator import NormalVelocityCommandGenerator, UniformVelocityCommandGenerator - -__all__ = [ - "CommandGeneratorBase", - "CommandGeneratorBaseCfg", - # pose command generators - "UniformPoseCommandGenerator", - "UniformPoseCommandGeneratorCfg", - # null command generator - "NullCommandGenerator", - "NullCommandGeneratorCfg", - # velocity command generators - "UniformVelocityCommandGenerator", - "UniformVelocityCommandGeneratorCfg", - "NormalVelocityCommandGenerator", - "NormalVelocityCommandGeneratorCfg", - # position command generators - "TerrainBasedPositionCommandGenerator", - "TerrainBasedPositionCommandGeneratorCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/__init__.py index b5a51aae51..2c84885c89 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/__init__.py @@ -3,12 +3,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This submodule contains code from previous release of Orbit that is kept for compatibility reasons. +"""Sub-package containing compatibility code with previous release of Orbit. .. note:: - This module is not part of the public API and may be removed in future releases. + This package is not part of the public API and may be removed in future releases. """ - -from __future__ import annotations diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/__init__.py deleted file mode 100644 index 3ee10ca142..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -""" -Module containing different actuator groups. - -- **default**: Direct control over the DOFs handled by the actuator group. -- **mimic**: Mimics given commands into each DOFs handled by actuator group. -- **non-holonomic**: Adds a 2D kinematics skid-steering constraint for the actuator group. -""" - -from __future__ import annotations - -from .actuator_control_cfg import ActuatorControlCfg -from .actuator_group import ActuatorGroup -from .actuator_group_cfg import ActuatorGroupCfg, GripperActuatorGroupCfg, NonHolonomicKinematicsGroupCfg - -__all__ = [ - # control - "ActuatorControlCfg", - # default - "ActuatorGroupCfg", - "ActuatorGroup", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_control_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_control_cfg.py deleted file mode 100644 index 7f55f8095d..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_control_cfg.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from dataclasses import MISSING - -from omni.isaac.orbit.utils import configclass - - -@configclass -class ActuatorControlCfg: - """Configuration for the joint-level controller used by the group. - - This configuration is used by the ActuatorGroup class to configure the commands types and their - respective scalings and offsets to apply on the input actions over the actuator group. If the - scales and offsets are set to :obj:`None`, then no scaling or offset is applied on the commands. - - Depending on the actuator model type, the gains are set either into the simulator (implicit) or to - the actuator model class (explicit). - - The command types are processed as a list of strings. Each string has two sub-strings joined by - underscore: - - - type of command mode: "p" (position), "v" (velocity), "t" (torque) - - type of command resolving: "abs" (absolute), "rel" (relative) - - For instance, the command type "p_abs" defines a position command in absolute mode, while "v_rel" - defines a velocity command in relative mode. For more information on the command types, see the - documentation of the :class:`ActuatorGroup` class. - """ - - command_types: list[str] = MISSING - """ - Command types applied on the DOF in the group. - - Note: - The first string in the list defines the type of DOF drive mode in simulation. It must contain either - "p" (position-controlled), "v" (velocity-controlled), or "t" (torque-controlled). - """ - - stiffness: dict[str, float] | None = None - """ - Stiffness gains of the DOFs in the group. Defaults to :obj:`None`. - - If :obj:`None`, these are loaded from the articulation prim. - """ - - damping: dict[str, float] | None = None - """ - Damping gains of the DOFs in the group. Defaults to :obj:`None`. - - If :obj:`None`, these are loaded from the articulation prim. - """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group.py deleted file mode 100644 index 8d4f05af88..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group.py +++ /dev/null @@ -1,458 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import re -import torch -from typing import Sequence - -from omni.isaac.core.articulations import ArticulationView -from omni.isaac.core.utils.types import ArticulationActions - -from omni.isaac.orbit.actuators.group.actuator_group_cfg import ActuatorGroupCfg -from omni.isaac.orbit.actuators.model import * # noqa: F403, F401 -from omni.isaac.orbit.actuators.model import DCMotor, IdealActuator, ImplicitActuatorCfg - - -class ActuatorGroup: - """A class for applying actuator models over a collection of actuated joints in an articulation. - - The default actuator group for applying the same actuator model over a collection of actuated joints in - an articulation. It is possible to specify multiple joint-level command types (position, velocity or - torque control). - - The joint names are specified in the configuration through a list of regular expressions. The regular - expressions are matched against the joint names in the articulation. The first match is used to determine - the joint indices in the articulation. - - The command types are applied in the order they are specified in the configuration. For each command, the - scaling and offset can be configured through the :class:`ActuatorControlCfg` class. - - In the default actuator group, no constraints or formatting is performed over the input actions. Thus, the - input actions are directly used to compute the joint actions in the :meth:`compute`. - """ - - cfg: ActuatorGroupCfg - """The configuration of the actuator group.""" - view: ArticulationView - """The simulation articulation view.""" - num_articulation: int - """Number of articulations in the view.""" - device: str - """Device used for processing.""" - dof_names: list[str] - """Articulation's DOF names that are part of the group.""" - dof_indices: list[int] - """Articulation's DOF indices that are part of the group.""" - model: IdealActuator | None - """Actuator model used by the group.""" - - def __init__(self, cfg: ActuatorGroupCfg, view: ArticulationView): - """Initialize the actuator group. - - Args: - cfg: The configuration of the actuator group. - view: The simulation articulation view. - - Raises: - ValueError: When no command types are specified in the configuration. - RuntimeError: When the articulation view is not initialized. - ValueError: When not able to find a match for all DOF names in the configuration. - ValueError: When the actuator model configuration is invalid, i.e. not "explicit" or "implicit". - """ - # check valid inputs - if len(cfg.control_cfg.command_types) < 1: - raise ValueError("Each actuator group must have at least one command type. Received: 0.") - if not view.initialized: - raise RuntimeError("Actuator group expects an initialized articulation view.") - # save parameters - self.cfg = cfg - self.view = view - # extract useful quantities - self.num_articulation = self.view.count - self.device = self.view._device - - # TODO (@mayank): Make regex resolving safer by throwing errors for keys that didn't find a match. - # -- from articulation dof names - self.dof_names = list() - self.dof_indices = list() - for dof_name in self.view.dof_names: - # dof-names and indices - for re_key in self.cfg.dof_names: - if re.fullmatch(re_key, dof_name): - # add to group details - self.dof_names.append(dof_name) - self.dof_indices.append(self.view.get_dof_index(dof_name)) - # check that group is valid - if len(self.dof_names) == 0: - raise ValueError(f"Unable to find any joints associated with actuator group. Input: {self.cfg.dof_names}.") - - # process configuration - # -- create actuator model - if self.model_type == "explicit": - actuator_model_cls = eval(self.cfg.model_cfg.class_type) - self.model = actuator_model_cls( - cfg=self.cfg.model_cfg, - num_actuators=self.num_actuators, - num_envs=self.num_articulation, - device=self.device, - ) - elif self.model_type == "implicit": - self.model = None - else: - raise ValueError( - f"Invalid articulation model type. Received: '{self.model_type}'. Expected: 'explicit' or 'implicit'." - ) - # -- action transforms - self._process_action_transforms_cfg() - # -- gains - self._process_actuator_gains_cfg() - # -- torque limits - self._process_actuator_torque_limit_cfg() - # -- control mode - self.view.switch_control_mode(self.control_mode, joint_indices=self.dof_indices) - - # create buffers for allocation - # -- state - self._dof_pos = torch.zeros(self.num_articulation, self.num_actuators, device=self.device) - self._dof_vel = torch.zeros_like(self._dof_pos) - # -- commands - self._computed_torques = torch.zeros_like(self._dof_pos) - self._applied_torques = torch.zeros_like(self._dof_pos) - self._gear_ratio = torch.ones_like(self._dof_pos) - - def __str__(self) -> str: - """A string representation of the actuator group.""" - return ( - " object:\n" - f"\tNumber of DOFs: {self.num_actuators}\n" - f"\tDOF names : {self.dof_names}\n" - f"\tDOF indices : {self.dof_indices}\n" - f"\tActuator model: {self.model_type}\n" - f"\tCommand types : {self.command_types}\n" - f"\tControl mode : {self.control_mode}\n" - f"\tControl dim : {self.control_dim}" - ) - - """ - Properties- Group. - """ - - @property - def num_actuators(self) -> int: - """Number of actuators in the group.""" - return len(self.dof_names) - - @property - def model_type(self) -> str: - """Type of actuator model: "explicit" or "implicit". - - - **explicit**: Computes joint torques to apply into the simulation. - - **implicit**: Uses the in-built PhysX joint controller. - """ - return self.cfg.model_cfg.model_type - - @property - def command_types(self) -> list[str]: - """Command type applied on the DOF in the group. - - It contains a list of strings. Each string has two sub-strings joined by underscore: - - type of command mode: "p" (position), "v" (velocity), "t" (torque) - - type of command resolving: "abs" (absolute), "rel" (relative) - """ - return self.cfg.control_cfg.command_types - - @property - def control_dim(self) -> int: - """Dimension of control actions. - - The number of control actions is a product of number of actuated joints in the group and the - number of command types provided in the control confgiruation. - """ - return self.num_actuators * len(self.command_types) - - @property - def control_mode(self) -> str: - """Simulation drive control mode. - - For explicit actuator models, the control mode is always "effort". For implicit actuators, the - control mode is prioritized by the first command type in the control configuration. It is either: - - - "position" (position-controlled) - - "velocity" (velocity-controlled) - - "effort" (torque-controlled). - - """ - if self.model_type == "explicit": - return "effort" - elif self.model_type == "implicit": - # get first command type. - drive_type = self.command_types[0] - # check valid drive type - if "p" in drive_type: - return "position" - elif "v" in drive_type: - return "velocity" - elif "t" in drive_type: - return "effort" - else: - raise ValueError(f"Invalid primary control mode '{drive_type}'. Expected substring: 'p', 'v', 't'.") - else: - raise ValueError( - f"Invalid actuator model in group '{self.model_type}'. Expected: 'explicit' or 'implicit'." - ) - - """ - Properties- Actuator Model. - """ - - @property - def gear_ratio(self) -> torch.Tensor: - """Gear-box conversion factor from motor axis to joint axis.""" - return self._gear_ratio - - @property - def computed_torques(self) -> torch.Tensor: - """DOF torques computed from the actuator model (before clipping). - - Note: The torques are zero for implicit actuator models. - """ - return self._computed_torques - - @property - def applied_torques(self) -> torch.Tensor: - """DOF torques applied from the actuator model to simulation (after clipping). - - Note: The torques are zero for implicit actuator models. - """ - return self._applied_torques - - @property - def velocity_limit(self) -> torch.Tensor | None: - """DOF velocity limits from actuator. - - Returns :obj:`None` for implicit and ideal actuator. - """ - if isinstance(self.model, DCMotor): - return self.model.cfg.motor_velocity_limit / self.model.gear_ratio - else: - return None - - """ - Operations. - """ - - def reset(self, env_ids: Sequence[int]): - """Reset the internals within the group. - - Args: - env_ids: List of environment IDs to reset. - """ - # actuator - if self.model is not None: - self.model.reset(env_ids) - - def compute(self, group_actions: torch.Tensor, dof_pos: torch.Tensor, dof_vel: torch.Tensor) -> ArticulationActions: - """Process the actuator group actions and compute the articulation actions. - - It performs the following operations: - - 1. formats the input actions to apply any extra constraints - 2. splits the formatted actions into individual tensors corresponding to each command type - 3. applies offset and scaling to individual commands based on absolute or relative command - 4. computes the articulation actions based on the actuator model type - - Args: - group_actions: The actuator group actions of shape (num_articulation, control_dim). - dof_pos: The current joint positions of the joints in the group. - dof_vel: The current joint velocities of the joints in the group. - - Raises: - ValueError: When the group actions has the wrong shape. Expected shape: (num_articulation, control_dim). - ValueError: Invalid command type. Valid: 'p_abs', 'p_rel', 'v_abs', 'v_rel', 't_abs'. - ValueError: Invalid actuator model type in group. Expected: 'explicit' or 'implicit'. - - Returns: - An instance of the articulation actions. - a. for explicit actuator models, it returns the computed joint torques (after clipping) - b. for implicit actuator models, it returns the actions with the processed desired joint - positions, velocities, and efforts (based on the command types) - """ - # check that group actions has the right dimension - control_shape = (self.num_articulation, self.control_dim) - if tuple(group_actions.shape) != control_shape: - raise ValueError( - f"Invalid actuator group actions shape '{group_actions.shape}'. Expected: '{control_shape}'." - ) - # store current dof state - self._dof_pos[:] = dof_pos - self._dof_vel[:] = dof_vel - # buffers for applying actions. - group_des_dof_pos = None - group_des_dof_vel = None - group_des_dof_torque = None - # pre-processing of commands based on group. - group_actions = self._format_command(group_actions) - # reshape actions for sub-groups - group_actions = group_actions.split([self.num_actuators] * len(self.command_types), dim=-1) - # pre-process relative commands - for command_type, command_value in zip(self.command_types, group_actions): - if command_type == "p_rel": - group_des_dof_pos = self.dof_pos_scale * command_value + self._dof_pos - elif command_type == "p_abs": - group_des_dof_pos = self.dof_pos_scale * command_value + self.dof_pos_offset - elif command_type == "v_rel": - group_des_dof_vel = self.dof_vel_scale * command_value + self._dof_vel - elif command_type == "v_abs": - group_des_dof_vel = self.dof_vel_scale * command_value # offset = 0 - elif command_type == "t_abs": - group_des_dof_torque = self.dof_torque_scale * command_value # offset = 0 - else: - raise ValueError(f"Invalid action command type for actuators: '{command_type}'.") - - # process commands based on actuators - if self.model_type == "explicit": - # assert that model is created - assert self.model is not None, "Actuator model is not created." - # update state and command - self.model.set_command(dof_pos=group_des_dof_pos, dof_vel=group_des_dof_vel, torque_ff=group_des_dof_torque) - # compute torques - self._computed_torques = self.model.compute_torque(dof_pos=self._dof_pos, dof_vel=self._dof_vel) - self._applied_torques = self.model.clip_torques( - self._computed_torques, dof_pos=self._dof_pos, dof_vel=self._dof_vel - ) - # store updated quantities - self._gear_ratio[:] = self.model.gear_ratio - # wrap into isaac-sim command - control_action = ArticulationActions(joint_efforts=self._applied_torques, joint_indices=self.dof_indices) - elif self.model_type == "implicit": - # wrap into isaac-sim command - control_action = ArticulationActions( - joint_positions=group_des_dof_pos, - joint_velocities=group_des_dof_vel, - joint_efforts=group_des_dof_torque, - joint_indices=self.dof_indices, - ) - else: - raise ValueError( - f"Invalid articulation model type. Received: '{self.model_type}'. Expected: 'explicit' or 'implicit'." - ) - # return the computed actions - return control_action - - """ - Implementation specifics. - """ - - def _format_command(self, command: torch.Tensor) -> torch.Tensor: - """Formatting of commands given to the group. - - In the default actuator group, an identity mapping is performed between the input - commands and the joint commands. - - Returns: - Desired commands for the DOFs in the group. - Shape is ``(num_articulation, num_actuators * len(command_types))``. - """ - return command - - """ - Helper functions. - """ - - def _process_action_transforms_cfg(self): - """Resolve the action scaling and offsets for different command types.""" - # default values - # -- scale - self.dof_pos_scale = torch.ones(self.num_actuators, device=self.device) - self.dof_vel_scale = torch.ones(self.num_actuators, device=self.device) - self.dof_torque_scale = torch.ones(self.num_actuators, device=self.device) - # -- offset - self.dof_pos_offset = torch.zeros(self.num_actuators, device=self.device) - # parse configuration - for index, dof_name in enumerate(self.dof_names): - # dof pos scale - if self.cfg.control_cfg.dof_pos_scale is not None: - for re_key, value in self.cfg.control_cfg.dof_pos_scale.items(): - if re.fullmatch(re_key, dof_name): - self.dof_pos_scale[index] = value - # dof vel scale - if self.cfg.control_cfg.dof_vel_scale is not None: - for re_key, value in self.cfg.control_cfg.dof_vel_scale.items(): - if re.fullmatch(re_key, dof_name): - self.dof_vel_scale[index] = value - # dof torque scale - if self.cfg.control_cfg.dof_torque_scale is not None: - for re_key, value in self.cfg.control_cfg.dof_torque_scale.items(): - if re.fullmatch(re_key, dof_name): - self.dof_torque_scale[index] = value - # dof pos offset - if self.cfg.control_cfg.dof_pos_offset is not None: - for re_key, value in self.cfg.control_cfg.dof_pos_offset.items(): - if re.fullmatch(re_key, dof_name): - self.dof_pos_offset[index] = value - - def _process_actuator_gains_cfg(self): - """Resolve the PD gains for the actuators and set them into actuator model. - - If the actuator model is implicit, then the gains are applied into the simulator. - If the actuator model is explicit, then the gains are applied into the actuator model. - """ - # get the default values from simulator/USD - stiffness, damping = self.view.get_gains(joint_indices=self.dof_indices, clone=False) - # parse configuration - for index, dof_name in enumerate(self.dof_names): - # stiffness - if self.cfg.control_cfg.stiffness is not None: - for re_key, value in self.cfg.control_cfg.stiffness.items(): - if re.fullmatch(re_key, dof_name): - if value is not None: - stiffness[:, index] = value - # damping - if self.cfg.control_cfg.damping is not None: - for re_key, value in self.cfg.control_cfg.damping.items(): - if re.fullmatch(re_key, dof_name): - if value is not None: - damping[:, index] = value - # set values into model - if self.model_type == "explicit": - # assert that model is created - assert self.model is not None, "Actuator model is not created." - # set values into the explicit models - self.model.set_command(p_gains=stiffness, d_gains=damping) - elif self.model_type == "implicit": - # set gains into simulation (implicit) - self.view.set_gains(kps=stiffness, kds=damping, joint_indices=self.dof_indices) - else: - raise ValueError( - f"Invalid articulation model type. Received: '{self.model_type}'. Expected: 'explicit' or 'implicit'." - ) - - def _process_actuator_torque_limit_cfg(self): - """Process the torque limit of the actuators and set them into simulation. - - If the actuator model is implicit, then the torque limits are applied into the simulator based on the parsed - model configuration. If the actuator model is explicit, the torque limits are set to high values since the - torque range is handled by the saturation model. - """ - # get the default values from simulator/USD - torque_limit = self.view.get_max_efforts(joint_indices=self.dof_indices, clone=False) - # parse configuration - if self.model_type == "explicit": - # for explicit actuators, clipping is handled by the forward model. - # so we set this high for PhysX to not do anything. - torque_limit[:] = 1.0e9 - elif self.model_type == "implicit": - # for implicit actuators, we let PhysX handle the actuator limits. - if isinstance(self.cfg.model_cfg, ImplicitActuatorCfg): - if self.cfg.model_cfg.torque_limit is not None: - torque_limit[:] = self.cfg.model_cfg.torque_limit - else: - raise ValueError( - f"Invalid articulation model type. Received: '{self.model_type}'. Expected: 'explicit' or 'implicit'." - ) - # set values into simulator - self.view.set_max_efforts(torque_limit, joint_indices=self.dof_indices) diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group_cfg.py deleted file mode 100644 index c8f2c55ec6..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/actuator_group_cfg.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -""" -This submodule contains the configuration for the ActuatorGroup classes. - -Currently, the following configuration classes are supported: - -* ActuatorGroupCfg: standard actuator group with joint-level controllers -* GripperActuatorGroupCfg: actuator group for grippers (mimics transmission across joints) -* NonHolonomicKinematicsGroupCfg: actuator group for a non-holonomic kinematic constraint - -""" - -from __future__ import annotations - -from dataclasses import MISSING -from typing import ClassVar - -from omni.isaac.orbit.actuators.model.actuator_cfg import BaseActuatorCfg -from omni.isaac.orbit.utils import configclass - -from .actuator_control_cfg import ActuatorControlCfg - - -@configclass -class ActuatorGroupCfg: - """Configuration for default group of actuators in an articulation.""" - - class_type: ClassVar[str] = "ActuatorGroup" - """Name of the associated actuator group class. Used to construct the actuator group.""" - - dof_names: list[str] = MISSING - """Articulation's DOF names that are part of the group.""" - - model_cfg: BaseActuatorCfg = MISSING - """Actuator model configuration used by the group.""" - - control_cfg: ActuatorControlCfg = MISSING - """Actuator control configuration used by the group.""" - - -@configclass -class GripperActuatorGroupCfg(ActuatorGroupCfg): - """Configuration for mimicking actuators in a gripper.""" - - class_type: ClassVar[str] = "GripperActuatorGroup" - - speed: float = MISSING - """The speed at which gripper should close. (used with velocity command type.)""" - - mimic_multiplier: dict[str, float] = MISSING - """ - Mapping of command from DOF names to command axis [-1, 1]. - - The first joint in the dictionary is considered the joint to mimic. - - For convention purposes: - - - :obj:`+1` -> opening direction - - :obj:`-1` -> closing direction - """ - - open_dof_pos: float = MISSING - """The DOF position at *open* configuration. (used with position command type.)""" - - close_dof_pos: float = MISSING - """The DOF position at *close* configuration. (used with position command type.)""" - - -@configclass -class NonHolonomicKinematicsGroupCfg(ActuatorGroupCfg): - """Configuration for formulating non-holonomic base constraint.""" - - class_type: ClassVar[str] = "NonHolonomicKinematicsGroup" diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/gripper_group.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/gripper_group.py deleted file mode 100644 index e0ece92d47..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/gripper_group.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import re -import torch -from typing import Sequence - -from omni.isaac.core.articulations import ArticulationView - -from .actuator_group import ActuatorGroup -from .actuator_group_cfg import GripperActuatorGroupCfg - - -class GripperActuatorGroup(ActuatorGroup): - """ - A mimicking actuator group to format a binary open/close command into the joint commands. - - The input actions are processed as a scalar which sign determines whether to open or close the gripper. - We consider the following convention: - - 1. Positive value (> 0): open command - 2. Negative value (< 0): close command - - The mimicking actuator group has only two valid command types: absolute positions (``"p_abs"``) or absolute - velocity (``"v_abs"``). Based on the chosen command type, the joint commands are computed by multiplying the - reference command with the mimicking multiplier. - - * **position mode:** The reference command is resolved as the joint position target for opening or closing the - gripper. These targets are read from the :class:`GripperActuatorGroupCfg` class. - * **velocity mode:** The reference command is resolved as the joint velocity target based on the configured speed - of the gripper. The reference commands are added to the previous command and clipped to the range of (-1.0, 1.0). - - Tip: - In general, we recommend using the velocity mode, as it simulates the delay in opening or closing the gripper. - - """ - - cfg: GripperActuatorGroupCfg - """The configuration of the actuator group.""" - - def __init__(self, cfg: GripperActuatorGroupCfg, view: ArticulationView): - """Initialize the actuator group. - - Args: - cfg: The configuration of the actuator group. - view: The simulation articulation view. - - Raises: - ValueError: When command type is not "p_abs" or "v_abs". - RuntimeError: When the articulation view is not initialized. - ValueError: When not able to find a match for all DOF names in the configuration. - ValueError: When the actuator model configuration is invalid, i.e. not "explicit" or "implicit". - """ - # check valid config - if cfg.control_cfg.command_types[0] not in ["p_abs", "v_abs"] or len(cfg.control_cfg.command_types) > 1: - raise ValueError(f"Gripper mimicking does not support command types: '{cfg.control_cfg.command_types}'.") - # initialize parent - super().__init__(cfg, view) - - # --from actuator dof names - self._mimic_multiplier = torch.ones(self.num_articulation, self.num_actuators, device=self.device) - for index, dof_name in enumerate(self.dof_names): - # multiplier command - for re_key, value in self.cfg.mimic_multiplier.items(): - if re.fullmatch(re_key, dof_name): - self._mimic_multiplier[:, index] = value - # -- dof positions - self._open_dof_pos = self._mimic_multiplier * self.cfg.open_dof_pos - self._close_dof_pos = self._mimic_multiplier * self.cfg.close_dof_pos - - # create buffers - self._previous_dof_targets = torch.zeros(self.num_articulation, self.num_actuators, device=self.device) - # constants - self._ALL_INDICES = torch.arange(self.view.count, device=self.view._device, dtype=torch.long) - - def __str__(self) -> str: - """String representation of the actuator group.""" - msg = super().__str__() + "\n" - msg = msg.replace("ActuatorGroup", "GripperActuatorGroup") - msg += ( - f"\tNumber of DOFs: {self.num_actuators}\n" - f"\tMimic multiply: {self._mimic_multiplier}\n" - f"\tOpen position : {self.cfg.open_dof_pos}\n" - f"\tClose position: {self.cfg.close_dof_pos}" - ) - return msg - - """ - Properties - """ - - @property - def control_dim(self) -> int: - """Dimension of control actions.""" - return 1 - - """ - Operations. - """ - - def reset(self, env_ids: Sequence[int]): - # reset super - super().reset(env_ids=env_ids) - # buffers - self._previous_dof_targets[env_ids] = 0.0 - - def _format_command(self, command: torch.Tensor) -> torch.Tensor: - """Pre-processing of the commands given to actuators. - - We consider the following convention: - - * Non-negative command (includes 0): open grippers - * Negative command: close grippers - - Returns: - The target joint commands for the gripper. - """ - # FIXME: mimic joint positions -- Gazebo plugin seems to do this. - # The following is commented out because Isaac Sim doesn't support setting joint positions - # of particular dof indices properly. It sets joint positions and joint position targets for - # the whole robot, i.e. all previous position targets is lost. This affects resetting the robot - # to a particular joint position. - # self.view._physics_sim_view.enable_warnings(False) - # # get current joint positions - # new_dof_pos = self.view._physics_view.get_dof_positions() - # # set joint positions of the mimic joints - # new_dof_pos[:, self.dof_indices] = self._dof_pos[:, 0].unsqueeze(1) * self._mimic_multiplier - # # set joint positions to the physics view - # self.view.set_joint_positions(new_dof_pos, self._ALL_INDICES) - # self.view._physics_sim_view.enable_warnings(True) - - # process actions - if self.control_mode == "velocity": - # compute new command - dof_vel_targets = torch.sign(command) * self.cfg.speed * self._mimic_multiplier - dof_vel_targets = self._previous_dof_targets[:] + dof_vel_targets - dof_vel_targets = torch.clamp(dof_vel_targets, -1.0, 1.0) - # store new command - self._previous_dof_targets[:] = dof_vel_targets - # return command - return dof_vel_targets - else: - # compute new command - dof_pos_targets = torch.where(command >= 0, self._open_dof_pos, self._close_dof_pos) - # store new command - self._previous_dof_targets[:] = dof_pos_targets - # return command - return dof_pos_targets diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/non_holonomic_group.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/non_holonomic_group.py deleted file mode 100644 index bc92eac689..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/actuators/group/non_holonomic_group.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -import torch - -from omni.isaac.core.articulations import ArticulationView - -from omni.isaac.orbit.utils.math import euler_xyz_from_quat - -from .actuator_group import ActuatorGroup -from .actuator_group_cfg import NonHolonomicKinematicsGroupCfg - - -class NonHolonomicKinematicsGroup(ActuatorGroup): - r""" - An actuator group that formulates the 2D-base constraint for vehicle kinematics control. - - In simulation, it is easier to consider the mobile base as a floating link controlled by three dummy joints - (prismatic joints along x and y, and revolute joint along z), in comparison to simulating wheels which is at times - is tricky because of friction settings. Thus, this class implements the non-holonomic kinematic constraint for - translating the input velocity command into joint velocity commands. - - A skid-steering base is under-actuated, i.e. the commands are forward velocity :math:`v_{B,x}` and the turning rate - :\omega_{B,z}: in the base frame. Using the current base orientation, the commands are transformed into dummy - joint velocity targets as: - - .. math:: - - \dot{q}_{0, des} &= v_{B,x} \cos(\theta) \\ - \dot{q}_{1, des} &= v_{B,x} \sin(\theta) \\ - \dot{q}_{2, des} &= \omega_{B,z} - - where :math:`\theta` is the yaw of the 2-D base. Since the base is simulated as a dummy joint, the yaw is directly - the value of the revolute joint along z, i.e., :math:`q_2 = \theta`. - - Tip: - For velocity control of the base with dummy mechanism, we recommend setting high damping gains to the joints. - This ensures that the base remains unperturbed from external disturbances, such as an arm mounted on the base. - - """ - - cfg: NonHolonomicKinematicsGroupCfg - """The configuration of the actuator group.""" - - def __init__(self, cfg: NonHolonomicKinematicsGroupCfg, view: ArticulationView): - """Initialize the actuator group. - - Args: - cfg: The configuration of the actuator group. - view: The simulation articulation view. - - Raises: - ValueError: When command type is not "v_abs". - RuntimeError: When the articulation view is not initialized. - ValueError: When not able to find a match for all DOF names in the configuration. - ValueError: When the actuator model configuration is invalid, i.e. not "explicit" or "implicit". - """ - # check valid config - if cfg.control_cfg.command_types[0] not in ["v_abs"] or len(cfg.control_cfg.command_types) > 1: - raise ValueError( - f"Non-holonomic kinematics group does not support command types: '{cfg.control_cfg.command_types}'." - ) - # initialize parent - super().__init__(cfg, view) - # check that the encapsulated joints are three - if self.num_actuators != 3: - raise ValueError(f"Non-holonomic kinematics group requires three joints, but got {self.num_actuators}.") - - """ - Properties - """ - - @property - def control_dim(self) -> int: - """Dimension of control actions.""" - return 2 - - """ - Operations. - """ - - def _format_command(self, command: torch.Tensor) -> torch.Tensor: - """Pre-processing of commands given to actuators. - - The input command is the base velocity and turning rate command. - - Returns: - The target joint commands for the gripper. - """ - # obtain current heading - quat_w = self.view.get_world_poses(clone=False)[1] - yaw_w = euler_xyz_from_quat(quat_w)[2] - # compute new command - dof_vel_targets = torch.zeros(self.num_articulation, 3, device=self.device) - dof_vel_targets[:, 0] = torch.cos(yaw_w) * command[:, 0] - dof_vel_targets[:, 1] = torch.sin(yaw_w) * command[:, 0] - dof_vel_targets[:, 2] = command[:, 1] - # return command - return dof_vel_targets diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/__init__.py index dd5a692129..1219df0f3a 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/__init__.py @@ -22,9 +22,5 @@ """ -from __future__ import annotations - from .point_marker import PointMarker from .static_marker import StaticMarker - -__all__ = ["StaticMarker", "PointMarker"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/point_marker.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/point_marker.py index 7468d26326..bcf32d7121 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/point_marker.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/point_marker.py @@ -137,12 +137,12 @@ def set_world_poses( Args: positions: - Positions in the world frame. Shape is (M, 3). Defaults to :obj:`None`, which means left unchanged. + Positions in the world frame. Shape is (M, 3). Defaults to None, which means left unchanged. orientations: Quaternion orientations (w, x, y, z) in the world frame of the prims. Shape is (M, 4). - Defaults to :obj:`None`, which means left unchanged. + Defaults to None, which means left unchanged. indices: Indices to specify which alter poses. - Shape is (M,), where M <= total number of markers. Defaults to :obj:`None` (i.e: all markers). + Shape is (M,), where M <= total number of markers. Defaults to None (i.e: all markers). """ # resolve inputs if positions is not None: @@ -168,7 +168,7 @@ def set_status(self, status: list[int] | np.ndarray | torch.Tensor, indices: Seq Args: status: Decides which prototype marker to visualize. Shape is (M) indices: Indices to specify which alter poses. - Shape is (M,), where M <= total number of markers. Defaults to :obj:`None` (i.e: all markers). + Shape is (M,), where M <= total number of markers. Defaults to None (i.e: all markers). """ # default values if indices is None: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/static_marker.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/static_marker.py index f50c701234..3948ce4c7f 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/static_marker.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/markers/static_marker.py @@ -151,11 +151,11 @@ def set_world_poses( """Update marker poses in the simulation world frame. Args: - positions: Positions in the world frame. Shape is (M, 3). Defaults to :obj:`None`, which means left unchanged. + positions: Positions in the world frame. Shape is (M, 3). Defaults to None, which means left unchanged. orientations: Quaternion orientations (w, x, y, z) in the world frame of the prims. Shape is (M, 4). - Defaults to :obj:`None`, which means left unchanged. - indices: Indices to specify which alter poses. Shape is ``(M,),`` where M <= total number of markers. - Defaults to :obj:`None` (i.e: all markers). + Defaults to None, which means left unchanged. + indices: Indices to specify which alter poses. Shape is (M,) where M <= total number of markers. + Defaults to None (i.e: all markers). """ # resolve inputs if positions is not None: @@ -181,7 +181,7 @@ def set_scales(self, scales: np.ndarray | torch.Tensor, indices: Sequence[int] | Args: scales: Scale applied before any rotation is applied. Shape is (M, 3). indices: Indices to specify which alter poses. - Shape is (M,), where M <= total number of markers. Defaults to :obj:`None` (i.e: all markers). + Shape is (M,), where M <= total number of markers. Defaults to None (i.e: all markers). """ # default arguments if indices is None: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/__init__.py index efcf238128..553f51d679 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/__init__.py @@ -2,5 +2,3 @@ # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/__init__.py index cedebbdeee..15014b74f6 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/__init__.py @@ -7,9 +7,5 @@ Camera wrapper around USD camera prim to provide an interface that follows the robotics convention. """ -from __future__ import annotations - from .camera import Camera, CameraData from .camera_cfg import FisheyeCameraCfg, PinholeCameraCfg - -__all__ = ["Camera", "CameraData", "PinholeCameraCfg", "FisheyeCameraCfg"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera.py index 1764eb756b..f8a854e09f 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera.py @@ -29,8 +29,6 @@ from ..sensor_base import SensorBase from .camera_cfg import FisheyeCameraCfg, PinholeCameraCfg -__all__ = ["Camera", "CameraData"] - @dataclass class CameraData: @@ -335,7 +333,7 @@ def spawn(self, parent_prim_path: str, translation: Sequence[float] = None, orie Args: parent_prim_path: The path of the parent prim to attach sensor to. translation: The local position offset w.r.t. parent prim. Defaults to None. - orientation: The local rotation offset in ``(w, x, y, z)`` w.r.t. + orientation: The local rotation offset in (w, x, y, z) w.r.t. parent prim. Defaults to None. """ # Check if sensor is already spawned @@ -368,7 +366,7 @@ def initialize(self, cam_prim_path: str = None): has_rig: Whether the passed camera prim path is attached to a rig. Defaults to False. Raises: - RuntimeError: When input `cam_prim_path` is :obj:`None`, the method defaults to using the last + RuntimeError: When input `cam_prim_path` is None, the method defaults to using the last camera prim path set when calling the :meth:`spawn` function. In case, the camera was not spawned and no valid `cam_prim_path` is provided, the function throws an error. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera_cfg.py index d7bd1394dd..ebc8e872cd 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/camera/camera_cfg.py @@ -11,8 +11,6 @@ from omni.isaac.orbit.utils import configclass -__all__ = ["PinholeCameraCfg", "FisheyeCameraCfg"] - @configclass class PinholeCameraCfg: @@ -22,7 +20,7 @@ class PinholeCameraCfg: class UsdCameraCfg: """USD related configuration of the sensor. - The parameter is kept default from USD if it is set to :obj:`None`. This includes the default + The parameter is kept default from USD if it is set to None. This includes the default parameters (in case the sensor is created) or the ones set by the user (in case the sensor is loaded from existing USD stage). diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/__init__.py index 84dfc95098..26b59f2805 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/__init__.py @@ -7,9 +7,5 @@ Height-scanner based on ray-casting operations using PhysX ray-caster. """ -from __future__ import annotations - from .height_scanner import HeightScanner, HeightScannerData from .height_scanner_cfg import HeightScannerCfg - -__all__ = ["HeightScanner", "HeightScannerData", "HeightScannerCfg"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner.py index e2f846267d..1f6532f27c 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner.py @@ -21,8 +21,6 @@ from .height_scanner_cfg import HeightScannerCfg from .height_scanner_marker import HeightScannerMarker -__all__ = ["HeightScanner", "HeightScannerData"] - @dataclass class HeightScannerData: @@ -31,15 +29,15 @@ class HeightScannerData: position: np.ndarray = None """Position of the sensor origin in world frame.""" orientation: np.ndarray = None - """Orientation of the sensor origin in quaternion ``(w, x, y, z)`` in world frame.""" + """Orientation of the sensor origin in quaternion (w, x, y, z) in world frame.""" hit_points: np.ndarray = None - """The end point locations of ray-casted rays. Shape is (N, 3), where ``N`` is + """The end point locations of ray-casted rays. Shape is (N, 3), where N is the number of scan points.""" hit_distance: np.ndarray = None - """The ray-cast travel distance from query point. Shape is (N,), where ``N`` is + """The ray-cast travel distance from query point. Shape is (N,), where N is the number of scan points.""" hit_status: np.ndarray = None - """Whether the ray hit an object or not. Shape is (N,), where ``N`` is + """Whether the ray hit an object or not. Shape is (N,), where N is the number of scan points. It is set to ``1`` if the ray hit an object, and ``0`` otherwise. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner_marker.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner_marker.py index b580449f32..f3d25f112f 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner_marker.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/height_scanner_marker.py @@ -115,12 +115,12 @@ def set_world_poses( Args: positions: - Positions in the world frame. Shape is (M, 3). Defaults to :obj:`None`, which means left unchanged. + Positions in the world frame. Shape is (M, 3). Defaults to None, which means left unchanged. orientations: Quaternion orientations (w, x, y, z) in the world frame of the prims. Shape is (M, 4). - Defaults to :obj:`None`, which means left unchanged. + Defaults to None, which means left unchanged. indices: Indices to specify which alter poses. - Shape is (M,), where M <= total number of markers. Defaults to :obj:`None` (i.e: all markers). + Shape is (M,), where M <= total number of markers. Defaults to None (i.e: all markers). """ # resolve inputs if positions is not None: @@ -146,7 +146,7 @@ def set_status(self, status: list[int] | np.ndarray | torch.Tensor, indices: Seq Args: status: Decides which prototype marker to visualize. Shape is (M) indices: Indices to specify which alter poses. Shape is (M,), where M <= total number of markers. - Defaults to :obj:`None` (i.e: all markers). + Defaults to None (i.e: all markers). """ # default values if indices is None: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/utils.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/utils.py index 7143558ef9..c8db85f2af 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/utils.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/sensors/height_scanner/utils.py @@ -12,8 +12,6 @@ from matplotlib.axes import Axes from matplotlib.image import AxesImage -__all__ = ["create_points_from_grid", "plot_height_grid"] - def create_points_from_grid(size: tuple[float, float], resolution: float) -> np.ndarray: """Creates a list of points from 2D mesh-grid. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/__init__.py index d7bdfa02e3..553f51d679 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/__init__.py @@ -2,12 +2,3 @@ # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause - -from __future__ import annotations - -from .configclass import configclass - -__all__ = [ - # config wrapper - "configclass", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/configclass.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/configclass.py deleted file mode 100644 index e0576c1e0b..0000000000 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/compat/utils/configclass.py +++ /dev/null @@ -1,226 +0,0 @@ -# Copyright (c) 2022-2023, The ORBIT Project Developers. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause - -"""Wrapper around the Python 3.7 onwards `dataclasses` module.""" - -from __future__ import annotations - -from copy import deepcopy -from dataclasses import Field, dataclass, field -from typing import Any, Callable, ClassVar - -from omni.isaac.orbit.utils.dict import class_to_dict, update_class_from_dict - -# List of all methods provided by sub-module. -__all__ = ["configclass"] - - -""" -Wrapper around dataclass. -""" - - -def __dataclass_transform__(): - """Add annotations decorator for PyLance.""" - return lambda a: a - - -@__dataclass_transform__() -def configclass(cls, **kwargs): - """Wrapper around `dataclass` functionality to add extra checks and utilities. - - As of Python3.8, the standard dataclasses have two main issues which makes them non-generic for configuration use-cases. - These include: - - 1. Requiring a type annotation for all its members. - 2. Requiring explicit usage of :meth:`field(default_factory=...)` to reinitialize mutable variables. - - This function wraps around :class:`dataclass` utility to deal with the above two issues. - - Usage: - .. code-block:: python - - from dataclasses import MISSING - - from omni.isaac.orbit.utils.configclass import configclass - - - @configclass - class ViewerCfg: - eye: list = [7.5, 7.5, 7.5] # field missing on purpose - lookat: list = field(default_factory=[0.0, 0.0, 0.0]) - - - @configclass - class EnvCfg: - num_envs: int = MISSING - episode_length: int = 2000 - viewer: ViewerCfg = ViewerCfg() - - # create configuration instance - env_cfg = EnvCfg(num_envs=24) - # print information - print(env_cfg.to_dict()) - - Reference: - https://docs.python.org/3/library/dataclasses.html#dataclasses.Field - """ - # add type annotations - _add_annotation_types(cls) - # add field factory - _process_mutable_types(cls) - # copy mutable members - setattr(cls, "__post_init__", _custom_post_init) - # add helper functions for dictionary conversion - setattr(cls, "to_dict", _class_to_dict) - setattr(cls, "from_dict", _update_class_from_dict) - # wrap around dataclass - cls = dataclass(cls, **kwargs) - # return wrapped class - return cls - - -""" -Dictionary <-> Class operations. - -These are redefined here to add new docstrings. -""" - - -def _class_to_dict(obj: object) -> dict[str, Any]: - """Convert an object into dictionary recursively. - - Returns: - Converted dictionary mapping. - """ - return class_to_dict(obj) - - -def _update_class_from_dict(obj, data: dict[str, Any]) -> None: - """Reads a dictionary and sets object variables recursively. - - This function performs in-place update of the class member attributes. - - Args: - data: Input (nested) dictionary to update from. - - Raises: - TypeError: When input is not a dictionary. - ValueError: When dictionary has a value that does not match default config type. - KeyError: When dictionary has a key that does not exist in the default config type. - """ - return update_class_from_dict(obj, data, _ns="") - - -""" -Private helper functions. -""" - - -def _add_annotation_types(cls): - """Add annotations to all elements in the dataclass. - - By definition in Python, a field is defined as a class variable that has a type annotation. - - In case type annotations are not provided, dataclass ignores those members when :func:`__dict__()` is called. - This function adds these annotations to the class variable to prevent any issues in case the user forgets to - specify the type annotation. - - This makes the following a feasible operation: - - @dataclass - class State: - pos = (0.0, 0.0, 0.0) - ^^ - If the function is NOT used, the following type-error is returned: - TypeError: 'pos' is a field but has no type annotation - """ - # Note: Do not change this line. `cls.__dict__.get("__annotations__", {})` is different from - # `cls.__annotations__` because of inheritance. - cls.__annotations__ = cls.__dict__.get("__annotations__", {}) - # cls.__annotations__ = dict() - - for key in dir(cls): - # skip dunder members - if key.startswith("__"): - continue - # skip class functions - if key in ["from_dict", "to_dict"]: - continue - # add type annotations for members that are not functions - var = getattr(cls, key) - if not isinstance(var, type): - if key not in cls.__annotations__: - cls.__annotations__[key] = type(var) - - -def _process_mutable_types(cls): - """Initialize all mutable elements through :obj:`dataclasses.Field` to avoid unnecessary complaints. - - By default, dataclass requires usage of :obj:`field(default_factory=...)` to reinitialize mutable objects every time a new - class instance is created. If a member has a mutable type and it is created without specifying the `field(default_factory=...)`, - then Python throws an error requiring the usage of `default_factory`. - - Additionally, Python only explicitly checks for field specification when the type is a list, set or dict. This misses the - use-case where the type is class itself. Thus, the code silently carries a bug with it which can lead to undesirable effects. - - This function deals with this issue - - This makes the following a feasible operation: - - @dataclass - class State: - pos: list = [0.0, 0.0, 0.0] - ^^ - If the function is NOT used, the following value-error is returned: - ValueError: mutable default for field pos is not allowed: use default_factory - """ - - def _return_f(f: Any) -> Callable[[], Any]: - """Returns default function for creating mutable/immutable variables.""" - - def _wrap(): - if isinstance(f, Field): - return f.default_factory - else: - return f - - return _wrap - - for key in dir(cls): - # skip dunder members - if key.startswith("__"): - continue - # skip class functions - if key in ["from_dict", "to_dict"]: - continue - # do not create field for class variables - if key in cls.__annotations__: - origin = getattr(cls.__annotations__[key], "__origin__", None) - if origin is ClassVar: - continue - # define explicit field for data members - f = getattr(cls, key) - # add field for mutable types - if not isinstance(f, type): - f = field(default_factory=_return_f(f)) - setattr(cls, key, f) - - -def _custom_post_init(obj): - """Deepcopy all elements to avoid shared memory issues for mutable objects in dataclasses initialization. - - This function is called explicitly instead of as a part of :func:`_process_mutable_types()` to prevent mapping - proxy type i.e. a read only proxy for mapping objects. The error is thrown when using hierarchical data-classes - for configuration. - """ - for key in dir(obj): - # skip dunder members - if key.startswith("__"): - continue - # duplicate data members - var = getattr(obj, key) - if not callable(var): - setattr(obj, key, deepcopy(var)) diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/__init__.py index af74d2f1e6..f5e1b926f9 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/__init__.py @@ -3,9 +3,13 @@ # # SPDX-License-Identifier: BSD-3-Clause -from __future__ import annotations +"""Sub-package for different controllers and motion-generators. + +Controllers or motion generators are responsible for closed-loop tracking of a given command. The +controller can be a simple PID controller or a more complex controller such as impedance control +or inverse kinematics control. The controller is responsible for generating the desired joint-level +commands to be sent to the robot. +""" from .differential_ik import DifferentialIKController from .differential_ik_cfg import DifferentialIKControllerCfg - -__all__ = ["DifferentialIKController", "DifferentialIKControllerCfg"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/differential_ik.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/differential_ik.py index 38ab75ce70..204a3d316b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/differential_ik.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/differential_ik.py @@ -185,6 +185,9 @@ def _compute_delta_joint_pos(self, delta_pose: torch.Tensor, jacobian: torch.Ten Returns: The desired delta in joint space. Shape is (N, num-jointsß). """ + if self.cfg.ik_params is None: + raise RuntimeError(f"Inverse-kinematics parameters for method '{self.cfg.ik_method}' is not defined!") + # compute the delta in joint-space if self.cfg.ik_method == "pinv": # Jacobian pseudo-inverse # parameters k_val = self.cfg.ik_params["k_val"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/joint_impedance.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/joint_impedance.py index fe10402821..8356be260a 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/joint_impedance.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/joint_impedance.py @@ -20,9 +20,9 @@ class JointImpedanceControllerCfg: """Type of command: p_abs (absolute) or p_rel (relative).""" dof_pos_offset: Sequence[float] | None = None - """Offset to DOF position command given to controller. (default: :obj:`None`). + """Offset to DOF position command given to controller. (default: None). - If :obj:`None` then position offsets are set to zero. + If None then position offsets are set to zero. """ impedance_mode: str = MISSING diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/operational_space.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/operational_space.py index a09d1af207..fd103d7829 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/operational_space.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/controllers/operational_space.py @@ -238,7 +238,7 @@ def compute( Args: jacobian: The Jacobian matrix of the end-effector. ee_pose: The current end-effector pose. It is a tensor of shape - (num_robots, 7), which contains the position and quaternion ``(w, x, y, z)``. Defaults to None. + (num_robots, 7), which contains the position and quaternion (w, x, y, z). Defaults to None. ee_vel: The current end-effector velocity. It is a tensor of shape (num_robots, 6), which contains the linear and angular velocities. Defaults to None. ee_force: The current external force on the end-effector. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/__init__.py index 9396a2e419..41ebf1d1c0 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/__init__.py @@ -3,78 +3,22 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Module providing interfaces to different teleoperation devices. +"""Sub-package providing interfaces to different teleoperation devices. -Currently, the module supports three categories of devices: +Currently, the following categories of devices are supported: -* Keyboard: Standard keyboard with WASD and arrow keys. -* Spacemouse: 3D mouse with 6 degrees of freedom. -* Gamepad: Gamepad with 2D two joysticks and buttons. Example: Xbox controller. +* **Keyboard**: Standard keyboard with WASD and arrow keys. +* **Spacemouse**: 3D mouse with 6 degrees of freedom. +* **Gamepad**: Gamepad with 2D two joysticks and buttons. Example: Xbox controller. All device interfaces inherit from the :class:`DeviceBase` class, which provides a common interface for all devices. The device interface reads the input data when -the :meth:`advance()` method is called. It also provides the function :meth:`add_callback()` +the :meth:`DeviceBase.advance` method is called. It also provides the function :meth:`DeviceBase.add_callback` to add user-defined callback functions to be called when a particular input is pressed from the peripheral device. - -Example usage showing the keyboard interface: - .. code-block:: python - - from omni.isaac.kit import SimulationApp - - # launch the simulator - simulation_app = SimulationApp({"headless": False}) - - from omni.isaac.core.simulation_context import SimulationContext - - from omni.isaac.orbit.devices import Se3Keyboard - - - def print_cb(): - print("Print callback") - - - def quit_cb(): - print("Quit callback") - simulation_app.close() - - - # Load kit helper - sim = SimulationContext(physics_dt=0.01, rendering_dt=0.01) - - # Create teleoperation interface - teleop_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) - # Add teleoperation callbacks - teleop_interface.add_callback("L", print_cb) - teleop_interface.add_callback("ESCAPE", quit_cb) - - # print instructions - print(teleoperation_interface) - print("Press 'L' to print a message. Press 'ESC' to quit.") - - # Reset interface internals - teleop_interface.reset() - - # Play simulation - sim.reset() - - # Run simulation - while simulation_app.is_running(): - # get keyboard command - delta_pose, gripper_command = teleop_interface.advance() - # step simulation - sim.step() - # check if simulator is stopped - if sim.is_stopped(): - break - """ -from __future__ import annotations - +from .device_base import DeviceBase from .gamepad import Se2Gamepad, Se3Gamepad from .keyboard import Se2Keyboard, Se3Keyboard from .spacemouse import Se2SpaceMouse, Se3SpaceMouse - -__all__ = ["Se2Keyboard", "Se3Keyboard", "Se2SpaceMouse", "Se3SpaceMouse", "Se2Gamepad", "Se3Gamepad"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/device_base.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/device_base.py index 6b3804ad6f..153cfb484b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/device_base.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/device_base.py @@ -20,7 +20,7 @@ def __init__(self): def __str__(self) -> str: """Returns: A string containing the information of joystick.""" - pass + return f"{self.__class__.__name__}" """ Operations diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/gamepad/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/gamepad/__init__.py index 7d4f766193..82aca8cd5f 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/gamepad/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/gamepad/__init__.py @@ -5,9 +5,5 @@ """Gamepad device for SE(2) and SE(3) control.""" -from __future__ import annotations - from .se2_gamepad import Se2Gamepad from .se3_gamepad import Se3Gamepad - -__all__ = ["Se2Gamepad", "Se3Gamepad"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/keyboard/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/keyboard/__init__.py index 4ae92faf72..281234f8b3 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/keyboard/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/keyboard/__init__.py @@ -5,9 +5,5 @@ """Keyboard device for SE(2) and SE(3) control.""" -from __future__ import annotations - from .se2_keyboard import Se2Keyboard from .se3_keyboard import Se3Keyboard - -__all__ = ["Se2Keyboard", "Se3Keyboard"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/__init__.py index 12b67a27af..8d91c93828 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/__init__.py @@ -5,9 +5,5 @@ """Spacemouse device for SE(2) and SE(3) control.""" -from __future__ import annotations - from .se2_spacemouse import Se2SpaceMouse from .se3_spacemouse import Se3SpaceMouse - -__all__ = ["Se2SpaceMouse", "Se3SpaceMouse"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/utils.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/utils.py index 00751aedb0..c8bfe77ce1 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/utils.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/devices/spacemouse/utils.py @@ -27,10 +27,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from __future__ import annotations - -__all__ = ["convert_buffer"] - def convert_buffer(b1, b2): """Converts raw SpaceMouse readings to commands. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/__init__.py index 9b7c955739..ac59ec41ca 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/__init__.py @@ -3,20 +3,26 @@ # # SPDX-License-Identifier: BSD-3-Clause -from .base_env import BaseEnv +"""Sub-package for environment definitions. + +Environments define the interface between the agent and the simulation. +In the simplest case, the environment provides the agent with the current +observations and executes the actions provided by the agent. However, the +environment can also provide additional information such as the current +reward, done flag, and information about the current episode. + +Based on these, there are two types of environments: + +* :class:`BaseEnv`: The base environment which only provides the agent with the + current observations and executes the actions provided by the agent. +* :class:`RLTaskEnv`: The RL task environment which besides the functionality of + the base environment also provides additional Markov Decision Process (MDP) + related information such as the current reward, done flag, and information. + +""" + +from . import mdp, ui +from .base_env import BaseEnv, VecEnvObs from .base_env_cfg import BaseEnvCfg, ViewerCfg -from .rl_task_env import RLTaskEnv, VecEnvObs, VecEnvStepReturn +from .rl_task_env import RLTaskEnv, VecEnvStepReturn from .rl_task_env_cfg import RLTaskEnvCfg - -__all__ = [ - # base - "BaseEnv", - "BaseEnvCfg", - "ViewerCfg", - # rl - "RLTaskEnv", - "RLTaskEnvCfg", - # env type variables - "VecEnvObs", - "VecEnvStepReturn", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env.py index d7c65bef0a..b7bf5d3bd9 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env.py @@ -181,8 +181,18 @@ def device(self): def load_managers(self): """Load the managers for the environment. - Note: - This must happen after the simulator is reset, i.e. after the first call to :meth:`self.sim.reset`. + This function is responsible for creating the various managers (action, observation, + randomization, etc.) for the environment. Since the managers require access to physics handles, + they can only be created after the simulator is reset (i.e. played for the first time). + + .. note:: + In case of standalone application (when running simulator from Python), the function is called + automatically when the class is initialized. + + However, in case of extension mode, the user must call this function manually after the simulator + is reset. This is because the simulator is only reset when the user calls + :meth:`SimulationContext.reset_async` and it isn't possible to call async functions in the constructor. + """ # prepare the managers # -- action manager @@ -227,11 +237,11 @@ def step(self, action: torch.Tensor) -> VecEnvObs: The environment steps forward at a fixed time-step, while the physics simulation is decimated at a lower time-step. This is to ensure that the simulation is stable. These two time-steps can be configured independently using the :attr:`BaseEnvCfg.decimation` (number of - simulation steps per environment step) and the :attr:`BaseEnvCfg.sim.dt` (physics time-step). + simulation steps per environment step) and the :attr:`BaseEnvCfg.physics_dt` (physics time-step). Based on these parameters, the environment time-step is computed as the product of the two. Args: - action: The actions to apply on the environment. Shape is ``(num_envs, action_dim)``. + action: The actions to apply on the environment. Shape is (num_envs, action_dim). Returns: A tuple containing the observations and extras. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env_cfg.py index 55854fb56b..a087a5b3d9 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/base_env_cfg.py @@ -21,8 +21,6 @@ from .ui import BaseEnvWindow -__all__ = ["BaseEnvCfg", "ViewerCfg"] - @configclass class ViewerCfg: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/__init__.py index 87ad16fad9..48e6ed7034 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/__init__.py @@ -3,14 +3,17 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-package contains implementations of various functions that can be used to create a Markov Decision Process (MDP). +"""Sub-module with implementation of manager terms. -The functions can be provided to different managers that are responsible for the different aspects of the MDP. These include -the observation, reward, termination, actions, randomization and curriculum managers. -""" +The functions can be provided to different managers that are responsible for the +different aspects of the MDP. These include the observation, reward, termination, +actions, randomization and curriculum managers. -from __future__ import annotations +The terms are defined under the ``envs`` module because they are used to define +the environment. However, they are not part of the environment directly, but +are used to define the environment through their managers. + +""" from .actions import * # noqa: F401, F403 from .curriculums import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/actions/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/actions/__init__.py index 4b62e62bc0..75d5b2d5ad 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/actions/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/actions/__init__.py @@ -3,11 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-module contains implementations of various action terms that can be used in the environment. -The action terms are responsible for processing the raw actions sent to the environment and applying them to the -asset managed by the term. -""" +"""Various action terms that can be used in the environment.""" from .actions_cfg import * from .binary_joint_actions import * diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/curriculums.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/curriculums.py index 60cb3f22ba..31362bbcaf 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/curriculums.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/curriculums.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the common functions that can be used to create curriculum for the learning environment. +"""Common functions that can be used to create curriculum for the learning environment. The functions can be passed to the :class:`omni.isaac.orbit.managers.CurriculumTermCfg` object to enable the curriculum introduced by the function. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/observations.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/observations.py index 2f6896627d..1e9d3a2d87 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/observations.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/observations.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the common functions that can be used to create observation terms. +"""Common functions that can be used to create observation terms. The functions can be passed to the :class:`omni.isaac.orbit.managers.ObservationTermCfg` object to enable the observation introduced by the function. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/randomizations.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/randomizations.py index 43308d1242..54348393e3 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/randomizations.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/randomizations.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the common functions that can be used to enable different randomizations. +"""Common functions that can be used to enable different randomizations. Randomization includes anything related to altering the simulation state. This includes changing the physics materials, applying external forces, and resetting the state of the asset. @@ -41,7 +41,7 @@ def randomize_rigid_body_material( uniform random values from the given ranges. The material properties are then assigned to the geometries of the asset. The assignment is done by - creating a random integer tensor of shape ``(total_body_count, num_shapes)`` where ``total_body_count`` + creating a random integer tensor of shape (total_body_count, num_shapes) where ``total_body_count`` is the number of assets spawned times the number of bodies per asset and ``num_shapes`` is the number of shapes per body. The integer values are used as indices to select the material properties from the material buckets. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/rewards.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/rewards.py index 838f0fbee8..b080512697 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/rewards.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/rewards.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the common functions that can be used to enable reward functions. +"""Common functions that can be used to enable reward functions. The functions can be passed to the :class:`omni.isaac.orbit.managers.RewardTermCfg` object to include the reward introduced by the function. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/terminations.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/terminations.py index b02f66d4aa..e29ffa4fe4 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/terminations.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/mdp/terminations.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains the common functions that can be used to activate certain terminations. +"""Common functions that can be used to activate certain terminations. The functions can be passed to the :class:`omni.isaac.orbit.managers.TerminationTermCfg` object to enable the termination introduced by the function. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/rl_task_env.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/rl_task_env.py index 6cd0fa9ec8..6928914280 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/rl_task_env.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/rl_task_env.py @@ -154,7 +154,7 @@ def step(self, action: torch.Tensor) -> VecEnvStepReturn: 7. Return the observations, rewards, resets and extras. Args: - action: The actions to apply on the environment. Shape is ``(num_envs, action_dim)``. + action: The actions to apply on the environment. Shape is (num_envs, action_dim). Returns: A tuple containing the observations, rewards, resets (terminated and truncated) and extras. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/ui/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/ui/__init__.py index dd4349ce32..4496bd26c9 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/ui/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/envs/ui/__init__.py @@ -3,14 +3,13 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-package contains implementations of UI elements for the environments. +"""Sub-module providing UI window implementation for environments. The UI elements are used to control the environment and visualize the state of the environment. +This includes functionalities such as tracking a robot in the simulation, +toggling different debug visualization tools, and other user-defined functionalities. """ -from __future__ import annotations - # enable the extension for UI elements # this only needs to be done once from omni.isaac.core.utils.extensions import enable_extension @@ -20,5 +19,3 @@ # import all UI elements here from .base_env_window import BaseEnvWindow from .rl_task_env_window import RLTaskEnvWindow - -__all__ = ["BaseEnvWindow", "RLTaskEnvWindow"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/__init__.py index da4d89b418..8ac857546f 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/__init__.py @@ -3,16 +3,13 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-module introduces the managers for handling various aspects of the environment. +"""Sub-module for environment managers. The managers are used to handle various aspects of the environment such as randomization, curriculum, and observations. Each manager implements a specific functionality for the environment. The managers are designed to be modular and can be easily extended to support new functionality. """ -from __future__ import annotations - from .action_manager import ActionManager, ActionTerm from .curriculum_manager import CurriculumManager from .manager_base import ManagerBase, ManagerTermBase @@ -31,31 +28,3 @@ from .reward_manager import RewardManager from .scene_entity_cfg import SceneEntityCfg from .termination_manager import TerminationManager - -__all__ = [ - "SceneEntityCfg", - # base - "ManagerTermBaseCfg", - "ManagerTermBase", - "ManagerBase", - # action - "ActionTermCfg", - "ActionTerm", - "ActionManager", - # curriculum - "CurriculumTermCfg", - "CurriculumManager", - # observation - "ObservationGroupCfg", - "ObservationTermCfg", - "ObservationManager", - # reward - "RewardTermCfg", - "RewardManager", - # randomization - "RandomizationTermCfg", - "RandomizationManager", - # termination - "TerminationTermCfg", - "TerminationManager", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/action_manager.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/action_manager.py index aab9709ed1..7745e99e04 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/action_manager.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/action_manager.py @@ -161,12 +161,12 @@ def action_term_dim(self) -> list[int]: @property def action(self) -> torch.Tensor: - """The actions sent to the environment. Shape is ``(num_envs, total_action_dim)``.""" + """The actions sent to the environment. Shape is (num_envs, total_action_dim).""" return self._action @property def prev_action(self) -> torch.Tensor: - """The previous actions sent to the environment. Shape is ``(num_envs, total_action_dim)``.""" + """The previous actions sent to the environment. Shape is (num_envs, total_action_dim).""" return self._prev_action """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_base.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_base.py index a75cfabd3f..1922ce5387 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_base.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_base.py @@ -156,7 +156,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: Args: env_ids: The environment ids for which to log data. - Defaults :obj:`None`, which logs data for all environments. + Defaults None, which logs data for all environments. Returns: Dictionary containing the logging information. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_term_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_term_cfg.py index 87323dc402..b0bacfad40 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_term_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/manager_term_cfg.py @@ -103,7 +103,7 @@ class ObservationTermCfg(ManagerTermBaseCfg): This function should take the environment object and any other parameters as input and return the observation signal as torch float tensors of - shape ``(num_envs, obs_term_dim)``. + shape (num_envs, obs_term_dim). """ noise: NoiseCfg | None = None @@ -187,7 +187,7 @@ class RewardTermCfg(ManagerTermBaseCfg): This function should take the environment object and any other parameters as input and return the reward signals as torch float tensors of - shape ``(num_envs,)``. + shape (num_envs,). """ weight: float = MISSING @@ -215,7 +215,7 @@ class TerminationTermCfg(ManagerTermBaseCfg): This function should take the environment object and any other parameters as input and return the termination signals as torch boolean tensors of - shape ``(num_envs,)``. + shape (num_envs,). """ time_out: bool = False diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/observation_manager.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/observation_manager.py index a0ac58afaf..6b2b60e64b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/observation_manager.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/observation_manager.py @@ -130,7 +130,7 @@ def compute_group(self, group_name: str) -> torch.Tensor | dict[str, torch.Tenso The observations for a given group are computed by calling the registered functions for each term in the group. The functions are called in the order of the terms in the group. The functions - are expected to return a tensor with shape ``(num_envs, ...)``. + are expected to return a tensor with shape (num_envs, ...). If a corruption/noise model is registered for a term, the function is called to corrupt the observation. The corruption function is expected to return a tensor with the same @@ -141,7 +141,7 @@ def compute_group(self, group_name: str) -> torch.Tensor | dict[str, torch.Tenso By default, no scaling or clipping is applied. Args: - group_name: The name of the group for which to compute the observations. Defaults to :obj:`None`, + group_name: The name of the group for which to compute the observations. Defaults to None, in which case observations for all the groups are computed and returned. Returns: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/termination_manager.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/termination_manager.py index aecd152c02..8bd48d1eb1 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/termination_manager.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/managers/termination_manager.py @@ -23,7 +23,7 @@ class TerminationManager(ManagerBase): The termination manager computes the termination signal (also called dones) as a combination of termination terms. Each termination term is a function which takes the environment as an - argument and returns a boolean tensor of shape ``(num_envs,)``. The termination manager + argument and returns a boolean tensor of shape (num_envs,). The termination manager computes the termination signal as the union (logical or) of all the termination terms. Following the `Gymnasium API `_, @@ -90,12 +90,12 @@ def active_terms(self) -> list[str]: @property def dones(self) -> torch.Tensor: - """The net termination signal. Shape is ``(num_envs,)``.""" + """The net termination signal. Shape is (num_envs,).""" return self._truncated_buf | self._terminated_buf @property def time_outs(self) -> torch.Tensor: - """The timeout signal (reaching max episode length). Shape is ``(num_envs,)``. + """The timeout signal (reaching max episode length). Shape is (num_envs,). This signal is set to true if the environment has ended after an externally defined condition (that is outside the scope of a MDP). For example, the environment may be terminated if the episode has @@ -105,7 +105,7 @@ def time_outs(self) -> torch.Tensor: @property def terminated(self) -> torch.Tensor: - """The terminated signal (reaching a terminal state). Shape is ``(num_envs,)``. + """The terminated signal (reaching a terminal state). Shape is (num_envs,). This signal is set to true if the environment has reached a terminal state defined by the environment. This state may correspond to task success, task failure, robot falling, etc. @@ -149,7 +149,7 @@ def compute(self) -> torch.Tensor: to compute the net termination signal. Returns: - The combined termination signal of shape ``(num_envs,)``. + The combined termination signal of shape (num_envs,). """ # reset computation self._truncated_buf[:] = False diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/__init__.py index cf85f96e89..04c7f19fda 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/__init__.py @@ -3,10 +3,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This submodule provides marker utilities for simplifying creation of UI elements in the GUI. +"""Sub-package for marker utilities to simplify creation of UI elements in the GUI. -Currently, the module provides the following classes: +Currently, the sub-package provides the following classes: * :class:`VisualizationMarkers` for creating a group of markers using `UsdGeom.PointInstancer `_. @@ -26,5 +25,3 @@ from .config import * # noqa: F401, F403 from .visualization_markers import VisualizationMarkers, VisualizationMarkersCfg - -__all__ = ["VisualizationMarkersCfg", "VisualizationMarkers"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/visualization_markers.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/visualization_markers.py index ab53fd2074..ecb81c091d 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/visualization_markers.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/markers/visualization_markers.py @@ -253,19 +253,19 @@ def visualize( Args: translations: Translations w.r.t. parent prim frame. Shape is (M, 3). - Defaults to :obj:`None`, which means left unchanged. + Defaults to None, which means left unchanged. orientations: Quaternion orientations (w, x, y, z) w.r.t. parent prim frame. Shape is (M, 4). - Defaults to :obj:`None`, which means left unchanged. + Defaults to None, which means left unchanged. scales: Scale applied before any rotation is applied. Shape is (M, 3). - Defaults to :obj:`None`, which means left unchanged. + Defaults to None, which means left unchanged. marker_indices: Decides which marker prototype to visualize. Shape is (M). - Defaults to :obj:`None`, which means left unchanged provided that the total number of markers + Defaults to None, which means left unchanged provided that the total number of markers is the same as the previous call. If the number of markers is different, the function will update the number of markers in the scene. Raises: ValueError: When input arrays do not follow the expected shapes. - ValueError: When the function is called with all :obj:`None` arguments. + ValueError: When the function is called with all None arguments. """ # check if it is visible (if not then let's not waste time) if not self.is_visible(): diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/__init__.py index aa9bd3caf7..8e0dc622cd 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/__init__.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-package contains the classes for an interactive scene. +"""Sub-package containing an interactive scene definition. A scene is a collection of entities (e.g., terrain, articulations, sensors, lights, etc.) that can be added to the simulation. However, only a subset of these entities are of direct interest for the user to interact with. @@ -28,5 +27,3 @@ from .interactive_scene import InteractiveScene from .interactive_scene_cfg import InteractiveSceneCfg - -__all__ = ["InteractiveScene", "InteractiveSceneCfg"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene.py index ee011bc8d2..7bc276baaa 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene.py @@ -216,7 +216,7 @@ def num_envs(self) -> int: @property def env_origins(self) -> torch.Tensor: - """The origins of the environments in the scene. Shape is ``(num_envs, 3)``.""" + """The origins of the environments in the scene. Shape is (num_envs, 3).""" if self.terrain is not None: return self.terrain.env_origins else: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene_cfg.py index f655217dd2..878e4ffcc5 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/scene/interactive_scene_cfg.py @@ -71,11 +71,14 @@ class MySceneCfg(InteractiveSceneCfg): num_envs: int = MISSING """Number of environment instances handled by the scene.""" + env_spacing: float = MISSING """Spacing between environments. - This is the default distance between environment origins in the scene. Used only when ``num_envs > 1``. + This is the default distance between environment origins in the scene. Used only when the + number of environments is greater than one. """ + lazy_sensor_update: bool = True """Whether to update sensors only when they are accessed. Default is True. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/__init__.py index 8e1638b580..da6c3394ea 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/__init__.py @@ -3,26 +3,35 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" +"""Sub-package containing various sensor classes implementations. + This subpackage contains the sensor classes that are compatible with Isaac Sim. We include both -USD-based and custom sensors. The USD-based sensors are the ones that are available in Omniverse and -require creating a USD prim for them. Custom sensors, on the other hand, are the ones that are -implemented in Python and do not require creating a USD prim for them. +USD-based and custom sensors: -A prim path (or expression) is still set for each sensor based on the following schema: +* **USD-prim sensors**: Available in Omniverse and require creating a USD prim for them. + For instance, RTX ray tracing camera and lidar sensors. +* **USD-schema sensors**: Available in Omniverse and require creating a USD schema on an existing prim. + For instance, contact sensors and frame transformers. +* **Custom sensors**: Implemented in Python and do not require creating any USD prim or schema. + For instance, warp-based ray-casters. -+-------------------+--------------------------+---------------------------------------------------------------+ -| Sensor Type | Example Prim Path | Pre-check | -+===================+==========================+===============================================================+ -| Camera | /World/robot/base/camera | Leaf is available, and it will spawn a USD camera | -| Contact Sensor | /World/robot/feet_* | Leaf is available and checks if the schema exists | -| Ray Casters | /World/robot/base | Leaf exists and is a physics body (Articulation / Rigid Body) | -| Frame Transformer | /World/robot/base | Leaf exists and is a physics body (Articulation / Rigid Body) | -+-------------------+--------------------------+---------------------------------------------------------------+ +Due to the above categorization, the prim paths passed to the sensor's configuration class +are interpreted differently based on the sensor type. The following table summarizes the +interpretation of the prim paths for different sensor types: -""" ++---------------------+---------------------------+---------------------------------------------------------------+ +| Sensor Type | Example Prim Path | Pre-check | ++=====================+===========================+===============================================================+ +| Camera | /World/robot/base/camera | Leaf is available, and it will spawn a USD camera | ++---------------------+---------------------------+---------------------------------------------------------------+ +| Contact Sensor | /World/robot/feet_* | Leaf is available and checks if the schema exists | ++---------------------+---------------------------+---------------------------------------------------------------+ +| Ray Caster | /World/robot/base | Leaf exists and is a physics body (Articulation / Rigid Body) | ++---------------------+---------------------------+---------------------------------------------------------------+ +| Frame Transformer | /World/robot/base | Leaf exists and is a physics body (Articulation / Rigid Body) | ++---------------------+---------------------------+---------------------------------------------------------------+ -from __future__ import annotations +""" from .camera import * # noqa: F401, F403 from .contact_sensor import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/__init__.py index 2f0349400e..0a9a4607f6 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/__init__.py @@ -3,13 +3,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Camera wrapper around USD camera prim to provide an interface that follows the robotics convention. -""" +"""Sub-module for camera wrapper around USD camera prim.""" from .camera import Camera from .camera_cfg import CameraCfg from .camera_data import CameraData from .utils import * # noqa: F401, F403 - -__all__ = ["Camera", "CameraData", "CameraCfg"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_cfg.py index 20b2258021..0bf6921a02 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_cfg.py @@ -27,7 +27,7 @@ class OffsetCfg: """Translation w.r.t. the parent frame. Defaults to (0.0, 0.0, 0.0).""" rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) - """Quaternion rotation ``(w, x, y, z)`` w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" + """Quaternion rotation (w, x, y, z) w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" convention: Literal["opengl", "ros", "world"] = "ros" """The convention in which the frame offset is applied. Defaults to "ros". @@ -51,7 +51,7 @@ class OffsetCfg: spawn: PinholeCameraCfg | FisheyeCameraCfg | None = MISSING """Spawn configuration for the asset. - If :obj:`None`, then the prim is not spawned by the asset. Instead, it is assumed that the + If None, then the prim is not spawned by the asset. Instead, it is assumed that the asset is already present in the scene. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_data.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_data.py index 23791cda8e..99230040fd 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_data.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/camera_data.py @@ -24,7 +24,7 @@ class CameraData: pos_w: torch.Tensor = None """Position of the sensor origin in world frame, following ROS convention. - Shape is (N, 3) where ``N`` is the number of sensors. + Shape is (N, 3) where N is the number of sensors. """ quat_w_world: torch.Tensor = None @@ -33,7 +33,7 @@ class CameraData: .. note:: World frame convention follows the camera aligned with forward axis +X and up axis +Z. - Shape is ``(N, 4)`` where ``N`` is the number of sensors. + Shape is (N, 4) where N is the number of sensors. """ ## @@ -46,7 +46,7 @@ class CameraData: intrinsic_matrices: torch.Tensor = None """The intrinsic matrices for the camera. - Shape is ``(N, 3, 3)`` where ``N`` is the number of sensors. + Shape is (N, 3, 3) where N is the number of sensors. """ output: TensorDict = None @@ -77,7 +77,7 @@ def quat_w_ros(self) -> torch.Tensor: .. note:: ROS convention follows the camera aligned with forward axis +Z and up axis -Y. - Shape is (N, 4) where ``N`` is the number of sensors. + Shape is (N, 4) where N is the number of sensors. """ return convert_orientation_convention(self.quat_w_world, origin="world", target="ros") @@ -89,6 +89,6 @@ def quat_w_opengl(self) -> torch.Tensor: .. note:: OpenGL convention follows the camera aligned with forward axis -Z and up axis +Y. - Shape is ``(N, 4)`` where ``N`` is the number of sensors. + Shape is (N, 4) where N is the number of sensors. """ return convert_orientation_convention(self.quat_w_world, origin="world", target="opengl") diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/utils.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/utils.py index accbe5c91d..c943eed733 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/utils.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/camera/utils.py @@ -21,15 +21,6 @@ import omni.isaac.orbit.utils.math as math_utils from omni.isaac.orbit.utils.array import TensorData, convert_to_torch -__all__ = [ - "transform_points", - "create_pointcloud_from_depth", - "create_pointcloud_from_rgbd", - "convert_orientation_convention", - "create_rotation_matrix_from_view", -] - - """ Depth <-> Pointcloud conversions. """ @@ -49,12 +40,12 @@ def transform_points( .. math:: p_{target} = R_{target} \times p_{source} + t_{target} - If either the inputs `position` and `orientation` are :obj:`None`, the corresponding transformation is not applied. + If either the inputs `position` and `orientation` are None, the corresponding transformation is not applied. Args: points: a tensor of shape (p, 3) or (n, p, 3) comprising of 3d points in source frame. position: The position of source frame in target frame. Defaults to None. - orientation: The orientation ``(w, x, y, z)`` of source frame in target frame. + orientation: The orientation (w, x, y, z) of source frame in target frame. Defaults to None. device: The device for torch where the computation should be executed. Defaults to None, i.e. takes the device that matches the depth image. @@ -120,7 +111,7 @@ def create_pointcloud_from_depth( keep_invalid: Whether to keep invalid points in the cloud or not. Invalid points correspond to pixels with depth values 0.0 or NaN. Defaults to False. position: The position of the camera in a target frame. Defaults to None. - orientation: The orientation ``(w, x, y, z)`` of the camera in a target frame. Defaults to None. + orientation: The orientation (w, x, y, z) of the camera in a target frame. Defaults to None. device: The device for torch where the computation should be executed. Defaults to None, i.e. takes the device that matches the depth image. @@ -183,7 +174,7 @@ def create_pointcloud_from_rgbd( - If a ``np.array``/``wp.array``/``torch.tensor`` of shape (H, W, 3), then the corresponding channels encode RGB values. - If a tuple, then the point cloud has a single color specified by the values (r, g, b). - - If :obj:`None`, then default color is white, i.e. (0, 0, 0). + - If None, then default color is white, i.e. (0, 0, 0). If the input ``normalize_rgb`` is set to :obj:`True`, then the RGB values are normalized to be in the range [0, 1]. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/__init__.py index 0c87f4c631..aa9f19e4dd 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/__init__.py @@ -3,14 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Rigid contact sensor based on :class:`omni.isaac.core.prims.RigidContactView`. -""" - -from __future__ import annotations +"""Sub-module for rigid contact sensor based on :class:`omni.isaac.core.prims.RigidContactView`.""" from .contact_sensor import ContactSensor from .contact_sensor_cfg import ContactSensorCfg from .contact_sensor_data import ContactSensorData - -__all__ = ["ContactSensor", "ContactSensorCfg", "ContactSensorData"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/contact_sensor_data.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/contact_sensor_data.py index ebec6b168f..0ce045df5b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/contact_sensor_data.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/contact_sensor/contact_sensor_data.py @@ -16,16 +16,16 @@ class ContactSensorData: pos_w: torch.Tensor | None = None """Position of the sensor origin in world frame. - Shape is (N, 3), where ``N`` is the number of sensors. + Shape is (N, 3), where N is the number of sensors. Note: If the :attr:`ContactSensorCfg.track_pose` is False, then this qunatity is None. """ quat_w: torch.Tensor | None = None - """Orientation of the sensor origin in quaternion ``(w, x, y, z)`` in world frame. + """Orientation of the sensor origin in quaternion (w, x, y, z) in world frame. - Shape is (N, 4), where ``N`` is the number of sensors. + Shape is (N, 4), where N is the number of sensors. Note: If the :attr:`ContactSensorCfg.track_pose` is False, then this qunatity is None. @@ -34,14 +34,14 @@ class ContactSensorData: net_forces_w: torch.Tensor = None """The net contact forces in world frame. - Shape is (N, B, 3), where ``N`` is the number of sensors and ``B`` is the number of bodies in each sensor. + Shape is (N, B, 3), where N is the number of sensors and B is the number of bodies in each sensor. """ net_forces_w_history: torch.Tensor = None """The net contact forces in world frame. - Shape is (N, T, B, 3), where ``N`` is the number of sensors, ``T`` is the configured history length - and ``B`` is the number of bodies in each sensor. + Shape is (N, T, B, 3), where N is the number of sensors, T is the configured history length + and B is the number of bodies in each sensor. In the history dimension, the first index is the most recent and the last index is the oldest. """ @@ -49,7 +49,7 @@ class ContactSensorData: force_matrix_w: torch.Tensor | None = None """The contact forces filtered between the sensor bodies and filtered bodies in world frame. - Shape is (N, B, S, M, 3), where ``N`` is the number of sensors, ``B`` is number of bodies in each sensor, + Shape is (N, B, S, M, 3), where N is the number of sensors, B is number of bodies in each sensor, ``S`` is number of shapes per body and ``M`` is the number of filtered bodies. Note: @@ -59,7 +59,7 @@ class ContactSensorData: last_air_time: torch.Tensor | None = None """Time spent (in s) in the air before the last contact. - Shape is (N,), where ``N`` is the number of sensors. + Shape is (N,), where N is the number of sensors. Note: If the :attr:`ContactSensorCfg.track_air_time` is False, then this quantity is None. @@ -68,7 +68,7 @@ class ContactSensorData: current_air_time: torch.Tensor | None = None """Time spent (in s) in the air since the last contact. - Shape is (N,), where ``N`` is the number of sensors. + Shape is (N,), where N is the number of sensors. Note: If the :attr:`ContactSensorCfg.track_air_time` is False, then this quantity is None. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/__init__.py index a1cc4ac91b..d698ba91a4 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/__init__.py @@ -3,14 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Frame transform sensor for calculating frame transform of articulations. -""" - -from __future__ import annotations +"""Sub-module for frame transformer sensor.""" from .frame_transformer import FrameTransformer from .frame_transformer_cfg import FrameTransformerCfg, OffsetCfg from .frame_transformer_data import FrameTransformerData - -__all__ = ["FrameTransformer", "FrameTransformerCfg", "FrameTransformerData", "OffsetCfg"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_cfg.py index 38eaa2cdd7..3def415ae9 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_cfg.py @@ -21,7 +21,7 @@ class OffsetCfg: pos: tuple[float, float, float] = (0.0, 0.0, 0.0) """Translation w.r.t. the parent frame. Defaults to (0.0, 0.0, 0.0).""" rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) - """Quaternion rotation ``(w, x, y, z)`` w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" + """Quaternion rotation (w, x, y, z) w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" @configclass diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_data.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_data.py index be41b68d6c..dad6cae0b5 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_data.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/frame_transformer/frame_transformer_data.py @@ -27,7 +27,7 @@ class FrameTransformerData: Shape is (N, M, 3), where N is the number of environments, and M is the number of target frames. """ target_rot_source: torch.Tensor = None - """Orientation of the target frame(s) relative to source frame quaternion ``(w, x, y, z)``. + """Orientation of the target frame(s) relative to source frame quaternion (w, x, y, z). Shape is (N, M, 4), where N is the number of environments, and M is the number of target frames. """ @@ -37,7 +37,7 @@ class FrameTransformerData: Shape is (N, M, 3), where N is the number of environments, and M is the number of target frames. """ target_rot_w: torch.Tensor = None - """Orientation of the target frame(s) after offset (in world frame) quaternion ``(w, x, y, z)``. + """Orientation of the target frame(s) after offset (in world frame) quaternion (w, x, y, z). Shape is (N, M, 4), where N is the number of environments, and M is the number of target frames. """ @@ -47,7 +47,7 @@ class FrameTransformerData: Shape is (N, 3), where N is the number of environments. """ source_rot_w: torch.Tensor = None - """Orientation of the source frame after offset (in world frame) quaternion ``(w, x, y, z)``. + """Orientation of the source frame after offset (in world frame) quaternion (w, x, y, z). Shape is (N, 4), where N is the number of environments. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/__init__.py index 3ac00b9e01..5d3d5c9971 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/__init__.py @@ -3,11 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Ray-caster based on warp. -""" - -from __future__ import annotations +"""Sub-module for Warp-based ray-cast sensor.""" from . import patterns from .ray_caster import RayCaster @@ -15,15 +11,3 @@ from .ray_caster_camera_cfg import RayCasterCameraCfg from .ray_caster_cfg import RayCasterCfg from .ray_caster_data import RayCasterData - -__all__ = [ - # ray caster - "RayCaster", - "RayCasterData", - "RayCasterCfg", - # camera - "RayCasterCameraCfg", - "RayCasterCamera", - # patterns - "patterns", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/__init__.py index c5499e6fe0..6918c39d30 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/__init__.py @@ -3,24 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Utility functions for different ray-casting patterns that are used by the ray-caster. -""" - -from __future__ import annotations +"""Sub-module for ray-casting patterns used by the ray-caster.""" from .patterns import bpearl_pattern, grid_pattern, pinhole_camera_pattern from .patterns_cfg import BpearlPatternCfg, GridPatternCfg, PatternBaseCfg, PinholeCameraPatternCfg - -__all__ = [ - "PatternBaseCfg", - # grid pattern - "GridPatternCfg", - "grid_pattern", - # pinhole camera pattern - "PinholeCameraPatternCfg", - "pinhole_camera_pattern", - # bpearl pattern - "BpearlPatternCfg", - "bpearl_pattern", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/patterns_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/patterns_cfg.py index 35bbde5400..f2b11c0cc5 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/patterns_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/patterns/patterns_cfg.py @@ -35,7 +35,7 @@ class GridPatternCfg(PatternBaseCfg): Defines a 2D grid of rays in the coordinates of the sensor. """ - func = patterns.grid_pattern + func: Callable = patterns.grid_pattern resolution: float = MISSING """Grid resolution (in meters).""" @@ -49,7 +49,7 @@ class GridPatternCfg(PatternBaseCfg): class PinholeCameraPatternCfg(PatternBaseCfg): """Configuration for a pinhole camera depth image pattern for ray-casting.""" - func = patterns.pinhole_camera_pattern + func: Callable = patterns.pinhole_camera_pattern focal_length: float = 24.0 """Perspective focal length (in cm). Defaults to 24.0cm. @@ -78,7 +78,7 @@ class PinholeCameraPatternCfg(PatternBaseCfg): class BpearlPatternCfg(PatternBaseCfg): """Configuration for the Bpearl pattern for ray-casting.""" - func = patterns.bpearl_pattern + func: Callable = patterns.bpearl_pattern horizontal_fov: float = 360.0 """Horizontal field of view (in degrees). Defaults to 360.0.""" diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera.py index 8b41e359ad..af251e7725 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera.py @@ -28,7 +28,7 @@ class RayCasterCamera(RayCaster): The ray-caster camera uses a set of rays to get the distances to meshes in the scene. The rays are defined in the sensor's local coordinate frame. The sensor has the same interface as the - :class:`omni.isaac.orbit.sensors.camera.Camera` that implements the camera class through USD camera prims. + :class:`omni.isaac.orbit.sensors.Camera` that implements the camera class through USD camera prims. However, this class provides a faster image generation. The sensor converts meshes from the list of primitive paths provided in the configuration to Warp meshes. The camera then ray-casts against these Warp meshes only. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera_cfg.py index b01e8801bd..62a323280f 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_camera_cfg.py @@ -27,7 +27,7 @@ class OffsetCfg: """Translation w.r.t. the parent frame. Defaults to (0.0, 0.0, 0.0).""" rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) - """Quaternion rotation ``(w, x, y, z)`` w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" + """Quaternion rotation (w, x, y, z) w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" convention: Literal["opengl", "ros", "world"] = "ros" """The convention in which the frame offset is applied. Defaults to "ros". diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_cfg.py index 15fa108058..327f663816 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_cfg.py @@ -29,7 +29,7 @@ class OffsetCfg: pos: tuple[float, float, float] = (0.0, 0.0, 0.0) """Translation w.r.t. the parent frame. Defaults to (0.0, 0.0, 0.0).""" rot: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) - """Quaternion rotation ``(w, x, y, z)`` w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" + """Quaternion rotation (w, x, y, z) w.r.t. the parent frame. Defaults to (1.0, 0.0, 0.0, 0.0).""" class_type: type = RayCaster diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_data.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_data.py index d37c3bf998..d898495d78 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_data.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/ray_caster/ray_caster_data.py @@ -16,16 +16,16 @@ class RayCasterData: pos_w: torch.Tensor = None """Position of the sensor origin in world frame. - Shape is (N, 3), where ``N`` is the number of sensors. + Shape is (N, 3), where N is the number of sensors. """ quat_w: torch.Tensor = None - """Orientation of the sensor origin in quaternion ``(w, x, y, z)`` in world frame. + """Orientation of the sensor origin in quaternion (w, x, y, z) in world frame. - Shape is (N, 4), where ``N`` is the number of sensors. + Shape is (N, 4), where N is the number of sensors. """ ray_hits_w: torch.Tensor = None """The ray hit positions in the world frame. - Shape is (N, B, 3), where ``N`` is the number of sensors, ``B`` is the number of rays + Shape is (N, B, 3), where N is the number of sensors, B is the number of rays in the scan pattern per sensor. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/sensor_base_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/sensor_base_cfg.py index 4f4413107c..d5d8e4c03b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/sensor_base_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sensors/sensor_base_cfg.py @@ -23,13 +23,14 @@ class SensorBaseCfg: """ prim_path: str = MISSING - """Prim path (or expression) to the asset. + """Prim path (or expression) to the sensor. .. note:: The expression can contain the environment namespace regex ``{ENV_REGEX_NS}`` which will be replaced with the environment namespace. - Example: ``{ENV_REGEX_NS}/Robot/sensor`` will be replaced with ``/World/envs/env_.*/Robot/sensor`. + Example: ``{ENV_REGEX_NS}/Robot/sensor`` will be replaced with ``/World/envs/env_.*/Robot/sensor``. + """ update_period: float = 0.0 diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/__init__.py index ad42fe7bd9..0b3213eb65 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/__init__.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module contains simulation-specific functionalities. +"""Sub-package containing simulation-specific functionalities. These include: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/__init__.py index 9c5f2bba00..50dbdf5abf 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/__init__.py @@ -4,7 +4,7 @@ # SPDX-License-Identifier: BSD-3-Clause -"""A utility to convert various file types to a USD file. +"""Sub-module containing converters for converting various file types to USD. In order to support direct loading of various file types into Omniverse, we provide a set of converters that can convert the file into a USD file. The converters are implemented as @@ -17,20 +17,9 @@ """ -from __future__ import annotations - from .asset_converter_base import AssetConverterBase from .asset_converter_base_cfg import AssetConverterBaseCfg from .mesh_converter import MeshConverter from .mesh_converter_cfg import MeshConverterCfg from .urdf_converter import UrdfConverter from .urdf_converter_cfg import UrdfConverterCfg - -__all__ = [ - "AssetConverterBase", - "AssetConverterBaseCfg", - "MeshConverter", - "MeshConverterCfg", - "UrdfConverter", - "UrdfConverterCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/asset_converter_base_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/asset_converter_base_cfg.py index 32dc2bf0a7..a6ff662b87 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/asset_converter_base_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/converters/asset_converter_base_cfg.py @@ -19,16 +19,16 @@ class AssetConverterBaseCfg: """The absolute path to the asset file to convert into USD.""" usd_dir: str | None = None - """The output directory path to store the generated USD file. Defaults to :obj:`None`. + """The output directory path to store the generated USD file. Defaults to None. - If set to :obj:`None`, it is resolved as ``/tmp/Orbit/usd_{date}_{time}_{random}``, where + If None, it is resolved as ``/tmp/Orbit/usd_{date}_{time}_{random}``, where the parameters in braces are runtime generated. """ usd_file_name: str | None = None - """The name of the generated usd file. Defaults to :obj:`None`. + """The name of the generated usd file. Defaults to None. - If set to :obj:`None`, it is resolved from the asset file name. For example, if the asset file + If None, it is resolved from the asset file name. For example, if the asset file name is ``"my_asset.urdf"``, then the generated USD file name is ``"my_asset.usd"``. If the providing file name does not end with ".usd" or ".usda", then the extension diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/__init__.py index 3dd9b0a650..262074d4a5 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/__init__.py @@ -32,8 +32,6 @@ """ -from __future__ import annotations - from .schemas import ( activate_contact_sensors, define_articulation_root_properties, @@ -51,23 +49,3 @@ MassPropertiesCfg, RigidBodyPropertiesCfg, ) - -__all__ = [ - # articulation root - "ArticulationRootPropertiesCfg", - "define_articulation_root_properties", - "modify_articulation_root_properties", - # rigid bodies - "RigidBodyPropertiesCfg", - "define_rigid_body_properties", - "modify_rigid_body_properties", - "activate_contact_sensors", - # colliders - "CollisionPropertiesCfg", - "define_collision_properties", - "modify_collision_properties", - # mass - "MassPropertiesCfg", - "define_mass_properties", - "modify_mass_properties", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas.py index fc6ec403af..1c7902c8c4 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas.py @@ -22,7 +22,7 @@ def define_articulation_root_properties( ): """Apply the articulation root schema on the input prim and set its properties. - See :func:`set_articulation_root_properties` for more details on how the properties are set. + See :func:`modify_articulation_root_properties` for more details on how the properties are set. Args: prim_path: The prim path where to apply the articulation root schema. @@ -112,7 +112,7 @@ def define_rigid_body_properties( ): """Apply the rigid body schema on the input prim and set its properties. - See :func:`set_rigid_body_properties` for more details on how the properties are set. + See :func:`modify_rigid_body_properties` for more details on how the properties are set. Args: prim_path: The prim path where to apply the rigid body schema. @@ -205,7 +205,7 @@ def define_collision_properties( ): """Apply the collision schema on the input prim and set its properties. - See :func:`set_collision_properties` for more details on how the properties are set. + See :func:`modify_collision_properties` for more details on how the properties are set. Args: prim_path: The prim path where to apply the rigid body schema. @@ -237,19 +237,19 @@ def modify_collision_properties( ): """Modify PhysX properties of collider prim. - These properties are based on the `UsdPhysics.CollisionAPI` and `PhysxSchema.PhysxCollisionAPI`_ schemas. + These properties are based on the `UsdPhysics.CollisionAPI`_ and `PhysxSchema.PhysxCollisionAPI`_ schemas. For more information on the properties, please refer to the official documentation. Tuning these parameters influence the contact behavior of the rigid body. For more information on tune them and their effect on the simulation, please refer to the - `PhysX documentation `_. + `PhysX documentation `__. .. note:: This function is decorated with :func:`apply_nested` that sets the properties to all the prims (that have the schema applied on them) under the input prim path. - .. UsdPhysics.CollisionAPI: https://openusd.org/dev/api/class_usd_physics_collision_a_p_i.html - .. PhysxSchema.PhysxCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_collision_a_p_i.html + .. _UsdPhysics.CollisionAPI: https://openusd.org/dev/api/class_usd_physics_collision_a_p_i.html + .. _PhysxSchema.PhysxCollisionAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_collision_a_p_i.html Args: prim_path: The prim path of parent. @@ -293,7 +293,7 @@ def modify_collision_properties( def define_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, stage: Usd.Stage | None = None): """Apply the mass schema on the input prim and set its properties. - See :func:`set_mass_properties` for more details on how the properties are set. + See :func:`modify_mass_properties` for more details on how the properties are set. Args: prim_path: The prim path where to apply the rigid body schema. @@ -326,7 +326,7 @@ def modify_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, s These properties are based on the `UsdPhysics.MassAPI` schema. If the mass is not defined, the density is used to compute the mass. However, in that case, a collision approximation of the rigid body is used to compute the density. For more information on the properties, please refer to the - `documentation `_. + `documentation `__. .. caution:: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas_cfg.py index c30a3898c6..30cdd79f96 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/schemas/schemas_cfg.py @@ -12,10 +12,10 @@ class ArticulationRootPropertiesCfg: """Properties to apply to the root of an articulation. - See :meth:`set_articulation_root_properties` for more information. + See :meth:`modify_articulation_root_properties` for more information. .. note:: - If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of + If the values are None, they are not modified. This is useful when you want to set only a subset of the properties and leave the rest as-is. """ @@ -37,10 +37,10 @@ class ArticulationRootPropertiesCfg: class RigidBodyPropertiesCfg: """Properties to apply to a rigid body. - See :meth:`set_rigid_body_properties` for more information. + See :meth:`modify_rigid_body_properties` for more information. .. note:: - If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of + If the values are None, they are not modified. This is useful when you want to set only a subset of the properties and leave the rest as-is. """ @@ -86,10 +86,10 @@ class RigidBodyPropertiesCfg: class CollisionPropertiesCfg: """Properties to apply to colliders in a rigid body. - See :meth:`set_collision_properties` for more information. + See :meth:`modify_collision_properties` for more information. .. note:: - If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of + If the values are None, they are not modified. This is useful when you want to set only a subset of the properties and leave the rest as-is. """ @@ -123,10 +123,10 @@ class CollisionPropertiesCfg: class MassPropertiesCfg: """Properties to define explicit mass properties of a rigid body. - See :meth:`set_mass_properties` for more information. + See :meth:`modify_mass_properties` for more information. .. note:: - If the values are :obj:`None`, they are not modified. This is useful when you want to set only a subset of + If the values are None, they are not modified. This is useful when you want to set only a subset of the properties and leave the rest as-is. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/simulation_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/simulation_cfg.py index 9bfd8bd7a8..e3a88b55a2 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/simulation_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/simulation_cfg.py @@ -20,7 +20,7 @@ @configclass class PhysxCfg: - """PhysX solver parameters. + """Configuration for PhysX solver-related parameters. These parameters are used to configure the PhysX solver. For more information, see the PhysX 5 SDK documentation. @@ -195,7 +195,7 @@ class SimulationCfg: functionality will not be available. However, this provides some performance speed-up. Note: - This flag is overridden to True inside the :class:`IsaacEnv` class when running the simulation + This flag is overridden to True inside the :class:`SimulationContext` class when running the simulation with the GUI enabled. This is to allow certain GUI features to work properly. """ @@ -233,7 +233,9 @@ class SimulationCfg: information can be expensive due to its combinatorial complexity. This flag allows disabling the contact processing and querying the contacts manually by the user over a limited set of primitives in the scene. - It is recommended to set this flag to :obj:`True` when using the TensorAPIs for contact reporting. + .. note:: + + It is required to set this flag to :obj:`True` when using the TensorAPIs for contact reporting. """ use_gpu_pipeline: bool = True diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/__init__.py index 4376840671..c15a858978 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/__init__.py @@ -5,24 +5,54 @@ """Sub-module containing utilities for creating prims in Omniverse. -Usage: - .. code-block:: python +Spawners are used to create prims into Omniverse simulator. At their core, they are calling the +USD Python API or Omniverse Kit Commands to create prims. However, they also provide a convenient +interface for creating prims from their respective config classes. - import omni.isaac.orbit.sim as sim_utils - from omni.isaac.orbit.utils.assets import ISAAC_ORBIT_NUCLEUS_DIR +There are two main ways of using the spawners: - # spawn from USD file - cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_ORBIT_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd") - prim_path = "/World/myAsset" +1. Using the function from the module - # Option 1: spawn using the function from the module - sim_utils.spawn_from_usd(prim_path, cfg) + .. code-block:: python - # Option 2: use the `func` reference in the config class - cfg.func(prim_path, cfg) -""" + import omni.isaac.orbit.sim as sim_utils + from omni.isaac.orbit.utils.assets import ISAAC_ORBIT_NUCLEUS_DIR + + # spawn from USD file + cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_ORBIT_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd") + prim_path = "/World/myAsset" + + # spawn using the function from the module + sim_utils.spawn_from_usd(prim_path, cfg) + +2. Using the `func` reference in the config class + + .. code-block:: python + + import omni.isaac.orbit.sim as sim_utils + from omni.isaac.orbit.utils.assets import ISAAC_ORBIT_NUCLEUS_DIR -from __future__ import annotations + # spawn from USD file + cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_ORBIT_NUCLEUS_DIR}/Robots/FrankaEmika/panda_instanceable.usd") + prim_path = "/World/myAsset" + + # use the `func` reference in the config class + cfg.func(prim_path, cfg) + +For convenience, we recommend using the second approach, as it allows to easily change the config +class and the function call in a single line of code. + +Depending on the type of prim, the spawning-functions can also deal with the creation of prims +over multiple prim path. These need to be provided as a regex prim path expressions, which are +resolved based on the parent prim paths using the :meth:`omni.isaac.orbit.sim.utils.clone` function decorator. +For example: + +* ``/World/Table_[1,2]/Robot`` will create the prims ``/World/Table_1/Robot`` and ``/World/Table_2/Robot`` + only if the parent prim ``/World/Table_1`` and ``/World/Table_2`` exist. +* ``/World/Robot_[1,2]`` will **NOT** create the prims ``/World/Robot_1`` and + ``/World/Robot_2`` as the prim path expression can be resolved to multiple prims. + +""" from .from_files import * # noqa: F401, F403 from .lights import * # noqa: F401, F403 diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/from_files/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/from_files/__init__.py index 88beb6223f..65bfd618a1 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/from_files/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/from_files/__init__.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-module contains spawners that spawn assets from files. +"""Sub-module for spawners that spawn assets from files. Currently, the following spawners are supported: @@ -14,19 +13,5 @@ """ -from __future__ import annotations - from .from_files import spawn_from_urdf, spawn_from_usd, spawn_ground_plane from .from_files_cfg import GroundPlaneCfg, UrdfFileCfg, UsdFileCfg - -__all__ = [ - # usd - "UsdFileCfg", - "spawn_from_usd", - # urdf - "UrdfFileCfg", - "spawn_from_urdf", - # ground plane - "GroundPlaneCfg", - "spawn_ground_plane", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/__init__.py index 5cb2ff3194..5d2da56210 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/__init__.py @@ -3,27 +3,12 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-module contains spawners that spawn USD-based light prims. +"""Sub-module for spawners that spawn lights in the simulation. There are various different kinds of lights that can be spawned into the USD stage. Please check the Omniverse documentation for `lighting overview `_. """ -from __future__ import annotations - from .lights import spawn_light from .lights_cfg import CylinderLightCfg, DiskLightCfg, DistantLightCfg, DomeLightCfg, LightCfg, SphereLightCfg - -__all__ = [ - # base class - "LightCfg", - "spawn_light", - # derived classes - "CylinderLightCfg", - "DiskLightCfg", - "DistantLightCfg", - "DomeLightCfg", - "SphereLightCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/lights.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/lights.py index 3cc1a12772..c6b760cb81 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/lights.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/lights/lights.py @@ -38,7 +38,7 @@ def spawn_light( then the asset is spawned at all the matching prim paths. cfg: The configuration for the light source. translation: The translation of the prim. Defaults to None, in which case this is set to the origin. - orientation: The orientation of the prim as ``(w, x, y, z)``. Defaults to None, in which case this + orientation: The orientation of the prim as (w, x, y, z). Defaults to None, in which case this is set to identity. Raises: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/__init__.py index 1fc7ac59d2..042182d3ec 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/__init__.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-module contains spawners that spawn USD-based and PhysX-based materials. +"""Sub-module for spawners that spawn USD-based and PhysX-based materials. `Materials`_ are used to define the appearance and physical properties of objects in the simulation. In Omniverse, they are defined using NVIDIA's `Material Definition Language (MDL)`_. MDL is based on @@ -59,17 +58,3 @@ from .physics_materials_cfg import PhysicsMaterialCfg, RigidBodyMaterialCfg from .visual_materials import spawn_from_mdl_file, spawn_preview_surface from .visual_materials_cfg import GlassMdlCfg, MdlFileCfg, PreviewSurfaceCfg, VisualMaterialCfg - -__all__ = [ - # visual materials - "VisualMaterialCfg", - "spawn_preview_surface", - "PreviewSurfaceCfg", - "spawn_from_mdl_file", - "MdlFileCfg", - "GlassMdlCfg", - # physics materials - "PhysicsMaterialCfg", - "spawn_rigid_body_material", - "RigidBodyMaterialCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials.py index b3f345d936..e98fb5e650 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials.py @@ -24,7 +24,7 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa A preview surface is a physically-based surface that handles simple shaders while supporting both *specular* and *metallic* workflows. All color inputs are in linear color space (RGB). - For more information, see the `documentation `_. + For more information, see the `documentation `__. The function calls the USD command `CreatePreviewSurfaceMaterialPrim`_ to create the prim. @@ -66,7 +66,7 @@ def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfa def spawn_from_mdl_file(prim_path: str, cfg: visual_materials_cfg.MdlMaterialCfg) -> Usd.Prim: """Load a material from its MDL file and override the settings with the given config. - NVIDIA's `Material Definition Language (MDL) `_ + NVIDIA's `Material Definition Language (MDL) `__ is a language for defining physically-based materials. The MDL file format is a binary format that can be loaded by Omniverse and other applications such as Adobe Substance Designer. To learn more about MDL, see the `documentation `_. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials_cfg.py index 9308f72d3a..50abce0775 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/materials/visual_materials_cfg.py @@ -83,7 +83,7 @@ class GlassMdlCfg(VisualMaterialCfg): """Configuration parameters for loading a glass MDL material. This is a convenience class for loading a glass MDL material. For more information on - glass materials, see the `documentation `_. + glass materials, see the `documentation `__. .. note:: The default values are taken from the glass material in the NVIDIA Nucleus. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/__init__.py index bbd4cca179..d49bd7a856 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/__init__.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This sub-module contains functions for spawning sensors in the simulation. +"""Sub-module for spawners that spawn sensors in the simulation. Currently, the following sensors are supported: @@ -12,14 +11,5 @@ """ -from __future__ import annotations - from .sensors import spawn_camera from .sensors_cfg import FisheyeCameraCfg, PinholeCameraCfg - -__all__ = [ - # camera - "spawn_camera", - "PinholeCameraCfg", - "FisheyeCameraCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/sensors_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/sensors_cfg.py index 589bec2e2d..59a7d78073 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/sensors_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/sensors/sensors_cfg.py @@ -18,10 +18,10 @@ class PinholeCameraCfg(SpawnerCfg): """Configuration parameters for a USD camera prim with pinhole camera settings. - For more information on the parameters, please refer to the `camera documentation `_. + For more information on the parameters, please refer to the `camera documentation `__. .. note:: - The default values are taken from the `Replicator camera `_ + The default values are taken from the `Replicator camera `__ function. """ @@ -79,10 +79,10 @@ class FisheyeCameraCfg(PinholeCameraCfg): """Configuration parameters for a USD camera prim with `fish-eye camera`_ settings. For more information on the parameters, please refer to the - `camera documentation `_. + `camera documentation `__. .. note:: - The default values are taken from the `Replicator camera `_ + The default values are taken from the `Replicator camera `__ function. .. _fish-eye camera: https://en.wikipedia.org/wiki/Fisheye_lens diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/shapes/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/shapes/__init__.py index c849ee6bab..f8e4fc21be 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/shapes/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/spawners/shapes/__init__.py @@ -3,34 +3,16 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" +"""Sub-module for spawning primitive shapes in the simulation. + NVIDIA Omniverse provides various primitive shapes that can be used to create USDGeom prims. Based -on the configuration, the spawned prim can be used a visual mesh (no physics), a static collider -(no rigid body), or a rigid body (with collision and rigid body properties). +on the configuration, the spawned prim can be: -Since this creates a prim manually, we follow the convention recommended by NVIDIA to prepare -`Sim-Ready assets `_. -""" +* a visual mesh (no physics) +* a static collider (no rigid body) +* a rigid body (with collision and rigid body properties). -from __future__ import annotations +""" from .shapes import spawn_capsule, spawn_cone, spawn_cuboid, spawn_cylinder, spawn_sphere -from .shapes_cfg import CapsuleCfg, ConeCfg, CuboidCfg, CylinderCfg, SphereCfg - -__all__ = [ - # capsule - "CapsuleCfg", - "spawn_capsule", - # cone - "ConeCfg", - "spawn_cone", - # cuboid - "CuboidCfg", - "spawn_cuboid", - # cylinder - "CylinderCfg", - "spawn_cylinder", - # sphere - "SphereCfg", - "spawn_sphere", -] +from .shapes_cfg import CapsuleCfg, ConeCfg, CuboidCfg, CylinderCfg, ShapeCfg, SphereCfg diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/utils.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/utils.py index 9196734171..328e3196df 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/utils.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/sim/utils.py @@ -3,6 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Sub-module with USD-related utilities.""" + from __future__ import annotations import functools diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/__init__.py index 052aa32d28..e2cf2e87b8 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/__init__.py @@ -3,22 +3,21 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -This module provides utilities to create different terrains procedurally. +"""Sub-package with utilities for creating terrains procedurally. -There are two main components in this module: +There are two main components in this package: * :class:`TerrainGenerator`: This class procedurally generates terrains based on the passed sub-terrain configuration. It creates a ``trimesh`` mesh object and contains the origins of each generated sub-terrain. * :class:`TerrainImporter`: This class mainly deals with importing terrains from different possible sources and adding them to the simulator as a prim object. It also stores the - terrain mesh into a dictionary called :obj:`warp_meshes` that later can be used + terrain mesh into a dictionary called :obj:`TerrainImporter.warp_meshes` that later can be used for ray-casting. The following functions are available for importing terrains: - * :meth:`import_ground_plane`: spawn a grid plane which is default in isaacsim/orbit. - * :meth:`import_mesh`: spawn a prim from a ``trimesh`` object. - * :meth:`import_usd`: spawn a prim as reference to input USD file. + * :meth:`TerrainImporter.import_ground_plane`: spawn a grid plane which is default in isaacsim/orbit. + * :meth:`TerrainImporter.import_mesh`: spawn a prim from a ``trimesh`` object. + * :meth:`TerrainImporter.import_usd`: spawn a prim as reference to input USD file. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/__init__.py index 461cff9ae2..084d35e750 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/__init__.py @@ -23,16 +23,8 @@ choose a discretization size that is small enough for the application. A larger discretization size will result in a faster simulation, but the terrain will be less accurate. -All sub-terrains must inherit from the :class:`HfTerrainBaseCfg` class which contains the common -parameters for all terrains generated from height fields. - -.. autoclass:: omni.isaac.orbit.terrains.height_field.hf_terrains_cfg.HfTerrainBaseCfg - :members: - :show-inheritance: """ -from __future__ import annotations - from .hf_terrains_cfg import ( HfDiscreteObstaclesTerrainCfg, HfInvertedPyramidSlopedTerrainCfg, @@ -44,15 +36,3 @@ HfTerrainBaseCfg, HfWaveTerrainCfg, ) - -__all__ = [ - "HfTerrainBaseCfg", - "HfRandomUniformTerrainCfg", - "HfPyramidSlopedTerrainCfg", - "HfInvertedPyramidSlopedTerrainCfg", - "HfPyramidStairsTerrainCfg", - "HfInvertedPyramidStairsTerrainCfg", - "HfDiscreteObstaclesTerrainCfg", - "HfWaveTerrainCfg", - "HfSteppingStonesTerrainCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains.py index c9e056d2ed..00c19980aa 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains.py @@ -21,7 +21,7 @@ def random_uniform_terrain(difficulty: float, cfg: hf_terrains_cfg.HfRandomUniformTerrainCfg) -> np.ndarray: """Generate a terrain with height sampled uniformly from a specified range. - .. image:: ../_static/terrains/height_field/random_uniform_terrain.jpg + .. image:: ../../_static/terrains/height_field/random_uniform_terrain.jpg :width: 40% :align: center @@ -90,10 +90,10 @@ def pyramid_sloped_terrain(difficulty: float, cfg: hf_terrains_cfg.HfPyramidSlop If the :obj:`cfg.inverted` flag is set to :obj:`True`, the terrain is inverted such that the platform is at the bottom. - .. image:: ../_static/terrains/height_field/pyramid_sloped_terrain.jpg + .. image:: ../../_static/terrains/height_field/pyramid_sloped_terrain.jpg :width: 40% - .. image:: ../_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg + .. image:: ../../_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg :width: 40% Args: @@ -157,10 +157,10 @@ def pyramid_stairs_terrain(difficulty: float, cfg: hf_terrains_cfg.HfPyramidStai If the :obj:`cfg.inverted` flag is set to :obj:`True`, the terrain is inverted such that the platform is at the bottom. - .. image:: ../_static/terrains/height_field/pyramid_stairs_terrain.jpg + .. image:: ../../_static/terrains/height_field/pyramid_stairs_terrain.jpg :width: 40% - .. image:: ../_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg + .. image:: ../../_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg :width: 40% Args: @@ -218,7 +218,7 @@ def discrete_obstacles_terrain(difficulty: float, cfg: hf_terrains_cfg.HfDiscret height. They are placed randomly on the terrain with a minimum distance of :obj:`cfg.platform_width` from the center of the terrain. - .. image:: ../_static/terrains/height_field/discrete_obstacles_terrain.jpg + .. image:: ../../_static/terrains/height_field/discrete_obstacles_terrain.jpg :width: 40% :align: center @@ -303,7 +303,7 @@ def wave_terrain(difficulty: float, cfg: hf_terrains_cfg.HfWaveTerrainCfg) -> np where :math:`A` is the amplitude of the waves, :math:`\lambda` is the wavelength of the waves. - .. image:: ../_static/terrains/height_field/wave_terrain.jpg + .. image:: ../../_static/terrains/height_field/wave_terrain.jpg :width: 40% :align: center @@ -355,7 +355,7 @@ def stepping_stones_terrain(difficulty: float, cfg: hf_terrains_cfg.HfSteppingSt The terrain is a stepping stones pattern which trims to a flat platform at the center of the terrain. - .. image:: ../_static/terrains/height_field/stepping_stones_terrain.jpg + .. image:: ../../_static/terrains/height_field/stepping_stones_terrain.jpg :width: 40% :align: center diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains_cfg.py index e3928dc300..bc71285577 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/hf_terrains_cfg.py @@ -28,10 +28,8 @@ class HfTerrainBaseCfg(SubTerrainBaseCfg): vertical_scale: float = 0.005 """The discretization of the terrain along the z axis (in m). Defaults to 0.005.""" slope_threshold: float | None = None - """The slope threshold above which surfaces are made vertical. Defaults to :obj:`None`. - - If :obj:`None` no correction is applied. - """ + """The slope threshold above which surfaces are made vertical. Defaults to None, + in which case no correction is applied.""" """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/utils.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/utils.py index cdb9237b4f..c75c5c5f39 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/utils.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/height_field/utils.py @@ -106,7 +106,7 @@ def convert_height_field_to_mesh( horizontal_scale: The discretization of the terrain along the x and y axis. vertical_scale: The discretization of the terrain along the z axis. slope_threshold: The slope threshold above which surfaces are made vertical. - If :obj:`None` no correction is applied. Defaults to :obj:`None`. + Defaults to None, in which case no correction is applied. Returns: The vertices and triangles of the mesh: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_generator_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_generator_cfg.py index d17eb7c7e4..a8b6a6544d 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_generator_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_generator_cfg.py @@ -58,10 +58,8 @@ class TerrainGeneratorCfg: """Configuration for the terrain generator.""" seed: int | None = None - """The seed for the random number generator. Defaults to :obj:`None`. - - If :obj:`None`, the seed is not set. - """ + """The seed for the random number generator. Defaults to None, + in which case the seed is not set.""" curriculum: bool = False """Whether to use the curriculum mode. Defaults to False. @@ -111,7 +109,7 @@ class TerrainGeneratorCfg: slope_threshold: float | None = 0.75 """The slope threshold above which surfaces are made vertical. Defaults to 0.75. - If :obj:`None` no correction is applied. + If None no correction is applied. This value is passed on to all the height field sub-terrain configurations. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer.py index 52e396f56d..b7c375af96 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer.py @@ -52,7 +52,7 @@ class TerrainImporter: terrain_origins: torch.Tensor | None """The origins of the sub-terrains in the added terrain mesh. Shape is (num_rows, num_cols, 3). - If :obj:`None`, then it is assumed no sub-terrains exist. The environment origins are computed in a grid. + If None, then it is assumed no sub-terrains exist. The environment origins are computed in a grid. """ env_origins: torch.Tensor """The origins of the environments. Shape is (num_envs, 3).""" diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer_cfg.py index 9b0d774783..96d5c6f4f7 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/terrain_importer_cfg.py @@ -89,7 +89,7 @@ class TerrainImporterCfg: """The maximum initial terrain level for defining environment origins. Defaults to None. The terrain levels are specified by the number of rows in the grid arrangement of - sub-terrains. If :obj:`None`, then the initial terrain level is set to the maximum + sub-terrains. If None, then the initial terrain level is set to the maximum terrain level available (``num_rows - 1``). Note: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/__init__.py index 116e411370..45da894a1b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/__init__.py @@ -12,8 +12,6 @@ efficient than the height-field representation, but it is not as flexible. """ -from __future__ import annotations - from .mesh_terrains_cfg import ( MeshBoxTerrainCfg, MeshFloatingRingTerrainCfg, @@ -29,19 +27,3 @@ MeshRepeatedPyramidsTerrainCfg, MeshStarTerrainCfg, ) - -__all__ = [ - "MeshPlaneTerrainCfg", - "MeshPyramidStairsTerrainCfg", - "MeshInvertedPyramidStairsTerrainCfg", - "MeshRandomGridTerrainCfg", - "MeshRailsTerrainCfg", - "MeshPitTerrainCfg", - "MeshBoxTerrainCfg", - "MeshGapTerrainCfg", - "MeshFloatingRingTerrainCfg", - "MeshStarTerrainCfg", - "MeshRepeatedPyramidsTerrainCfg", - "MeshRepeatedBoxesTerrainCfg", - "MeshRepeatedCylindersTerrainCfg", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/mesh_terrains.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/mesh_terrains.py index 98ba6b65b2..fe1f28e61d 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/mesh_terrains.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/terrains/trimesh/mesh_terrains.py @@ -25,7 +25,7 @@ def flat_terrain( ) -> tuple[list[trimesh.Trimesh], np.ndarray]: """Generate a flat terrain as a plane. - .. image:: ../_static/terrains/trimesh/flat_terrain.jpg + .. image:: ../../_static/terrains/trimesh/flat_terrain.jpg :width: 45% :align: center @@ -58,10 +58,10 @@ def pyramid_stairs_terrain( :obj:`cfg.platform_width` (depending on the direction) with no steps in the remaining area. Additionally, no border will be added. - .. image:: ../_static/terrains/trimesh/pyramid_stairs_terrain.jpg + .. image:: ../../_static/terrains/trimesh/pyramid_stairs_terrain.jpg :width: 45% - .. image:: ../_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg + .. image:: ../../_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg :width: 45% Args: @@ -158,10 +158,10 @@ def inverted_pyramid_stairs_terrain( :obj:`cfg.platform_width` (depending on the direction) with no steps in the remaining area. Additionally, no border will be added. - .. image:: ../_static/terrains/trimesh/inverted_pyramid_stairs_terrain.jpg + .. image:: ../../_static/terrains/trimesh/inverted_pyramid_stairs_terrain.jpg :width: 45% - .. image:: ../_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg + .. image:: ../../_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg :width: 45% Args: @@ -260,10 +260,10 @@ def random_grid_terrain( If :obj:`cfg.holes` is True, the terrain will have randomized grid cells only along the plane extending from the platform (like a plus sign). The remaining area remains empty and no border will be added. - .. image:: ../_static/terrains/trimesh/random_grid_terrain.jpg + .. image:: ../../_static/terrains/trimesh/random_grid_terrain.jpg :width: 45% - .. image:: ../_static/terrains/trimesh/random_grid_terrain_with_holes.jpg + .. image:: ../../_static/terrains/trimesh/random_grid_terrain_with_holes.jpg :width: 45% Args: @@ -385,7 +385,7 @@ def rails_terrain( the platform at the center of the terrain, and the second set is extruded between the first set of rails and the terrain border. Each set of rails is extruded to the same height. - .. image:: ../_static/terrains/trimesh/rails_terrain.jpg + .. image:: ../../_static/terrains/trimesh/rails_terrain.jpg :width: 40% :align: center @@ -440,10 +440,10 @@ def pit_terrain( created by extruding a ring along the x- and y- axis. If :obj:`is_double_pit` is True, the pit contains two levels. - .. image:: ../_static/terrains/trimesh/pit_terrain.jpg + .. image:: ../../_static/terrains/trimesh/pit_terrain.jpg :width: 40% - .. image:: ../_static/terrains/trimesh/pit_terrain_with_two_levels.jpg + .. image:: ../../_static/terrains/trimesh/pit_terrain_with_two_levels.jpg :width: 40% Args: @@ -502,10 +502,10 @@ def box_terrain( The boxes are created by extruding a rectangle along the z-axis. If :obj:`double_box` is True, then two boxes of height :obj:`box_height` are stacked on top of each other. - .. image:: ../_static/terrains/trimesh/box_terrain.jpg + .. image:: ../../_static/terrains/trimesh/box_terrain.jpg :width: 40% - .. image:: ../_static/terrains/trimesh/box_terrain_with_two_boxes.jpg + .. image:: ../../_static/terrains/trimesh/box_terrain_with_two_boxes.jpg :width: 40% Args: @@ -563,7 +563,7 @@ def gap_terrain( The terrain has a ground with a platform in the middle. The platform is surrounded by a gap of width :obj:`gap_width` on all sides. - .. image:: ../_static/terrains/trimesh/gap_terrain.jpg + .. image:: ../../_static/terrains/trimesh/gap_terrain.jpg :width: 40% :align: center @@ -607,7 +607,7 @@ def floating_ring_terrain( The thickness of the ring is :obj:`ring_thickness` and the height of the ring from the terrain is :obj:`ring_height`. - .. image:: ../_static/terrains/trimesh/floating_ring_terrain.jpg + .. image:: ../../_static/terrains/trimesh/floating_ring_terrain.jpg :width: 40% :align: center @@ -653,7 +653,7 @@ def star_terrain( with a width of :obj:`bar_width` and a height of :obj:`bar_height`. The bars are evenly spaced around the cylinder and connect to the peripheral of the terrain. - .. image:: ../_static/terrains/trimesh/star_terrain.jpg + .. image:: ../../_static/terrains/trimesh/star_terrain.jpg :width: 40% :align: center @@ -732,13 +732,13 @@ def repeated_objects_terrain( The object parameters are specified in the configuration as curriculum parameters. The difficulty is used to linearly interpolate between the minimum and maximum values of the parameters. - .. image:: ../_static/terrains/trimesh/repeated_objects_cylinder_terrain.jpg + .. image:: ../../_static/terrains/trimesh/repeated_objects_cylinder_terrain.jpg :width: 30% - .. image:: ../_static/terrains/trimesh/repeated_objects_box_terrain.jpg + .. image:: ../../_static/terrains/trimesh/repeated_objects_box_terrain.jpg :width: 30% - .. image:: ../_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg + .. image:: ../../_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg :width: 30% Args: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/__init__.py index 3c54008711..d81f600d31 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/__init__.py @@ -3,53 +3,10 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Sub-module containing utilities for the Orbit framework. +"""Sub-package containing utilities for common operations and helper functions.""" -* `configclass`: Provides wrapper around `dataclass` for working with configurations. -* `dict`: Provides helper functions for converting dictionaries and type-cases. -* `kit`: Provides helper functions for Omniverse kit (USD operations). -* `math`: Provides helper functions for math operations. -* `string`: Provides helper functions for string operations. -* `timer`: Provides a timer class (uses contextlib) for benchmarking. -""" - -from .array import TENSOR_TYPE_CONVERSIONS, TENSOR_TYPES, TensorData, convert_to_torch +from .array import * from .configclass import configclass -from .dict import class_to_dict, convert_dict_to_backend, print_dict, update_class_from_dict, update_dict -from .string import ( - callable_to_string, - is_lambda_expression, - resolve_matching_names, - resolve_matching_names_values, - string_to_callable, - to_camel_case, - to_snake_case, -) +from .dict import * +from .string import * from .timer import Timer - -__all__ = [ - # arrays - "TensorData", - "TENSOR_TYPES", - "TENSOR_TYPE_CONVERSIONS", - "convert_to_torch", - # config wrapper - "configclass", - # dictionary utilities - "class_to_dict", - "convert_dict_to_backend", - "print_dict", - "update_class_from_dict", - "update_dict", - # string utilities - "to_camel_case", - "to_snake_case", - "is_lambda_expression", - "string_to_callable", - "callable_to_string", - "resolve_matching_names", - "resolve_matching_names_values", - # timer - "Timer", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/array.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/array.py index 0b50e80bfd..fb377278fa 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/array.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/array.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Utilities for working with different array backends.""" +"""Sub-module containing utilities for working with different array backends.""" from __future__ import annotations @@ -13,8 +13,6 @@ import warp as wp -__all__ = ["TensorData", "TENSOR_TYPES", "TENSOR_TYPE_CONVERSIONS", "convert_to_torch"] - TensorData = Union[np.ndarray, torch.Tensor, wp.array] """Type definition for a tensor data. @@ -55,7 +53,7 @@ def convert_to_torch( list/tuples, it is converted to a torch tensor. If the array is already a torch tensor, it is returned directly. - If ``device`` is :obj:`None`, then the function deduces the current device of the data. For numpy arrays, + If ``device`` is None, then the function deduces the current device of the data. For numpy arrays, this defaults to "cpu", for torch tensors it is "cpu" or "cuda", and for warp arrays it is "cuda". Note: diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/assets.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/assets.py index 8b15ad22ad..1ffc0fc779 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/assets.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/assets.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Defines the host-server where assets and resources are stored. +"""Sub-module that defines the host-server where assets and resources are stored. By default, we use the Isaac Sim Nucleus Server for hosting assets and resources. This makes distribution of the assets easier and makes the repository smaller in size code-wise. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/configclass.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/configclass.py index f7c2f9b30c..f772c7e853 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/configclass.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/configclass.py @@ -5,7 +5,7 @@ from __future__ import annotations -"""Wrapper around the Python 3.7 onwards `dataclasses` module.""" +"""Sub-module that provides a wrapper around the Python 3.7 onwards ``dataclasses`` module.""" import inspect import sys @@ -15,10 +15,6 @@ from .dict import class_to_dict, update_class_from_dict -# List of all methods provided by sub-module. -__all__ = ["configclass"] - - _CONFIGCLASS_METHODS = ["to_dict", "from_dict", "replace", "copy"] """List of class methods added at runtime to dataclass.""" @@ -36,41 +32,57 @@ def __dataclass_transform__(): def configclass(cls, **kwargs): """Wrapper around `dataclass` functionality to add extra checks and utilities. - As of Python3.8, the standard dataclasses have two main issues which makes them non-generic for configuration use-cases. - These include: + As of Python 3.7, the standard dataclasses have two main issues which makes them non-generic for + configuration use-cases. These include: 1. Requiring a type annotation for all its members. 2. Requiring explicit usage of :meth:`field(default_factory=...)` to reinitialize mutable variables. - This function wraps around :class:`dataclass` utility to deal with the above two issues. + This function provides a decorator that wraps around Python's `dataclass`_ utility to deal with + the above two issues. It also provides additional helper functions for dictionary <-> class + conversion and easily copying class instances. Usage: - .. code-block:: python - from dataclasses import MISSING + .. code-block:: python + + from dataclasses import MISSING + + from omni.isaac.orbit.utils.configclass import configclass + - from omni.isaac.orbit.utils.configclass import configclass + @configclass + class ViewerCfg: + eye: list = [7.5, 7.5, 7.5] # field missing on purpose + lookat: list = field(default_factory=[0.0, 0.0, 0.0]) - @configclass - class ViewerCfg: - eye: list = [7.5, 7.5, 7.5] # field missing on purpose - lookat: list = field(default_factory=[0.0, 0.0, 0.0]) + @configclass + class EnvCfg: + num_envs: int = MISSING + episode_length: int = 2000 + viewer: ViewerCfg = ViewerCfg() + # create configuration instance + env_cfg = EnvCfg(num_envs=24) - @configclass - class EnvCfg: - num_envs: int = MISSING - episode_length: int = 2000 - viewer: ViewerCfg = ViewerCfg() + # print information as a dictionary + print(env_cfg.to_dict()) - # create configuration instance - env_cfg = EnvCfg(num_envs=24) - # print information - print(env_cfg.to_dict()) + # create a copy of the configuration + env_cfg_copy = env_cfg.copy() - Reference: - https://docs.python.org/3/library/dataclasses.html#dataclasses.Field + # replace arbitrary fields using keyword arguments + env_cfg_copy = env_cfg_copy.replace(num_envs=32) + + Args: + cls: The class to wrap around. + **kwargs: Additional arguments to pass to :func:`dataclass`. + + Returns: + The wrapped class. + + .. _dataclass: https://docs.python.org/3/library/dataclasses.html """ # add type annotations _add_annotation_types(cls) @@ -130,14 +142,16 @@ def _replace_class_with_kwargs(obj: object, **kwargs) -> object: This is especially useful for frozen classes. Example usage: - @configclass(frozen=True) - class C: - x: int - y: int + .. code-block:: python + + @configclass(frozen=True) + class C: + x: int + y: int - c = C(1, 2) - c1 = c.replace(x=3) - assert c1.x == 3 and c1.y == 2 + c = C(1, 2) + c1 = c.replace(x=3) + assert c1.x == 3 and c1.y == 2 Args: obj: The object to replace. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/dict.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/dict.py index 487bf15dd9..729490cb96 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/dict.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/dict.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Utilities for working with dictionaries.""" +"""Sub-module for utilities for working with dictionaries.""" from __future__ import annotations @@ -15,15 +15,6 @@ from .array import TENSOR_TYPE_CONVERSIONS, TENSOR_TYPES from .string import callable_to_string, string_to_callable -__all__ = [ - "class_to_dict", - "update_class_from_dict", - "dict_to_md5_hash", - "convert_dict_to_backend", - "update_dict", - "print_dict", -] - """ Dictionary <-> Class operations. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/io/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/io/__init__.py index eb117746f3..a841953a4b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/io/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/io/__init__.py @@ -9,5 +9,3 @@ from .pkl import dump_pickle, load_pickle from .yaml import dump_yaml, load_yaml - -__all__ = ["load_pickle", "dump_pickle", "load_yaml", "dump_yaml"] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/math.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/math.py index b3361861f3..472dbe4a53 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/math.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/math.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Provides utilities for math operations. +"""Sub-module containing utilities for various math operations. Some of these are imported from the module `omni.isaac.core.utils.torch` for convenience. """ @@ -15,81 +15,51 @@ import torch.nn.functional from typing_extensions import Literal -from omni.isaac.core.utils.torch.maths import normalize, scale_transform, unscale_transform -from omni.isaac.core.utils.torch.rotations import ( - quat_apply, - quat_conjugate, - quat_from_angle_axis, - quat_mul, - quat_rotate, - quat_rotate_inverse, -) - -__all__ = [ - # General - "wrap_to_pi", - "saturate", - "copysign", - # General-Isaac Sim - "normalize", - "scale_transform", - "unscale_transform", - # Rotation - "matrix_from_quat", - "matrix_from_euler", - "quat_inv", - "quat_from_euler_xyz", - "quat_from_matrix", - "quat_apply_yaw", - "quat_box_minus", - "quat_error_magnitude", - "yaw_quat", - "euler_xyz_from_quat", - "axis_angle_from_quat", - # Rotation-Isaac Sim - "quat_apply", - "quat_from_angle_axis", - "quat_mul", - "quat_conjugate", - "quat_rotate", - "quat_rotate_inverse", - # Transformations - "is_identity_pose", - "combine_frame_transforms", - "subtract_frame_transforms", - "compute_pose_error", - "apply_delta_pose", - "transform_points", - # Projection - "unproject_depth", - "project_points", - # Sampling - "default_orientation", - "random_orientation", - "random_yaw_orientation", - "sample_triangle", - "sample_uniform", -] - """ General """ @torch.jit.script -def wrap_to_pi(angles: torch.Tensor) -> torch.Tensor: - """Wraps input angles (in radians) to the range [-pi, pi]. +def scale_transform(x: torch.Tensor, lower: torch.Tensor, upper: torch.Tensor) -> torch.Tensor: + """Normalizes a given input tensor to a range of [-1, 1]. + + .. note:: + It uses pytorch broadcasting functionality to deal with batched input. Args: - angles: Input angles. + x: Input tensor of shape (N, dims). + lower: The minimum value of the tensor. Shape is (N, dims) or (dims,). + upper: The maximum value of the tensor. Shape is (N, dims) or (dims,). Returns: - Angles in the range [-pi, pi]. + Normalized transform of the tensor. Shape is (N, dims). """ - angles = angles.clone() - angles %= 2 * torch.pi - angles -= 2 * torch.pi * (angles > torch.pi) - return angles + # default value of center + offset = (lower + upper) * 0.5 + # return normalized tensor + return 2 * (x - offset) / (upper - lower) + + +@torch.jit.script +def unscale_transform(x: torch.Tensor, lower: torch.Tensor, upper: torch.Tensor) -> torch.Tensor: + """De-normalizes a given input tensor from range of [-1, 1] to (lower, upper). + + .. note:: + It uses pytorch broadcasting functionality to deal with batched input. + + Args: + x: Input tensor of shape (N, dims). + lower: The minimum value of the tensor. Shape is (N, dims) or (dims,). + upper: The maximum value of the tensor. Shape is (N, dims) or (dims,). + + Returns: + De-normalized transform of the tensor. Shape is (N, dims). + """ + # default value of center + offset = (lower + upper) * 0.5 + # return normalized tensor + return x * (upper - lower) * 0.5 + offset @torch.jit.script @@ -100,15 +70,45 @@ def saturate(x: torch.Tensor, lower: torch.Tensor, upper: torch.Tensor) -> torch Args: x: Input tensor of shape (N, dims). - lower: The minimum value of the tensor. Shape (dims,) - upper: The maximum value of the tensor. Shape (dims,) + lower: The minimum value of the tensor. Shape is (N, dims) or (dims,). + upper: The maximum value of the tensor. Shape is (N, dims) or (dims,). Returns: - Clamped transform of the tensor. Shape (N, dims) + Clamped transform of the tensor. Shape is (N, dims). """ return torch.max(torch.min(x, upper), lower) +@torch.jit.script +def normalize(x: torch.Tensor, eps: float = 1e-9) -> torch.Tensor: + """Normalizes a given input tensor to unit length. + + Args: + x: Input tensor of shape (N, dims). + eps: A small value to avoid division by zero. Defaults to 1e-9. + + Returns: + Normalized tensor of shape (N, dims). + """ + return x / x.norm(p=2, dim=-1).clamp(min=eps, max=None).unsqueeze(-1) + + +@torch.jit.script +def wrap_to_pi(angles: torch.Tensor) -> torch.Tensor: + """Wraps input angles (in radians) to the range [-pi, pi]. + + Args: + angles: Input angles of any shape. + + Returns: + Angles in the range [-pi, pi]. + """ + angles = angles.clone() + angles %= 2 * torch.pi + angles -= 2 * torch.pi * (angles > torch.pi) + return angles + + @torch.jit.script def copysign(mag: float, other: torch.Tensor) -> torch.Tensor: """Create a new floating-point tensor with the magnitude of input and the sign of other, element-wise. @@ -137,14 +137,13 @@ def matrix_from_quat(quaternions: torch.Tensor) -> torch.Tensor: """Convert rotations given as quaternions to rotation matrices. Args: - quaternions: quaternions with real part first, - as tensor of shape (..., 4). + quaternions: The quaternion orientation in (w, x, y, z). Shape is (..., 4). Returns: - Rotation matrices as tensor of shape (..., 3, 3). + Rotation matrices. The shape is (..., 3, 3). Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L41-L70) + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L41-L70 """ r, i, j, k = torch.unbind(quaternions, -1) # pyre-fixme[58]: `/` is not supported for operand types `float` and `Tensor`. @@ -174,7 +173,7 @@ def convert_quat(quat: torch.Tensor | np.ndarray, to: Literal["xyzw", "wxyz"] = then the input is in 'wxyz' format, and vice-versa. Args: - quat: Input quaternion of shape (..., 4). + quat: The quaternion of shape (..., 4). to: Convention to convert quaternion to.. Defaults to "xyzw". Returns: @@ -213,15 +212,30 @@ def convert_quat(quat: torch.Tensor | np.ndarray, to: Literal["xyzw", "wxyz"] = return quat.roll(1, dims=-1) +@torch.jit.script +def quat_conjugate(q: torch.Tensor) -> torch.Tensor: + """Computes the conjugate of a quaternion. + + Args: + q: The quaternion orientation in (w, x, y, z). Shape is (..., 4). + + Returns: + The conjugate quaternion in (w, x, y, z). Shape is (..., 4). + """ + shape = q.shape + q = q.reshape(-1, 4) + return torch.cat((q[:, 0:1], -q[:, 1:]), dim=-1).view(shape) + + @torch.jit.script def quat_inv(q: torch.Tensor) -> torch.Tensor: """Compute the inverse of a quaternion. Args: - q: The input quaternion (w, x, y, z). + q: The quaternion orientation in (w, x, y, z). Shape is (N, 4). Returns: - The inverse quaternion (w, x, y, z). + The inverse quaternion in (w, x, y, z). Shape is (N, 4). """ return normalize(quat_conjugate(q)) @@ -234,12 +248,12 @@ def quat_from_euler_xyz(roll: torch.Tensor, pitch: torch.Tensor, yaw: torch.Tens The euler angles are assumed in XYZ convention. Args: - roll: Rotation around x-axis (in radians). Shape is ``(N,)`` - pitch: Rotation around y-axis (in radians). Shape is ``(N,)`` - yaw: Rotation around z-axis (in radians). Shape is ``(N,)`` + roll: Rotation around x-axis (in radians). Shape is (N,). + pitch: Rotation around y-axis (in radians). Shape is (N,). + yaw: Rotation around z-axis (in radians). Shape is (N,). Returns: - Quaternion with real part in the start. Shape is ``(N, 4)`` + The quaternion in (w, x, y, z). Shape is (N, 4). """ cy = torch.cos(yaw * 0.5) sy = torch.sin(yaw * 0.5) @@ -261,7 +275,7 @@ def _sqrt_positive_part(x: torch.Tensor) -> torch.Tensor: """Returns torch.sqrt(torch.max(0, x)) but with a zero sub-gradient where x is 0. Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L91-L99) + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L91-L99 """ ret = torch.zeros_like(x) positive_mask = x > 0 @@ -274,13 +288,13 @@ def quat_from_matrix(matrix: torch.Tensor) -> torch.Tensor: """Convert rotations given as rotation matrices to quaternions. Args: - matrix: Rotation matrices as tensor of shape (..., 3, 3). + matrix: The rotation matrices. Shape is (..., 3, 3). Returns: - quaternions with real part first, as tensor of shape (..., 4). + The quaternion in (w, x, y, z). Shape is (..., 4). Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L102-L161) + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L102-L161 """ if matrix.size(-1) != 3 or matrix.size(-2) != 3: raise ValueError(f"Invalid rotation matrix shape {matrix.shape}.") @@ -303,17 +317,13 @@ def quat_from_matrix(matrix: torch.Tensor) -> torch.Tensor: # we produce the desired quaternion multiplied by each of r, i, j, k quat_by_rijk = torch.stack( [ - # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and - # `int`. + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and `int`. torch.stack([q_abs[..., 0] ** 2, m21 - m12, m02 - m20, m10 - m01], dim=-1), - # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and - # `int`. + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and `int`. torch.stack([m21 - m12, q_abs[..., 1] ** 2, m10 + m01, m02 + m20], dim=-1), - # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and - # `int`. + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and `int`. torch.stack([m02 - m20, m10 + m01, q_abs[..., 2] ** 2, m12 + m21], dim=-1), - # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and - # `int`. + # pyre-fixme[58]: `**` is not supported for operand types `Tensor` and `int`. torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1), ], dim=-2, @@ -326,7 +336,6 @@ def quat_from_matrix(matrix: torch.Tensor) -> torch.Tensor: # if not for numerical problems, quat_candidates[i] should be same (up to a sign), # forall i; we pick the best-conditioned one (with the largest denominator) - return quat_candidates[torch.nn.functional.one_hot(q_abs.argmax(dim=-1), num_classes=4) > 0.5, :].reshape( batch_dim + (4,) ) @@ -338,15 +347,14 @@ def _axis_angle_rotation(axis: Literal["X", "Y", "Z"], angle: torch.Tensor) -> t Args: axis: Axis label "X" or "Y or "Z". - angle: Any shape tensor of Euler angles in radians. + angle: Euler angles in radians of any shape. Returns: - Rotation matrices as tensor of shape (..., 3, 3). + Rotation matrices. Shape is (..., 3, 3). Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L164-L191) + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L164-L191 """ - cos = torch.cos(angle) sin = torch.sin(angle) one = torch.ones_like(angle) @@ -369,16 +377,16 @@ def matrix_from_euler(euler_angles: torch.Tensor, convention: str) -> torch.Tens Convert rotations given as Euler angles in radians to rotation matrices. Args: - euler_angles: Euler angles in radians as tensor of shape (..., 3). + euler_angles: Euler angles in radians. Shape is (..., 3). convention: Convention string of three uppercase letters from {"X", "Y", and "Z"}. For example, "XYZ" means that the rotations should be applied first about x, then y, then z. Returns: - Rotation matrices as tensor of shape (..., 3, 3). + Rotation matrices. Shape is (..., 3, 3). Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L194-L220) + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L194-L220 """ if euler_angles.dim() == 0 or euler_angles.shape[-1] != 3: raise ValueError("Invalid input euler angles.") @@ -401,14 +409,14 @@ def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, Note: The euler angles are assumed in XYZ convention. - Reference: - https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles - Args: - quat: Quaternion with real part in the start. Shape is ``(N, 4)`` + quat: The quaternion orientation in (w, x, y, z). Shape is (N, 4). Returns: - A tuple containing roll-pitch-yaw. + A tuple containing roll-pitch-yaw. Each element is a tensor of shape (N,). + + Reference: + https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles """ q_w, q_x, q_y, q_z = quat[:, 0], quat[:, 1], quat[:, 2], quat[:, 3] # roll (x-axis rotation) @@ -428,16 +436,78 @@ def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, return roll % (2 * torch.pi), pitch % (2 * torch.pi), yaw % (2 * torch.pi) # TODO: why not wrap_to_pi here ? +@torch.jit.script +def quat_mul(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: + """Multiply two quaternions together. + + Args: + q1: The first quaternion in (w, x, y, z). Shape is (..., 4). + q2: The second quaternion in (w, x, y, z). Shape is (..., 4). + + Returns: + The product of the two quaternions in (w, x, y, z). Shape is (..., 4). + + Raises: + ValueError: Input shapes of ``q1`` and ``q2`` are not matching. + """ + # check input is correct + if q1.shape != q2.shape: + msg = f"Expected input quaternion shape mismatch: {q1.shape} != {q2.shape}." + raise ValueError(msg) + # reshape to (N, 4) for multiplication + shape = q1.shape + q1 = q1.reshape(-1, 4) + q2 = q2.reshape(-1, 4) + # extract components from quaternions + w1, x1, y1, z1 = q1[:, 0], q1[:, 1], q1[:, 2], q1[:, 3] + w2, x2, y2, z2 = q2[:, 0], q2[:, 1], q2[:, 2], q2[:, 3] + # perform multiplication + ww = (z1 + x1) * (x2 + y2) + yy = (w1 - y1) * (w2 + z2) + zz = (w1 + y1) * (w2 - z2) + xx = ww + yy + zz + qq = 0.5 * (xx + (z1 - x1) * (x2 - y2)) + w = qq - ww + (z1 - y1) * (y2 - z2) + x = qq - xx + (x1 + w1) * (x2 + w2) + y = qq - yy + (w1 - x1) * (y2 + z2) + z = qq - zz + (z1 + y1) * (w2 - x2) + + return torch.stack([w, x, y, z], dim=-1).view(shape) + + +@torch.jit.script +def quat_box_minus(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: + """The box-minus operator (quaternion difference) between two quaternions. + + Args: + q1: The first quaternion in (w, x, y, z). Shape is (N, 4). + q2: The second quaternion in (w, x, y, z). Shape is (N, 4). + + Returns: + The difference between the two quaternions. Shape is (N, 3). + + Reference: + https://docs.leggedrobotics.com/kindr/cheatsheet_latest.pdf + """ + quat_diff = quat_mul(q1, quat_conjugate(q2)) # q1 * q2^-1 + re = quat_diff[:, 0] # real part, q = [w, x, y, z] = [re, im] + im = quat_diff[:, 1:] # imaginary part + norm_im = torch.norm(im, dim=1) + scale = 2.0 * torch.where(norm_im > 1.0e-7, torch.atan(norm_im / re) / norm_im, torch.sign(re)) + return scale.unsqueeze(-1) * im + + @torch.jit.script def yaw_quat(quat: torch.Tensor) -> torch.Tensor: """Extract the yaw component of a quaternion. Args: - quat: Input orientation in ``(w, x, y, z)`` to extract yaw from. + quat: The orientation in (w, x, y, z). Shape is (..., 4) Returns: A quaternion with only yaw component. """ + shape = quat.shape quat_yaw = quat.clone().view(-1, 4) qw = quat_yaw[:, 0] qx = quat_yaw[:, 1] @@ -448,7 +518,29 @@ def yaw_quat(quat: torch.Tensor) -> torch.Tensor: quat_yaw[:, 3] = torch.sin(yaw / 2) quat_yaw[:, 0] = torch.cos(yaw / 2) quat_yaw = normalize(quat_yaw) - return quat_yaw + return quat_yaw.view(shape) + + +@torch.jit.script +def quat_apply(quat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor: + """Apply a quaternion rotation to a vector. + + Args: + quat: The quaternion in (w, x, y, z). Shape is (..., 4). + vec: The vector in (x, y, z). Shape is (..., 3). + + Returns: + The rotated vector in (x, y, z). Shape is (..., 3). + """ + # store shape + shape = vec.shape + # reshape to (N, 3) for multiplication + quat = quat.reshape(-1, 4) + vec = vec.reshape(-1, 3) + # extract components from quaternions + xyz = quat[:, 1:] + t = xyz.cross(vec, dim=-1) * 2 + return (vec + quat[:, 0:1] * t + xyz.cross(t, dim=-1)).view(shape) @torch.jit.script @@ -456,36 +548,71 @@ def quat_apply_yaw(quat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor: """Rotate a vector only around the yaw-direction. Args: - quat: Input orientation in ``(w, x, y, z)`` to extract yaw from. - vec: Input vector. + quat: The orientation in (w, x, y, z). Shape is (N, 4). + vec: The vector in (x, y, z). Shape is (N, 3). Returns: - Rotated vector. + The rotated vector in (x, y, z). Shape is (N, 3). """ quat_yaw = yaw_quat(quat) return quat_apply(quat_yaw, vec) @torch.jit.script -def quat_box_minus(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: - """Implements box-minus operator (quaternion difference). +def quat_rotate(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + """Rotate a vector by a quaternion. Args: - q1: A (N, 4) tensor for quaternion (w, x, y, z). - q2: A (N, 4) tensor for quaternion (w, x, y, z). + q: The quaternion in (w, x, y, z). Shape is (N, 4). + v: The vector in (x, y, z). Shape is (N, 3). Returns: - q1 box-minus q2 + The rotated vector in (x, y, z). Shape is (N, 3). + """ + shape = q.shape + q_w = q[:, 0] + q_vec = q[:, 1:] + a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1) + b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0 + c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0 + return a + b + c - Reference: - https://docs.leggedrobotics.com/kindr/cheatsheet_latest.pdf + +@torch.jit.script +def quat_rotate_inverse(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + """Rotate a vector by the inverse of a quaternion. + + Args: + q: The quaternion in (w, x, y, z). Shape is (N, 4). + v: The vector in (x, y, z). Shape is (N, 3). + + Returns: + The rotated vector in (x, y, z). Shape is (N, 3). """ - quat_diff = quat_mul(q1, quat_conjugate(q2)) # q1 * q2^-1 - re = quat_diff[:, 0] # real part, q = [w, x, y, z] = [re, im] - im = quat_diff[:, 1:] # imaginary part - norm_im = torch.norm(im, dim=1) - scale = 2.0 * torch.where(norm_im > 1.0e-7, torch.atan(norm_im / re) / norm_im, torch.sign(re)) - return scale.unsqueeze(-1) * im + shape = q.shape + q_w = q[:, 0] + q_vec = q[:, 1:] + a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1) + b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0 + c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0 + return a - b + c + + +@torch.jit.script +def quat_from_angle_axis(angle: torch.Tensor, axis: torch.Tensor) -> torch.Tensor: + """Convert rotations given as angle-axis to quaternions. + + Args: + angle: The angle turned anti-clockwise in radians around the vector's direction. Shape is (N,). + axis: The axis of rotation. Shape is (N, 3). + + Returns: + The quaternion in (w, x, y, z). Shape is (N, 4). + """ + theta = (angle / 2).unsqueeze(-1) + xyz = normalize(axis) * theta.sin() + w = theta.cos() + return normalize(torch.cat([w, xyz], dim=-1)) @torch.jit.script @@ -493,15 +620,15 @@ def axis_angle_from_quat(quat: torch.Tensor, eps: float = 1.0e-6) -> torch.Tenso """Convert rotations given as quaternions to axis/angle. Args: - quat: quaternions with real part first, as tensor of shape (..., 4). + quat: The quaternion orientation in (w, x, y, z). Shape is (..., 4). eps: The tolerance for Taylor approximation. Defaults to 1.0e-6. Returns: - Rotations given as a vector in axis angle form, as a tensor of shape (..., 3), - where the magnitude is the angle turned anti-clockwise in radians around the vector's direction. + Rotations given as a vector in axis angle form. Shape is (..., 3). + The vector's magnitude is the angle turned anti-clockwise in radians around the vector's direction. Reference: - Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L526-L554) + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L526-L554 """ # Modified to take in quat as [q_w, q_x, q_y, q_z] # Quaternion is [q_w, q_x, q_y, q_z] = [cos(theta/2), n_x * sin(theta/2), n_y * sin(theta/2), n_z * sin(theta/2)] @@ -525,8 +652,8 @@ def quat_error_magnitude(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: """Computes the rotation difference between two quaternions. Args: - q1: A (N, 4) tensor for quaternion (w, x, y, z). - q2: A (N, 4) tensor for quaternion (w, x, y, z). + q1: The first quaternion in (w, x, y, z). Shape is (N, 4). + q2: The second quaternion in (w, x, y, z). Shape is (N, 4). Returns: Angular error between input quaternions in radians. @@ -548,7 +675,7 @@ def is_identity_pose(pos: torch.tensor, rot: torch.tensor) -> bool: Args: pos: The cartesian position. Shape is (N, 3). - rot: The quaternion orientation in (w, x, y, z). Shape is (N, 4). + rot: The quaternion in (w, x, y, z). Shape is (N, 4). Returns: True if all the input poses result in identity transform. Otherwise, False. @@ -571,13 +698,14 @@ def combine_frame_transforms( where :math:`T_{AB}` is the homogeneous transformation matrix from frame A to B. Args: - t01: Position of frame 1 w.r.t. frame 0. - q01: Quaternion orientation of frame 1 w.r.t. frame 0. - t12: Position of frame 2 w.r.t. frame 1. - q12: Quaternion orientation of frame 2 w.r.t. frame 1. + t01: Position of frame 1 w.r.t. frame 0. Shape is (N, 3). + q01: Quaternion orientation of frame 1 w.r.t. frame 0 in (w, x, y, z). Shape is (N, 4). + t12: Position of frame 2 w.r.t. frame 1. Shape is (N, 3). + q12: Quaternion orientation of frame 2 w.r.t. frame 1 in (w, x, y, z). Shape is (N, 4). Returns: A tuple containing the position and orientation of frame 2 w.r.t. frame 0. + Shape of the tensors are (N, 3) and (N, 4) respectively. """ # compute orientation if q12 is not None: @@ -603,13 +731,14 @@ def subtract_frame_transforms( where :math:`T_{AB}` is the homogeneous transformation matrix from frame A to B. Args: - t01: Position of frame 1 w.r.t. frame 0. - q01: Quaternion orientation of frame 1 w.r.t. frame 0 in (w, x, y, z). - t02: Position of frame 2 w.r.t. frame 0. - q02: Quaternion orientation of frame 2 w.r.t. frame 0 in (w, x, y, z). + t01: Position of frame 1 w.r.t. frame 0. Shape is (N, 3). + q01: Quaternion orientation of frame 1 w.r.t. frame 0 in (w, x, y, z). Shape is (N, 4). + t02: Position of frame 2 w.r.t. frame 0. Shape is (N, 3). + q02: Quaternion orientation of frame 2 w.r.t. frame 0 in (w, x, y, z). Shape is (N, 4). Returns: A tuple containing the position and orientation of frame 2 w.r.t. frame 1. + Shape of the tensors are (N, 3) and (N, 4) respectively. """ # compute orientation q10 = quat_inv(q01) @@ -631,18 +760,21 @@ def compute_pose_error( """Compute the position and orientation error between source and target frames. Args: - t01: Position of source frame. - q01: Quaternion orientation of source frame. - t02: Position of target frame. - q02: Quaternion orientation of target frame. + t01: Position of source frame. Shape is (N, 3). + q01: Quaternion orientation of source frame in (w, x, y, z). Shape is (N, 4). + t02: Position of target frame. Shape is (N, 3). + q02: Quaternion orientation of target frame in (w, x, y, z). Shape is (N, 4). rot_error_type: The rotation error type to return: "quat", "axis_angle". Defaults to "axis_angle". Returns: - A tuple containing position and orientation error. + A tuple containing position and orientation error. Shape of position error is (N, 3). + Shape of orientation error depends on the value of :attr:`rot_error_type`: - - If :attr:`rot_error_type` is "quat", the orientation error is returned as a quaternion. - - If :attr:`rot_error_type` is "axis_angle", the orientation error is returned as an axis-angle vector. + - If :attr:`rot_error_type` is "quat", the orientation error is returned + as a quaternion. Shape is (N, 4). + - If :attr:`rot_error_type` is "axis_angle", the orientation error is + returned as an axis-angle vector. Shape is (N, 3). Raises: ValueError: Invalid rotation error type. @@ -688,6 +820,7 @@ def apply_delta_pose( Returns: A tuple containing the displaced position and orientation frames. + Shape of the tensors are (N, 3) and (N, 4) respectively. """ # number of poses given num_poses = source_pos.shape[0] @@ -726,7 +859,7 @@ def transform_points( positions and quaternions or a single position and quaternion. If the inputs `pos` and `quat` are a single position and quaternion, the same transformation is applied to all points in the batch. - If either the inputs `pos` and `quat` are :obj:`None`, the corresponding transformation is not applied. + If either the inputs :attr:`pos` and :attr:`quat` are None, the corresponding transformation is not applied. Args: points: Points to transform. Shape is (N, P, 3) or (P, 3). @@ -813,7 +946,6 @@ def unproject_depth(depth: torch.Tensor, intrinsics: torch.Tensor) -> torch.Tens Raises: ValueError: When depth is not of shape (H, W) or (H, W, 1) or (N, H, W) or (N, H, W, 1). ValueError: When intrinsics is not of shape (3, 3) or (N, 3, 3). - """ depth_batch = depth.clone() intrinsics_batch = intrinsics.clone() @@ -928,7 +1060,7 @@ def default_orientation(num: int, device: str) -> torch.Tensor: device: Device to create tensor on. Returns: - Identity quaternion (w, x, y, z). + Identity quaternion in (w, x, y, z). Shape is (num, 4). """ quat = torch.zeros((num, 4), dtype=torch.float, device=device) quat[..., 0] = 1.0 @@ -940,15 +1072,15 @@ def default_orientation(num: int, device: str) -> torch.Tensor: def random_orientation(num: int, device: str) -> torch.Tensor: """Returns sampled rotation in 3D as quaternion. - Reference: - https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.random.html - Args: num: The number of rotations to sample. device: Device to create tensor on. Returns: - Sampled quaternion (w, x, y, z). + Sampled quaternion in (w, x, y, z). Shape is (num, 4). + + Reference: + https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.random.html """ # sample random orientation from normal distribution quat = torch.randn((num, 4), dtype=torch.float, device=device) @@ -965,7 +1097,7 @@ def random_yaw_orientation(num: int, device: str) -> torch.Tensor: device: Device to create tensor on. Returns: - Sampled quaternion (w, x, y, z). + Sampled quaternion in (w, x, y, z). Shape is (num, 4). """ roll = torch.zeros(num, dtype=torch.float, device=device) pitch = torch.zeros(num, dtype=torch.float, device=device) @@ -984,7 +1116,7 @@ def sample_triangle(lower: float, upper: float, size: int | tuple[int, ...], dev device: Device to create tensor on. Returns: - Sampled tensor of shape :obj:`size`. + Sampled tensor. Shape is based on :attr:`size`. """ # convert to tuple if isinstance(size, int): @@ -1011,7 +1143,7 @@ def sample_uniform( device: Device to create tensor on. Returns: - Sampled tensor of shape :obj:`size`. + Sampled tensor. Shape is based on :attr:`size`. """ # convert to tuple if isinstance(size, int): @@ -1038,7 +1170,7 @@ def sample_cylinder( device: Device to create tensor on. Returns: - Sampled tensor of shape :obj:`(*size, 3)`. + Sampled tensor. Shape is :obj:`(*size, 3)`. """ # sample angles angles = (torch.rand(size, device=device) * 2 - 1) * torch.pi diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/__init__.py index 933723e234..d51b0db6ce 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/__init__.py @@ -3,6 +3,27 @@ # # SPDX-License-Identifier: BSD-3-Clause -from .noise_cfg import AdditiveGaussianNoiseCfg, AdditiveUniformNoiseCfg, ConstantBiasNoiseCfg, NoiseCfg +"""Sub-module containing different noise models implementations. -__all__ = ["NoiseCfg", "AdditiveGaussianNoiseCfg", "AdditiveUniformNoiseCfg", "ConstantBiasNoiseCfg"] +The noise models are implemented as functions that take in a tensor and a configuration and return a tensor +with the noise applied. These functions are then used in the :class:`NoiseCfg` configuration class. + +Usage: + +.. code-block:: python + + import torch + from omni.isaac.orbit.utils.noise import AdditiveGaussianNoiseCfg + + # create a random tensor + my_tensor = torch.rand(128, 128, device="cuda") + + # create a noise configuration + cfg = AdditiveGaussianNoiseCfg(mean=0.0, std=1.0) + + # apply the noise + my_noisified_tensor = cfg.func(my_tensor, cfg) + +""" +from .noise_cfg import NoiseCfg # noqa: F401 +from .noise_cfg import AdditiveGaussianNoiseCfg, AdditiveUniformNoiseCfg, ConstantBiasNoiseCfg diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/noise_cfg.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/noise_cfg.py index c03964f98d..4ada243ebe 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/noise_cfg.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/noise/noise_cfg.py @@ -16,7 +16,7 @@ @configclass class NoiseCfg: - """Configuration for a noise term.""" + """Base configuration for a noise term.""" func: Callable[[torch.Tensor, NoiseCfg], torch.Tensor] = MISSING """The function to be called for applying the noise. diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/string.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/string.py index 31aa93938a..2cf8bbfa51 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/string.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/string.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Transformations of strings.""" +"""Sub-module containing utilities for transforming strings and regular expressions.""" from __future__ import annotations @@ -13,17 +13,6 @@ import re from typing import Any, Callable, Sequence -__all__ = [ - "to_camel_case", - "to_snake_case", - "is_lambda_expression", - "string_to_callable", - "callable_to_string", - "resolve_matching_names", - "resolve_matching_names_values", -] - - """ String formatting. """ diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/timer.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/timer.py index 009447d124..07ce0af03b 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/timer.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/timer.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Defines timer class for performance measurements.""" +"""Sub-module for a timer class that can be used for performance measurements.""" from __future__ import annotations diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/__init__.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/__init__.py index 404c8e8be3..dd8455e971 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/__init__.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/__init__.py @@ -3,13 +3,6 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Operations based on warp.""" - -from __future__ import annotations +"""Sub-module containing operations based on warp.""" from .ops import convert_to_warp_mesh, raycast_mesh - -__all__ = [ - "raycast_mesh", - "convert_to_warp_mesh", -] diff --git a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/ops.py b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/ops.py index a300cb13fb..d5cc4a3ad5 100644 --- a/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/ops.py +++ b/source/extensions/omni.isaac.orbit/omni/isaac/orbit/utils/warp/ops.py @@ -136,7 +136,6 @@ def convert_to_warp_mesh(points: np.ndarray, indices: np.ndarray, device: str) - Returns: The warp mesh object. """ - # create warp mesh return wp.Mesh( points=wp.array(points.astype(np.float32), dtype=wp.vec3, device=device), indices=wp.array(indices.astype(np.int32).flatten(), dtype=wp.int32, device=device), diff --git a/source/extensions/omni.isaac.orbit/test/managers/test_observation_manager.py b/source/extensions/omni.isaac.orbit/test/managers/test_observation_manager.py index 710be13250..ef8ef44bbe 100644 --- a/source/extensions/omni.isaac.orbit/test/managers/test_observation_manager.py +++ b/source/extensions/omni.isaac.orbit/test/managers/test_observation_manager.py @@ -75,7 +75,7 @@ class TestObservationManager(unittest.TestCase): """Test cases for various situations with observation manager.""" def setUp(self) -> None: - self.env = namedtuple("IsaacEnv", ["num_envs", "device"])(20, "cpu") + self.env = namedtuple("BaseEnv", ["num_envs", "device"])(20, "cpu") def test_str(self): """Test the string representation of the observation manager.""" diff --git a/source/extensions/omni.isaac.orbit/test/managers/test_reward_manager.py b/source/extensions/omni.isaac.orbit/test/managers/test_reward_manager.py index f675327461..bbd57350f9 100644 --- a/source/extensions/omni.isaac.orbit/test/managers/test_reward_manager.py +++ b/source/extensions/omni.isaac.orbit/test/managers/test_reward_manager.py @@ -42,7 +42,7 @@ class TestRewardManager(unittest.TestCase): """Test cases for various situations with reward manager.""" def setUp(self) -> None: - self.env = namedtuple("IsaacEnv", ["num_envs", "dt", "device"])(20, 0.1, "cpu") + self.env = namedtuple("RLTaskEnv", ["num_envs", "dt", "device"])(20, 0.1, "cpu") def test_str(self): """Test the string representation of the reward manager.""" diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/__init__.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/__init__.py index ec7c77088f..a953d8e246 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/__init__.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/__init__.py @@ -3,31 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Module containing environments with OpenAI Gym interface. - - -We use OpenAI Gym registry to register the environment and their default configuration file. -The default configuration file is passed to the argument "kwargs" in the Gym specification registry. -The string is parsed into respective configuration container which needs to be passed to the environment -class. This is done using the function :meth:`load_cfg_from_registry` in the sub-module -:mod:`omni.isaac.orbit.utils.parse_cfg`. - -Note: - This is a slight abuse of kwargs since they are meant to be directly passed into the environment class. - Instead, we remove the key :obj:`cfg_file` from the "kwargs" dictionary and the user needs to provide - the kwarg argument :obj:`cfg` while creating the environment. - -Usage: - >>> import gymnasium as gym - >>> import omni.isaac.orbit_tasks - >>> from omni.isaac.orbit_tasks.utils.parse_cfg import load_cfg_from_registry - >>> - >>> task_name = "Isaac-Cartpole-v0" - >>> cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point") - >>> env = gym.make(task_name, cfg=cfg) -""" - -from __future__ import annotations +"""Package containing task implementations for various robotic environments.""" import os import toml diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/__init__.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/__init__.py index 89c0c5781e..99965907f6 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/__init__.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/__init__.py @@ -3,9 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Utilities and wrappers for environments.""" +"""Sub-package with utilities, data collectors and environment wrappers.""" from .importer import import_packages from .parse_cfg import get_checkpoint_path, load_cfg_from_registry, parse_env_cfg - -__all__ = ["load_cfg_from_registry", "parse_env_cfg", "get_checkpoint_path", "import_packages"] diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/data_collector/__init__.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/data_collector/__init__.py index c285402ff6..cf0085f137 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/data_collector/__init__.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/data_collector/__init__.py @@ -3,8 +3,82 @@ # # SPDX-License-Identifier: BSD-3-Clause -from __future__ import annotations +"""Sub-module for data collection utilities. -from .robomimic_data_collector import RobomimicDataCollector +All post-processed robomimic compatible datasets share the same data structure. +A single dataset is a single HDF5 file. The stored data follows the structure provided +`here `_. + +The collector takes input data in its batched format and stores them as different +demonstrations, each corresponding to a given environment index. The demonstrations are +flushed to disk when the :meth:`RobomimicDataCollector.flush` is called for the +respective environments. All the data is saved when the +:meth:`RobomimicDataCollector.close()` is called. + +The following sample shows how to use the :class:`RobomimicDataCollector` to store +random data in a dataset. + +.. code-block:: python + + import os + import torch + + from omni.isaac.orbit_tasks.utils.data_collector import RobomimicDataCollector + + # name of the environment (needed by robomimic) + task_name = "Isaac-Franka-Lift-v0" + # specify directory for logging experiments + test_dir = os.path.dirname(os.path.abspath(__file__)) + log_dir = os.path.join(test_dir, "logs", "demos") + # name of the file to save data + filename = "hdf_dataset.hdf5" + # number of episodes to collect + num_demos = 10 + # number of environments to simulate + num_envs = 4 + + # create data-collector + collector_interface = RobomimicDataCollector(task_name, log_dir, filename, num_demos) -__all__ = ["RobomimicDataCollector"] + # reset the collector + collector_interface.reset() + + while not collector_interface.is_stopped(): + # generate random data to store + # -- obs + obs = { + "joint_pos": torch.randn(num_envs, 10), + "joint_vel": torch.randn(num_envs, 10) + } + # -- actions + actions = torch.randn(num_envs, 10) + # -- rewards + rewards = torch.randn(num_envs) + # -- dones + dones = torch.rand(num_envs) > 0.5 + + # store signals + # -- obs + for key, value in obs.items(): + collector_interface.add(f"obs/{key}", value) + # -- actions + collector_interface.add("actions", actions) + # -- next_obs + for key, value in obs.items(): + collector_interface.add(f"next_obs/{key}", value.cpu().numpy()) + # -- rewards + collector_interface.add("rewards", rewards) + # -- dones + collector_interface.add("dones", dones) + + # flush data from collector for successful environments + # note: in this case we flush all the time + reset_env_ids = dones.nonzero(as_tuple=False).squeeze(-1) + collector_interface.flush(reset_env_ids) + + # close collector + collector_interface.close() + +""" + +from .robomimic_data_collector import RobomimicDataCollector diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/importer.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/importer.py index e0c9f5b23d..7b8df294d9 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/importer.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/importer.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Utilities for importing all configs in a package recursively.""" +"""Sub-module with utility for importing all modules in a package recursively.""" from __future__ import annotations @@ -11,8 +11,6 @@ import pkgutil import sys -__all__ = ["import_packages"] - def import_packages(package_name: str, blacklist_pkgs: list[str] = None): """Import all sub-packages in a package recursively. diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/parse_cfg.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/parse_cfg.py index 80641ee13a..78a5dce297 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/parse_cfg.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/parse_cfg.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""Utilities for parsing and loading configurations.""" +"""Sub-module with utilities for parsing and loading configurations.""" from __future__ import annotations @@ -34,12 +34,13 @@ def load_cfg_from_registry(task_name: str, entry_point_key: str) -> dict | Any: kwargs={"env_entry_point_cfg": "path.to.config:ConfigClass"}, ) - Usage: - .. code-block:: python + The parsed configuration object for above example can be obtained as: - from omni.isaac.orbit_tasks.utils.parse_cfg import load_cfg_from_registry + .. code-block:: python + + from omni.isaac.orbit_tasks.utils.parse_cfg import load_cfg_from_registry - cfg = load_cfg_from_registry("My-Awesome-Task-v0", "env_entry_point_cfg") + cfg = load_cfg_from_registry("My-Awesome-Task-v0", "env_entry_point_cfg") Args: task_name: The name of the environment. @@ -144,7 +145,7 @@ def get_checkpoint_path( ) -> str: """Get path to the model checkpoint in input directory. - The checkpoint file is resolved as: //<*other_dirs>/, where the + The checkpoint file is resolved as: ``//<*other_dirs>/``, where the :attr:`other_dirs` are intermediate folder names to concatenate. These cannot be regex expressions. If :attr:`run_dir` and :attr:`checkpoint` are regex expressions then the most recent (highest alphabetical order) @@ -153,11 +154,11 @@ def get_checkpoint_path( Args: log_path: The log directory path to find models in. run_dir: The regex expression for the name of the directory containing the run. Defaults to the most - recent directory created inside :obj:`log_dir`. + recent directory created inside :attr:`log_path`. other_dirs: The intermediate directories between the run directory and the checkpoint file. Defaults to None, which implies that checkpoint file is directly under the run directory. checkpoint: The regex expression for the model checkpoint file. Defaults to the most recent - torch-model saved in the :obj:`run_dir` directory. + torch-model saved in the :attr:`run_dir` directory. sort_alpha: Whether to sort the runs by alphabetical order. Defaults to True. If False, the folders in :attr:`run_dir` are sorted by the last modified time. diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/__init__.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/__init__.py index 7f71530395..28eb7d22b9 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/__init__.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/__init__.py @@ -3,13 +3,32 @@ # # SPDX-License-Identifier: BSD-3-Clause -"""This sub-module includes wrappers for various RL frameworks. +"""Sub-module for environment wrappers to different learning frameworks. -Currently the following are supported: +Wrappers allow you to modify the behavior of an environment without modifying the environment itself. +This is useful for modifying the observation space, action space, or reward function. Additionally, +they can be used to cast a given environment into the respective environment class definition used by +different learning frameworks. This operation may include handling of asymmetric actor-critic observations, +casting the data between different backends such `numpy` and `pytorch`, or organizing the returned data +into the expected data structure by the learning framework. -* Stable-Baselines3: https://github.com/DLR-RM/stable-baselines3 -* RL-Games: https://github.com/Denys88/rl_games -* RSL-RL: https://github.com/leggedrobotics/rsl_rl -* skrl: https://github.com/Toni-SM/skrl +All wrappers work similar to the :class:`gymnasium.Wrapper` class. Using a wrapper is as simple as passing +the initialized environment instance to the wrapper constructor. However, since learning frameworks +expect different input and output data structures, their wrapper classes are not compatible with each other. +Thus, they should always be used in conjunction with the respective learning framework. + +For instance, to wrap an environment in the `Stable-Baselines3`_ wrapper, you can do the following: + +.. code-block:: python + + from omni.isaac.orbit_tasks.utils.wrappers.sb3 import Sb3VecEnvWrapper + + env = Sb3VecEnvWrapper(env) + + +.. _RL-Games: https://github.com/Denys88/rl_games +.. _RSL-RL: https://github.com/leggedrobotics/rsl_rl +.. _skrl: https://github.com/Toni-SM/skrl +.. _Stable-Baselines3: https://github.com/DLR-RM/stable-baselines3 """ diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rl_games.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rl_games.py index 85cc7a055c..53d640d6fa 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rl_games.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rl_games.py @@ -42,9 +42,6 @@ from omni.isaac.orbit.envs import RLTaskEnv, VecEnvObs -__all__ = ["RlGamesVecEnvWrapper", "RlGamesGpuEnv"] - - """ Vectorized environment wrapper. """ diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/__init__.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/__init__.py index 12214a2e90..f6bddee9ec 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/__init__.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/__init__.py @@ -3,18 +3,8 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Wrappers and utilities to configure an :class:`RLTaskEnv` for RSL-RL library.""" + from .exporter import export_policy_as_jit, export_policy_as_onnx -from .rl_cfg import * +from .rl_cfg import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg from .vecenv_wrapper import RslRlVecEnvWrapper - -__all__ = [ - # wrapper - "RslRlVecEnvWrapper", - # rl-config - "RslRlPpoActorCriticCfg", - "RslRlPpoAlgorithmCfg", - "RslRlOnPolicyRunnerCfg", - # exporters - "export_policy_as_jit", - "export_policy_as_onnx", -] diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/exporter.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/exporter.py index ecb0ff87de..7fc6533283 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/exporter.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/exporter.py @@ -7,8 +7,6 @@ import os import torch -__all__ = ["export_policy_as_jit", "export_policy_as_onnx"] - def export_policy_as_jit(actor_critic: object, path: str, filename="policy.pt"): """Export policy into a Torch JIT file. diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/rl_cfg.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/rl_cfg.py index 3f3d432e73..895303ee82 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/rl_cfg.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/rl_cfg.py @@ -9,8 +9,6 @@ from omni.isaac.orbit.utils import configclass -__all__ = ["RslRlPpoActorCriticCfg", "RslRlPpoAlgorithmCfg", "RslRlOnPolicyRunnerCfg"] - @configclass class RslRlPpoActorCriticCfg: @@ -114,7 +112,7 @@ class RslRlOnPolicyRunnerCfg: """ load_checkpoint: str = "model_.*.pt" - """The checkpoint file to load. Default is "model_.*.pt" (all). + """The checkpoint file to load. Default is ``"model_.*.pt"`` (all). If regex expression, the latest (alphabetical order) matching file will be loaded. """ diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py index 9faedcc438..158a07309c 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.py @@ -133,7 +133,8 @@ def episode_length_buf(self) -> torch.Tensor: def episode_length_buf(self, value: torch.Tensor): """Set the episode length buffer. - Note: This is needed to perform random initialization of episode lengths in RSL-RL. + Note: + This is needed to perform random initialization of episode lengths in RSL-RL. """ self.unwrapped.episode_length_buf = value diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/sb3.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/sb3.py index 634d44c670..9408d18ffb 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/sb3.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/sb3.py @@ -28,8 +28,6 @@ from omni.isaac.orbit.envs import RLTaskEnv -__all__ = ["process_sb3_cfg", "Sb3VecEnvWrapper"] - """ Configuration Parser. """ @@ -225,7 +223,7 @@ def step_wait(self) -> VecEnvStepReturn: # noqa: D102 reset_ids = (dones > 0).nonzero(as_tuple=False) # convert data types to numpy depending on backend - # Note: RLTaskEnv uses torch backend (by default). + # note: RLTaskEnv uses torch backend (by default). obs = self._process_obs(obs_dict) rew = rew.detach().cpu().numpy() terminated = terminated.detach().cpu().numpy() @@ -285,7 +283,7 @@ def _process_obs(self, obs_dict: torch.Tensor | dict[str, torch.Tensor]) -> np.n """Convert observations into NumPy data type.""" # Sb3 doesn't support asymmetric observation spaces, so we only use "policy" obs = obs_dict["policy"] - # Note: RLTaskEnv uses torch backend (by default). + # note: RLTaskEnv uses torch backend (by default). if isinstance(obs, dict): for key, value in obs.items(): obs[key] = value.detach().cpu().numpy() @@ -302,7 +300,7 @@ def _process_extras( # create empty list of dictionaries to fill infos: list[dict[str, Any]] = [dict.fromkeys(extras.keys()) for _ in range(self.num_envs)] # fill-in information for each sub-environment - # Note: This loop becomes slow when number of environments is large. + # note: This loop becomes slow when number of environments is large. for idx in range(self.num_envs): # fill-in episode monitoring info if idx in reset_ids: diff --git a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/skrl.py b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/skrl.py index c7553a1410..49e7e807be 100644 --- a/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/skrl.py +++ b/source/extensions/omni.isaac.orbit_tasks/omni/isaac/orbit_tasks/utils/wrappers/skrl.py @@ -39,8 +39,6 @@ from omni.isaac.orbit.envs import RLTaskEnv -__all__ = ["SkrlVecEnvWrapper", "SkrlSequentialLogTrainer"] - """ Configuration Parser. """ diff --git a/source/standalone/demo/play_empty.py b/source/standalone/demo/play_empty.py index cb8bc8df99..528c4388b6 100644 --- a/source/standalone/demo/play_empty.py +++ b/source/standalone/demo/play_empty.py @@ -8,19 +8,16 @@ from __future__ import annotations """Launch Isaac Sim Simulator first.""" - - import argparse from omni.isaac.orbit.app import AppLauncher -# add argparse arguments -parser = argparse.ArgumentParser(description="This script creates an empty stage in Isaac Sim.") +# create argparser +parser = argparse.ArgumentParser(description="Create an empty stage.") # append AppLauncher cli args AppLauncher.add_app_launcher_args(parser) # parse the arguments args_cli = parser.parse_args() - # launch omniverse app app_launcher = AppLauncher(args_cli) simulation_app = app_launcher.app @@ -31,26 +28,18 @@ import carb -import omni.isaac.orbit.sim as sim_utils -from omni.isaac.orbit.sim import SimulationContext +from omni.isaac.orbit.sim import SimulationCfg, SimulationContext def main(): """Spawns lights in the stage and sets the camera view.""" - # Load kit helper - sim = SimulationContext(sim_utils.SimulationCfg(dt=0.01, substeps=1)) + # Initialize the simulation context + sim_cfg = SimulationCfg(dt=0.01, substeps=1) + sim = SimulationContext(sim_cfg) # Set main camera sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0]) - # Spawn things into stage - # Ground-plane - cfg = sim_utils.GroundPlaneCfg() - cfg.func("/World/defaultGroundPlane", cfg) - # Lights - cfg = sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) - cfg.func("/World/Light", cfg) - # Play the simulator sim.reset() # Now we are ready! diff --git a/source/standalone/environments/state_machine/play_lift.py b/source/standalone/environments/state_machine/play_lift.py index cb0194fadf..c105ca396f 100644 --- a/source/standalone/environments/state_machine/play_lift.py +++ b/source/standalone/environments/state_machine/play_lift.py @@ -15,7 +15,7 @@ from __future__ import annotations -"""Launch Omniverse Toolkit first.""" +"""Launch Isaac Sim Simulator first.""" import argparse @@ -34,7 +34,7 @@ app_launcher = AppLauncher(headless=args_cli.headless) simulation_app = app_launcher.app -"""Rest everything else.""" +"""Rest everything follows.""" import gymnasium as gym import torch diff --git a/source/tools/convert_mesh.py b/source/tools/convert_mesh.py index 1d9931b678..5f44e22dc4 100644 --- a/source/tools/convert_mesh.py +++ b/source/tools/convert_mesh.py @@ -30,11 +30,11 @@ optional arguments: -h, --help Show this help message and exit --headless Force display off at all times. (default: False) - --make_instanceable, -i Make the asset instanceable for efficient cloning. (default: False) - --force_usd_conversion -f Convert the input file to USD even if the output file already exists. - --collision_approximation -c The method used for approximating collision mesh. Defaults to convexDecomposition. Set to \"none\" " - to not add a collision mesh to the converted mesh. - --mass -m The mass (in kg) to assign to the converted asset. + --make_instanceable, Make the asset instanceable for efficient cloning. (default: False) + --force_usd_conversion Convert the input file to USD even if the output file already exists. (default: False) + --collision_approximation The method used for approximating collision mesh. Defaults to convexDecomposition. + Set to \"none\" to not add a collision mesh to the converted mesh. (default: convexDecomposition) + --mass The mass (in kg) to assign to the converted asset. (default: None) """ @@ -53,21 +53,18 @@ parser.add_argument("--headless", action="store_true", default=False, help="Force display off at all times.") parser.add_argument( "--make_instanceable", - "-i", action="store_true", default=False, help="Make the asset instanceable for efficient cloning.", ) parser.add_argument( "--force_usd_conversion", - "-f", action="store_true", default=False, help="Convert the input file to USD even if the output file already exists.", ) parser.add_argument( "--collision_approximation", - "-c", type=str, default="convexDecomposition", choices=["convexDecomposition", "convexHull", "none"], @@ -78,7 +75,6 @@ ) parser.add_argument( "--mass", - "-m", type=float, default=None, help="The mass (in kg) to assign to the converted asset. If not provided, then no mass is added.", diff --git a/source/tools/convert_urdf.py b/source/tools/convert_urdf.py index f695cc23d9..2f8074f40a 100644 --- a/source/tools/convert_urdf.py +++ b/source/tools/convert_urdf.py @@ -22,9 +22,9 @@ optional arguments: -h, --help Show this help message and exit --headless Force display off at all times. (default: False) - --merge-joints, -m Consolidate links that are connected by fixed joints. (default: False) - --fix-base, -f Fix the base to where it is imported. (default: False) - --make-instanceable, -i Make the asset instanceable for efficient cloning. (default: False) + --merge-joints Consolidate links that are connected by fixed joints. (default: False) + --fix-base Fix the base to where it is imported. (default: False) + --make-instanceable Make the asset instanceable for efficient cloning. (default: False) """ @@ -46,17 +46,13 @@ parser.add_argument("--headless", action="store_true", default=False, help="Force display off at all times.") parser.add_argument( "--merge-joints", - "-m", action="store_true", default=False, help="Consolidate links that are connected by fixed joints.", ) -parser.add_argument( - "--fix-base", "-f", action="store_true", default=False, help="Fix the base to where it is imported." -) +parser.add_argument("--fix-base", action="store_true", default=False, help="Fix the base to where it is imported.") parser.add_argument( "--make-instanceable", - "-i", action="store_true", default=False, help="Make the asset instanceable for efficient cloning.",