diff --git a/_images/a1_flat.jpg b/_images/a1_flat.jpg new file mode 100644 index 0000000000..c47be98f18 Binary files /dev/null and b/_images/a1_flat.jpg differ diff --git a/_images/a1_rough.jpg b/_images/a1_rough.jpg new file mode 100644 index 0000000000..fdee8d2981 Binary files /dev/null and b/_images/a1_rough.jpg differ diff --git a/_images/a3c-dark.svg b/_images/a3c-dark.svg new file mode 100644 index 0000000000..48cdb84914 --- /dev/null +++ b/_images/a3c-dark.svg @@ -0,0 +1,364 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/a3c-light.svg b/_images/a3c-light.svg new file mode 100644 index 0000000000..d24bfc20b7 --- /dev/null +++ b/_images/a3c-light.svg @@ -0,0 +1,385 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_images/actuator-dark.svg b/_images/actuator-dark.svg new file mode 100644 index 0000000000..b9e0682395 --- /dev/null +++ b/_images/actuator-dark.svg @@ -0,0 +1,522 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + DC Motor + Actuator Net(MLP/LSTM) + + Gripper + + + Arm + Base + Mimic Group + + + + open/close (1) + joint position(6) + joint position(12) + joint torque(12) + joint torque(6) + joint velocity(6) + + Simulation + + + + Actions + + + + + + + + + + + diff --git a/_images/actuator-light.svg b/_images/actuator-light.svg new file mode 100644 index 0000000000..214b5a7fee --- /dev/null +++ b/_images/actuator-light.svg @@ -0,0 +1,10214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + DC Motor + Actuator Net(MLP/LSTM) + + Gripper + + + Arm + Base + Mimic Group + + + + open/close (1) + joint position(6) + joint position(12) + joint torque(12) + joint torque(6) + joint velocity(6) + + Simulation + + + + Actions + + + + + + + + + + + diff --git a/_images/allegro_cube.jpg b/_images/allegro_cube.jpg new file mode 100644 index 0000000000..7ada7d8c1a Binary files /dev/null and b/_images/allegro_cube.jpg differ diff --git a/_images/ant.jpg b/_images/ant.jpg new file mode 100644 index 0000000000..d5eadb6b8e Binary files /dev/null and b/_images/ant.jpg differ diff --git a/_images/anymal_b_flat.jpg b/_images/anymal_b_flat.jpg new file mode 100644 index 0000000000..f2b6f0bfef Binary files /dev/null and b/_images/anymal_b_flat.jpg differ diff --git a/_images/anymal_b_rough.jpg b/_images/anymal_b_rough.jpg new file mode 100644 index 0000000000..f00c50761e Binary files /dev/null and b/_images/anymal_b_rough.jpg differ diff --git a/_images/anymal_c_flat.jpg b/_images/anymal_c_flat.jpg new file mode 100644 index 0000000000..295f441f2c Binary files /dev/null and b/_images/anymal_c_flat.jpg differ diff --git a/_images/anymal_c_nav.jpg b/_images/anymal_c_nav.jpg new file mode 100644 index 0000000000..12766e6075 Binary files /dev/null and b/_images/anymal_c_nav.jpg differ diff --git a/_images/anymal_c_rough.jpg b/_images/anymal_c_rough.jpg new file mode 100644 index 0000000000..92ebc2fc42 Binary files /dev/null and b/_images/anymal_c_rough.jpg differ diff --git a/_images/anymal_d_flat.jpg b/_images/anymal_d_flat.jpg new file mode 100644 index 0000000000..1e33d34c7d Binary files /dev/null and b/_images/anymal_d_flat.jpg differ diff --git a/_images/anymal_d_rough.jpg b/_images/anymal_d_rough.jpg new file mode 100644 index 0000000000..fd57324fdd Binary files /dev/null and b/_images/anymal_d_rough.jpg differ diff --git a/_images/arms.jpg b/_images/arms.jpg new file mode 100644 index 0000000000..6dbb524658 Binary files /dev/null and b/_images/arms.jpg differ diff --git a/_images/box_terrain.jpg b/_images/box_terrain.jpg new file mode 100644 index 0000000000..2694433c06 Binary files /dev/null and b/_images/box_terrain.jpg differ diff --git a/_images/box_terrain_with_two_boxes.jpg b/_images/box_terrain_with_two_boxes.jpg new file mode 100644 index 0000000000..c2715442a7 Binary files /dev/null and b/_images/box_terrain_with_two_boxes.jpg differ diff --git a/_images/cart_double_pendulum.jpg b/_images/cart_double_pendulum.jpg new file mode 100644 index 0000000000..12c3cdb5a0 Binary files /dev/null and b/_images/cart_double_pendulum.jpg differ diff --git a/_images/cartpole.jpg b/_images/cartpole.jpg new file mode 100644 index 0000000000..4eabbb80a7 Binary files /dev/null and b/_images/cartpole.jpg differ diff --git a/_images/cartpole1.jpg b/_images/cartpole1.jpg new file mode 100644 index 0000000000..9165663ba9 Binary files /dev/null and b/_images/cartpole1.jpg differ diff --git a/_images/cartpole_camera.jpg b/_images/cartpole_camera.jpg new file mode 100644 index 0000000000..e46763539d Binary files /dev/null and b/_images/cartpole_camera.jpg differ diff --git a/_images/deformables.jpg b/_images/deformables.jpg new file mode 100644 index 0000000000..6645e821d8 Binary files /dev/null and b/_images/deformables.jpg differ diff --git a/_images/deployment-dark.svg b/_images/deployment-dark.svg new file mode 100644 index 0000000000..ba8c739724 --- /dev/null +++ b/_images/deployment-dark.svg @@ -0,0 +1,3 @@ + + +
Robot Hardware
NVIDIA Isaac Perceptor
Custom Estimator
Isaac ROS Packages
State Estimator
Extract Observation
Actions Controller
Trained Model (.onnx, .pt)
Commanded Actions
Model Inference Runtime
Scaled Actions
1
2
3
4
5
6
7
diff --git a/_images/deployment-light.svg b/_images/deployment-light.svg new file mode 100644 index 0000000000..33857dbdbe --- /dev/null +++ b/_images/deployment-light.svg @@ -0,0 +1,3 @@ + + +
Robot Hardware
NVIDIA Isaac Perceptor
Custom Estimator
Isaac ROS Packages
State Estimator
Extract Observation
Actions Controller
Trained Model (.onnx, .pt)
Commanded Actions
Model Inference Runtime
Scaled Actions
1
2
3
4
5
6
7
diff --git a/_images/direct-based-dark.svg b/_images/direct-based-dark.svg new file mode 100644 index 0000000000..2709dae140 --- /dev/null +++ b/_images/direct-based-dark.svg @@ -0,0 +1,3 @@ + + +
Sensors
Observations
Apply Actions
Define Task
Define Step
Environment Scripting
NVIDIA
Isaac Sim
Perform Resets
Rewards
Commands
Learning Agent
Scene Creation
Articulation
Objects
Compute Signals
Actions
Perform
Randomization
diff --git a/_images/direct-based-light.svg b/_images/direct-based-light.svg new file mode 100644 index 0000000000..b39bffa353 --- /dev/null +++ b/_images/direct-based-light.svg @@ -0,0 +1,3 @@ + + +
Sensors
Observations
Apply Actions
Define Task
Define Step
Environment Scripting
NVIDIA
Isaac Sim
Perform Resets
Rewards
Commands
Learning Agent
Scene Creation
Articulation
Objects
Compute Signals
Actions
Perform
Randomization
diff --git a/_images/discrete_obstacles_terrain.jpg b/_images/discrete_obstacles_terrain.jpg new file mode 100644 index 0000000000..ad56e56e55 Binary files /dev/null and b/_images/discrete_obstacles_terrain.jpg differ diff --git a/_images/ecosystem-dark.jpg b/_images/ecosystem-dark.jpg new file mode 100644 index 0000000000..9057aeb48b Binary files /dev/null and b/_images/ecosystem-dark.jpg differ diff --git a/_images/ecosystem-light.jpg b/_images/ecosystem-light.jpg new file mode 100644 index 0000000000..5c2bbf5fe1 Binary files /dev/null and b/_images/ecosystem-light.jpg differ diff --git a/_images/flat_terrain.jpg b/_images/flat_terrain.jpg new file mode 100644 index 0000000000..5464cc9117 Binary files /dev/null and b/_images/flat_terrain.jpg differ diff --git a/_images/floating_ring_terrain.jpg b/_images/floating_ring_terrain.jpg new file mode 100644 index 0000000000..e00a7afbd7 Binary files /dev/null and b/_images/floating_ring_terrain.jpg differ diff --git a/_images/franka_lift.jpg b/_images/franka_lift.jpg new file mode 100644 index 0000000000..1784ced522 Binary files /dev/null and b/_images/franka_lift.jpg differ diff --git a/_images/franka_open_drawer.jpg b/_images/franka_open_drawer.jpg new file mode 100644 index 0000000000..42249d5aff Binary files /dev/null and b/_images/franka_open_drawer.jpg differ diff --git a/_images/franka_reach.jpg b/_images/franka_reach.jpg new file mode 100644 index 0000000000..19f140c167 Binary files /dev/null and b/_images/franka_reach.jpg differ diff --git a/_images/g1_flat.jpg b/_images/g1_flat.jpg new file mode 100644 index 0000000000..cf9e1b1482 Binary files /dev/null and b/_images/g1_flat.jpg differ diff --git a/_images/g1_rough.jpg b/_images/g1_rough.jpg new file mode 100644 index 0000000000..25ed68d6e1 Binary files /dev/null and b/_images/g1_rough.jpg differ diff --git a/_images/g1_rough1.jpg b/_images/g1_rough1.jpg new file mode 100644 index 0000000000..25ed68d6e1 Binary files /dev/null and b/_images/g1_rough1.jpg differ diff --git a/_images/gap_terrain.jpg b/_images/gap_terrain.jpg new file mode 100644 index 0000000000..22e024455b Binary files /dev/null and b/_images/gap_terrain.jpg differ diff --git a/_images/go1_flat.jpg b/_images/go1_flat.jpg new file mode 100644 index 0000000000..4c9319d61b Binary files /dev/null and b/_images/go1_flat.jpg differ diff --git a/_images/go1_rough.jpg b/_images/go1_rough.jpg new file mode 100644 index 0000000000..6a4e832bd2 Binary files /dev/null and b/_images/go1_rough.jpg differ diff --git a/_images/go2_flat.jpg b/_images/go2_flat.jpg new file mode 100644 index 0000000000..0a42c60b7a Binary files /dev/null and b/_images/go2_flat.jpg differ diff --git a/_images/go2_rough.jpg b/_images/go2_rough.jpg new file mode 100644 index 0000000000..2baa1eeaf9 Binary files /dev/null and b/_images/go2_rough.jpg differ diff --git a/_images/h1_flat.jpg b/_images/h1_flat.jpg new file mode 100644 index 0000000000..2c7cb58bd8 Binary files /dev/null and b/_images/h1_flat.jpg differ diff --git a/_images/h1_rough.jpg b/_images/h1_rough.jpg new file mode 100644 index 0000000000..dc6052fc94 Binary files /dev/null and b/_images/h1_rough.jpg differ diff --git a/_images/hands.jpg b/_images/hands.jpg new file mode 100644 index 0000000000..7c9829b6a3 Binary files /dev/null and b/_images/hands.jpg differ diff --git a/_images/humanoid.jpg b/_images/humanoid.jpg new file mode 100644 index 0000000000..84dc55d2b2 Binary files /dev/null and b/_images/humanoid.jpg differ diff --git a/_images/inverted_pyramid_sloped_terrain.jpg b/_images/inverted_pyramid_sloped_terrain.jpg new file mode 100644 index 0000000000..9ca4ece222 Binary files /dev/null and b/_images/inverted_pyramid_sloped_terrain.jpg differ diff --git a/_images/inverted_pyramid_stairs_terrain.jpg b/_images/inverted_pyramid_stairs_terrain.jpg new file mode 100644 index 0000000000..47ff07070d Binary files /dev/null and b/_images/inverted_pyramid_stairs_terrain.jpg differ diff --git a/_images/inverted_pyramid_stairs_terrain1.jpg b/_images/inverted_pyramid_stairs_terrain1.jpg new file mode 100644 index 0000000000..3b92cc216a Binary files /dev/null and b/_images/inverted_pyramid_stairs_terrain1.jpg differ diff --git a/_images/inverted_pyramid_stairs_terrain_with_holes.jpg b/_images/inverted_pyramid_stairs_terrain_with_holes.jpg new file mode 100644 index 0000000000..77120d29b6 Binary files /dev/null and b/_images/inverted_pyramid_stairs_terrain_with_holes.jpg differ diff --git a/_images/isaac-lab-ra-dark.svg b/_images/isaac-lab-ra-dark.svg new file mode 100644 index 0000000000..90ed29a93e --- /dev/null +++ b/_images/isaac-lab-ra-dark.svg @@ -0,0 +1,3 @@ + + +
Scene Assets
Robot Assets (.usd, .urdf)
Asset Input
Design Robot Learning Task
Scene Configuration
Asset Configuration
Configuration
Register Environment with Gymnasium
Learning Framework Wrapper
Video Wrapper
Wrap Environment
Wrapper API
Test Model
Multi-Node Training
Single GPU Training
Cloud-based Training
Multi-GPU Training
Run Training
diff --git a/_images/isaac-lab-ra-light.svg b/_images/isaac-lab-ra-light.svg new file mode 100644 index 0000000000..a58a6af1ec --- /dev/null +++ b/_images/isaac-lab-ra-light.svg @@ -0,0 +1,3 @@ + + +
Scene Assets
Robot Assets (.usd, .urdf)
Asset Input
Design Robot Learning Task
Scene Configuration
Asset Configuration
Configuration
Register Environment with Gymnasium
Learning Framework Wrapper
Video Wrapper
Wrap Environment
Wrapper API
Test Model
Multi-Node Training
Single GPU Training
Cloud-based Training
Multi-GPU Training
Run Training
diff --git a/_images/isaaclab.jpg b/_images/isaaclab.jpg new file mode 100644 index 0000000000..16072e704a Binary files /dev/null and b/_images/isaaclab.jpg differ diff --git a/_images/manager-based-dark.svg b/_images/manager-based-dark.svg new file mode 100644 index 0000000000..b66ba1bfee --- /dev/null +++ b/_images/manager-based-dark.svg @@ -0,0 +1,3 @@ + + +
Action Manager
Task-space
Joint-space
Termination Manager
Reward Manager
Curriculum Manager
Learning
Agent
Custom
Command Manager
Custom
Velocity
Pose
Observation Manager
Proprioception
Custom
Exteroception
NVIDIA
Isaac Sim
Articulation
Objects
Sensors
Interactive Scene
Event Manager
External Disturbances
Domain Randomization
diff --git a/_images/manager-based-light.svg b/_images/manager-based-light.svg new file mode 100644 index 0000000000..60fe6119d4 --- /dev/null +++ b/_images/manager-based-light.svg @@ -0,0 +1,3 @@ + + +
Action Manager
Task-space
Joint-space
Termination Manager
Reward Manager
Curriculum Manager
Learning
Agent
Custom
Command Manager
Custom
Velocity
Pose
Observation Manager
Proprioception
Custom
Exteroception
NVIDIA
Isaac Sim
Articulation
Objects
Sensors
Interactive Scene
Event Manager
External Disturbances
Domain Randomization
diff --git a/_images/markers.jpg b/_images/markers.jpg new file mode 100644 index 0000000000..8a7ed4e371 Binary files /dev/null and b/_images/markers.jpg differ diff --git a/_images/multi-gpu-training-dark.svg b/_images/multi-gpu-training-dark.svg new file mode 100644 index 0000000000..dd63d5769c --- /dev/null +++ b/_images/multi-gpu-training-dark.svg @@ -0,0 +1,3 @@ + + +
Environment 0
Gradients
Global Network
Updated Model
GPU 0
Learner 0
Environment 1
GPU 1
Learner 1
Environment N
GPU N
Learner N
diff --git a/_images/multi-gpu-training-light.svg b/_images/multi-gpu-training-light.svg new file mode 100644 index 0000000000..a279bff5e8 --- /dev/null +++ b/_images/multi-gpu-training-light.svg @@ -0,0 +1,3 @@ + + +
Environment 0
Gradients
Global Network
Updated Model
GPU 0
Learner 0
Environment 1
GPU 1
Learner 1
Environment N
GPU N
Learner N
diff --git a/_images/multi_asset.jpg b/_images/multi_asset.jpg new file mode 100644 index 0000000000..a59388532b Binary files /dev/null and b/_images/multi_asset.jpg differ diff --git a/_images/pit_terrain.jpg b/_images/pit_terrain.jpg new file mode 100644 index 0000000000..6456c2d1f1 Binary files /dev/null and b/_images/pit_terrain.jpg differ diff --git a/_images/pit_terrain_with_two_levels.jpg b/_images/pit_terrain_with_two_levels.jpg new file mode 100644 index 0000000000..d71f828895 Binary files /dev/null and b/_images/pit_terrain_with_two_levels.jpg differ diff --git a/_images/procedural_terrain.jpg b/_images/procedural_terrain.jpg new file mode 100644 index 0000000000..54bb16b7f7 Binary files /dev/null and b/_images/procedural_terrain.jpg differ diff --git a/_images/pyramid_sloped_terrain.jpg b/_images/pyramid_sloped_terrain.jpg new file mode 100644 index 0000000000..eccf3884ed Binary files /dev/null and b/_images/pyramid_sloped_terrain.jpg differ diff --git a/_images/pyramid_stairs_terrain.jpg b/_images/pyramid_stairs_terrain.jpg new file mode 100644 index 0000000000..eec9f84de6 Binary files /dev/null and b/_images/pyramid_stairs_terrain.jpg differ diff --git a/_images/pyramid_stairs_terrain1.jpg b/_images/pyramid_stairs_terrain1.jpg new file mode 100644 index 0000000000..8c77619e2f Binary files /dev/null and b/_images/pyramid_stairs_terrain1.jpg differ diff --git a/_images/pyramid_stairs_terrain_with_holes.jpg b/_images/pyramid_stairs_terrain_with_holes.jpg new file mode 100644 index 0000000000..cac7e9671e Binary files /dev/null and b/_images/pyramid_stairs_terrain_with_holes.jpg differ diff --git a/_images/quadcopter.jpg b/_images/quadcopter.jpg new file mode 100644 index 0000000000..1b62fd8ed6 Binary files /dev/null and b/_images/quadcopter.jpg differ diff --git a/_images/quadrupeds.jpg b/_images/quadrupeds.jpg new file mode 100644 index 0000000000..c238b93515 Binary files /dev/null and b/_images/quadrupeds.jpg differ diff --git a/_images/rails_terrain.jpg b/_images/rails_terrain.jpg new file mode 100644 index 0000000000..89c2b21bb5 Binary files /dev/null and b/_images/rails_terrain.jpg differ diff --git a/_images/random_grid_terrain.jpg b/_images/random_grid_terrain.jpg new file mode 100644 index 0000000000..fb0eac1234 Binary files /dev/null and b/_images/random_grid_terrain.jpg differ diff --git a/_images/random_grid_terrain_with_holes.jpg b/_images/random_grid_terrain_with_holes.jpg new file mode 100644 index 0000000000..57fa80efc6 Binary files /dev/null and b/_images/random_grid_terrain_with_holes.jpg differ diff --git a/_images/random_uniform_terrain.jpg b/_images/random_uniform_terrain.jpg new file mode 100644 index 0000000000..5ea4796cd0 Binary files /dev/null and b/_images/random_uniform_terrain.jpg differ diff --git a/_images/repeated_objects_box_terrain.jpg b/_images/repeated_objects_box_terrain.jpg new file mode 100644 index 0000000000..3c42279ec6 Binary files /dev/null and b/_images/repeated_objects_box_terrain.jpg differ diff --git a/_images/repeated_objects_cylinder_terrain.jpg b/_images/repeated_objects_cylinder_terrain.jpg new file mode 100644 index 0000000000..2864e86469 Binary files /dev/null and b/_images/repeated_objects_cylinder_terrain.jpg differ diff --git a/_images/repeated_objects_pyramid_terrain.jpg b/_images/repeated_objects_pyramid_terrain.jpg new file mode 100644 index 0000000000..3c7585d7d5 Binary files /dev/null and b/_images/repeated_objects_pyramid_terrain.jpg differ diff --git a/_images/shadow.jpg b/_images/shadow.jpg new file mode 100644 index 0000000000..151e6512b5 Binary files /dev/null and b/_images/shadow.jpg differ diff --git a/_images/shadow_cube.jpg b/_images/shadow_cube.jpg new file mode 100644 index 0000000000..ba30f096e4 Binary files /dev/null and b/_images/shadow_cube.jpg differ diff --git a/_images/shadow_hand_over.jpg b/_images/shadow_hand_over.jpg new file mode 100644 index 0000000000..79b71de047 Binary files /dev/null and b/_images/shadow_hand_over.jpg differ diff --git a/_images/single-gpu-training-dark.svg b/_images/single-gpu-training-dark.svg new file mode 100644 index 0000000000..69f8e0f3c0 --- /dev/null +++ b/_images/single-gpu-training-dark.svg @@ -0,0 +1,3 @@ + + +
Isaac Lab
2
States
Add Noise
Assets
Rendering
Trained Model (.pt, .onnx)
Isaac Sim
1
Physics Sim
4
5
RL Libraries
3
Policy
Actions
Observations
6
diff --git a/_images/single-gpu-training-light.svg b/_images/single-gpu-training-light.svg new file mode 100644 index 0000000000..7463c470d8 --- /dev/null +++ b/_images/single-gpu-training-light.svg @@ -0,0 +1,3 @@ + + +
Isaac Lab
2
States
Add Noise
Assets
Rendering
Trained Model (.pt, .onnx)
Isaac Sim
1
Physics Sim
4
5
RL Libraries
3
Policy
Actions
Observations
6
diff --git a/_images/spot_flat.jpg b/_images/spot_flat.jpg new file mode 100644 index 0000000000..5176156596 Binary files /dev/null and b/_images/spot_flat.jpg differ diff --git a/_images/star_terrain.jpg b/_images/star_terrain.jpg new file mode 100644 index 0000000000..86e92aefe8 Binary files /dev/null and b/_images/star_terrain.jpg differ diff --git a/_images/stepping_stones_terrain.jpg b/_images/stepping_stones_terrain.jpg new file mode 100644 index 0000000000..ecc08fd788 Binary files /dev/null and b/_images/stepping_stones_terrain.jpg differ diff --git a/_images/tasks.jpg b/_images/tasks.jpg new file mode 100644 index 0000000000..8aa96c179c Binary files /dev/null and b/_images/tasks.jpg differ diff --git a/_images/thanks.png b/_images/thanks.png new file mode 100644 index 0000000000..34748a5016 Binary files /dev/null and b/_images/thanks.png differ diff --git a/_images/tutorial_add_sensors.jpg b/_images/tutorial_add_sensors.jpg new file mode 100644 index 0000000000..d4d2e743de Binary files /dev/null and b/_images/tutorial_add_sensors.jpg differ diff --git a/_images/tutorial_convert_mesh.jpg b/_images/tutorial_convert_mesh.jpg new file mode 100644 index 0000000000..62779590e9 Binary files /dev/null and b/_images/tutorial_convert_mesh.jpg differ diff --git a/_images/tutorial_convert_mjcf.jpg b/_images/tutorial_convert_mjcf.jpg new file mode 100644 index 0000000000..12725379db Binary files /dev/null and b/_images/tutorial_convert_mjcf.jpg differ diff --git a/_images/tutorial_convert_urdf.jpg b/_images/tutorial_convert_urdf.jpg new file mode 100644 index 0000000000..e1ae5dea40 Binary files /dev/null and b/_images/tutorial_convert_urdf.jpg differ diff --git a/_images/tutorial_create_direct_workflow.jpg b/_images/tutorial_create_direct_workflow.jpg new file mode 100644 index 0000000000..ce201b0155 Binary files /dev/null and b/_images/tutorial_create_direct_workflow.jpg differ diff --git a/_images/tutorial_create_empty.jpg b/_images/tutorial_create_empty.jpg new file mode 100644 index 0000000000..3d5bc23243 Binary files /dev/null and b/_images/tutorial_create_empty.jpg differ diff --git a/_images/tutorial_create_manager_rl_env.jpg b/_images/tutorial_create_manager_rl_env.jpg new file mode 100644 index 0000000000..6d15966ce8 Binary files /dev/null and b/_images/tutorial_create_manager_rl_env.jpg differ diff --git a/_images/tutorial_creating_a_scene.jpg b/_images/tutorial_creating_a_scene.jpg new file mode 100644 index 0000000000..6e1729101b Binary files /dev/null and b/_images/tutorial_creating_a_scene.jpg differ diff --git a/_images/tutorial_launch_app.jpg b/_images/tutorial_launch_app.jpg new file mode 100644 index 0000000000..eb0673d876 Binary files /dev/null and b/_images/tutorial_launch_app.jpg differ diff --git a/_images/tutorial_modify_direct_rl_env.jpg b/_images/tutorial_modify_direct_rl_env.jpg new file mode 100644 index 0000000000..635c437b75 Binary files /dev/null and b/_images/tutorial_modify_direct_rl_env.jpg differ diff --git a/_images/tutorial_register_environment.jpg b/_images/tutorial_register_environment.jpg new file mode 100644 index 0000000000..a446710413 Binary files /dev/null and b/_images/tutorial_register_environment.jpg differ diff --git a/_images/tutorial_run_articulation.jpg b/_images/tutorial_run_articulation.jpg new file mode 100644 index 0000000000..3178e3c4bb Binary files /dev/null and b/_images/tutorial_run_articulation.jpg differ diff --git a/_images/tutorial_run_deformable_object.jpg b/_images/tutorial_run_deformable_object.jpg new file mode 100644 index 0000000000..752a484085 Binary files /dev/null and b/_images/tutorial_run_deformable_object.jpg differ diff --git a/_images/tutorial_run_rigid_object.jpg b/_images/tutorial_run_rigid_object.jpg new file mode 100644 index 0000000000..d71e15ff16 Binary files /dev/null and b/_images/tutorial_run_rigid_object.jpg differ diff --git a/_images/tutorial_spawn_prims.jpg b/_images/tutorial_spawn_prims.jpg new file mode 100644 index 0000000000..c7b2ecc028 Binary files /dev/null and b/_images/tutorial_spawn_prims.jpg differ diff --git a/_images/tutorial_task_space_controller.jpg b/_images/tutorial_task_space_controller.jpg new file mode 100644 index 0000000000..f122f414b2 Binary files /dev/null and b/_images/tutorial_task_space_controller.jpg differ diff --git a/_images/ur10_reach.jpg b/_images/ur10_reach.jpg new file mode 100644 index 0000000000..f32a241272 Binary files /dev/null and b/_images/ur10_reach.jpg differ diff --git a/_images/verify_install.jpg b/_images/verify_install.jpg new file mode 100644 index 0000000000..166840dc70 Binary files /dev/null and b/_images/verify_install.jpg differ diff --git a/_images/vscode_tasks.png b/_images/vscode_tasks.png new file mode 100644 index 0000000000..41d5cdb471 Binary files /dev/null and b/_images/vscode_tasks.png differ diff --git a/_images/wave_terrain.jpg b/_images/wave_terrain.jpg new file mode 100644 index 0000000000..2b759f113c Binary files /dev/null and b/_images/wave_terrain.jpg differ diff --git a/_images/wechat-group2-1121.jpg b/_images/wechat-group2-1121.jpg new file mode 100644 index 0000000000..b40b99cc1e Binary files /dev/null and b/_images/wechat-group2-1121.jpg differ diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000000..94b3a0c624 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,707 @@ + + + + + + + + + + + 概览:模块代码 — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

代码可用的所有模块

+ + +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/actuators/actuator_base.html b/_modules/omni/isaac/lab/actuators/actuator_base.html new file mode 100644 index 0000000000..f58e0a05e8 --- /dev/null +++ b/_modules/omni/isaac/lab/actuators/actuator_base.html @@ -0,0 +1,820 @@ + + + + + + + + + + + omni.isaac.lab.actuators.actuator_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.actuators.actuator_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.isaac.lab.utils.string as string_utils
+from omni.isaac.lab.utils.types import ArticulationActions
+
+if TYPE_CHECKING:
+    from .actuator_cfg import ActuatorBaseCfg
+
+
+
[文档]class ActuatorBase(ABC): + """Base class for actuator models over a collection of actuated joints in an articulation. + + Actuator models augment the simulated articulation joints with an external drive dynamics model. + The model is used to convert the user-provided joint commands (positions, velocities and efforts) + into the desired joint positions, velocities and efforts that are applied to the simulated articulation. + + The base class provides the interface for the actuator models. It is responsible for parsing the + actuator parameters from the configuration and storing them as buffers. It also provides the + interface for resetting the actuator state and computing the desired joint commands for the simulation. + + For each actuator model, a corresponding configuration class is provided. The configuration class + is used to parse the actuator parameters from the configuration. It also specifies the joint names + for which the actuator model is applied. These names can be specified as regular expressions, which + are matched against the joint names in the articulation. + + To see how the class is used, check the :class:`omni.isaac.lab.assets.Articulation` class. + """ + + computed_effort: torch.Tensor + """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).""" + effort_limit: torch.Tensor + """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).""" + stiffness: torch.Tensor + """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).""" + armature: torch.Tensor + """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).""" + +
[文档] def __init__( + self, + cfg: ActuatorBaseCfg, + joint_names: list[str], + joint_ids: slice | Sequence[int], + num_envs: int, + device: str, + stiffness: torch.Tensor | float = 0.0, + damping: torch.Tensor | float = 0.0, + armature: torch.Tensor | float = 0.0, + friction: torch.Tensor | float = 0.0, + effort_limit: torch.Tensor | float = torch.inf, + velocity_limit: torch.Tensor | float = torch.inf, + ): + """Initialize the actuator. + + Note: + The actuator parameters are parsed from the configuration and stored as buffers. If the parameters + are not specified in the configuration, then the default values provided in the arguments are used. + + Args: + cfg: The configuration of the actuator model. + joint_names: The joint names in the articulation. + joint_ids: The joint indices in the articulation. If :obj:`slice(None)`, then all + the joints in the articulation are part of the group. + 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). + damping: The default joint damping (D gain). Defaults to 0.0. + 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). + friction: The default joint friction. Defaults to 0.0. + 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). + velocity_limit: The default velocity limit. Defaults to infinity. + If a tensor, then the shape is (num_envs, num_joints). + """ + # save parameters + self.cfg = cfg + self._num_envs = num_envs + self._device = device + self._joint_names = joint_names + self._joint_indices = joint_ids + + # parse joint stiffness and damping + self.stiffness = self._parse_joint_parameter(self.cfg.stiffness, stiffness) + self.damping = self._parse_joint_parameter(self.cfg.damping, damping) + # parse joint armature and friction + self.armature = self._parse_joint_parameter(self.cfg.armature, armature) + self.friction = self._parse_joint_parameter(self.cfg.friction, friction) + # parse joint limits + # note: for velocity limits, we don't have USD parameter, so default is infinity + self.effort_limit = self._parse_joint_parameter(self.cfg.effort_limit, effort_limit) + self.velocity_limit = self._parse_joint_parameter(self.cfg.velocity_limit, velocity_limit) + + # create commands buffers for allocation + self.computed_effort = torch.zeros(self._num_envs, self.num_joints, device=self._device) + self.applied_effort = torch.zeros_like(self.computed_effort)
+ + def __str__(self) -> str: + """Returns: A string representation of the actuator group.""" + # resolve joint indices for printing + joint_indices = self.joint_indices + if joint_indices == slice(None): + joint_indices = list(range(self.num_joints)) + return ( + f"<class {self.__class__.__name__}> object:\n" + f"\tNumber of joints : {self.num_joints}\n" + f"\tJoint names expression: {self.cfg.joint_names_expr}\n" + f"\tJoint names : {self.joint_names}\n" + f"\tJoint indices : {joint_indices}\n" + ) + + """ + Properties. + """ + + @property + def num_joints(self) -> int: + """Number of actuators in the group.""" + return len(self._joint_names) + + @property + def joint_names(self) -> list[str]: + """Articulation's joint names that are part of the group.""" + return self._joint_names + + @property + def joint_indices(self) -> slice | Sequence[int]: + """Articulation's joint indices that are part of the group. + + Note: + If :obj:`slice(None)` is returned, then the group contains all the joints in the articulation. + We do this to avoid unnecessary indexing of the joints for performance reasons. + """ + return self._joint_indices + + """ + Operations. + """ + +
[文档] @abstractmethod + def reset(self, env_ids: Sequence[int]): + """Reset the internals within the group. + + Args: + env_ids: List of environment IDs to reset. + """ + raise NotImplementedError
+ +
[文档] @abstractmethod + def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + """Process the actuator group actions and compute the articulation actions. + + It computes the articulation actions based on the actuator model type + + 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). + + Returns: + The computed desired joint positions, joint velocities and joint efforts. + """ + raise NotImplementedError
+ + """ + Helper functions. + """ + + def _parse_joint_parameter( + self, cfg_value: float | dict[str, float] | None, default_value: float | torch.Tensor | None + ) -> torch.Tensor: + """Parse the joint parameter from the configuration. + + Args: + cfg_value: The parameter value from the configuration. If None, then use the default value. + default_value: The default value to use if the parameter is None. If it is also None, + then an error is raised. + + Returns: + The parsed parameter value. + + Raises: + TypeError: If the parameter value is not of the expected type. + TypeError: If the default value is not of the expected type. + ValueError: If the parameter value is None and no default value is provided. + ValueError: If the default value tensor is the wrong shape. + """ + # create parameter buffer + param = torch.zeros(self._num_envs, self.num_joints, device=self._device) + # parse the parameter + if cfg_value is not None: + if isinstance(cfg_value, (float, int)): + # if float, then use the same value for all joints + param[:] = float(cfg_value) + elif isinstance(cfg_value, dict): + # if dict, then parse the regular expression + indices, _, values = string_utils.resolve_matching_names_values(cfg_value, self.joint_names) + # note: need to specify type to be safe (e.g. values are ints, but we want floats) + param[:, indices] = torch.tensor(values, dtype=torch.float, device=self._device) + else: + raise TypeError( + f"Invalid type for parameter value: {type(cfg_value)} for " + + f"actuator on joints {self.joint_names}. Expected float or dict." + ) + elif default_value is not None: + if isinstance(default_value, (float, int)): + # if float, then use the same value for all joints + param[:] = float(default_value) + elif isinstance(default_value, torch.Tensor): + # if tensor, then use the same tensor for all joints + if default_value.shape == (self._num_envs, self.num_joints): + param = default_value.float() + else: + raise ValueError( + "Invalid default value tensor shape.\n" + f"Got: {default_value.shape}\n" + f"Expected: {(self._num_envs, self.num_joints)}" + ) + else: + raise TypeError( + f"Invalid type for default value: {type(default_value)} for " + + f"actuator on joints {self.joint_names}. Expected float or Tensor." + ) + else: + raise ValueError("The parameter value is None and no default value is provided.") + + return param + + def _clip_effort(self, effort: torch.Tensor) -> torch.Tensor: + """Clip the desired torques based on the motor limits. + + Args: + desired_torques: The desired torques to clip. + + Returns: + The clipped torques. + """ + return torch.clip(effort, min=-self.effort_limit, max=self.effort_limit)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/actuators/actuator_cfg.html b/_modules/omni/isaac/lab/actuators/actuator_cfg.html new file mode 100644 index 0000000000..68d0498c29 --- /dev/null +++ b/_modules/omni/isaac/lab/actuators/actuator_cfg.html @@ -0,0 +1,748 @@ + + + + + + + + + + + omni.isaac.lab.actuators.actuator_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.actuators.actuator_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from collections.abc import Iterable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+from . import actuator_net, actuator_pd
+from .actuator_base import ActuatorBase
+
+
+
[文档]@configclass +class ActuatorBaseCfg: + """Configuration for default actuators in an articulation.""" + + class_type: type[ActuatorBase] = MISSING + """The associated actuator class. + + The class should inherit from :class:`omni.isaac.lab.actuators.ActuatorBase`. + """ + + joint_names_expr: list[str] = MISSING + """Articulation's joint names that are part of the group. + + Note: + This can be a list of joint names or a list of regex expressions (e.g. ".*"). + """ + + effort_limit: dict[str, float] | float | None = None + """Force/Torque limit of the joints in the group. Defaults to None. + + If None, the limit is set to the value specified in the USD joint prim. + """ + + velocity_limit: dict[str, float] | float | None = None + """Velocity limit of the joints in the group. Defaults to None. + + If None, the limit is set to the value specified in the USD joint prim. + """ + + stiffness: dict[str, float] | float | None = MISSING + """Stiffness gains (also known as p-gain) of the joints in the group. + + 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 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. Defaults to None. + + 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. Defaults to None. + + If None, the joint friction is set to the value from the USD joint prim. + """
+ + +""" +Implicit Actuator Models. +""" + + +
[文档]@configclass +class ImplicitActuatorCfg(ActuatorBaseCfg): + """Configuration for an implicit actuator. + + Note: + The PD control is handled implicitly by the simulation. + """ + + class_type: type = actuator_pd.ImplicitActuator
+ + +""" +Explicit Actuator Models. +""" + + +
[文档]@configclass +class IdealPDActuatorCfg(ActuatorBaseCfg): + """Configuration for an ideal PD actuator.""" + + class_type: type = actuator_pd.IdealPDActuator
+ + +
[文档]@configclass +class DCMotorCfg(IdealPDActuatorCfg): + """Configuration for direct control (DC) motor actuator model.""" + + class_type: type = actuator_pd.DCMotor + + saturation_effort: float = MISSING + """Peak motor force/torque of the electric DC motor (in N-m)."""
+ + +
[文档]@configclass +class ActuatorNetLSTMCfg(DCMotorCfg): + """Configuration for LSTM-based actuator model.""" + + class_type: type = actuator_net.ActuatorNetLSTM + # we don't use stiffness and damping for actuator net + stiffness = None + damping = None + + network_file: str = MISSING + """Path to the file containing network weights."""
+ + +
[文档]@configclass +class ActuatorNetMLPCfg(DCMotorCfg): + """Configuration for MLP-based actuator model.""" + + class_type: type = actuator_net.ActuatorNetMLP + # we don't use stiffness and damping for actuator net + stiffness = None + damping = None + + network_file: str = MISSING + """Path to the file containing network weights.""" + + pos_scale: float = MISSING + """Scaling of the joint position errors input to the network.""" + vel_scale: float = MISSING + """Scaling of the joint velocities input to the network.""" + torque_scale: float = MISSING + """Scaling of the joint efforts output from the network.""" + + input_order: Literal["pos_vel", "vel_pos"] = MISSING + """Order of the inputs to the network. + + The order can be one of the following: + + * ``"pos_vel"``: joint position errors followed by joint velocities + * ``"vel_pos"``: joint velocities followed by joint position errors + """ + + input_idx: Iterable[int] = MISSING + """ + Indices of the actuator history buffer passed as inputs to the network. + + The index *0* corresponds to current time-step, while *n* corresponds to n-th + time-step in the past. The allocated history length is `max(input_idx) + 1`. + """
+ + +
[文档]@configclass +class DelayedPDActuatorCfg(IdealPDActuatorCfg): + """Configuration for a delayed PD actuator.""" + + class_type: type = actuator_pd.DelayedPDActuator + + min_delay: int = 0 + """Minimum number of physics time-steps with which the actuator command may be delayed. Defaults to 0.""" + + max_delay: int = 0 + """Maximum number of physics time-steps with which the actuator command may be delayed. Defaults to 0."""
+ + +
[文档]@configclass +class RemotizedPDActuatorCfg(DelayedPDActuatorCfg): + """Configuration for a remotized PD actuator. + + Note: + The torque output limits for this actuator is derived from a linear interpolation of a lookup table + in :attr:`joint_parameter_lookup`. This table describes the relationship between joint angles and + the output torques. + """ + + class_type: type = actuator_pd.RemotizedPDActuator + + joint_parameter_lookup: torch.Tensor = MISSING + """Joint parameter lookup table. Shape is (num_lookup_points, 3). + + This tensor describes the relationship between the joint angle (rad), the transmission ratio (in/out), + and the output torque (N*m). The table is used to interpolate the output torque based on the joint angle. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/actuators/actuator_net.html b/_modules/omni/isaac/lab/actuators/actuator_net.html new file mode 100644 index 0000000000..10a5f36adc --- /dev/null +++ b/_modules/omni/isaac/lab/actuators/actuator_net.html @@ -0,0 +1,747 @@ + + + + + + + + + + + omni.isaac.lab.actuators.actuator_net — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.actuators.actuator_net 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Neural network models for actuators.
+
+Currently, the following models are supported:
+
+* Multi-Layer Perceptron (MLP)
+* Long Short-Term Memory (LSTM)
+
+"""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+from omni.isaac.lab.utils.assets import read_file
+from omni.isaac.lab.utils.types import ArticulationActions
+
+from .actuator_pd import DCMotor
+
+if TYPE_CHECKING:
+    from .actuator_cfg import ActuatorNetLSTMCfg, ActuatorNetMLPCfg
+
+
+
[文档]class ActuatorNetLSTM(DCMotor): + """Actuator model based on recurrent neural network (LSTM). + + Unlike the MLP implementation :cite:t:`hwangbo2019learning`, this class implements + the learned model as a temporal neural network (LSTM) based on the work from + :cite:t:`rudin2022learning`. This removes the need of storing a history as the + hidden states of the recurrent network captures the history. + + Note: + Only the desired joint positions are used as inputs to the network. + """ + + cfg: ActuatorNetLSTMCfg + """The configuration of the actuator model.""" + +
[文档] def __init__(self, cfg: ActuatorNetLSTMCfg, *args, **kwargs): + super().__init__(cfg, *args, **kwargs) + + # load the model from JIT file + file_bytes = read_file(self.cfg.network_file) + self.network = torch.jit.load(file_bytes, map_location=self._device) + + # extract number of lstm layers and hidden dim from the shape of weights + num_layers = len(self.network.lstm.state_dict()) // 4 + hidden_dim = self.network.lstm.state_dict()["weight_hh_l0"].shape[1] + # create buffers for storing LSTM inputs + self.sea_input = torch.zeros(self._num_envs * self.num_joints, 1, 2, device=self._device) + self.sea_hidden_state = torch.zeros( + num_layers, self._num_envs * self.num_joints, hidden_dim, device=self._device + ) + self.sea_cell_state = torch.zeros(num_layers, self._num_envs * self.num_joints, hidden_dim, device=self._device) + # reshape via views (doesn't change the actual memory layout) + layer_shape_per_env = (num_layers, self._num_envs, self.num_joints, hidden_dim) + self.sea_hidden_state_per_env = self.sea_hidden_state.view(layer_shape_per_env) + self.sea_cell_state_per_env = self.sea_cell_state.view(layer_shape_per_env)
+ + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int]): + # reset the hidden and cell states for the specified environments + with torch.no_grad(): + self.sea_hidden_state_per_env[:, env_ids] = 0.0 + self.sea_cell_state_per_env[:, env_ids] = 0.0
+ +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + # compute network inputs + self.sea_input[:, 0, 0] = (control_action.joint_positions - joint_pos).flatten() + self.sea_input[:, 0, 1] = joint_vel.flatten() + # save current joint vel for dc-motor clipping + self._joint_vel[:] = joint_vel + + # run network inference + with torch.inference_mode(): + torques, (self.sea_hidden_state[:], self.sea_cell_state[:]) = self.network( + self.sea_input, (self.sea_hidden_state, self.sea_cell_state) + ) + self.computed_effort = torques.reshape(self._num_envs, self.num_joints) + + # clip the computed effort based on the motor limits + self.applied_effort = self._clip_effort(self.computed_effort) + + # return torques + control_action.joint_efforts = self.applied_effort + control_action.joint_positions = None + control_action.joint_velocities = None + return control_action
+ + +
[文档]class ActuatorNetMLP(DCMotor): + """Actuator model based on multi-layer perceptron and joint history. + + Many times the analytical model is not sufficient to capture the actuator dynamics, the + delay in the actuator response, or the non-linearities in the actuator. In these cases, + a neural network model can be used to approximate the actuator dynamics. This model is + trained using data collected from the physical actuator and maps the joint state and the + desired joint command to the produced torque by the actuator. + + This class implements the learned model as a neural network based on the work from + :cite:t:`hwangbo2019learning`. The class stores the history of the joint positions errors + and velocities which are used to provide input to the neural network. The model is loaded + as a TorchScript. + + Note: + Only the desired joint positions are used as inputs to the network. + + """ + + cfg: ActuatorNetMLPCfg + """The configuration of the actuator model.""" + +
[文档] def __init__(self, cfg: ActuatorNetMLPCfg, *args, **kwargs): + super().__init__(cfg, *args, **kwargs) + + # load the model from JIT file + file_bytes = read_file(self.cfg.network_file) + self.network = torch.jit.load(file_bytes, map_location=self._device) + + # create buffers for MLP history + history_length = max(self.cfg.input_idx) + 1 + self._joint_pos_error_history = torch.zeros( + self._num_envs, history_length, self.num_joints, device=self._device + ) + self._joint_vel_history = torch.zeros(self._num_envs, history_length, self.num_joints, device=self._device)
+ + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int]): + # reset the history for the specified environments + self._joint_pos_error_history[env_ids] = 0.0 + self._joint_vel_history[env_ids] = 0.0
+ +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + # move history queue by 1 and update top of history + # -- positions + self._joint_pos_error_history = self._joint_pos_error_history.roll(1, 1) + self._joint_pos_error_history[:, 0] = control_action.joint_positions - joint_pos + # -- velocity + self._joint_vel_history = self._joint_vel_history.roll(1, 1) + self._joint_vel_history[:, 0] = joint_vel + # save current joint vel for dc-motor clipping + self._joint_vel[:] = joint_vel + + # compute network inputs + # -- positions + pos_input = torch.cat([self._joint_pos_error_history[:, i].unsqueeze(2) for i in self.cfg.input_idx], dim=2) + pos_input = pos_input.view(self._num_envs * self.num_joints, -1) + # -- velocity + vel_input = torch.cat([self._joint_vel_history[:, i].unsqueeze(2) for i in self.cfg.input_idx], dim=2) + vel_input = vel_input.view(self._num_envs * self.num_joints, -1) + # -- scale and concatenate inputs + if self.cfg.input_order == "pos_vel": + network_input = torch.cat([pos_input * self.cfg.pos_scale, vel_input * self.cfg.vel_scale], dim=1) + elif self.cfg.input_order == "vel_pos": + network_input = torch.cat([vel_input * self.cfg.vel_scale, pos_input * self.cfg.pos_scale], dim=1) + else: + raise ValueError( + f"Invalid input order for MLP actuator net: {self.cfg.input_order}. Must be 'pos_vel' or 'vel_pos'." + ) + + # run network inference + torques = self.network(network_input).view(self._num_envs, self.num_joints) + self.computed_effort = torques.view(self._num_envs, self.num_joints) * self.cfg.torque_scale + + # clip the computed effort based on the motor limits + self.applied_effort = self._clip_effort(self.computed_effort) + + # return torques + control_action.joint_efforts = self.applied_effort + control_action.joint_positions = None + control_action.joint_velocities = None + return control_action
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/actuators/actuator_pd.html b/_modules/omni/isaac/lab/actuators/actuator_pd.html new file mode 100644 index 0000000000..62996df7fb --- /dev/null +++ b/_modules/omni/isaac/lab/actuators/actuator_pd.html @@ -0,0 +1,922 @@ + + + + + + + + + + + omni.isaac.lab.actuators.actuator_pd — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.actuators.actuator_pd 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+from omni.isaac.lab.utils import DelayBuffer, LinearInterpolation
+from omni.isaac.lab.utils.types import ArticulationActions
+
+from .actuator_base import ActuatorBase
+
+if TYPE_CHECKING:
+    from .actuator_cfg import (
+        DCMotorCfg,
+        DelayedPDActuatorCfg,
+        IdealPDActuatorCfg,
+        ImplicitActuatorCfg,
+        RemotizedPDActuatorCfg,
+    )
+
+
+"""
+Implicit Actuator Models.
+"""
+
+
+
[文档]class ImplicitActuator(ActuatorBase): + """Implicit actuator model that is handled by the simulation. + + This performs a similar function as the :class:`IdealPDActuator` class. However, the PD control is handled + implicitly by the simulation which performs continuous-time integration of the PD control law. This is + generally more accurate than the explicit PD control law used in :class:`IdealPDActuator` when the simulation + time-step is large. + + .. note:: + + The articulation class sets the stiffness and damping parameters from the configuration into the simulation. + Thus, the parameters are not used in this class. + + .. caution:: + + The class is only provided for consistency with the other actuator models. It does not implement any + functionality and should not be used. All values should be set to the simulation directly. + """ + + cfg: ImplicitActuatorCfg + """The configuration for the actuator model.""" + + """ + Operations. + """ + +
[文档] def reset(self, *args, **kwargs): + # This is a no-op. There is no state to reset for implicit actuators. + pass
+ +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + """Process the actuator group actions and compute the articulation actions. + + In case of implicit actuator, the control action is directly returned as the computed action. + This function is a no-op and does not perform any computation on the input control action. + However, it computes the approximate torques for the actuated joint since PhysX does not compute + this quantity explicitly. + + 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). + + Returns: + The computed desired joint positions, joint velocities and joint efforts. + """ + # store approximate torques for reward computation + error_pos = control_action.joint_positions - joint_pos + error_vel = control_action.joint_velocities - joint_vel + self.computed_effort = self.stiffness * error_pos + self.damping * error_vel + control_action.joint_efforts + # clip the torques based on the motor limits + self.applied_effort = self._clip_effort(self.computed_effort) + return control_action
+ + +""" +Explicit Actuator Models. +""" + + +
[文档]class IdealPDActuator(ActuatorBase): + r"""Ideal torque-controlled actuator model with a simple saturation model. + + It employs the following model for computing torques for the actuated joint :math:`j`: + + .. math:: + + \tau_{j, computed} = k_p * (q - q_{des}) + k_d * (\dot{q} - \dot{q}_{des}) + \tau_{ff} + + where, :math:`k_p` and :math:`k_d` are joint stiffness and damping gains, :math:`q` and :math:`\dot{q}` + are the current joint positions and velocities, :math:`q_{des}`, :math:`\dot{q}_{des}` and :math:`\tau_{ff}` + are the desired joint positions, velocities and torques commands. + + The clipping model is based on the maximum torque applied by the motor. It is implemented as: + + .. math:: + + \tau_{j, max} & = \gamma \times \tau_{motor, max} \\ + \tau_{j, applied} & = clip(\tau_{computed}, -\tau_{j, max}, \tau_{j, max}) + + where the clipping function is defined as :math:`clip(x, x_{min}, x_{max}) = min(max(x, x_{min}), x_{max})`. + The parameters :math:`\gamma` is the gear ratio of the gear box connecting the motor and the actuated joint ends, + and :math:`\tau_{motor, max}` is the maximum motor effort possible. These parameters are read from + the configuration instance passed to the class. + """ + + cfg: IdealPDActuatorCfg + """The configuration for the actuator model.""" + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int]): + pass
+ +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + # compute errors + error_pos = control_action.joint_positions - joint_pos + error_vel = control_action.joint_velocities - joint_vel + # calculate the desired joint torques + self.computed_effort = self.stiffness * error_pos + self.damping * error_vel + control_action.joint_efforts + # clip the torques based on the motor limits + self.applied_effort = self._clip_effort(self.computed_effort) + # set the computed actions back into the control action + control_action.joint_efforts = self.applied_effort + control_action.joint_positions = None + control_action.joint_velocities = None + return control_action
+ + +
[文档]class DCMotor(IdealPDActuator): + r"""Direct control (DC) motor actuator model with velocity-based saturation model. + + It uses the same model as the :class:`IdealActuator` for computing the torques from input commands. + However, it implements a saturation model defined by DC motor characteristics. + + A DC motor is a type of electric motor that is powered by direct current electricity. In most cases, + the motor is connected to a constant source of voltage supply, and the current is controlled by a rheostat. + Depending on various design factors such as windings and materials, the motor can draw a limited maximum power + from the electronic source, which limits the produced motor torque and speed. + + A DC motor characteristics are defined by the following parameters: + + * Continuous-rated speed (:math:`\dot{q}_{motor, max}`) : The maximum-rated speed of the motor. + * Continuous-stall torque (:math:`\tau_{motor, max}`): The maximum-rated torque produced at 0 speed. + * Saturation torque (:math:`\tau_{motor, sat}`): The maximum torque that can be outputted for a short period. + + Based on these parameters, the instantaneous minimum and maximum torques are defined as follows: + + .. math:: + + \tau_{j, max}(\dot{q}) & = clip \left (\tau_{j, sat} \times \left(1 - + \frac{\dot{q}}{\dot{q}_{j, max}}\right), 0.0, \tau_{j, max} \right) \\ + \tau_{j, min}(\dot{q}) & = clip \left (\tau_{j, sat} \times \left( -1 - + \frac{\dot{q}}{\dot{q}_{j, max}}\right), - \tau_{j, max}, 0.0 \right) + + where :math:`\gamma` is the gear ratio of the gear box connecting the motor and the actuated joint ends, + :math:`\dot{q}_{j, max} = \gamma^{-1} \times \dot{q}_{motor, max}`, :math:`\tau_{j, max} = + \gamma \times \tau_{motor, max}` and :math:`\tau_{j, peak} = \gamma \times \tau_{motor, peak}` + are the maximum joint velocity, maximum joint torque and peak torque, respectively. These parameters + are read from the configuration instance passed to the class. + + Using these values, the computed torques are clipped to the minimum and maximum values based on the + instantaneous joint velocity: + + .. math:: + + \tau_{j, applied} = clip(\tau_{computed}, \tau_{j, min}(\dot{q}), \tau_{j, max}(\dot{q})) + + """ + + cfg: DCMotorCfg + """The configuration for the actuator model.""" + +
[文档] def __init__(self, cfg: DCMotorCfg, *args, **kwargs): + super().__init__(cfg, *args, **kwargs) + # parse configuration + if self.cfg.saturation_effort is not None: + self._saturation_effort = self.cfg.saturation_effort + else: + self._saturation_effort = torch.inf + # prepare joint vel buffer for max effort computation + self._joint_vel = torch.zeros_like(self.computed_effort) + # create buffer for zeros effort + self._zeros_effort = torch.zeros_like(self.computed_effort) + # check that quantities are provided + if self.cfg.velocity_limit is None: + raise ValueError("The velocity limit must be provided for the DC motor actuator model.")
+ + """ + Operations. + """ + +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + # save current joint vel + self._joint_vel[:] = joint_vel + # calculate the desired joint torques + return super().compute(control_action, joint_pos, joint_vel)
+ + """ + Helper functions. + """ + + def _clip_effort(self, effort: torch.Tensor) -> torch.Tensor: + # compute torque limits + # -- max limit + max_effort = self._saturation_effort * (1.0 - self._joint_vel / self.velocity_limit) + max_effort = torch.clip(max_effort, min=self._zeros_effort, max=self.effort_limit) + # -- min limit + min_effort = self._saturation_effort * (-1.0 - self._joint_vel / self.velocity_limit) + min_effort = torch.clip(min_effort, min=-self.effort_limit, max=self._zeros_effort) + + # clip the torques based on the motor limits + return torch.clip(effort, min=min_effort, max=max_effort)
+ + +
[文档]class DelayedPDActuator(IdealPDActuator): + """Ideal PD actuator with delayed command application. + + This class extends the :class:`IdealPDActuator` class by adding a delay to the actuator commands. The delay + is implemented using a circular buffer that stores the actuator commands for a certain number of physics steps. + The most recent actuation value is pushed to the buffer at every physics step, but the final actuation value + applied to the simulation is lagged by a certain number of physics steps. + + The amount of time lag is configurable and can be set to a random value between the minimum and maximum time + lag bounds at every reset. The minimum and maximum time lag values are set in the configuration instance passed + to the class. + """ + + cfg: DelayedPDActuatorCfg + """The configuration for the actuator model.""" + +
[文档] def __init__(self, cfg: DelayedPDActuatorCfg, *args, **kwargs): + super().__init__(cfg, *args, **kwargs) + # instantiate the delay buffers + self.positions_delay_buffer = DelayBuffer(cfg.max_delay, self._num_envs, device=self._device) + self.velocities_delay_buffer = DelayBuffer(cfg.max_delay, self._num_envs, device=self._device) + self.efforts_delay_buffer = DelayBuffer(cfg.max_delay, self._num_envs, device=self._device) + # all of the envs + self._ALL_INDICES = torch.arange(self._num_envs, dtype=torch.long, device=self._device)
+ +
[文档] def reset(self, env_ids: Sequence[int]): + super().reset(env_ids) + # number of environments (since env_ids can be a slice) + if env_ids is None or env_ids == slice(None): + num_envs = self._num_envs + else: + num_envs = len(env_ids) + # set a new random delay for environments in env_ids + time_lags = torch.randint( + low=self.cfg.min_delay, + high=self.cfg.max_delay + 1, + size=(num_envs,), + dtype=torch.int, + device=self._device, + ) + # set delays + self.positions_delay_buffer.set_time_lag(time_lags, env_ids) + self.velocities_delay_buffer.set_time_lag(time_lags, env_ids) + self.efforts_delay_buffer.set_time_lag(time_lags, env_ids) + # reset buffers + self.positions_delay_buffer.reset(env_ids) + self.velocities_delay_buffer.reset(env_ids) + self.efforts_delay_buffer.reset(env_ids)
+ +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + # apply delay based on the delay the model for all the setpoints + control_action.joint_positions = self.positions_delay_buffer.compute(control_action.joint_positions) + control_action.joint_velocities = self.velocities_delay_buffer.compute(control_action.joint_velocities) + control_action.joint_efforts = self.efforts_delay_buffer.compute(control_action.joint_efforts) + # compte actuator model + return super().compute(control_action, joint_pos, joint_vel)
+ + +
[文档]class RemotizedPDActuator(DelayedPDActuator): + """Ideal PD actuator with angle-dependent torque limits. + + This class extends the :class:`DelayedPDActuator` class by adding angle-dependent torque limits to the actuator. + The torque limits are applied by querying a lookup table describing the relationship between the joint angle + and the maximum output torque. The lookup table is provided in the configuration instance passed to the class. + + The torque limits are interpolated based on the current joint positions and applied to the actuator commands. + """ + +
[文档] def __init__( + self, + cfg: RemotizedPDActuatorCfg, + joint_names: list[str], + joint_ids: Sequence[int], + num_envs: int, + device: str, + stiffness: torch.Tensor | float = 0.0, + damping: torch.Tensor | float = 0.0, + armature: torch.Tensor | float = 0.0, + friction: torch.Tensor | float = 0.0, + effort_limit: torch.Tensor | float = torch.inf, + velocity_limit: torch.Tensor | float = torch.inf, + ): + # remove effort and velocity box constraints from the base class + cfg.effort_limit = torch.inf + cfg.velocity_limit = torch.inf + # call the base method and set default effort_limit and velocity_limit to inf + super().__init__( + cfg, joint_names, joint_ids, num_envs, device, stiffness, damping, armature, friction, torch.inf, torch.inf + ) + self._joint_parameter_lookup = cfg.joint_parameter_lookup.to(device=device) + # define remotized joint torque limit + self._torque_limit = LinearInterpolation(self.angle_samples, self.max_torque_samples, device=device)
+ + """ + Properties. + """ + + @property + def angle_samples(self) -> torch.Tensor: + return self._joint_parameter_lookup[:, 0] + + @property + def transmission_ratio_samples(self) -> torch.Tensor: + return self._joint_parameter_lookup[:, 1] + + @property + def max_torque_samples(self) -> torch.Tensor: + return self._joint_parameter_lookup[:, 2] + + """ + Operations. + """ + +
[文档] def compute( + self, control_action: ArticulationActions, joint_pos: torch.Tensor, joint_vel: torch.Tensor + ) -> ArticulationActions: + # call the base method + control_action = super().compute(control_action, joint_pos, joint_vel) + # compute the absolute torque limits for the current joint positions + abs_torque_limits = self._torque_limit.compute(joint_pos) + # apply the limits + control_action.joint_efforts = torch.clamp( + control_action.joint_efforts, min=-abs_torque_limits, max=abs_torque_limits + ) + self.applied_effort = control_action.joint_efforts + return control_action
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/app/app_launcher.html b/_modules/omni/isaac/lab/app/app_launcher.html new file mode 100644 index 0000000000..b3da5c6408 --- /dev/null +++ b/_modules/omni/isaac/lab/app/app_launcher.html @@ -0,0 +1,1279 @@ + + + + + + + + + + + omni.isaac.lab.app.app_launcher — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.app.app_launcher 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-package with the utility class to configure the :class:`omni.isaac.kit.SimulationApp`.
+
+The :class:`AppLauncher` parses environment variables and input CLI arguments to launch the simulator in
+various different modes. This includes with or without GUI and switching between different Omniverse remote
+clients. Some of these require the extensions to be loaded in a specific order, otherwise a segmentation
+fault occurs. The launched :class:`omni.isaac.kit.SimulationApp` instance is accessible via the
+:attr:`AppLauncher.app` property.
+"""
+
+import argparse
+import contextlib
+import os
+import re
+import signal
+import sys
+from typing import Any, Literal
+
+with contextlib.suppress(ModuleNotFoundError):
+    import isaacsim  # noqa: F401
+
+from omni.isaac.kit import SimulationApp
+
+
+
[文档]class AppLauncher: + """A utility class to launch Isaac Sim application based on command-line arguments and environment variables. + + The class resolves the simulation app settings that appear through environments variables, + command-line arguments (CLI) or as input keyword arguments. Based on these settings, it launches the + simulation app and configures the extensions to load (as a part of post-launch setup). + + The input arguments provided to the class are given higher priority than the values set + from the corresponding environment variables. This provides flexibility to deal with different + users' preferences. + + .. note:: + Explicitly defined arguments are only given priority when their value is set to something outside + their default configuration. For example, the ``livestream`` argument is -1 by default. It only + overrides the ``LIVESTREAM`` environment variable when ``livestream`` argument is set to a + value >-1. In other words, if ``livestream=-1``, then the value from the environment variable + ``LIVESTREAM`` is used. + + """ + +
[文档] def __init__(self, launcher_args: argparse.Namespace | dict | None = None, **kwargs): + """Create a `SimulationApp`_ instance based on the input settings. + + Args: + launcher_args: Input arguments to parse using the AppLauncher and set into the SimulationApp. + Defaults to None, which is equivalent to passing an empty dictionary. A detailed description of + the possible arguments is available in the `SimulationApp`_ documentation. + **kwargs : Additional keyword arguments that will be merged into :attr:`launcher_args`. + They serve as a convenience for those who want to pass some arguments using the argparse + interface and others directly into the AppLauncher. Duplicated arguments with + the :attr:`launcher_args` will raise a ValueError. + + Raises: + ValueError: If there are common/duplicated arguments between ``launcher_args`` and ``kwargs``. + ValueError: If combination of ``launcher_args`` and ``kwargs`` are missing the necessary arguments + that are needed by the AppLauncher to resolve the desired app configuration. + ValueError: If incompatible or undefined values are assigned to relevant environment values, + such as ``LIVESTREAM``. + + .. _argparse.Namespace: https://docs.python.org/3/library/argparse.html?highlight=namespace#argparse.Namespace + .. _SimulationApp: https://docs.omniverse.nvidia.com/py/isaacsim/source/extensions/omni.isaac.kit/docs/index.html + """ + # We allow users to pass either a dict or an argparse.Namespace into + # __init__, anticipating that these will be all of the argparse arguments + # used by the calling script. Those which we appended via add_app_launcher_args + # will be used to control extension loading logic. Additional arguments are allowed, + # and will be passed directly to the SimulationApp initialization. + # + # We could potentially require users to enter each argument they want passed here + # as a kwarg, but this would require them to pass livestream, headless, and + # any other options we choose to add here explicitly, and with the correct keywords. + # + # @hunter: I feel that this is cumbersome and could introduce error, and would prefer to do + # some sanity checking in the add_app_launcher_args function + if launcher_args is None: + launcher_args = {} + elif isinstance(launcher_args, argparse.Namespace): + launcher_args = launcher_args.__dict__ + + # Check that arguments are unique + if len(kwargs) > 0: + if not set(kwargs.keys()).isdisjoint(launcher_args.keys()): + overlapping_args = set(kwargs.keys()).intersection(launcher_args.keys()) + raise ValueError( + f"Input `launcher_args` and `kwargs` both provided common attributes: {overlapping_args}." + " Please ensure that each argument is supplied to only one of them, as the AppLauncher cannot" + " discern priority between them." + ) + launcher_args.update(kwargs) + + # Define config members that are read from env-vars or keyword args + self._headless: bool # 0: GUI, 1: Headless + self._livestream: Literal[0, 1, 2] # 0: Disabled, 1: Native, 2: WebRTC + self._offscreen_render: bool # 0: Disabled, 1: Enabled + self._sim_experience_file: str # Experience file to load + + # Exposed to train scripts + self.device_id: int # device ID for GPU simulation (defaults to 0) + self.local_rank: int # local rank of GPUs in the current node + self.global_rank: int # global rank for multi-node training + + # Integrate env-vars and input keyword args into simulation app config + self._config_resolution(launcher_args) + # Create SimulationApp, passing the resolved self._config to it for initialization + self._create_app() + # Load IsaacSim extensions + self._load_extensions() + # Hide the stop button in the toolbar + self._hide_stop_button() + + # Set up signal handlers for graceful shutdown + # -- during interrupts + signal.signal(signal.SIGINT, self._interrupt_signal_handle_callback) + # -- during explicit `kill` commands + signal.signal(signal.SIGTERM, self._abort_signal_handle_callback) + # -- during segfaults + signal.signal(signal.SIGABRT, self._abort_signal_handle_callback) + signal.signal(signal.SIGSEGV, self._abort_signal_handle_callback)
+ + """ + Properties. + """ + + @property + def app(self) -> SimulationApp: + """The launched SimulationApp.""" + if self._app is not None: + return self._app + else: + raise RuntimeError("The `AppLauncher.app` member cannot be retrieved until the class is initialized.") + + """ + Operations. + """ + +
[文档] @staticmethod + def add_app_launcher_args(parser: argparse.ArgumentParser) -> None: + """Utility function to configure AppLauncher arguments with an existing argument parser object. + + This function takes an ``argparse.ArgumentParser`` object and does some sanity checking on the existing + arguments for ingestion by the SimulationApp. It then appends custom command-line arguments relevant + to the SimulationApp to the input :class:`argparse.ArgumentParser` instance. This allows overriding the + environment variables using command-line arguments. + + Currently, it adds the following parameters to the argparser object: + + * ``headless`` (bool): If True, the app will be launched in headless (no-gui) mode. The values map the same + as that for the ``HEADLESS`` environment variable. If False, then headless mode is determined by the + ``HEADLESS`` environment variable. + * ``livestream`` (int): If one of {1, 2}, then livestreaming and headless mode is enabled. The values + map the same as that for the ``LIVESTREAM`` environment variable. If :obj:`-1`, then livestreaming is + determined by the ``LIVESTREAM`` environment variable. + Valid options are: + + - ``0``: Disabled + - ``1``: `Native <https://docs.omniverse.nvidia.com/extensions/latest/ext_livestream/native.html>`_ + - ``2``: `WebRTC <https://docs.omniverse.nvidia.com/extensions/latest/ext_livestream/webrtc.html>`_ + + * ``enable_cameras`` (bool): If True, the app will enable camera sensors and render them, even when in + headless mode. This flag must be set to True if the environments contains any camera sensors. + The values map the same as that for the ``ENABLE_CAMERAS`` environment variable. + If False, then enable_cameras mode is determined by the ``ENABLE_CAMERAS`` environment variable. + * ``device`` (str): The device to run the simulation on. + Valid options are: + + - ``cpu``: Use CPU. + - ``cuda``: Use GPU with device ID ``0``. + - ``cuda:N``: Use GPU, where N is the device ID. For example, "cuda:0". + + * ``experience`` (str): The experience file to load when launching the SimulationApp. If a relative path + is provided, it is resolved relative to the ``apps`` folder in Isaac Sim and Isaac Lab (in that order). + + If provided as an empty string, the experience file is determined based on the command-line flags: + + * If headless and enable_cameras are True, the experience file is set to ``isaaclab.python.headless.rendering.kit``. + * If headless is False and enable_cameras is True, the experience file is set to ``isaaclab.python.rendering.kit``. + * If headless and enable_cameras are False, the experience file is set to ``isaaclab.python.kit``. + * If headless is True and enable_cameras is False, the experience file is set to ``isaaclab.python.headless.kit``. + + * ``kit_args`` (str): Optional command line arguments to be passed to Omniverse Kit directly. + Arguments should be combined into a single string separated by space. + Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2" + + Args: + parser: An argument parser instance to be extended with the AppLauncher specific options. + """ + # If the passed parser has an existing _HelpAction when passed, + # we here remove the options which would invoke it, + # to be added back after the additional AppLauncher args + # have been added. This is equivalent to + # initially constructing the ArgParser with add_help=False, + # but this means we don't have to require that behavior + # in users and can handle it on our end. + # We do this because calling parse_known_args() will handle + # any -h/--help options being passed and then exit immediately, + # before the additional arguments can be added to the help readout. + parser_help = None + if len(parser._actions) > 0 and isinstance(parser._actions[0], argparse._HelpAction): # type: ignore + parser_help = parser._actions[0] + parser._option_string_actions.pop("-h") + parser._option_string_actions.pop("--help") + + # Parse known args for potential name collisions/type mismatches + # between the config fields SimulationApp expects and the ArgParse + # arguments that the user passed. + known, _ = parser.parse_known_args() + config = vars(known) + if len(config) == 0: + print( + "[WARN][AppLauncher]: There are no arguments attached to the ArgumentParser object." + " If you have your own arguments, please load your own arguments before calling the" + " `AppLauncher.add_app_launcher_args` method. This allows the method to check the validity" + " of the arguments and perform checks for argument names." + ) + else: + AppLauncher._check_argparser_config_params(config) + + # Add custom arguments to the parser + arg_group = parser.add_argument_group( + "app_launcher arguments", + description="Arguments for the AppLauncher. For more details, please check the documentation.", + ) + arg_group.add_argument( + "--headless", + action="store_true", + default=AppLauncher._APPLAUNCHER_CFG_INFO["headless"][1], + help="Force display off at all times.", + ) + arg_group.add_argument( + "--livestream", + type=int, + default=AppLauncher._APPLAUNCHER_CFG_INFO["livestream"][1], + choices={0, 1, 2}, + help="Force enable livestreaming. Mapping corresponds to that for the `LIVESTREAM` environment variable.", + ) + arg_group.add_argument( + "--enable_cameras", + action="store_true", + default=AppLauncher._APPLAUNCHER_CFG_INFO["enable_cameras"][1], + help="Enable camera sensors and relevant extension dependencies.", + ) + arg_group.add_argument( + "--device", + type=str, + default=AppLauncher._APPLAUNCHER_CFG_INFO["device"][1], + help='The device to run the simulation on. Can be "cpu", "cuda", "cuda:N", where N is the device ID', + ) + # Add the deprecated cpu flag to raise an error if it is used + arg_group.add_argument("--cpu", action="store_true", help=argparse.SUPPRESS) + arg_group.add_argument( + "--verbose", # Note: This is read by SimulationApp through sys.argv + action="store_true", + help="Enable verbose-level log output from the SimulationApp.", + ) + arg_group.add_argument( + "--info", # Note: This is read by SimulationApp through sys.argv + action="store_true", + help="Enable info-level log output from the SimulationApp.", + ) + arg_group.add_argument( + "--experience", + type=str, + default="", + help=( + "The experience file to load when launching the SimulationApp. If an empty string is provided," + " the experience file is determined based on the headless flag. If a relative path is provided," + " it is resolved relative to the `apps` folder in Isaac Sim and Isaac Lab (in that order)." + ), + ) + arg_group.add_argument( + "--kit_args", + type=str, + default="", + help=( + "Command line arguments for Omniverse Kit as a string separated by a space delimiter." + ' Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2"' + ), + ) + + # Corresponding to the beginning of the function, + # if we have removed -h/--help handling, we add it back. + if parser_help is not None: + parser._option_string_actions["-h"] = parser_help + parser._option_string_actions["--help"] = parser_help
+ + """ + Internal functions. + """ + + _APPLAUNCHER_CFG_INFO: dict[str, tuple[list[type], Any]] = { + "headless": ([bool], False), + "livestream": ([int], -1), + "enable_cameras": ([bool], False), + "device": ([str], "cuda:0"), + "experience": ([str], ""), + } + """A dictionary of arguments added manually by the :meth:`AppLauncher.add_app_launcher_args` method. + + The values are a tuple of the expected type and default value. This is used to check against name collisions + for arguments passed to the :class:`AppLauncher` class as well as for type checking. + + They have corresponding environment variables as detailed in the documentation. + """ + + # TODO: Find some internally managed NVIDIA list of these types. + # SimulationApp.DEFAULT_LAUNCHER_CONFIG almost works, except that + # it is ambiguous where the default types are None + _SIM_APP_CFG_TYPES: dict[str, list[type]] = { + "headless": [bool], + "hide_ui": [bool, type(None)], + "active_gpu": [int, type(None)], + "physics_gpu": [int], + "multi_gpu": [bool], + "sync_loads": [bool], + "width": [int], + "height": [int], + "window_width": [int], + "window_height": [int], + "display_options": [int], + "subdiv_refinement_level": [int], + "renderer": [str], + "anti_aliasing": [int], + "samples_per_pixel_per_frame": [int], + "denoiser": [bool], + "max_bounces": [int], + "max_specular_transmission_bounces": [int], + "max_volume_bounces": [int], + "open_usd": [str, type(None)], + "livesync_usd": [str, type(None)], + "fast_shutdown": [bool], + "experience": [str], + } + """A dictionary containing the type of arguments passed to SimulationApp. + + 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 None types are allowed. + """ + + @staticmethod + def _check_argparser_config_params(config: dict) -> None: + """Checks that input argparser object has parameters with valid settings with no name conflicts. + + First, we inspect the dictionary to ensure that the passed ArgParser object is not attempting to add arguments + which should be assigned by calling :meth:`AppLauncher.add_app_launcher_args`. + + Then, we check that if the key corresponds to a config setting expected by SimulationApp, then the type of + that key's value corresponds to the type expected by the SimulationApp. If it passes the check, the function + prints out that the setting with be passed to the SimulationApp. Otherwise, we raise a ValueError exception. + + Args: + config: A configuration parameters which will be passed to the SimulationApp constructor. + + Raises: + ValueError: If a key is an already existing field in the configuration parameters but + should be added by calling the :meth:`AppLauncher.add_app_launcher_args. + ValueError: If keys corresponding to those used to initialize SimulationApp + (as found in :attr:`_SIM_APP_CFG_TYPES`) are of the wrong value type. + """ + # check that no config key conflicts with AppLauncher config names + applauncher_keys = set(AppLauncher._APPLAUNCHER_CFG_INFO.keys()) + for key, value in config.items(): + if key in applauncher_keys: + raise ValueError( + f"The passed ArgParser object already has the field '{key}'. This field will be added by" + " `AppLauncher.add_app_launcher_args()`, and should not be added directly. Please remove the" + " argument or rename it to a non-conflicting name." + ) + # check that type of the passed keys are valid + simulationapp_keys = set(AppLauncher._SIM_APP_CFG_TYPES.keys()) + for key, value in config.items(): + if key in simulationapp_keys: + given_type = type(value) + expected_types = AppLauncher._SIM_APP_CFG_TYPES[key] + if type(value) not in set(expected_types): + raise ValueError( + f"Invalid value type for the argument '{key}': {given_type}. Expected one of {expected_types}," + " if intended to be ingested by the SimulationApp object. Please change the type if this" + " intended for the SimulationApp or change the name of the argument to avoid name conflicts." + ) + # Print out values which will be used + print(f"[INFO][AppLauncher]: The argument '{key}' will be used to configure the SimulationApp.") + + def _config_resolution(self, launcher_args: dict): + """Resolve the input arguments and environment variables. + + Args: + launcher_args: A dictionary of all input arguments passed to the class object. + """ + # Handle all control logic resolution + + # --LIVESTREAM logic-- + # + livestream_env = int(os.environ.get("LIVESTREAM", 0)) + livestream_arg = launcher_args.pop("livestream", AppLauncher._APPLAUNCHER_CFG_INFO["livestream"][1]) + livestream_valid_vals = {0, 1, 2} + # Value checking on LIVESTREAM + if livestream_env not in livestream_valid_vals: + raise ValueError( + f"Invalid value for environment variable `LIVESTREAM`: {livestream_env} ." + f" Expected: {livestream_valid_vals}." + ) + # We allow livestream kwarg to supersede LIVESTREAM envvar + if livestream_arg >= 0: + if livestream_arg in livestream_valid_vals: + self._livestream = livestream_arg + # print info that we overrode the env-var + print( + f"[INFO][AppLauncher]: Input keyword argument `livestream={livestream_arg}` has overridden" + f" the environment variable `LIVESTREAM={livestream_env}`." + ) + else: + raise ValueError( + f"Invalid value for input keyword argument `livestream`: {livestream_arg} ." + f" Expected: {livestream_valid_vals}." + ) + else: + self._livestream = livestream_env + + # --HEADLESS logic-- + # + # Resolve headless execution of simulation app + # HEADLESS is initially passed as an int instead of + # the bool of headless_arg to avoid messy string processing, + headless_env = int(os.environ.get("HEADLESS", 0)) + headless_arg = launcher_args.pop("headless", AppLauncher._APPLAUNCHER_CFG_INFO["headless"][1]) + headless_valid_vals = {0, 1} + # Value checking on HEADLESS + if headless_env not in headless_valid_vals: + raise ValueError( + f"Invalid value for environment variable `HEADLESS`: {headless_env} . Expected: {headless_valid_vals}." + ) + # We allow headless kwarg to supersede HEADLESS envvar if headless_arg does not have the default value + # Note: Headless is always true when livestreaming + if headless_arg is True: + self._headless = headless_arg + elif self._livestream in {1, 2}: + # we are always headless on the host machine + self._headless = True + # inform who has toggled the headless flag + if self._livestream == livestream_arg: + print( + f"[INFO][AppLauncher]: Input keyword argument `livestream={self._livestream}` has implicitly" + f" overridden the environment variable `HEADLESS={headless_env}` to True." + ) + elif self._livestream == livestream_env: + print( + f"[INFO][AppLauncher]: Environment variable `LIVESTREAM={self._livestream}` has implicitly" + f" overridden the environment variable `HEADLESS={headless_env}` to True." + ) + else: + # Headless needs to be a bool to be ingested by SimulationApp + self._headless = bool(headless_env) + # Headless needs to be passed to the SimulationApp so we keep it here + launcher_args["headless"] = self._headless + + # --enable_cameras logic-- + # + enable_cameras_env = int(os.environ.get("ENABLE_CAMERAS", 0)) + enable_cameras_arg = launcher_args.pop("enable_cameras", AppLauncher._APPLAUNCHER_CFG_INFO["enable_cameras"][1]) + enable_cameras_valid_vals = {0, 1} + if enable_cameras_env not in enable_cameras_valid_vals: + raise ValueError( + f"Invalid value for environment variable `ENABLE_CAMERAS`: {enable_cameras_env} ." + f"Expected: {enable_cameras_valid_vals} ." + ) + # We allow enable_cameras kwarg to supersede ENABLE_CAMERAS envvar + if enable_cameras_arg is True: + self._enable_cameras = enable_cameras_arg + else: + self._enable_cameras = bool(enable_cameras_env) + self._offscreen_render = False + if self._enable_cameras and self._headless: + self._offscreen_render = True + + # Check if we can disable the viewport to improve performance + # This should only happen if we are running headless and do not require livestreaming or video recording + # This is different from offscreen_render because this only affects the default viewport and not other renderproducts in the scene + self._render_viewport = True + if self._headless and not self._livestream and not launcher_args.get("video", False): + self._render_viewport = False + + # hide_ui flag + launcher_args["hide_ui"] = False + if self._headless and not self._livestream: + launcher_args["hide_ui"] = True + + # --simulation GPU device logic -- + self.device_id = 0 + device = launcher_args.get("device", AppLauncher._APPLAUNCHER_CFG_INFO["device"][1]) + if "cuda" not in device and "cpu" not in device: + raise ValueError( + f"Invalid value for input keyword argument `device`: {device}." + " Expected: a string with the format 'cuda', 'cuda:<device_id>', or 'cpu'." + ) + if "cuda:" in device: + self.device_id = int(device.split(":")[-1]) + + # Raise an error for the deprecated cpu flag + if launcher_args.get("cpu", False): + raise ValueError("The `--cpu` flag is deprecated. Please use `--device cpu` instead.") + + if "distributed" in launcher_args and launcher_args["distributed"]: + # local rank (GPU id) in a current multi-gpu mode + self.local_rank = int(os.getenv("LOCAL_RANK", "0")) + int(os.getenv("JAX_LOCAL_RANK", "0")) + # global rank (GPU id) in multi-gpu multi-node mode + self.global_rank = int(os.getenv("RANK", "0")) + int(os.getenv("JAX_RANK", "0")) + + self.device_id = self.local_rank + launcher_args["multi_gpu"] = False + # limit CPU threads to minimize thread context switching + # this ensures processes do not take up all available threads and fight for resources + num_cpu_cores = os.cpu_count() + num_threads_per_process = num_cpu_cores // int(os.getenv("WORLD_SIZE", 1)) + # set environment variables to limit CPU threads + os.environ["PXR_WORK_THREAD_LIMIT"] = str(num_threads_per_process) + os.environ["OPENBLAS_NUM_THREADS"] = str(num_threads_per_process) + # pass command line variable to kit + sys.argv.append(f"--/plugins/carb.tasking.plugin/threadCount={num_threads_per_process}") + + # set physics and rendering device + launcher_args["physics_gpu"] = self.device_id + launcher_args["active_gpu"] = self.device_id + + # Check if input keywords contain an 'experience' file setting + # Note: since experience is taken as a separate argument by Simulation App, we store it separately + self._sim_experience_file = launcher_args.pop("experience", "") + + # If nothing is provided resolve the experience file based on the headless flag + kit_app_exp_path = os.environ["EXP_PATH"] + isaaclab_app_exp_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), *[".."] * 6, "apps") + if self._sim_experience_file == "": + # check if the headless flag is setS + if self._enable_cameras: + if self._headless and not self._livestream: + self._sim_experience_file = os.path.join( + isaaclab_app_exp_path, "isaaclab.python.headless.rendering.kit" + ) + else: + self._sim_experience_file = os.path.join(isaaclab_app_exp_path, "isaaclab.python.rendering.kit") + elif self._headless and not self._livestream: + self._sim_experience_file = os.path.join(isaaclab_app_exp_path, "isaaclab.python.headless.kit") + else: + self._sim_experience_file = os.path.join(isaaclab_app_exp_path, "isaaclab.python.kit") + elif not os.path.isabs(self._sim_experience_file): + option_1_app_exp_path = os.path.join(kit_app_exp_path, self._sim_experience_file) + option_2_app_exp_path = os.path.join(isaaclab_app_exp_path, self._sim_experience_file) + if os.path.exists(option_1_app_exp_path): + self._sim_experience_file = option_1_app_exp_path + elif os.path.exists(option_2_app_exp_path): + self._sim_experience_file = option_2_app_exp_path + else: + raise FileNotFoundError( + f"Invalid value for input keyword argument `experience`: {self._sim_experience_file}." + "\n No such file exists in either the Kit or Isaac Lab experience paths. Checked paths:" + f"\n\t [1]: {option_1_app_exp_path}" + f"\n\t [2]: {option_2_app_exp_path}" + ) + elif not os.path.exists(self._sim_experience_file): + raise FileNotFoundError( + f"Invalid value for input keyword argument `experience`: {self._sim_experience_file}." + " The file does not exist." + ) + + # Resolve additional arguments passed to Kit + self._kit_args = [] + if "kit_args" in launcher_args: + self._kit_args = [arg for arg in launcher_args["kit_args"].split()] + sys.argv += self._kit_args + + # Resolve the absolute path of the experience file + self._sim_experience_file = os.path.abspath(self._sim_experience_file) + print(f"[INFO][AppLauncher]: Loading experience file: {self._sim_experience_file}") + # Remove all values from input keyword args which are not meant for SimulationApp + # Assign all the passed settings to a dictionary for the simulation app + self._sim_app_config = { + key: launcher_args[key] for key in set(AppLauncher._SIM_APP_CFG_TYPES.keys()) & set(launcher_args.keys()) + } + + def _create_app(self): + """Launch and create the SimulationApp based on the parsed simulation config.""" + # Initialize SimulationApp + # hack sys module to make sure that the SimulationApp is initialized correctly + # this is to avoid the warnings from the simulation app about not ok modules + r = re.compile(".*lab.*") + found_modules = list(filter(r.match, list(sys.modules.keys()))) + found_modules += ["omni.isaac.kit.app_framework"] + # remove Isaac Lab modules from sys.modules + hacked_modules = dict() + for key in found_modules: + hacked_modules[key] = sys.modules[key] + del sys.modules[key] + + # disable sys stdout and stderr to avoid printing the warning messages + # this is mainly done to purge the print statements from the simulation app + if "--verbose" not in sys.argv and "--info" not in sys.argv: + sys.stdout = open(os.devnull, "w") # noqa: SIM115 + # launch simulation app + self._app = SimulationApp(self._sim_app_config, experience=self._sim_experience_file) + # enable sys stdout and stderr + sys.stdout = sys.__stdout__ + + # add Isaac Lab modules back to sys.modules + for key, value in hacked_modules.items(): + sys.modules[key] = value + # remove the threadCount argument from sys.argv if it was added for distributed training + pattern = r"--/plugins/carb\.tasking\.plugin/threadCount=\d+" + sys.argv = [arg for arg in sys.argv if not re.match(pattern, arg)] + # remove additional OV args from sys.argv + if len(self._kit_args) > 0: + sys.argv = [arg for arg in sys.argv if arg not in self._kit_args] + + def _rendering_enabled(self) -> bool: + """Check if rendering is required by the app.""" + # Indicates whether rendering is required by the app. + # Extensions required for rendering bring startup and simulation costs, so we do not enable them if not required. + return not self._headless or self._livestream >= 1 or self._enable_cameras + + def _load_extensions(self): + """Load correct extensions based on AppLauncher's resolved config member variables.""" + # These have to be loaded after SimulationApp is initialized + import carb + import omni.physx.bindings._physx as physx_impl + from omni.isaac.core.utils.extensions import enable_extension + + # Retrieve carb settings for modification + carb_settings_iface = carb.settings.get_settings() + + if self._livestream >= 1: + # Ensure that a viewport exists in case an experience has been + # loaded which does not load it by default + enable_extension("omni.kit.viewport.window") + # Set carb settings to allow for livestreaming + carb_settings_iface.set_bool("/app/livestream/enabled", True) + carb_settings_iface.set_bool("/app/window/drawMouse", True) + carb_settings_iface.set_bool("/ngx/enabled", False) + carb_settings_iface.set_string("/app/livestream/proto", "ws") + carb_settings_iface.set_int("/app/livestream/websocket/framerate_limit", 120) + # Note: Only one livestream extension can be enabled at a time + if self._livestream == 1: + # Enable Native Livestream extension + # Default App: Streaming Client from the Omniverse Launcher + enable_extension("omni.kit.streamsdk.plugins-3.2.1") + enable_extension("omni.kit.livestream.core-3.2.0") + enable_extension("omni.kit.livestream.native-4.1.0") + elif self._livestream == 2: + # Enable WebRTC Livestream extension + # Default URL: http://localhost:8211/streaming/webrtc-client/ + enable_extension("omni.services.streamclient.webrtc") + else: + raise ValueError(f"Invalid value for livestream: {self._livestream}. Expected: 1, 2 .") + else: + carb_settings_iface.set_bool("/app/livestream/enabled", False) + + # set carb setting to indicate Isaac Lab's offscreen_render pipeline should be enabled + # this flag is used by the SimulationContext class to enable the offscreen_render pipeline + # when the render() method is called. + carb_settings_iface.set_bool("/isaaclab/render/offscreen", self._offscreen_render) + + # set carb setting to indicate Isaac Lab's render_viewport pipeline should be enabled + # this flag is used by the SimulationContext class to enable the render_viewport pipeline + # when the render() method is called. + carb_settings_iface.set_bool("/isaaclab/render/active_viewport", self._render_viewport) + + # set carb setting to indicate no RTX sensors are used + # this flag is set to True when an RTX-rendering related sensor is created + # for example: the `Camera` sensor class + carb_settings_iface.set_bool("/isaaclab/render/rtx_sensors", False) + + # set fabric update flag to disable updating transforms when rendering is disabled + carb_settings_iface.set_bool("/physics/fabricUpdateTransformations", self._rendering_enabled()) + + # set the nucleus directory manually to the latest published Nucleus + # note: this is done to ensure prior versions of Isaac Sim still use the latest assets + assets_path = "http://omniverse-content-production.s3-us-west-2.amazonaws.com/Assets/Isaac/4.2" + carb_settings_iface.set_string("/persistent/isaac/asset_root/default", assets_path) + carb_settings_iface.set_string("/persistent/isaac/asset_root/cloud", assets_path) + carb_settings_iface.set_string("/persistent/isaac/asset_root/nvidia", assets_path) + + # disable physics backwards compatibility check + carb_settings_iface.set_int(physx_impl.SETTING_BACKWARD_COMPATIBILITY, 0) + + def _hide_stop_button(self): + """Hide the stop button in the toolbar. + + For standalone executions, having a stop button is confusing since it invalidates the whole simulation. + Thus, we hide the button so that users don't accidentally click it. + """ + # when we are truly headless, then we can't import the widget toolbar + # thus, we only hide the stop button when we are not headless (i.e. GUI is enabled) + if self._livestream >= 1 or not self._headless: + import omni.kit.widget.toolbar + + # grey out the stop button because we don't want to stop the simulation manually in standalone mode + toolbar = omni.kit.widget.toolbar.get_instance() + play_button_group = toolbar._builtin_tools._play_button_group # type: ignore + if play_button_group is not None: + play_button_group._stop_button.visible = False # type: ignore + play_button_group._stop_button.enabled = False # type: ignore + play_button_group._stop_button = None # type: ignore + + def _interrupt_signal_handle_callback(self, signal, frame): + """Handle the interrupt signal from the keyboard.""" + # close the app + self._app.close() + # raise the error for keyboard interrupt + raise KeyboardInterrupt + + def _abort_signal_handle_callback(self, signal, frame): + """Handle the abort/segmentation/kill signals.""" + # close the app + self._app.close()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/articulation/articulation.html b/_modules/omni/isaac/lab/assets/articulation/articulation.html new file mode 100644 index 0000000000..f906aeacb0 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/articulation/articulation.html @@ -0,0 +1,1933 @@ + + + + + + + + + + + omni.isaac.lab.assets.articulation.articulation — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.articulation.articulation 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# Flag for pyright to ignore type errors in this file.
+# pyright: reportPrivateUsage=false
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.stage as stage_utils
+import omni.log
+import omni.physics.tensors.impl.api as physx
+from pxr import PhysxSchema, UsdPhysics
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.math as math_utils
+import omni.isaac.lab.utils.string as string_utils
+from omni.isaac.lab.actuators import ActuatorBase, ActuatorBaseCfg, ImplicitActuator
+from omni.isaac.lab.utils.types import ArticulationActions
+
+from ..asset_base import AssetBase
+from .articulation_data import ArticulationData
+
+if TYPE_CHECKING:
+    from .articulation_cfg import ArticulationCfg
+
+
+
[文档]class Articulation(AssetBase): + """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 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.lab.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.""" + + actuators: dict[str, ActuatorBase] + """Dictionary of actuator instances for the articulation. + + The keys are the actuator names and the values are the actuator instances. The actuator instances + are initialized based on the actuator configurations specified in the :attr:`ArticulationCfg.actuators` + attribute. They are used to compute the joint commands during the :meth:`write_data_to_sim` function. + """ + +
[文档] def __init__(self, cfg: ArticulationCfg): + """Initialize the articulation. + + Args: + cfg: A configuration instance. + """ + super().__init__(cfg)
+ + """ + Properties + """ + + @property + def data(self) -> ArticulationData: + return self._data + + @property + def num_instances(self) -> int: + return self.root_physx_view.count + + @property + def is_fixed_base(self) -> bool: + """Whether the articulation is a fixed-base or floating-base system.""" + return self.root_physx_view.shared_metatype.fixed_base + + @property + def num_joints(self) -> int: + """Number of joints in articulation.""" + return self.root_physx_view.shared_metatype.dof_count + + @property + def num_fixed_tendons(self) -> int: + """Number of fixed tendons in articulation.""" + return self.root_physx_view.max_fixed_tendons + + @property + def num_bodies(self) -> int: + """Number of bodies in articulation.""" + return self.root_physx_view.shared_metatype.link_count + + @property + def joint_names(self) -> list[str]: + """Ordered names of joints in articulation.""" + return self.root_physx_view.shared_metatype.dof_names + + @property + def fixed_tendon_names(self) -> list[str]: + """Ordered names of fixed tendons in articulation.""" + return self._fixed_tendon_names + + @property + def body_names(self) -> list[str]: + """Ordered names of bodies in articulation.""" + return self.root_physx_view.shared_metatype.link_names + + @property + def root_physx_view(self) -> physx.ArticulationView: + """Articulation view for the asset (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._root_physx_view + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # use ellipses object to skip initial indices. + if env_ids is None: + env_ids = slice(None) + # reset actuators + for actuator in self.actuators.values(): + actuator.reset(env_ids) + # reset external wrench + self._external_force_b[env_ids] = 0.0 + self._external_torque_b[env_ids] = 0.0
+ +
[文档] def write_data_to_sim(self): + """Write external wrenches and joint commands to the simulation. + + If any explicit actuators are present, then the actuator models are used to compute the + joint commands. Otherwise, the joint commands are directly set into the simulation. + + Note: + We write external wrench to the simulation here since this function is called before the simulation step. + This ensures that the external wrench is applied at every simulation step. + """ + # write external wrench + if self.has_external_wrench: + self.root_physx_view.apply_forces_and_torques_at_position( + force_data=self._external_force_b.view(-1, 3), + torque_data=self._external_torque_b.view(-1, 3), + position_data=None, + indices=self._ALL_INDICES, + is_global=False, + ) + + # apply actuator models + self._apply_actuator_model() + # write actions into simulation + self.root_physx_view.set_dof_actuation_forces(self._joint_effort_target_sim, self._ALL_INDICES) + # position and velocity targets only for implicit actuators + if self._has_implicit_actuators: + self.root_physx_view.set_dof_position_targets(self._joint_pos_target_sim, self._ALL_INDICES) + self.root_physx_view.set_dof_velocity_targets(self._joint_vel_target_sim, self._ALL_INDICES)
+ +
[文档] def update(self, dt: float): + self._data.update(dt)
+ + """ + Operations - Finders. + """ + +
[文档] def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]: + """Find bodies in the articulation based on the name keys. + + Please check the :meth:`omni.isaac.lab.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. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple of lists containing the body indices and names. + """ + return string_utils.resolve_matching_names(name_keys, self.body_names, preserve_order)
+ +
[文档] def find_joints( + self, name_keys: str | Sequence[str], joint_subset: list[str] | None = None, preserve_order: bool = False + ) -> tuple[list[int], list[str]]: + """Find joints in the articulation based on the name keys. + + Please see the :func:`omni.isaac.lab.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 + in the articulation are searched. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple of lists containing the joint indices and names. + """ + if joint_subset is None: + joint_subset = self.joint_names + # find joints + return string_utils.resolve_matching_names(name_keys, joint_subset, preserve_order)
+ +
[文档] def find_fixed_tendons( + self, name_keys: str | Sequence[str], tendon_subsets: list[str] | None = None, preserve_order: bool = False + ) -> tuple[list[int], list[str]]: + """Find fixed tendons in the articulation based on the name keys. + + Please see the :func:`omni.isaac.lab.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 with fixed tendons. + tendon_subsets: A subset of joints with fixed tendons to search for. Defaults to None, which means + all joints in the articulation are searched. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple of lists containing the tendon indices and names. + """ + if tendon_subsets is None: + # tendons follow the joint names they are attached to + tendon_subsets = self.fixed_tendon_names + # find tendons + return string_utils.resolve_matching_names(name_keys, tendon_subsets, preserve_order)
+ + """ + Operations - Writers. + """ + +
[文档] def write_root_state_to_sim(self, root_state: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the root state over selected environment indices into the simulation. + + The root state comprises of the cartesian position, quaternion orientation in (w, x, y, z), and linear + 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 None, then all indices are used. + """ + # set into simulation + self.write_root_pose_to_sim(root_state[:, :7], env_ids=env_ids) + self.write_root_velocity_to_sim(root_state[:, 7:], env_ids=env_ids)
+ +
[文档] def write_root_pose_to_sim(self, root_pose: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the root pose over selected environment indices into the simulation. + + 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 None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.root_state_w[env_ids, :7] = root_pose.clone() + # convert root quaternion from wxyz to xyzw + root_poses_xyzw = self._data.root_state_w[:, :7].clone() + root_poses_xyzw[:, 3:] = math_utils.convert_quat(root_poses_xyzw[:, 3:], to="xyzw") + # Need to invalidate the buffer to trigger the update with the new root pose. + self._data._body_state_w.timestamp = -1.0 + # set into simulation + self.root_physx_view.set_root_transforms(root_poses_xyzw, indices=physx_env_ids)
+ +
[文档] def write_root_velocity_to_sim(self, root_velocity: torch.Tensor, env_ids: Sequence[int] | None = None): + """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 None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.root_state_w[env_ids, 7:] = root_velocity.clone() + self._data.body_acc_w[env_ids] = 0.0 + # set into simulation + self.root_physx_view.set_root_velocities(self._data.root_state_w[:, 7:], indices=physx_env_ids)
+ +
[文档] def write_joint_state_to_sim( + self, + position: torch.Tensor, + velocity: torch.Tensor, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | slice | None = None, + ): + """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)). + 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). + """ + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set into internal buffers + self._data.joint_pos[env_ids, joint_ids] = position + self._data.joint_vel[env_ids, joint_ids] = velocity + self._data._previous_joint_vel[env_ids, joint_ids] = velocity + self._data.joint_acc[env_ids, joint_ids] = 0.0 + # Need to invalidate the buffer to trigger the update with the new root pose. + self._data._body_state_w.timestamp = -1.0 + # set into simulation + self.root_physx_view.set_dof_positions(self._data.joint_pos, indices=physx_env_ids) + self.root_physx_view.set_dof_velocities(self._data.joint_vel, indices=physx_env_ids)
+ +
[文档] def write_joint_stiffness_to_sim( + self, + stiffness: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint stiffness into the simulation. + + Args: + 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). + """ + # note: This function isn't setting the values for actuator models. (#128) + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set into internal buffers + self._data.joint_stiffness[env_ids, joint_ids] = stiffness + # set into simulation + self.root_physx_view.set_dof_stiffnesses(self._data.joint_stiffness.cpu(), indices=physx_env_ids.cpu())
+ +
[文档] def write_joint_damping_to_sim( + self, + damping: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint damping into the simulation. + + Args: + 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. + Defaults to None (all environments). + """ + # note: This function isn't setting the values for actuator models. (#128) + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set into internal buffers + self._data.joint_damping[env_ids, joint_ids] = damping + # set into simulation + self.root_physx_view.set_dof_dampings(self._data.joint_damping.cpu(), indices=physx_env_ids.cpu())
+ +
[文档] def write_joint_effort_limit_to_sim( + self, + limits: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint effort limits into the simulation. + + Args: + 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). + """ + # note: This function isn't setting the values for actuator models. (#128) + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # move tensor to cpu if needed + if isinstance(limits, torch.Tensor): + limits = limits.cpu() + # set into internal buffers + torque_limit_all = self.root_physx_view.get_dof_max_forces() + torque_limit_all[env_ids, joint_ids] = limits + # set into simulation + self.root_physx_view.set_dof_max_forces(torque_limit_all.cpu(), indices=physx_env_ids.cpu())
+ +
[文档] def write_joint_armature_to_sim( + self, + armature: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint armature into the simulation. + + Args: + 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). + """ + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set into internal buffers + self._data.joint_armature[env_ids, joint_ids] = armature + # set into simulation + self.root_physx_view.set_dof_armatures(self._data.joint_armature.cpu(), indices=physx_env_ids.cpu())
+ +
[文档] def write_joint_friction_to_sim( + self, + joint_friction: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint friction into the simulation. + + Args: + 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). + """ + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set into internal buffers + self._data.joint_friction[env_ids, joint_ids] = joint_friction + # set into simulation + self.root_physx_view.set_dof_friction_coefficients(self._data.joint_friction.cpu(), indices=physx_env_ids.cpu())
+ +
[文档] def write_joint_limits_to_sim( + self, + limits: torch.Tensor | float, + joint_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write joint limits into the simulation. + + Args: + limits: Joint limits. Shape is (len(env_ids), len(joint_ids), 2). + joint_ids: The joint indices to set the limits for. Defaults to None (all joints). + env_ids: The environment indices to set the limits for. Defaults to None (all environments). + """ + # note: This function isn't setting the values for actuator models. (#128) + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set into internal buffers + self._data.joint_limits[env_ids, joint_ids] = limits + # update default joint pos to stay within the new limits + if torch.any( + (self._data.default_joint_pos[env_ids, joint_ids] < limits[..., 0]) + | (self._data.default_joint_pos[env_ids, joint_ids] > limits[..., 1]) + ): + self._data.default_joint_pos[env_ids, joint_ids] = torch.clamp( + self._data.default_joint_pos[env_ids, joint_ids], limits[..., 0], limits[..., 1] + ) + omni.log.warn( + "Some default joint positions are outside of the range of the new joint limits. Default joint positions" + " will be clamped to be within the new joint limits." + ) + # set into simulation + self.root_physx_view.set_dof_limits(self._data.joint_limits.cpu(), indices=physx_env_ids.cpu())
+ + """ + Operations - Setters. + """ + +
[文档] def set_external_force_and_torque( + self, + forces: torch.Tensor, + torques: torch.Tensor, + body_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set external force and torque to apply on the asset's bodies in their local frame. + + For many applications, we want to keep the applied external force on rigid bodies constant over a period of + time (for instance, during the policy control). This function allows us to store the external force and torque + into buffers which are then applied to the simulation at every step. + + .. caution:: + If the function is called with empty forces and torques, then this function disables the application + 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)) + + .. note:: + This function does not apply the external wrench to the simulation. It only fills the buffers with + the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function + right before the simulation step. + + 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). + 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). + """ + if forces.any() or torques.any(): + self.has_external_wrench = True + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_INDICES + elif not isinstance(env_ids, torch.Tensor): + env_ids = torch.tensor(env_ids, dtype=torch.long, device=self.device) + # -- body_ids + if body_ids is None: + body_ids = torch.arange(self.num_bodies, dtype=torch.long, device=self.device) + elif isinstance(body_ids, slice): + body_ids = torch.arange(self.num_bodies, dtype=torch.long, device=self.device)[body_ids] + elif not isinstance(body_ids, torch.Tensor): + body_ids = torch.tensor(body_ids, dtype=torch.long, device=self.device) + + # note: we need to do this complicated indexing since torch doesn't support multi-indexing + # create global body indices from env_ids and env_body_ids + # (env_id * total_bodies_per_env) + body_id + indices = body_ids.repeat(len(env_ids), 1) + env_ids.unsqueeze(1) * self.num_bodies + indices = indices.view(-1) + # set into internal buffers + # note: these are applied in the write_to_sim function + self._external_force_b.flatten(0, 1)[indices] = forces.flatten(0, 1) + self._external_torque_b.flatten(0, 1)[indices] = torques.flatten(0, 1) + else: + self.has_external_wrench = False
+ +
[文档] def set_joint_position_target( + self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None + ): + """Set joint position targets into internal buffers. + + .. note:: + This function does not apply the joint targets to the simulation. It only fills the buffers with + 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)). + 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). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set targets + self._data.joint_pos_target[env_ids, joint_ids] = target
+ +
[文档] def set_joint_velocity_target( + self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None + ): + """Set joint velocity targets into internal buffers. + + .. note:: + This function does not apply the joint targets to the simulation. It only fills the buffers with + 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)). + 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). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set targets + self._data.joint_vel_target[env_ids, joint_ids] = target
+ +
[文档] def set_joint_effort_target( + self, target: torch.Tensor, joint_ids: Sequence[int] | slice | None = None, env_ids: Sequence[int] | None = None + ): + """Set joint efforts into internal buffers. + + .. note:: + This function does not apply the joint targets to the simulation. It only fills the buffers with + 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)). + 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). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if joint_ids is None: + joint_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and joint_ids != slice(None): + env_ids = env_ids[:, None] + # set targets + self._data.joint_effort_target[env_ids, joint_ids] = target
+ + """ + Operations - Tendons. + """ + +
[文档] def set_fixed_tendon_stiffness( + self, + stiffness: torch.Tensor, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set fixed tendon stiffness into internal buffers. + + .. note:: + This function does not apply the tendon stiffness to the simulation. It only fills the buffers with + the desired values. To apply the tendon stiffness, call the :meth:`write_fixed_tendon_properties_to_sim` function. + + Args: + stiffness: Fixed tendon stiffness. Shape is (len(env_ids), len(fixed_tendon_ids)). + fixed_tendon_ids: The tendon indices to set the stiffness for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the stiffness for. Defaults to None (all environments). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + if env_ids != slice(None) and fixed_tendon_ids != slice(None): + env_ids = env_ids[:, None] + # set stiffness + self._data.fixed_tendon_stiffness[env_ids, fixed_tendon_ids] = stiffness
+ +
[文档] def set_fixed_tendon_damping( + self, + damping: torch.Tensor, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set fixed tendon damping into internal buffers. + + .. note:: + This function does not apply the tendon damping to the simulation. It only fills the buffers with + the desired values. To apply the tendon damping, call the :meth:`write_fixed_tendon_properties_to_sim` function. + + Args: + damping: Fixed tendon damping. Shape is (len(env_ids), len(fixed_tendon_ids)). + fixed_tendon_ids: The tendon indices to set the damping for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the damping for. Defaults to None (all environments). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + if env_ids != slice(None) and fixed_tendon_ids != slice(None): + env_ids = env_ids[:, None] + # set damping + self._data.fixed_tendon_damping[env_ids, fixed_tendon_ids] = damping
+ +
[文档] def set_fixed_tendon_limit_stiffness( + self, + limit_stiffness: torch.Tensor, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set fixed tendon limit stiffness efforts into internal buffers. + + .. note:: + This function does not apply the tendon limit stiffness to the simulation. It only fills the buffers with + the desired values. To apply the tendon limit stiffness, call the :meth:`write_fixed_tendon_properties_to_sim` function. + + Args: + limit_stiffness: Fixed tendon limit stiffness. Shape is (len(env_ids), len(fixed_tendon_ids)). + fixed_tendon_ids: The tendon indices to set the limit stiffness for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the limit stiffness for. Defaults to None (all environments). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + if env_ids != slice(None) and fixed_tendon_ids != slice(None): + env_ids = env_ids[:, None] + # set limit_stiffness + self._data.fixed_tendon_limit_stiffness[env_ids, fixed_tendon_ids] = limit_stiffness
+ +
[文档] def set_fixed_tendon_limit( + self, + limit: torch.Tensor, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set fixed tendon limit efforts into internal buffers. + + .. note:: + This function does not apply the tendon limit to the simulation. It only fills the buffers with + the desired values. To apply the tendon limit, call the :meth:`write_fixed_tendon_properties_to_sim` function. + + Args: + limit: Fixed tendon limit. Shape is (len(env_ids), len(fixed_tendon_ids)). + fixed_tendon_ids: The tendon indices to set the limit for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the limit for. Defaults to None (all environments). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + if env_ids != slice(None) and fixed_tendon_ids != slice(None): + env_ids = env_ids[:, None] + # set limit + self._data.fixed_tendon_limit[env_ids, fixed_tendon_ids] = limit
+ +
[文档] def set_fixed_tendon_rest_length( + self, + rest_length: torch.Tensor, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set fixed tendon rest length efforts into internal buffers. + + .. note:: + This function does not apply the tendon rest length to the simulation. It only fills the buffers with + the desired values. To apply the tendon rest length, call the :meth:`write_fixed_tendon_properties_to_sim` function. + + Args: + rest_length: Fixed tendon rest length. Shape is (len(env_ids), len(fixed_tendon_ids)). + fixed_tendon_ids: The tendon indices to set the rest length for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the rest length for. Defaults to None (all environments). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + if env_ids != slice(None) and fixed_tendon_ids != slice(None): + env_ids = env_ids[:, None] + # set rest_length + self._data.fixed_tendon_rest_length[env_ids, fixed_tendon_ids] = rest_length
+ +
[文档] def set_fixed_tendon_offset( + self, + offset: torch.Tensor, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set fixed tendon offset efforts into internal buffers. + + .. note:: + This function does not apply the tendon offset to the simulation. It only fills the buffers with + the desired values. To apply the tendon offset, call the :meth:`write_fixed_tendon_properties_to_sim` function. + + Args: + offset: Fixed tendon offset. Shape is (len(env_ids), len(fixed_tendon_ids)). + fixed_tendon_ids: The tendon indices to set the offset for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the offset for. Defaults to None (all environments). + """ + # resolve indices + if env_ids is None: + env_ids = slice(None) + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + if env_ids != slice(None) and fixed_tendon_ids != slice(None): + env_ids = env_ids[:, None] + # set offset + self._data.fixed_tendon_offset[env_ids, fixed_tendon_ids] = offset
+ +
[文档] def write_fixed_tendon_properties_to_sim( + self, + fixed_tendon_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Write fixed tendon properties into the simulation. + + Args: + fixed_tendon_ids: The fixed tendon indices to set the limits for. Defaults to None (all fixed tendons). + env_ids: The environment indices to set the limits for. Defaults to None (all environments). + """ + # resolve indices + physx_env_ids = env_ids + if env_ids is None: + physx_env_ids = self._ALL_INDICES + if fixed_tendon_ids is None: + fixed_tendon_ids = slice(None) + + # set into simulation + self.root_physx_view.set_fixed_tendon_properties( + self._data.fixed_tendon_stiffness, + self._data.fixed_tendon_damping, + self._data.fixed_tendon_limit_stiffness, + self._data.fixed_tendon_limit, + self._data.fixed_tendon_rest_length, + self._data.fixed_tendon_offset, + indices=physx_env_ids, + )
+ + """ + Internal helper. + """ + + def _initialize_impl(self): + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # obtain the first prim in the regex expression (all others are assumed to be a copy of this) + template_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if template_prim is None: + raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.") + template_prim_path = template_prim.GetPath().pathString + + # find articulation root prims + root_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + ) + if len(root_prims) == 0: + raise RuntimeError( + f"Failed to find an articulation when resolving '{self.cfg.prim_path}'." + " Please ensure that the prim has 'USD ArticulationRootAPI' applied." + ) + if len(root_prims) > 1: + raise RuntimeError( + f"Failed to find a single articulation when resolving '{self.cfg.prim_path}'." + f" Found multiple '{root_prims}' under '{template_prim_path}'." + " Please ensure that there is only one articulation in the prim path tree." + ) + + # resolve articulation root prim back into regex expression + root_prim_path = root_prims[0].GetPath().pathString + root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :] + # -- articulation + self._root_physx_view = self._physics_sim_view.create_articulation_view(root_prim_path_expr.replace(".*", "*")) + + # check if the articulation was created + if self._root_physx_view._backend is None: + raise RuntimeError(f"Failed to create articulation at: {self.cfg.prim_path}. Please check PhysX logs.") + + # log information about the articulation + omni.log.info(f"Articulation initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") + omni.log.info(f"Is fixed root: {self.is_fixed_base}") + omni.log.info(f"Number of bodies: {self.num_bodies}") + omni.log.info(f"Body names: {self.body_names}") + omni.log.info(f"Number of joints: {self.num_joints}") + omni.log.info(f"Joint names: {self.joint_names}") + omni.log.info(f"Number of fixed tendons: {self.num_fixed_tendons}") + + # container for data access + self._data = ArticulationData(self.root_physx_view, self.device) + + # create buffers + self._create_buffers() + # process configuration + self._process_cfg() + self._process_actuators_cfg() + self._process_fixed_tendons() + # validate configuration + self._validate_cfg() + # update the robot data + self.update(0.0) + # log joint information + self._log_articulation_joint_info() + + def _create_buffers(self): + # constants + self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + + # external forces and torques + self.has_external_wrench = False + self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device) + self._external_torque_b = torch.zeros_like(self._external_force_b) + + # asset data + # -- properties + self._data.joint_names = self.joint_names + self._data.body_names = self.body_names + + # -- bodies + self._data.default_mass = self.root_physx_view.get_masses().clone() + self._data.default_inertia = self.root_physx_view.get_inertias().clone() + + # -- default joint state + self._data.default_joint_pos = torch.zeros(self.num_instances, self.num_joints, device=self.device) + self._data.default_joint_vel = torch.zeros_like(self._data.default_joint_pos) + + # -- joint commands + self._data.joint_pos_target = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_vel_target = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_effort_target = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_stiffness = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_damping = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_armature = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_friction = torch.zeros_like(self._data.default_joint_pos) + self._data.joint_limits = torch.zeros(self.num_instances, self.num_joints, 2, device=self.device) + + # -- joint commands (explicit) + self._data.computed_torque = torch.zeros_like(self._data.default_joint_pos) + self._data.applied_torque = torch.zeros_like(self._data.default_joint_pos) + + # -- tendons + if self.num_fixed_tendons > 0: + self._data.fixed_tendon_stiffness = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.fixed_tendon_damping = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.fixed_tendon_limit_stiffness = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.fixed_tendon_limit = torch.zeros( + self.num_instances, self.num_fixed_tendons, 2, device=self.device + ) + self._data.fixed_tendon_rest_length = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.fixed_tendon_offset = torch.zeros(self.num_instances, self.num_fixed_tendons, device=self.device) + + # -- other data + self._data.soft_joint_pos_limits = torch.zeros(self.num_instances, self.num_joints, 2, device=self.device) + self._data.soft_joint_vel_limits = torch.zeros(self.num_instances, self.num_joints, device=self.device) + self._data.gear_ratio = torch.ones(self.num_instances, self.num_joints, device=self.device) + + # -- initialize default buffers related to joint properties + self._data.default_joint_stiffness = torch.zeros(self.num_instances, self.num_joints, device=self.device) + self._data.default_joint_damping = torch.zeros(self.num_instances, self.num_joints, device=self.device) + self._data.default_joint_armature = torch.zeros(self.num_instances, self.num_joints, device=self.device) + self._data.default_joint_friction = torch.zeros(self.num_instances, self.num_joints, device=self.device) + self._data.default_joint_limits = torch.zeros(self.num_instances, self.num_joints, 2, device=self.device) + + # -- initialize default buffers related to fixed tendon properties + if self.num_fixed_tendons > 0: + self._data.default_fixed_tendon_stiffness = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.default_fixed_tendon_damping = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.default_fixed_tendon_limit_stiffness = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.default_fixed_tendon_limit = torch.zeros( + self.num_instances, self.num_fixed_tendons, 2, device=self.device + ) + self._data.default_fixed_tendon_rest_length = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + self._data.default_fixed_tendon_offset = torch.zeros( + self.num_instances, self.num_fixed_tendons, device=self.device + ) + + # soft joint position limits (recommended not to be too close to limits). + joint_pos_limits = self.root_physx_view.get_dof_limits() + joint_pos_mean = (joint_pos_limits[..., 0] + joint_pos_limits[..., 1]) / 2 + joint_pos_range = joint_pos_limits[..., 1] - joint_pos_limits[..., 0] + soft_limit_factor = self.cfg.soft_joint_pos_limit_factor + # add to data + self._data.soft_joint_pos_limits[..., 0] = joint_pos_mean - 0.5 * joint_pos_range * soft_limit_factor + self._data.soft_joint_pos_limits[..., 1] = joint_pos_mean + 0.5 * joint_pos_range * soft_limit_factor + + # create buffers to store processed actions from actuator models + self._joint_pos_target_sim = torch.zeros_like(self._data.joint_pos_target) + self._joint_vel_target_sim = torch.zeros_like(self._data.joint_pos_target) + self._joint_effort_target_sim = torch.zeros_like(self._data.joint_pos_target) + + def _process_cfg(self): + """Post processing of configuration parameters.""" + # default state + # -- root state + # note: we cast to tuple to avoid torch/numpy type mismatch. + default_root_state = ( + tuple(self.cfg.init_state.pos) + + tuple(self.cfg.init_state.rot) + + tuple(self.cfg.init_state.lin_vel) + + tuple(self.cfg.init_state.ang_vel) + ) + default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device) + self._data.default_root_state = default_root_state.repeat(self.num_instances, 1) + # -- joint state + # joint pos + indices_list, _, values_list = string_utils.resolve_matching_names_values( + self.cfg.init_state.joint_pos, self.joint_names + ) + self._data.default_joint_pos[:, indices_list] = torch.tensor(values_list, device=self.device) + # joint vel + indices_list, _, values_list = string_utils.resolve_matching_names_values( + self.cfg.init_state.joint_vel, self.joint_names + ) + self._data.default_joint_vel[:, indices_list] = torch.tensor(values_list, device=self.device) + + # -- joint limits + self._data.default_joint_limits = self.root_physx_view.get_dof_limits().to(device=self.device).clone() + self._data.joint_limits = self._data.default_joint_limits.clone() + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._root_physx_view = None + + """ + Internal helpers -- Actuators. + """ + + def _process_actuators_cfg(self): + """Process and apply articulation joint properties.""" + # create actuators + self.actuators = dict() + # flag for implicit actuators + # if this is false, we by-pass certain checks when doing actuator-related operations + self._has_implicit_actuators = False + + # cache the values coming from the usd + self._data.default_joint_stiffness = self.root_physx_view.get_dof_stiffnesses().to(self.device).clone() + self._data.default_joint_damping = self.root_physx_view.get_dof_dampings().to(self.device).clone() + self._data.default_joint_armature = self.root_physx_view.get_dof_armatures().to(self.device).clone() + self._data.default_joint_friction = self.root_physx_view.get_dof_friction_coefficients().to(self.device).clone() + + # iterate over all actuator configurations + for actuator_name, actuator_cfg in self.cfg.actuators.items(): + # type annotation for type checkers + actuator_cfg: ActuatorBaseCfg + # create actuator group + joint_ids, joint_names = self.find_joints(actuator_cfg.joint_names_expr) + # check if any joints are found + if len(joint_names) == 0: + raise ValueError( + f"No joints found for actuator group: {actuator_name} with joint name expression:" + f" {actuator_cfg.joint_names_expr}." + ) + # create actuator collection + # note: for efficiency avoid indexing when over all indices + actuator: ActuatorBase = actuator_cfg.class_type( + cfg=actuator_cfg, + joint_names=joint_names, + joint_ids=( + slice(None) if len(joint_names) == self.num_joints else torch.tensor(joint_ids, device=self.device) + ), + num_envs=self.num_instances, + device=self.device, + stiffness=self._data.default_joint_stiffness[:, joint_ids], + damping=self._data.default_joint_damping[:, joint_ids], + armature=self._data.default_joint_armature[:, joint_ids], + friction=self._data.default_joint_friction[:, joint_ids], + effort_limit=self.root_physx_view.get_dof_max_forces().to(self.device).clone()[:, joint_ids], + velocity_limit=self.root_physx_view.get_dof_max_velocities().to(self.device).clone()[:, joint_ids], + ) + # log information on actuator groups + omni.log.info( + f"Actuator collection: {actuator_name} with model '{actuator_cfg.class_type.__name__}' and" + f" joint names: {joint_names} [{joint_ids}]." + ) + # store actuator group + self.actuators[actuator_name] = actuator + # set the passed gains and limits into the simulation + if isinstance(actuator, ImplicitActuator): + self._has_implicit_actuators = True + # the gains and limits are set into the simulation since actuator model is implicit + self.write_joint_stiffness_to_sim(actuator.stiffness, joint_ids=actuator.joint_indices) + self.write_joint_damping_to_sim(actuator.damping, joint_ids=actuator.joint_indices) + self.write_joint_effort_limit_to_sim(actuator.effort_limit, joint_ids=actuator.joint_indices) + self.write_joint_armature_to_sim(actuator.armature, joint_ids=actuator.joint_indices) + self.write_joint_friction_to_sim(actuator.friction, joint_ids=actuator.joint_indices) + else: + # the gains and limits are processed by the actuator model + # we set gains to zero, and torque limit to a high value in simulation to avoid any interference + self.write_joint_stiffness_to_sim(0.0, joint_ids=actuator.joint_indices) + self.write_joint_damping_to_sim(0.0, joint_ids=actuator.joint_indices) + self.write_joint_effort_limit_to_sim(1.0e9, joint_ids=actuator.joint_indices) + self.write_joint_armature_to_sim(actuator.armature, joint_ids=actuator.joint_indices) + self.write_joint_friction_to_sim(actuator.friction, joint_ids=actuator.joint_indices) + # Store the actual default stiffness and damping values for explicit actuators (not written the sim) + self._data.default_joint_stiffness[:, actuator.joint_indices] = actuator.stiffness + self._data.default_joint_damping[:, actuator.joint_indices] = actuator.damping + + # perform some sanity checks to ensure actuators are prepared correctly + total_act_joints = sum(actuator.num_joints for actuator in self.actuators.values()) + if total_act_joints != (self.num_joints - self.num_fixed_tendons): + omni.log.warn( + "Not all actuators are configured! Total number of actuated joints not equal to number of" + f" joints available: {total_act_joints} != {self.num_joints - self.num_fixed_tendons}." + ) + + def _process_fixed_tendons(self): + """Process fixed tendons.""" + # create a list to store the fixed tendon names + self._fixed_tendon_names = list() + + # parse fixed tendons properties if they exist + if self.num_fixed_tendons > 0: + stage = stage_utils.get_current_stage() + joint_paths = self.root_physx_view.dof_paths[0] + + # iterate over all joints to find tendons attached to them + for j in range(self.num_joints): + usd_joint_path = joint_paths[j] + # check whether joint has tendons - tendon name follows the joint name it is attached to + joint = UsdPhysics.Joint.Get(stage, usd_joint_path) + if joint.GetPrim().HasAPI(PhysxSchema.PhysxTendonAxisRootAPI): + joint_name = usd_joint_path.split("/")[-1] + self._fixed_tendon_names.append(joint_name) + + self._data.fixed_tendon_names = self._fixed_tendon_names + self._data.default_fixed_tendon_stiffness = self.root_physx_view.get_fixed_tendon_stiffnesses().clone() + self._data.default_fixed_tendon_damping = self.root_physx_view.get_fixed_tendon_dampings().clone() + self._data.default_fixed_tendon_limit_stiffness = ( + self.root_physx_view.get_fixed_tendon_limit_stiffnesses().clone() + ) + self._data.default_fixed_tendon_limit = self.root_physx_view.get_fixed_tendon_limits().clone() + self._data.default_fixed_tendon_rest_length = self.root_physx_view.get_fixed_tendon_rest_lengths().clone() + self._data.default_fixed_tendon_offset = self.root_physx_view.get_fixed_tendon_offsets().clone() + + def _apply_actuator_model(self): + """Processes joint commands for the articulation by forwarding them to the actuators. + + The actions are first processed using actuator models. Depending on the robot configuration, + the actuator models compute the joint level simulation commands and sets them into the PhysX buffers. + """ + # process actions per group + for actuator in self.actuators.values(): + # prepare input for actuator model based on cached data + # TODO : A tensor dict would be nice to do the indexing of all tensors together + control_action = ArticulationActions( + joint_positions=self._data.joint_pos_target[:, actuator.joint_indices], + joint_velocities=self._data.joint_vel_target[:, actuator.joint_indices], + joint_efforts=self._data.joint_effort_target[:, actuator.joint_indices], + joint_indices=actuator.joint_indices, + ) + # compute joint command from the actuator model + control_action = actuator.compute( + control_action, + joint_pos=self._data.joint_pos[:, actuator.joint_indices], + joint_vel=self._data.joint_vel[:, actuator.joint_indices], + ) + # update targets (these are set into the simulation) + if control_action.joint_positions is not None: + self._joint_pos_target_sim[:, actuator.joint_indices] = control_action.joint_positions + if control_action.joint_velocities is not None: + self._joint_vel_target_sim[:, actuator.joint_indices] = control_action.joint_velocities + if control_action.joint_efforts is not None: + self._joint_effort_target_sim[:, actuator.joint_indices] = control_action.joint_efforts + # update state of the actuator model + # -- torques + self._data.computed_torque[:, actuator.joint_indices] = actuator.computed_effort + self._data.applied_torque[:, actuator.joint_indices] = actuator.applied_effort + # -- actuator data + self._data.soft_joint_vel_limits[:, actuator.joint_indices] = actuator.velocity_limit + # TODO: find a cleaner way to handle gear ratio. Only needed for variable gear ratio actuators. + if hasattr(actuator, "gear_ratio"): + self._data.gear_ratio[:, actuator.joint_indices] = actuator.gear_ratio + + """ + Internal helpers -- Debugging. + """ + + def _validate_cfg(self): + """Validate the configuration after processing. + + Note: + This function should be called only after the configuration has been processed and the buffers have been + created. Otherwise, some settings that are altered during processing may not be validated. + For instance, the actuator models may change the joint max velocity limits. + """ + # check that the default values are within the limits + joint_pos_limits = self.root_physx_view.get_dof_limits()[0].to(self.device) + out_of_range = self._data.default_joint_pos[0] < joint_pos_limits[:, 0] + out_of_range |= self._data.default_joint_pos[0] > joint_pos_limits[:, 1] + violated_indices = torch.nonzero(out_of_range, as_tuple=False).squeeze(-1) + # throw error if any of the default joint positions are out of the limits + if len(violated_indices) > 0: + # prepare message for violated joints + msg = "The following joints have default positions out of the limits: \n" + for idx in violated_indices: + joint_name = self.data.joint_names[idx] + joint_limits = joint_pos_limits[idx] + joint_pos = self.data.default_joint_pos[0, idx] + # add to message + msg += f"\t- '{joint_name}': {joint_pos:.3f} not in [{joint_limits[0]:.3f}, {joint_limits[1]:.3f}]\n" + raise ValueError(msg) + + # check that the default joint velocities are within the limits + joint_max_vel = self.root_physx_view.get_dof_max_velocities()[0].to(self.device) + out_of_range = torch.abs(self._data.default_joint_vel[0]) > joint_max_vel + violated_indices = torch.nonzero(out_of_range, as_tuple=False).squeeze(-1) + if len(violated_indices) > 0: + # prepare message for violated joints + msg = "The following joints have default velocities out of the limits: \n" + for idx in violated_indices: + joint_name = self.data.joint_names[idx] + joint_limits = [-joint_max_vel[idx], joint_max_vel[idx]] + joint_vel = self.data.default_joint_vel[0, idx] + # add to message + msg += f"\t- '{joint_name}': {joint_vel:.3f} not in [{joint_limits[0]:.3f}, {joint_limits[1]:.3f}]\n" + raise ValueError(msg) + + def _log_articulation_joint_info(self): + """Log information about the articulation's simulated joints.""" + # read out all joint parameters from simulation + # -- gains + stiffnesses = self.root_physx_view.get_dof_stiffnesses()[0].tolist() + dampings = self.root_physx_view.get_dof_dampings()[0].tolist() + # -- properties + armatures = self.root_physx_view.get_dof_armatures()[0].tolist() + frictions = self.root_physx_view.get_dof_friction_coefficients()[0].tolist() + # -- limits + position_limits = self.root_physx_view.get_dof_limits()[0].tolist() + velocity_limits = self.root_physx_view.get_dof_max_velocities()[0].tolist() + effort_limits = self.root_physx_view.get_dof_max_forces()[0].tolist() + # create table for term information + table = PrettyTable(float_format=".3f") + table.title = f"Simulation Joint Information (Prim path: {self.cfg.prim_path})" + table.field_names = [ + "Index", + "Name", + "Stiffness", + "Damping", + "Armature", + "Friction", + "Position Limits", + "Velocity Limits", + "Effort Limits", + ] + # set alignment of table columns + table.align["Name"] = "l" + # add info on each term + for index, name in enumerate(self.joint_names): + table.add_row([ + index, + name, + stiffnesses[index], + dampings[index], + armatures[index], + frictions[index], + position_limits[index], + velocity_limits[index], + effort_limits[index], + ]) + # convert table to string + omni.log.info(f"Simulation parameters for joints in {self.cfg.prim_path}:\n" + table.get_string()) + + # read out all tendon parameters from simulation + if self.num_fixed_tendons > 0: + # -- gains + ft_stiffnesses = self.root_physx_view.get_fixed_tendon_stiffnesses()[0].tolist() + ft_dampings = self.root_physx_view.get_fixed_tendon_dampings()[0].tolist() + # -- limits + ft_limit_stiffnesses = self.root_physx_view.get_fixed_tendon_limit_stiffnesses()[0].tolist() + ft_limits = self.root_physx_view.get_fixed_tendon_limits()[0].tolist() + ft_rest_lengths = self.root_physx_view.get_fixed_tendon_rest_lengths()[0].tolist() + ft_offsets = self.root_physx_view.get_fixed_tendon_offsets()[0].tolist() + # create table for term information + tendon_table = PrettyTable(float_format=".3f") + tendon_table.title = f"Simulation Tendon Information (Prim path: {self.cfg.prim_path})" + tendon_table.field_names = [ + "Index", + "Stiffness", + "Damping", + "Limit Stiffness", + "Limit", + "Rest Length", + "Offset", + ] + # add info on each term + for index in range(self.num_fixed_tendons): + tendon_table.add_row([ + index, + ft_stiffnesses[index], + ft_dampings[index], + ft_limit_stiffnesses[index], + ft_limits[index], + ft_rest_lengths[index], + ft_offsets[index], + ]) + # convert table to string + omni.log.info(f"Simulation parameters for tendons in {self.cfg.prim_path}:\n" + tendon_table.get_string())
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/articulation/articulation_cfg.html b/_modules/omni/isaac/lab/assets/articulation/articulation_cfg.html new file mode 100644 index 0000000000..83eecb76ac --- /dev/null +++ b/_modules/omni/isaac/lab/assets/articulation/articulation_cfg.html @@ -0,0 +1,611 @@ + + + + + + + + + + + omni.isaac.lab.assets.articulation.articulation_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.articulation.articulation_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.actuators import ActuatorBaseCfg
+from omni.isaac.lab.utils import configclass
+
+from ..asset_base_cfg import AssetBaseCfg
+from .articulation import Articulation
+
+
+
[文档]@configclass +class ArticulationCfg(AssetBaseCfg): + """Configuration parameters for an articulation.""" + +
[文档] @configclass + class InitialStateCfg(AssetBaseCfg.InitialStateCfg): + """Initial state of the articulation.""" + + # root velocity + lin_vel: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Linear velocity of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0).""" + ang_vel: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Angular velocity of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0).""" + + # joint state + joint_pos: dict[str, float] = {".*": 0.0} + """Joint positions of the joints. Defaults to 0.0 for all joints.""" + joint_vel: dict[str, float] = {".*": 0.0} + """Joint velocities of the joints. Defaults to 0.0 for all joints."""
+ + ## + # Initialize configurations. + ## + + class_type: type = Articulation + + init_state: InitialStateCfg = InitialStateCfg() + """Initial state of the articulated object. Defaults to identity pose with zero velocity and zero joint state.""" + + soft_joint_pos_limit_factor: float = 1.0 + """Fraction specifying the range of DOF position limits (parsed from the asset) to use. Defaults to 1.0. + + The joint position limits are scaled by this factor to allow for a limited range of motion. + This is accessible in the articulation data through :attr:`ArticulationData.soft_joint_pos_limits` attribute. + """ + + actuators: dict[str, ActuatorBaseCfg] = MISSING + """Actuators for the robot with corresponding joint names."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/articulation/articulation_data.html b/_modules/omni/isaac/lab/assets/articulation/articulation_data.html new file mode 100644 index 0000000000..866b082605 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/articulation/articulation_data.html @@ -0,0 +1,1032 @@ + + + + + + + + + + + omni.isaac.lab.assets.articulation.articulation_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.articulation.articulation_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+import weakref
+
+import omni.physics.tensors.impl.api as physx
+
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.utils.buffers import TimestampedBuffer
+
+
+
[文档]class ArticulationData: + """Data container for an articulation. + + This class contains the data for an articulation in the simulation. The data includes the state of + the root rigid body, the state of all the bodies in the articulation, and the joint state. The data is + stored in the simulation world frame unless otherwise specified. + + An articulation is comprised of multiple rigid bodies or links. For a rigid body, there are two frames + of reference that are used: + + - Actor frame: The frame of reference of the rigid body prim. This typically corresponds to the Xform prim + with the rigid body schema. + - Center of mass frame: The frame of reference of the center of mass of the rigid body. + + Depending on the settings, the two frames may not coincide with each other. In the robotics sense, the actor frame + can be interpreted as the link frame. + """ + + def __init__(self, root_physx_view: physx.ArticulationView, device: str): + """Initializes the articulation data. + + Args: + root_physx_view: The root articulation view. + device: The device used for processing. + """ + # Set the parameters + self.device = device + # Set the root articulation view + # note: this is stored as a weak reference to avoid circular references between the asset class + # and the data container. This is important to avoid memory leaks. + self._root_physx_view: physx.ArticulationView = weakref.proxy(root_physx_view) + + # Set initial time stamp + self._sim_timestamp = 0.0 + + # Obtain global physics sim view + self._physics_sim_view = physx.create_simulation_view("torch") + self._physics_sim_view.set_subspace_roots("/") + gravity = self._physics_sim_view.get_gravity() + # Convert to direction vector + gravity_dir = torch.tensor((gravity[0], gravity[1], gravity[2]), device=self.device) + gravity_dir = math_utils.normalize(gravity_dir.unsqueeze(0)).squeeze(0) + + # Initialize constants + self.GRAVITY_VEC_W = gravity_dir.repeat(self._root_physx_view.count, 1) + self.FORWARD_VEC_B = torch.tensor((1.0, 0.0, 0.0), device=self.device).repeat(self._root_physx_view.count, 1) + + # Initialize history for finite differencing + self._previous_joint_vel = self._root_physx_view.get_dof_velocities().clone() + + # Initialize the lazy buffers. + self._root_state_w = TimestampedBuffer() + self._body_state_w = TimestampedBuffer() + self._body_acc_w = TimestampedBuffer() + self._joint_pos = TimestampedBuffer() + self._joint_acc = TimestampedBuffer() + self._joint_vel = TimestampedBuffer() + + def update(self, dt: float): + # update the simulation timestamp + self._sim_timestamp += dt + # Trigger an update of the joint acceleration buffer at a higher frequency + # since we do finite differencing. + self.joint_acc + + ## + # Names. + ## + + body_names: list[str] = None + """Body names in the order parsed by the simulation view.""" + + joint_names: list[str] = None + """Joint names in the order parsed by the simulation view.""" + + fixed_tendon_names: list[str] = None + """Fixed tendon names in the order parsed by the simulation view.""" + + ## + # Defaults. + ## + + default_root_state: torch.Tensor = None + """Default root state ``[pos, quat, lin_vel, ang_vel]`` in local environment frame. Shape is (num_instances, 13). + + The position and quaternion are of the articulation root's actor frame. Meanwhile, the linear and angular + velocities are of its center of mass frame. + """ + + default_mass: torch.Tensor = None + """Default mass read from the simulation. Shape is (num_instances, num_bodies).""" + + default_inertia: torch.Tensor = None + """Default inertia read from the simulation. Shape is (num_instances, num_bodies, 9). + + The inertia is the inertia tensor relative to the center of mass frame. The values are stored in + the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + """ + + default_joint_pos: torch.Tensor = None + """Default joint positions of all joints. Shape is (num_instances, num_joints).""" + + default_joint_vel: torch.Tensor = None + """Default joint velocities of all joints. Shape is (num_instances, num_joints).""" + + default_joint_stiffness: torch.Tensor = None + """Default joint stiffness of all joints. Shape is (num_instances, num_joints).""" + + default_joint_damping: torch.Tensor = None + """Default joint damping of all joints. Shape is (num_instances, num_joints).""" + + default_joint_armature: torch.Tensor = None + """Default joint armature of all joints. Shape is (num_instances, num_joints).""" + + default_joint_friction: torch.Tensor = None + """Default joint friction of all joints. Shape is (num_instances, num_joints).""" + + default_joint_limits: torch.Tensor = None + """Default joint limits of all joints. Shape is (num_instances, num_joints, 2).""" + + default_fixed_tendon_stiffness: torch.Tensor = None + """Default tendon stiffness of all tendons. Shape is (num_instances, num_fixed_tendons).""" + + default_fixed_tendon_damping: torch.Tensor = None + """Default tendon damping of all tendons. Shape is (num_instances, num_fixed_tendons).""" + + default_fixed_tendon_limit_stiffness: torch.Tensor = None + """Default tendon limit stiffness of all tendons. Shape is (num_instances, num_fixed_tendons).""" + + default_fixed_tendon_rest_length: torch.Tensor = None + """Default tendon rest length of all tendons. Shape is (num_instances, num_fixed_tendons).""" + + default_fixed_tendon_offset: torch.Tensor = None + """Default tendon offset of all tendons. Shape is (num_instances, num_fixed_tendons).""" + + default_fixed_tendon_limit: torch.Tensor = None + """Default tendon limits of all tendons. Shape is (num_instances, num_fixed_tendons, 2).""" + + ## + # Joint commands -- Set into simulation. + ## + + joint_pos_target: torch.Tensor = None + """Joint position targets commanded by the user. Shape is (num_instances, 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`), + which are then set into the simulation. + """ + + joint_vel_target: torch.Tensor = None + """Joint velocity targets commanded by the user. Shape is (num_instances, 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`), + which are then set into the simulation. + """ + + joint_effort_target: torch.Tensor = None + """Joint effort targets commanded by the user. Shape is (num_instances, 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`), + which are then set into the simulation. + """ + + ## + # Joint commands -- Explicit actuators. + ## + + computed_torque: torch.Tensor = None + """Joint torques computed from the actuator model (before clipping). Shape is (num_instances, 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. + For instance, to penalize the learning agent for a difference between the computed and applied torques. + + Note: The torques are zero for implicit actuator models. + """ + + applied_torque: torch.Tensor = None + """Joint torques applied from the actuator model (after clipping). Shape is (num_instances, num_joints). + + These torques are set into the simulation, after clipping the :attr:`computed_torque` based on the + actuator model. + + Note: The torques are zero for implicit actuator models. + """ + + ## + # Joint properties. + ## + + joint_stiffness: torch.Tensor = None + """Joint stiffness provided to simulation. Shape is (num_instances, num_joints).""" + + joint_damping: torch.Tensor = None + """Joint damping provided to simulation. Shape is (num_instances, num_joints).""" + + joint_armature: torch.Tensor = None + """Joint armature provided to simulation. Shape is (num_instances, num_joints).""" + + joint_friction: torch.Tensor = None + """Joint friction provided to simulation. Shape is (num_instances, num_joints).""" + + joint_limits: torch.Tensor = None + """Joint limits provided to simulation. Shape is (num_instances, num_joints, 2).""" + + ## + # Fixed tendon properties. + ## + + fixed_tendon_stiffness: torch.Tensor = None + """Fixed tendon stiffness provided to simulation. Shape is (num_instances, num_fixed_tendons).""" + + fixed_tendon_damping: torch.Tensor = None + """Fixed tendon damping provided to simulation. Shape is (num_instances, num_fixed_tendons).""" + + fixed_tendon_limit_stiffness: torch.Tensor = None + """Fixed tendon limit stiffness provided to simulation. Shape is (num_instances, num_fixed_tendons).""" + + fixed_tendon_rest_length: torch.Tensor = None + """Fixed tendon rest length provided to simulation. Shape is (num_instances, num_fixed_tendons).""" + + fixed_tendon_offset: torch.Tensor = None + """Fixed tendon offset provided to simulation. Shape is (num_instances, num_fixed_tendons).""" + + fixed_tendon_limit: torch.Tensor = None + """Fixed tendon limits provided to simulation. Shape is (num_instances, num_fixed_tendons, 2).""" + + ## + # Other Data. + ## + + soft_joint_pos_limits: torch.Tensor = None + """Joint positions limits for all joints. Shape is (num_instances, num_joints, 2).""" + + soft_joint_vel_limits: torch.Tensor = None + """Joint velocity limits for all joints. Shape is (num_instances, num_joints).""" + + gear_ratio: torch.Tensor = None + """Gear ratio for relating motor torques to applied Joint torques. Shape is (num_instances, num_joints).""" + + ## + # Properties. + ## + + @property + def root_state_w(self): + """Root state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is (num_instances, 13). + + The position and quaternion are of the articulation root's actor frame. Meanwhile, the linear and angular + velocities are of the articulation root's center of mass frame. + """ + if self._root_state_w.timestamp < self._sim_timestamp: + # read data from simulation + pose = self._root_physx_view.get_root_transforms().clone() + pose[:, 3:7] = math_utils.convert_quat(pose[:, 3:7], to="wxyz") + velocity = self._root_physx_view.get_root_velocities() + # set the buffer data and timestamp + self._root_state_w.data = torch.cat((pose, velocity), dim=-1) + self._root_state_w.timestamp = self._sim_timestamp + return self._root_state_w.data + + @property + def body_state_w(self): + """State of all bodies `[pos, quat, lin_vel, ang_vel]` in simulation world frame. + Shape is (num_instances, num_bodies, 13). + + The position and quaternion are of all the articulation links's actor frame. Meanwhile, the linear and angular + velocities are of the articulation links's center of mass frame. + """ + if self._body_state_w.timestamp < self._sim_timestamp: + self._physics_sim_view.update_articulations_kinematic() + # read data from simulation + poses = self._root_physx_view.get_link_transforms().clone() + poses[..., 3:7] = math_utils.convert_quat(poses[..., 3:7], to="wxyz") + velocities = self._root_physx_view.get_link_velocities() + # set the buffer data and timestamp + self._body_state_w.data = torch.cat((poses, velocities), dim=-1) + self._body_state_w.timestamp = self._sim_timestamp + return self._body_state_w.data + + @property + def body_acc_w(self): + """Acceleration of all bodies. Shape is (num_instances, num_bodies, 6). + + This quantity is the acceleration of the articulation links' center of mass frame. + """ + if self._body_acc_w.timestamp < self._sim_timestamp: + # read data from simulation and set the buffer data and timestamp + self._body_acc_w.data = self._root_physx_view.get_link_accelerations() + self._body_acc_w.timestamp = self._sim_timestamp + return self._body_acc_w.data + + @property + def projected_gravity_b(self): + """Projection of the gravity direction on base frame. Shape is (num_instances, 3).""" + return math_utils.quat_rotate_inverse(self.root_quat_w, self.GRAVITY_VEC_W) + + @property + def heading_w(self): + """Yaw heading of the base frame (in radians). Shape is (num_instances,). + + Note: + This quantity is computed by assuming that the forward-direction of the base + frame is along x-direction, i.e. :math:`(1, 0, 0)`. + """ + forward_w = math_utils.quat_apply(self.root_quat_w, self.FORWARD_VEC_B) + return torch.atan2(forward_w[:, 1], forward_w[:, 0]) + + @property + def joint_pos(self): + """Joint positions of all joints. Shape is (num_instances, num_joints).""" + if self._joint_pos.timestamp < self._sim_timestamp: + # read data from simulation and set the buffer data and timestamp + self._joint_pos.data = self._root_physx_view.get_dof_positions() + self._joint_pos.timestamp = self._sim_timestamp + return self._joint_pos.data + + @property + def joint_vel(self): + """Joint velocities of all joints. Shape is (num_instances, num_joints).""" + if self._joint_vel.timestamp < self._sim_timestamp: + # read data from simulation and set the buffer data and timestamp + self._joint_vel.data = self._root_physx_view.get_dof_velocities() + self._joint_vel.timestamp = self._sim_timestamp + return self._joint_vel.data + + @property + def joint_acc(self): + """Joint acceleration of all joints. Shape is (num_instances, num_joints).""" + if self._joint_acc.timestamp < self._sim_timestamp: + # note: we use finite differencing to compute acceleration + time_elapsed = self._sim_timestamp - self._joint_acc.timestamp + self._joint_acc.data = (self.joint_vel - self._previous_joint_vel) / time_elapsed + self._joint_acc.timestamp = self._sim_timestamp + # update the previous joint velocity + self._previous_joint_vel[:] = self.joint_vel + return self._joint_acc.data + + ## + # Derived properties. + ## + + @property + def root_pos_w(self) -> torch.Tensor: + """Root position in simulation world frame. Shape is (num_instances, 3). + + This quantity is the position of the actor frame of the articulation root. + """ + 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 (num_instances, 4). + + This quantity is the orientation of the actor frame of the articulation root. + """ + return self.root_state_w[:, 3:7] + + @property + def root_vel_w(self) -> torch.Tensor: + """Root velocity in simulation world frame. Shape is (num_instances, 6). + + This quantity contains the linear and angular velocities of the articulation root's center of + mass frame. + """ + 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 (num_instances, 3). + + This quantity is the linear velocity of the articulation root's center of mass frame. + """ + 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 (num_instances, 3). + + This quantity is the angular velocity of the articulation root's center of mass frame. + """ + return self.root_state_w[:, 10:13] + + @property + def root_lin_vel_b(self) -> torch.Tensor: + """Root linear velocity in base frame. Shape is (num_instances, 3). + + This quantity is the linear velocity of the articulation root's center of mass frame with + respect to the articulation root's actor frame. + """ + return math_utils.quat_rotate_inverse(self.root_quat_w, self.root_lin_vel_w) + + @property + def root_ang_vel_b(self) -> torch.Tensor: + """Root angular velocity in base world frame. Shape is (num_instances, 3). + + This quantity is the angular velocity of the articulation root's center of mass frame with respect to the + articulation root's actor frame. + """ + return math_utils.quat_rotate_inverse(self.root_quat_w, self.root_ang_vel_w) + + @property + def body_pos_w(self) -> torch.Tensor: + """Positions of all bodies in simulation world frame. Shape is (num_instances, num_bodies, 3). + + This quantity is the position of the rigid bodies' actor frame. + """ + 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 (num_instances, num_bodies, 4). + + This quantity is the orientation of the rigid bodies' actor frame. + """ + 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 (num_instances, num_bodies, 6). + + This quantity contains the linear and angular velocities of the rigid bodies' center of mass frame. + """ + 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 (num_instances, num_bodies, 3). + + This quantity is the linear velocity of the rigid bodies' center of mass frame. + """ + 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 (num_instances, num_bodies, 3). + + This quantity is the angular velocity of the rigid bodies' center of mass frame. + """ + 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 (num_instances, num_bodies, 3). + + This quantity is the linear acceleration of the rigid bodies' center of mass frame. + """ + 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 (num_instances, num_bodies, 3). + + This quantity is the angular acceleration of the rigid bodies' center of mass frame. + """ + return self.body_acc_w[..., 3:6]
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/asset_base.html b/_modules/omni/isaac/lab/assets/asset_base.html new file mode 100644 index 0000000000..6b9a5f0981 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/asset_base.html @@ -0,0 +1,830 @@ + + + + + + + + + + + omni.isaac.lab.assets.asset_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.asset_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import inspect
+import re
+import weakref
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any
+
+import omni.kit.app
+import omni.timeline
+
+import omni.isaac.lab.sim as sim_utils
+
+if TYPE_CHECKING:
+    from .asset_base_cfg import AssetBaseCfg
+
+
+
[文档]class AssetBase(ABC): + """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.lab.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. + + Args: + cfg: The configuration class for the asset. + + Raises: + RuntimeError: If no prims found at input prim path or prim path expression. + """ + # check that the config is valid + cfg.validate() + # store inputs + self.cfg = cfg + # flag for whether the asset is initialized + self._is_initialized = False + + # check if base asset path is valid + # note: currently the spawner does not work if there is a regex pattern in the leaf + # For example, if the prim path is "/World/Robot_[1,2]" since the spawner will not + # know which prim to spawn. This is a limitation of the spawner and not the asset. + asset_path = self.cfg.prim_path.split("/")[-1] + asset_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", asset_path) is None + # spawn the asset + if self.cfg.spawn is not None and not asset_path_is_regex: + self.cfg.spawn.func( + self.cfg.prim_path, + self.cfg.spawn, + translation=self.cfg.init_state.pos, + orientation=self.cfg.init_state.rot, + ) + # check that spawn was successful + matching_prims = sim_utils.find_matching_prims(self.cfg.prim_path) + if len(matching_prims) == 0: + raise RuntimeError(f"Could not find prim with path {self.cfg.prim_path}.") + + # note: Use weakref on all callbacks to ensure that this object can be deleted when its destructor is called. + # add callbacks for stage play/stop + # The order is set to 10 which is arbitrary but should be lower priority than the default order of 0 + timeline_event_stream = omni.timeline.get_timeline_interface().get_timeline_event_stream() + self._initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.PLAY), + lambda event, obj=weakref.proxy(self): obj._initialize_callback(event), + order=10, + ) + self._invalidate_initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.STOP), + lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event), + order=10, + ) + # add handle for debug visualization (this is set to a valid handle inside set_debug_vis) + self._debug_vis_handle = None + # set initial state of debug visualization + self.set_debug_vis(self.cfg.debug_vis)
+ + def __del__(self): + """Unsubscribe from the callbacks.""" + # clear physics events handles + if self._initialize_handle: + self._initialize_handle.unsubscribe() + self._initialize_handle = None + if self._invalidate_initialize_handle: + self._invalidate_initialize_handle.unsubscribe() + self._invalidate_initialize_handle = None + # clear debug visualization + if self._debug_vis_handle: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + + """ + Properties + """ + + @property + def is_initialized(self) -> bool: + """Whether the asset is initialized. + + Returns True if the asset is initialized, False otherwise. + """ + return self._is_initialized + + @property + @abstractmethod + def num_instances(self) -> int: + """Number of instances of the asset. + + This is equal to the number of asset instances per environment multiplied by the number of environments. + """ + return NotImplementedError + + @property + def device(self) -> str: + """Memory device for computation.""" + return self._device + + @property + @abstractmethod + def data(self) -> Any: + """Data related to the asset.""" + return NotImplementedError + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the asset has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_debug_vis_impl) + return "NotImplementedError" not in source_code + + """ + Operations. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Sets whether to visualize the asset data. + + Args: + debug_vis: Whether to visualize the asset data. + + Returns: + Whether the debug visualization was successfully set. False if the asset + does not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_debug_vis_implementation: + return False + # toggle debug visualization objects + self._set_debug_vis_impl(debug_vis) + # toggle debug visualization handles + if debug_vis: + # create a subscriber for the post update event if it doesn't exist + if self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + # return success + return True
+ +
[文档] @abstractmethod + def reset(self, env_ids: Sequence[int] | None = None): + """Resets all internal buffers of selected environments. + + Args: + env_ids: The indices of the object to reset. Defaults to None (all instances). + """ + raise NotImplementedError
+ +
[文档] @abstractmethod + def write_data_to_sim(self): + """Writes data to the simulator.""" + raise NotImplementedError
+ +
[文档] @abstractmethod + def update(self, dt: float): + """Update the internal buffers. + + The time step ``dt`` is used to compute numerical derivatives of quantities such as joint + accelerations which are not provided by the simulator. + + Args: + dt: The amount of time passed from last ``update`` call. + """ + raise NotImplementedError
+ + """ + Implementation specific. + """ + + @abstractmethod + def _initialize_impl(self): + """Initializes the PhysX handles and internal buffers.""" + raise NotImplementedError + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects. + + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + def _debug_vis_callback(self, event): + """Callback for debug visualization. + + This function calls the visualization objects and sets the data to visualize into them. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + """ + Internal simulation callbacks. + """ + + def _initialize_callback(self, event): + """Initializes the scene elements. + + Note: + PhysX handles are only enabled once the simulator starts playing. Hence, this function needs to be + called whenever the simulator "plays" from a "stop" state. + """ + if not self._is_initialized: + # obtain simulation related information + sim = sim_utils.SimulationContext.instance() + if sim is None: + raise RuntimeError("SimulationContext is not initialized! Please initialize SimulationContext first.") + self._backend = sim.backend + self._device = sim.device + # initialize the asset + self._initialize_impl() + # set flag + self._is_initialized = True + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + self._is_initialized = False
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/asset_base_cfg.html b/_modules/omni/isaac/lab/assets/asset_base_cfg.html new file mode 100644 index 0000000000..32ed9c536f --- /dev/null +++ b/_modules/omni/isaac/lab/assets/asset_base_cfg.html @@ -0,0 +1,636 @@ + + + + + + + + + + + omni.isaac.lab.assets.asset_base_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.asset_base_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.sim import SpawnerCfg
+from omni.isaac.lab.utils import configclass
+
+from .asset_base import AssetBase
+
+
+
[文档]@configclass +class AssetBaseCfg: + """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. + + 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. + Defaults to (1.0, 0.0, 0.0, 0.0). + """
+ + class_type: type[AssetBase] = None + """The associated asset class. Defaults to None, which means that the asset will be spawned + but cannot be interacted with via the asset class. + + The class should inherit from :class:`omni.isaac.lab.assets.asset_base.AssetBase`. + """ + + prim_path: str = MISSING + """Prim path (or expression) to the asset. + + .. 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`` will be replaced with ``/World/envs/env_.*/Robot``. + """ + + spawn: SpawnerCfg | None = None + """Spawn configuration for the asset. Defaults to None. + + 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``. + + * ``-1``: global collision group (collides with all assets in the scene). + * ``0``: local collision group (collides with other assets in the same environment). + """ + + debug_vis: bool = False + """Whether to enable debug visualization for the asset. Defaults to ``False``."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/deformable_object/deformable_object.html b/_modules/omni/isaac/lab/assets/deformable_object/deformable_object.html new file mode 100644 index 0000000000..769907fe26 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/deformable_object/deformable_object.html @@ -0,0 +1,972 @@ + + + + + + + + + + + omni.isaac.lab.assets.deformable_object.deformable_object — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.deformable_object.deformable_object 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.log
+import omni.physics.tensors.impl.api as physx
+from pxr import PhysxSchema, UsdShade
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.markers import VisualizationMarkers
+
+from ..asset_base import AssetBase
+from .deformable_object_data import DeformableObjectData
+
+if TYPE_CHECKING:
+    from .deformable_object_cfg import DeformableObjectCfg
+
+
+
[文档]class DeformableObject(AssetBase): + """A deformable object asset class. + + Deformable objects are assets that can be deformed in the simulation. They are typically used for + soft bodies, such as stuffed animals and food items. + + Unlike rigid object assets, deformable objects have a more complex structure and require additional + handling for simulation. The simulation of deformable objects follows a finite element approach, where + the object is discretized into a mesh of nodes and elements. The nodes are connected by elements, which + define the material properties of the object. The nodes can be moved and deformed, and the elements + respond to these changes. + + The state of a deformable object comprises of its nodal positions and velocities, and not the object's root + position and orientation. The nodal positions and velocities are in the simulation frame. + + Soft bodies can be `partially kinematic`_, where some nodes are driven by kinematic targets, and the rest are + simulated. The kinematic targets are the desired positions of the nodes, and the simulation drives the nodes + towards these targets. This is useful for partial control of the object, such as moving a stuffed animal's + head while the rest of the body is simulated. + + .. attention:: + This class is experimental and subject to change due to changes on the underlying PhysX API on which + it depends. We will try to maintain backward compatibility as much as possible but some changes may be + necessary. + + .. _partially kinematic: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html#kinematic-soft-bodies + """ + + cfg: DeformableObjectCfg + """Configuration instance for the deformable object.""" + +
[文档] def __init__(self, cfg: DeformableObjectCfg): + """Initialize the deformable object. + + Args: + cfg: A configuration instance. + """ + super().__init__(cfg)
+ + """ + Properties + """ + + @property + def data(self) -> DeformableObjectData: + return self._data + + @property + def num_instances(self) -> int: + return self.root_physx_view.count + + @property + def num_bodies(self) -> int: + """Number of bodies in the asset. + + This is always 1 since each object is a single deformable body. + """ + return 1 + + @property + def root_physx_view(self) -> physx.SoftBodyView: + """Deformable body view for the asset (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._root_physx_view + + @property + def material_physx_view(self) -> physx.SoftBodyMaterialView | None: + """Deformable material view for the asset (PhysX). + + This view is optional and may not be available if the material is not bound to the deformable body. + If the material is not available, then the material properties will be set to default values. + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._material_physx_view + + @property + def max_sim_elements_per_body(self) -> int: + """The maximum number of simulation mesh elements per deformable body.""" + return self.root_physx_view.max_sim_elements_per_body + + @property + def max_collision_elements_per_body(self) -> int: + """The maximum number of collision mesh elements per deformable body.""" + return self.root_physx_view.max_elements_per_body + + @property + def max_sim_vertices_per_body(self) -> int: + """The maximum number of simulation mesh vertices per deformable body.""" + return self.root_physx_view.max_sim_vertices_per_body + + @property + def max_collision_vertices_per_body(self) -> int: + """The maximum number of collision mesh vertices per deformable body.""" + return self.root_physx_view.max_vertices_per_body + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # Think: Should we reset the kinematic targets when resetting the object? + # This is not done in the current implementation. We assume users will reset the kinematic targets. + pass
+ +
[文档] def write_data_to_sim(self): + pass
+ +
[文档] def update(self, dt: float): + self._data.update(dt)
+ + """ + Operations - Write to simulation. + """ + +
[文档] def write_nodal_state_to_sim(self, nodal_state: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the nodal state over selected environment indices into the simulation. + + The nodal state comprises of the nodal positions and velocities. Since these are nodes, the velocity only has + a translational component. All the quantities are in the simulation frame. + + Args: + nodal_state: Nodal state in simulation frame. + Shape is (len(env_ids), max_sim_vertices_per_body, 6). + env_ids: Environment indices. If None, then all indices are used. + """ + # set into simulation + self.write_nodal_pos_to_sim(nodal_state[..., :3], env_ids=env_ids) + self.write_nodal_velocity_to_sim(nodal_state[..., 3:], env_ids=env_ids)
+ +
[文档] def write_nodal_pos_to_sim(self, nodal_pos: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the nodal positions over selected environment indices into the simulation. + + The nodal position comprises of individual nodal positions of the simulation mesh for the deformable body. + The positions are in the simulation frame. + + Args: + nodal_pos: Nodal positions in simulation frame. + Shape is (len(env_ids), max_sim_vertices_per_body, 3). + env_ids: Environment indices. If None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.nodal_pos_w[env_ids] = nodal_pos.clone() + # set into simulation + self.root_physx_view.set_sim_nodal_positions(self._data.nodal_pos_w, indices=physx_env_ids)
+ +
[文档] def write_nodal_velocity_to_sim(self, nodal_vel: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the nodal velocity over selected environment indices into the simulation. + + The nodal velocity comprises of individual nodal velocities of the simulation mesh for the deformable + body. Since these are nodes, the velocity only has a translational component. The velocities are in the + simulation frame. + + Args: + nodal_vel: Nodal velocities in simulation frame. + Shape is (len(env_ids), max_sim_vertices_per_body, 3). + env_ids: Environment indices. If None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.nodal_vel_w[env_ids] = nodal_vel.clone() + # set into simulation + self.root_physx_view.set_sim_nodal_velocities(self._data.nodal_vel_w, indices=physx_env_ids)
+ +
[文档] def write_nodal_kinematic_target_to_sim(self, targets: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the kinematic targets of the simulation mesh for the deformable bodies indicated by the indices. + + The kinematic targets comprise of individual nodal positions of the simulation mesh for the deformable body + and a flag indicating whether the node is kinematically driven or not. The positions are in the simulation frame. + + Note: + The flag is set to 0.0 for kinematically driven nodes and 1.0 for free nodes. + + Args: + targets: The kinematic targets comprising of nodal positions and flags. + Shape is (len(env_ids), max_sim_vertices_per_body, 4). + env_ids: Environment indices. If None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # store into internal buffers + self._data.nodal_kinematic_target[env_ids] = targets.clone() + # set into simulation + self.root_physx_view.set_sim_kinematic_targets(self._data.nodal_kinematic_target, indices=physx_env_ids)
+ + """ + Operations - Helper. + """ + +
[文档] def transform_nodal_pos( + self, nodal_pos: torch.tensor, pos: torch.Tensor | None = None, quat: torch.Tensor | None = None + ) -> torch.Tensor: + """Transform the nodal positions based on the pose transformation. + + This function computes the transformation of the nodal positions based on the pose transformation. + It multiplies the nodal positions with the rotation matrix of the pose and adds the translation. + Internally, it calls the :meth:`omni.isaac.lab.utils.math.transform_points` function. + + Args: + nodal_pos: The nodal positions in the simulation frame. Shape is (N, max_sim_vertices_per_body, 3). + pos: The position transformation. Shape is (N, 3). + Defaults to None, in which case the position is assumed to be zero. + quat: The orientation transformation as quaternion (w, x, y, z). Shape is (N, 4). + Defaults to None, in which case the orientation is assumed to be identity. + + Returns: + The transformed nodal positions. Shape is (N, max_sim_vertices_per_body, 3). + """ + # offset the nodal positions to center them around the origin + mean_nodal_pos = nodal_pos.mean(dim=1, keepdim=True) + nodal_pos = nodal_pos - mean_nodal_pos + # transform the nodal positions based on the pose around the origin + return math_utils.transform_points(nodal_pos, pos, quat) + mean_nodal_pos
+ + """ + Internal helper. + """ + + def _initialize_impl(self): + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # obtain the first prim in the regex expression (all others are assumed to be a copy of this) + template_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if template_prim is None: + raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.") + template_prim_path = template_prim.GetPath().pathString + + # find deformable root prims + root_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI) + ) + if len(root_prims) == 0: + raise RuntimeError( + f"Failed to find a deformable body when resolving '{self.cfg.prim_path}'." + " Please ensure that the prim has 'PhysxSchema.PhysxDeformableBodyAPI' applied." + ) + if len(root_prims) > 1: + raise RuntimeError( + f"Failed to find a single deformable body when resolving '{self.cfg.prim_path}'." + f" Found multiple '{root_prims}' under '{template_prim_path}'." + " Please ensure that there is only one deformable body in the prim path tree." + ) + # we only need the first one from the list + root_prim = root_prims[0] + + # find deformable material prims + material_prim = None + # obtain material prim from the root prim + # note: here we assume that all the root prims have their material prims at similar paths + # and we only need to find the first one. This may not be the case for all scenarios. + # However, the checks in that case get cumbersome and are not included here. + if root_prim.HasAPI(UsdShade.MaterialBindingAPI): + # check the materials that are bound with the purpose 'physics' + material_paths = UsdShade.MaterialBindingAPI(root_prim).GetDirectBindingRel("physics").GetTargets() + # iterate through targets and find the deformable body material + if len(material_paths) > 0: + for mat_path in material_paths: + mat_prim = root_prim.GetStage().GetPrimAtPath(mat_path) + if mat_prim.HasAPI(PhysxSchema.PhysxDeformableBodyMaterialAPI): + material_prim = mat_prim + break + if material_prim is None: + omni.log.info( + f"Failed to find a deformable material binding for '{root_prim.GetPath().pathString}'." + " The material properties will be set to default values and are not modifiable at runtime." + " If you want to modify the material properties, please ensure that the material is bound" + " to the deformable body." + ) + + # resolve root path back into regex expression + # -- root prim expression + root_prim_path = root_prim.GetPath().pathString + root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :] + # -- object view + self._root_physx_view = self._physics_sim_view.create_soft_body_view(root_prim_path_expr.replace(".*", "*")) + + # Return if the asset is not found + if self._root_physx_view._backend is None: + raise RuntimeError(f"Failed to create deformable body at: {self.cfg.prim_path}. Please check PhysX logs.") + + # resolve material path back into regex expression + if material_prim is not None: + # -- material prim expression + material_prim_path = material_prim.GetPath().pathString + # check if the material prim is under the template prim + # if not then we are assuming that the single material prim is used for all the deformable bodies + if template_prim_path in material_prim_path: + material_prim_path_expr = self.cfg.prim_path + material_prim_path[len(template_prim_path) :] + else: + material_prim_path_expr = material_prim_path + # -- material view + self._material_physx_view = self._physics_sim_view.create_soft_body_material_view( + material_prim_path_expr.replace(".*", "*") + ) + else: + self._material_physx_view = None + + # log information about the deformable body + omni.log.info(f"Deformable body initialized at: {root_prim_path_expr}") + omni.log.info(f"Number of instances: {self.num_instances}") + omni.log.info(f"Number of bodies: {self.num_bodies}") + if self._material_physx_view is not None: + omni.log.info(f"Deformable material initialized at: {material_prim_path_expr}") + omni.log.info(f"Number of instances: {self._material_physx_view.count}") + else: + omni.log.info("No deformable material found. Material properties will be set to default values.") + + # container for data access + self._data = DeformableObjectData(self.root_physx_view, self.device) + + # create buffers + self._create_buffers() + # update the deformable body data + self.update(0.0) + + def _create_buffers(self): + """Create buffers for storing data.""" + # constants + self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + + # default state + # we use the initial nodal positions at spawn time as the default state + # note: these are all in the simulation frame + nodal_positions = self.root_physx_view.get_sim_nodal_positions() + nodal_velocities = torch.zeros_like(nodal_positions) + self._data.default_nodal_state_w = torch.cat((nodal_positions, nodal_velocities), dim=-1) + + # kinematic targets + self._data.nodal_kinematic_target = self.root_physx_view.get_sim_kinematic_targets() + # set all nodes as non-kinematic targets by default + self._data.nodal_kinematic_target[..., -1] = 1.0 + + """ + Internal simulation callbacks. + """ + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + if not hasattr(self, "target_visualizer"): + self.target_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.target_visualizer.set_visibility(True) + else: + if hasattr(self, "target_visualizer"): + self.target_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # check where to visualize + targets_enabled = self.data.nodal_kinematic_target[:, :, 3] == 0.0 + num_enabled = int(torch.sum(targets_enabled).item()) + # get positions if any targets are enabled + if num_enabled == 0: + # create a marker below the ground + positions = torch.tensor([[0.0, 0.0, -10.0]], device=self.device) + else: + positions = self.data.nodal_kinematic_target[targets_enabled][..., :3] + # show target visualizer + self.target_visualizer.visualize(positions) + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._root_physx_view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/deformable_object/deformable_object_cfg.html b/_modules/omni/isaac/lab/assets/deformable_object/deformable_object_cfg.html new file mode 100644 index 0000000000..827dd66482 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/deformable_object/deformable_object_cfg.html @@ -0,0 +1,588 @@ + + + + + + + + + + + omni.isaac.lab.assets.deformable_object.deformable_object_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.deformable_object.deformable_object_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from omni.isaac.lab.markers import VisualizationMarkersCfg
+from omni.isaac.lab.markers.config import DEFORMABLE_TARGET_MARKER_CFG
+from omni.isaac.lab.utils import configclass
+
+from ..asset_base_cfg import AssetBaseCfg
+from .deformable_object import DeformableObject
+
+
+
[文档]@configclass +class DeformableObjectCfg(AssetBaseCfg): + """Configuration parameters for a deformable object.""" + + class_type: type = DeformableObject + + visualizer_cfg: VisualizationMarkersCfg = DEFORMABLE_TARGET_MARKER_CFG.replace( + prim_path="/Visuals/DeformableTarget" + ) + """The configuration object for the visualization markers. Defaults to DEFORMABLE_TARGET_MARKER_CFG. + + Note: + This attribute is only used when debug visualization is enabled. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/deformable_object/deformable_object_data.html b/_modules/omni/isaac/lab/assets/deformable_object/deformable_object_data.html new file mode 100644 index 0000000000..f795b6faf8 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/deformable_object/deformable_object_data.html @@ -0,0 +1,797 @@ + + + + + + + + + + + omni.isaac.lab.assets.deformable_object.deformable_object_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.deformable_object.deformable_object_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+import weakref
+
+import omni.physics.tensors.impl.api as physx
+
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.utils.buffers import TimestampedBuffer
+
+
+
[文档]class DeformableObjectData: + """Data container for a deformable object. + + This class contains the data for a deformable object in the simulation. The data includes the nodal states of + the root deformable body in the object. The data is stored in the simulation world frame unless otherwise specified. + + A deformable object in PhysX uses two tetrahedral meshes to represent the object: + + 1. **Simulation mesh**: This mesh is used for the simulation and is the one that is deformed by the solver. + 2. **Collision mesh**: This mesh only needs to match the surface of the simulation mesh and is used for + collision detection. + + The APIs exposed provides the data for both the simulation and collision meshes. These are specified + by the `sim` and `collision` prefixes in the property names. + + The data is lazily updated, meaning that the data is only updated when it is accessed. This is useful + when the data is expensive to compute or retrieve. The data is updated when the timestamp of the buffer + is older than the current simulation timestamp. The timestamp is updated whenever the data is updated. + """ + + def __init__(self, root_physx_view: physx.SoftBodyView, device: str): + """Initializes the deformable object data. + + Args: + root_physx_view: The root deformable body view of the object. + device: The device used for processing. + """ + # Set the parameters + self.device = device + # Set the root deformable body view + # note: this is stored as a weak reference to avoid circular references between the asset class + # and the data container. This is important to avoid memory leaks. + self._root_physx_view: physx.SoftBodyView = weakref.proxy(root_physx_view) + + # Set initial time stamp + self._sim_timestamp = 0.0 + + # Initialize the lazy buffers. + # -- node state in simulation world frame + self._nodal_pos_w = TimestampedBuffer() + self._nodal_vel_w = TimestampedBuffer() + self._nodal_state_w = TimestampedBuffer() + # -- mesh element-wise rotations + self._sim_element_quat_w = TimestampedBuffer() + self._collision_element_quat_w = TimestampedBuffer() + # -- mesh element-wise deformation gradients + self._sim_element_deform_gradient_w = TimestampedBuffer() + self._collision_element_deform_gradient_w = TimestampedBuffer() + # -- mesh element-wise stresses + self._sim_element_stress_w = TimestampedBuffer() + self._collision_element_stress_w = TimestampedBuffer() + +
[文档] def update(self, dt: float): + """Updates the data for the deformable object. + + Args: + dt: The time step for the update. This must be a positive value. + """ + # update the simulation timestamp + self._sim_timestamp += dt
+ + ## + # Defaults. + ## + + default_nodal_state_w: torch.Tensor = None + """Default nodal state ``[nodal_pos, nodal_vel]`` in simulation world frame. + Shape is (num_instances, max_sim_vertices_per_body, 6). + """ + + ## + # Kinematic commands + ## + + nodal_kinematic_target: torch.Tensor = None + """Simulation mesh kinematic targets for the deformable bodies. + Shape is (num_instances, max_sim_vertices_per_body, 4). + + The kinematic targets are used to drive the simulation mesh vertices to the target positions. + The targets are stored as (x, y, z, is_not_kinematic) where "is_not_kinematic" is a binary + flag indicating whether the vertex is kinematic or not. The flag is set to 0 for kinematic vertices + and 1 for non-kinematic vertices. + """ + + ## + # Properties. + ## + + @property + def nodal_pos_w(self): + """Nodal positions in simulation world frame. Shape is (num_instances, max_sim_vertices_per_body, 3).""" + if self._nodal_pos_w.timestamp < self._sim_timestamp: + self._nodal_pos_w.data = self._root_physx_view.get_sim_nodal_positions() + self._nodal_pos_w.timestamp = self._sim_timestamp + return self._nodal_pos_w.data + + @property + def nodal_vel_w(self): + """Nodal velocities in simulation world frame. Shape is (num_instances, max_sim_vertices_per_body, 3).""" + if self._nodal_vel_w.timestamp < self._sim_timestamp: + self._nodal_vel_w.data = self._root_physx_view.get_sim_nodal_velocities() + self._nodal_vel_w.timestamp = self._sim_timestamp + return self._nodal_vel_w.data + + @property + def nodal_state_w(self): + """Nodal state ``[nodal_pos, nodal_vel]`` in simulation world frame. + Shape is (num_instances, max_sim_vertices_per_body, 6). + """ + if self._nodal_state_w.timestamp < self._sim_timestamp: + nodal_positions = self.nodal_pos_w + nodal_velocities = self.nodal_vel_w + # set the buffer data and timestamp + self._nodal_state_w.data = torch.cat((nodal_positions, nodal_velocities), dim=-1) + self._nodal_state_w.timestamp = self._sim_timestamp + return self._nodal_state_w.data + + @property + def sim_element_quat_w(self): + """Simulation mesh element-wise rotations as quaternions for the deformable bodies in simulation world frame. + Shape is (num_instances, max_sim_elements_per_body, 4). + + The rotations are stored as quaternions in the order (w, x, y, z). + """ + if self._sim_element_quat_w.timestamp < self._sim_timestamp: + # convert from xyzw to wxyz + quats = self._root_physx_view.get_sim_element_rotations().view(self._root_physx_view.count, -1, 4) + quats = math_utils.convert_quat(quats, to="wxyz") + # set the buffer data and timestamp + self._sim_element_quat_w.data = quats + self._sim_element_quat_w.timestamp = self._sim_timestamp + return self._sim_element_quat_w.data + + @property + def collision_element_quat_w(self): + """Collision mesh element-wise rotations as quaternions for the deformable bodies in simulation world frame. + Shape is (num_instances, max_collision_elements_per_body, 4). + + The rotations are stored as quaternions in the order (w, x, y, z). + """ + if self._collision_element_quat_w.timestamp < self._sim_timestamp: + # convert from xyzw to wxyz + quats = self._root_physx_view.get_element_rotations().view(self._root_physx_view.count, -1, 4) + quats = math_utils.convert_quat(quats, to="wxyz") + # set the buffer data and timestamp + self._collision_element_quat_w.data = quats + self._collision_element_quat_w.timestamp = self._sim_timestamp + return self._collision_element_quat_w.data + + @property + def sim_element_deform_gradient_w(self): + """Simulation mesh element-wise second-order deformation gradient tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_sim_elements_per_body, 3, 3). + """ + if self._sim_element_deform_gradient_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._sim_element_deform_gradient_w.data = ( + self._root_physx_view.get_sim_element_deformation_gradients().view( + self._root_physx_view.count, -1, 3, 3 + ) + ) + self._sim_element_deform_gradient_w.timestamp = self._sim_timestamp + return self._sim_element_deform_gradient_w.data + + @property + def collision_element_deform_gradient_w(self): + """Collision mesh element-wise second-order deformation gradient tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_collision_elements_per_body, 3, 3). + """ + if self._collision_element_deform_gradient_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._collision_element_deform_gradient_w.data = ( + self._root_physx_view.get_element_deformation_gradients().view(self._root_physx_view.count, -1, 3, 3) + ) + self._collision_element_deform_gradient_w.timestamp = self._sim_timestamp + return self._collision_element_deform_gradient_w.data + + @property + def sim_element_stress_w(self): + """Simulation mesh element-wise second-order Cauchy stress tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_sim_elements_per_body, 3, 3). + """ + if self._sim_element_stress_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._sim_element_stress_w.data = self._root_physx_view.get_sim_element_stresses().view( + self._root_physx_view.count, -1, 3, 3 + ) + self._sim_element_stress_w.timestamp = self._sim_timestamp + return self._sim_element_stress_w.data + + @property + def collision_element_stress_w(self): + """Collision mesh element-wise second-order Cauchy stress tensors for the deformable bodies + in simulation world frame. Shape is (num_instances, max_collision_elements_per_body, 3, 3). + """ + if self._collision_element_stress_w.timestamp < self._sim_timestamp: + # set the buffer data and timestamp + self._collision_element_stress_w.data = self._root_physx_view.get_element_stresses().view( + self._root_physx_view.count, -1, 3, 3 + ) + self._collision_element_stress_w.timestamp = self._sim_timestamp + return self._collision_element_stress_w.data + + ## + # Derived properties. + ## + + @property + def root_pos_w(self) -> torch.Tensor: + """Root position from nodal positions of the simulation mesh for the deformable bodies in simulation world frame. + Shape is (num_instances, 3). + + This quantity is computed as the mean of the nodal positions. + """ + return self.nodal_pos_w.mean(dim=1) + + @property + def root_vel_w(self) -> torch.Tensor: + """Root velocity from vertex velocities for the deformable bodies in simulation world frame. + Shape is (num_instances, 3). + + This quantity is computed as the mean of the nodal velocities. + """ + return self.nodal_vel_w.mean(dim=1)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/rigid_object/rigid_object.html b/_modules/omni/isaac/lab/assets/rigid_object/rigid_object.html new file mode 100644 index 0000000000..035b10aa12 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/rigid_object/rigid_object.html @@ -0,0 +1,915 @@ + + + + + + + + + + + omni.isaac.lab.assets.rigid_object.rigid_object — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.rigid_object.rigid_object 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.log
+import omni.physics.tensors.impl.api as physx
+from pxr import UsdPhysics
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.math as math_utils
+import omni.isaac.lab.utils.string as string_utils
+
+from ..asset_base import AssetBase
+from .rigid_object_data import RigidObjectData
+
+if TYPE_CHECKING:
+    from .rigid_object_cfg import RigidObjectCfg
+
+
+
[文档]class RigidObject(AssetBase): + """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` attribute. + + .. note:: + + For users familiar with Isaac Sim, the PhysX view class API is not the exactly same as Isaac Sim view + class API. Similar to Isaac Lab, Isaac Sim wraps around the PhysX view API. However, as of now (2023.1 release), + we see a large difference in initializing the view classes in Isaac Sim. This is because the view classes + in Isaac Sim perform additional USD-related operations which are slow and also not required. + + .. _`USD RigidBodyAPI`: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html + """ + + cfg: RigidObjectCfg + """Configuration instance for the rigid object.""" + +
[文档] def __init__(self, cfg: RigidObjectCfg): + """Initialize the rigid object. + + Args: + cfg: A configuration instance. + """ + super().__init__(cfg)
+ + """ + Properties + """ + + @property + def data(self) -> RigidObjectData: + return self._data + + @property + def num_instances(self) -> int: + return self.root_physx_view.count + + @property + def num_bodies(self) -> int: + """Number of bodies in the asset. + + This is always 1 since each object is a single rigid body. + """ + return 1 + + @property + def body_names(self) -> list[str]: + """Ordered names of bodies in the rigid object.""" + prim_paths = self.root_physx_view.prim_paths[: self.num_bodies] + return [path.split("/")[-1] for path in prim_paths] + + @property + def root_physx_view(self) -> physx.RigidBodyView: + """Rigid body view for the asset (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._root_physx_view + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # resolve all indices + if env_ids is None: + env_ids = slice(None) + # reset external wrench + self._external_force_b[env_ids] = 0.0 + self._external_torque_b[env_ids] = 0.0
+ +
[文档] def write_data_to_sim(self): + """Write external wrench to the simulation. + + Note: + We write external wrench to the simulation here since this function is called before the simulation step. + This ensures that the external wrench is applied at every simulation step. + """ + # write external wrench + if self.has_external_wrench: + self.root_physx_view.apply_forces_and_torques_at_position( + force_data=self._external_force_b.view(-1, 3), + torque_data=self._external_torque_b.view(-1, 3), + position_data=None, + indices=self._ALL_INDICES, + is_global=False, + )
+ +
[文档] def update(self, dt: float): + self._data.update(dt)
+ + """ + Operations - Finders. + """ + +
[文档] def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]: + """Find bodies in the rigid body based on the name keys. + + Please check the :meth:`omni.isaac.lab.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. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple of lists containing the body indices and names. + """ + return string_utils.resolve_matching_names(name_keys, self.body_names, preserve_order)
+ + """ + Operations - Write to simulation. + """ + +
[文档] def write_root_state_to_sim(self, root_state: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the root state over selected environment indices into the simulation. + + The root state comprises of the cartesian position, quaternion orientation in (w, x, y, z), and linear + 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 None, then all indices are used. + """ + # set into simulation + self.write_root_pose_to_sim(root_state[:, :7], env_ids=env_ids) + self.write_root_velocity_to_sim(root_state[:, 7:], env_ids=env_ids)
+ +
[文档] def write_root_pose_to_sim(self, root_pose: torch.Tensor, env_ids: Sequence[int] | None = None): + """Set the root pose over selected environment indices into the simulation. + + 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 None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.root_state_w[env_ids, :7] = root_pose.clone() + # convert root quaternion from wxyz to xyzw + root_poses_xyzw = self._data.root_state_w[:, :7].clone() + root_poses_xyzw[:, 3:] = math_utils.convert_quat(root_poses_xyzw[:, 3:], to="xyzw") + # set into simulation + self.root_physx_view.set_transforms(root_poses_xyzw, indices=physx_env_ids)
+ +
[文档] def write_root_velocity_to_sim(self, root_velocity: torch.Tensor, env_ids: Sequence[int] | None = None): + """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 None, then all indices are used. + """ + # resolve all indices + physx_env_ids = env_ids + if env_ids is None: + env_ids = slice(None) + physx_env_ids = self._ALL_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.root_state_w[env_ids, 7:] = root_velocity.clone() + self._data.body_acc_w[env_ids] = 0.0 + # set into simulation + self.root_physx_view.set_velocities(self._data.root_state_w[:, 7:], indices=physx_env_ids)
+ + """ + Operations - Setters. + """ + +
[文档] def set_external_force_and_torque( + self, + forces: torch.Tensor, + torques: torch.Tensor, + body_ids: Sequence[int] | slice | None = None, + env_ids: Sequence[int] | None = None, + ): + """Set external force and torque to apply on the asset's bodies in their local frame. + + For many applications, we want to keep the applied external force on rigid bodies constant over a period of + time (for instance, during the policy control). This function allows us to store the external force and torque + into buffers which are then applied to the simulation at every step. + + .. caution:: + If the function is called with empty forces and torques, then this function disables the application + 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)) + + .. note:: + This function does not apply the external wrench to the simulation. It only fills the buffers with + the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function + right before the simulation step. + + 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). + 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). + """ + if forces.any() or torques.any(): + self.has_external_wrench = True + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = slice(None) + # -- body_ids + if body_ids is None: + body_ids = slice(None) + # broadcast env_ids if needed to allow double indexing + if env_ids != slice(None) and body_ids != slice(None): + env_ids = env_ids[:, None] + + # set into internal buffers + self._external_force_b[env_ids, body_ids] = forces + self._external_torque_b[env_ids, body_ids] = torques + else: + self.has_external_wrench = False
+ + """ + Internal helper. + """ + + def _initialize_impl(self): + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # obtain the first prim in the regex expression (all others are assumed to be a copy of this) + template_prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if template_prim is None: + raise RuntimeError(f"Failed to find prim for expression: '{self.cfg.prim_path}'.") + template_prim_path = template_prim.GetPath().pathString + + # find rigid root prims + root_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI) + ) + if len(root_prims) == 0: + raise RuntimeError( + f"Failed to find a rigid body when resolving '{self.cfg.prim_path}'." + " Please ensure that the prim has 'USD RigidBodyAPI' applied." + ) + if len(root_prims) > 1: + raise RuntimeError( + f"Failed to find a single rigid body when resolving '{self.cfg.prim_path}'." + f" Found multiple '{root_prims}' under '{template_prim_path}'." + " Please ensure that there is only one rigid body in the prim path tree." + ) + + # resolve root prim back into regex expression + root_prim_path = root_prims[0].GetPath().pathString + root_prim_path_expr = self.cfg.prim_path + root_prim_path[len(template_prim_path) :] + # -- object view + self._root_physx_view = self._physics_sim_view.create_rigid_body_view(root_prim_path_expr.replace(".*", "*")) + + # check if the rigid body was created + if self._root_physx_view._backend is None: + raise RuntimeError(f"Failed to create rigid body at: {self.cfg.prim_path}. Please check PhysX logs.") + + # log information about the rigid body + omni.log.info(f"Rigid body initialized at: {self.cfg.prim_path} with root '{root_prim_path_expr}'.") + omni.log.info(f"Number of instances: {self.num_instances}") + omni.log.info(f"Number of bodies: {self.num_bodies}") + omni.log.info(f"Body names: {self.body_names}") + + # container for data access + self._data = RigidObjectData(self.root_physx_view, self.device) + + # create buffers + self._create_buffers() + # process configuration + self._process_cfg() + # update the rigid body data + self.update(0.0) + + def _create_buffers(self): + """Create buffers for storing data.""" + # constants + self._ALL_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + + # external forces and torques + self.has_external_wrench = False + self._external_force_b = torch.zeros((self.num_instances, self.num_bodies, 3), device=self.device) + self._external_torque_b = torch.zeros_like(self._external_force_b) + + # set information about rigid body into data + self._data.body_names = self.body_names + self._data.default_mass = self.root_physx_view.get_masses().clone() + self._data.default_inertia = self.root_physx_view.get_inertias().clone() + + def _process_cfg(self): + """Post processing of configuration parameters.""" + # default state + # -- root state + # note: we cast to tuple to avoid torch/numpy type mismatch. + default_root_state = ( + tuple(self.cfg.init_state.pos) + + tuple(self.cfg.init_state.rot) + + tuple(self.cfg.init_state.lin_vel) + + tuple(self.cfg.init_state.ang_vel) + ) + default_root_state = torch.tensor(default_root_state, dtype=torch.float, device=self.device) + self._data.default_root_state = default_root_state.repeat(self.num_instances, 1) + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._root_physx_view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/rigid_object/rigid_object_cfg.html b/_modules/omni/isaac/lab/assets/rigid_object/rigid_object_cfg.html new file mode 100644 index 0000000000..165e5a76af --- /dev/null +++ b/_modules/omni/isaac/lab/assets/rigid_object/rigid_object_cfg.html @@ -0,0 +1,591 @@ + + + + + + + + + + + omni.isaac.lab.assets.rigid_object.rigid_object_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.rigid_object.rigid_object_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from omni.isaac.lab.utils import configclass
+
+from ..asset_base_cfg import AssetBaseCfg
+from .rigid_object import RigidObject
+
+
+
[文档]@configclass +class RigidObjectCfg(AssetBaseCfg): + """Configuration parameters for a rigid object.""" + +
[文档] @configclass + class InitialStateCfg(AssetBaseCfg.InitialStateCfg): + """Initial state of the rigid body.""" + + lin_vel: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Linear velocity of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0).""" + ang_vel: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Angular velocity of the root in simulation world frame. Defaults to (0.0, 0.0, 0.0)."""
+ + ## + # Initialize configurations. + ## + + class_type: type = RigidObject + + init_state: InitialStateCfg = InitialStateCfg() + """Initial state of the rigid object. Defaults to identity pose with zero velocity."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/rigid_object/rigid_object_data.html b/_modules/omni/isaac/lab/assets/rigid_object/rigid_object_data.html new file mode 100644 index 0000000000..cbabbe1b98 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/rigid_object/rigid_object_data.html @@ -0,0 +1,839 @@ + + + + + + + + + + + omni.isaac.lab.assets.rigid_object.rigid_object_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.rigid_object.rigid_object_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+import weakref
+
+import omni.physics.tensors.impl.api as physx
+
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.utils.buffers import TimestampedBuffer
+
+
+
[文档]class RigidObjectData: + """Data container for a rigid object. + + This class contains the data for a rigid object in the simulation. The data includes the state of + the root rigid body and the state of all the bodies in the object. The data is stored in the simulation + world frame unless otherwise specified. + + For a rigid body, there are two frames of reference that are used: + + - Actor frame: The frame of reference of the rigid body prim. This typically corresponds to the Xform prim + with the rigid body schema. + - Center of mass frame: The frame of reference of the center of mass of the rigid body. + + Depending on the settings of the simulation, the actor frame and the center of mass frame may be the same. + This needs to be taken into account when interpreting the data. + + The data is lazily updated, meaning that the data is only updated when it is accessed. This is useful + when the data is expensive to compute or retrieve. The data is updated when the timestamp of the buffer + is older than the current simulation timestamp. The timestamp is updated whenever the data is updated. + """ + + def __init__(self, root_physx_view: physx.RigidBodyView, device: str): + """Initializes the rigid object data. + + Args: + root_physx_view: The root rigid body view. + device: The device used for processing. + """ + # Set the parameters + self.device = device + # Set the root rigid body view + # note: this is stored as a weak reference to avoid circular references between the asset class + # and the data container. This is important to avoid memory leaks. + self._root_physx_view: physx.RigidBodyView = weakref.proxy(root_physx_view) + + # Set initial time stamp + self._sim_timestamp = 0.0 + + # Obtain global physics sim view + physics_sim_view = physx.create_simulation_view("torch") + physics_sim_view.set_subspace_roots("/") + gravity = physics_sim_view.get_gravity() + # Convert to direction vector + gravity_dir = torch.tensor((gravity[0], gravity[1], gravity[2]), device=self.device) + gravity_dir = math_utils.normalize(gravity_dir.unsqueeze(0)).squeeze(0) + + # Initialize constants + self.GRAVITY_VEC_W = gravity_dir.repeat(self._root_physx_view.count, 1) + self.FORWARD_VEC_B = torch.tensor((1.0, 0.0, 0.0), device=self.device).repeat(self._root_physx_view.count, 1) + + # Initialize the lazy buffers. + self._root_state_w = TimestampedBuffer() + self._body_acc_w = TimestampedBuffer() + +
[文档] def update(self, dt: float): + """Updates the data for the rigid object. + + Args: + dt: The time step for the update. This must be a positive value. + """ + # update the simulation timestamp + self._sim_timestamp += dt
+ + ## + # Names. + ## + + body_names: list[str] = None + """Body names in the order parsed by the simulation view.""" + + ## + # Defaults. + ## + + default_root_state: torch.Tensor = None + """Default root state ``[pos, quat, lin_vel, ang_vel]`` in local environment frame. Shape is (num_instances, 13). + + The position and quaternion are of the rigid body's actor frame. Meanwhile, the linear and angular velocities are + of the center of mass frame. + """ + + default_mass: torch.Tensor = None + """Default mass read from the simulation. Shape is (num_instances, 1).""" + + default_inertia: torch.Tensor = None + """Default inertia tensor read from the simulation. Shape is (num_instances, 9). + + The inertia is the inertia tensor relative to the center of mass frame. The values are stored in + the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + """ + + ## + # Properties. + ## + + @property + def root_state_w(self): + """Root state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is (num_instances, 13). + + The position and orientation are of the rigid body's actor frame. Meanwhile, the linear and angular + velocities are of the rigid body's center of mass frame. + """ + if self._root_state_w.timestamp < self._sim_timestamp: + # read data from simulation + pose = self._root_physx_view.get_transforms().clone() + pose[:, 3:7] = math_utils.convert_quat(pose[:, 3:7], to="wxyz") + velocity = self._root_physx_view.get_velocities() + # set the buffer data and timestamp + self._root_state_w.data = torch.cat((pose, velocity), dim=-1) + self._root_state_w.timestamp = self._sim_timestamp + return self._root_state_w.data + + @property + def body_state_w(self): + """State of all bodies `[pos, quat, lin_vel, ang_vel]` in simulation world frame. Shape is (num_instances, 1, 13). + + The position and orientation are of the rigid bodies' actor frame. Meanwhile, the linear and angular + velocities are of the rigid bodies' center of mass frame. + """ + return self.root_state_w.view(-1, 1, 13) + + @property + def body_acc_w(self): + """Acceleration of all bodies. Shape is (num_instances, 1, 6). + + This quantity is the acceleration of the rigid bodies' center of mass frame. + """ + if self._body_acc_w.timestamp < self._sim_timestamp: + # note: we use finite differencing to compute acceleration + self._body_acc_w.data = self._root_physx_view.get_accelerations().unsqueeze(1) + self._body_acc_w.timestamp = self._sim_timestamp + return self._body_acc_w.data + + @property + def projected_gravity_b(self): + """Projection of the gravity direction on base frame. Shape is (num_instances, 3).""" + return math_utils.quat_rotate_inverse(self.root_quat_w, self.GRAVITY_VEC_W) + + @property + def heading_w(self): + """Yaw heading of the base frame (in radians). Shape is (num_instances,). + + Note: + This quantity is computed by assuming that the forward-direction of the base + frame is along x-direction, i.e. :math:`(1, 0, 0)`. + """ + forward_w = math_utils.quat_apply(self.root_quat_w, self.FORWARD_VEC_B) + return torch.atan2(forward_w[:, 1], forward_w[:, 0]) + + ## + # Derived properties. + ## + + @property + def root_pos_w(self) -> torch.Tensor: + """Root position in simulation world frame. Shape is (num_instances, 3). + + This quantity is the position of the actor frame of the root rigid body. + """ + 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 (num_instances, 4). + + This quantity is the orientation of the actor frame of the root rigid body. + """ + return self.root_state_w[:, 3:7] + + @property + def root_vel_w(self) -> torch.Tensor: + """Root velocity in simulation world frame. Shape is (num_instances, 6). + + This quantity contains the linear and angular velocities of the root rigid body's center of mass frame. + """ + 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 (num_instances, 3). + + This quantity is the linear velocity of the root rigid body's center of mass frame. + """ + 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 (num_instances, 3). + + This quantity is the angular velocity of the root rigid body's center of mass frame. + """ + return self.root_state_w[:, 10:13] + + @property + def root_lin_vel_b(self) -> torch.Tensor: + """Root linear velocity in base frame. Shape is (num_instances, 3). + + This quantity is the linear velocity of the root rigid body's center of mass frame with respect to the + rigid body's actor frame. + """ + return math_utils.quat_rotate_inverse(self.root_quat_w, self.root_lin_vel_w) + + @property + def root_ang_vel_b(self) -> torch.Tensor: + """Root angular velocity in base world frame. Shape is (num_instances, 3). + + This quantity is the angular velocity of the root rigid body's center of mass frame with respect to the + rigid body's actor frame. + """ + return math_utils.quat_rotate_inverse(self.root_quat_w, self.root_ang_vel_w) + + @property + def body_pos_w(self) -> torch.Tensor: + """Positions of all bodies in simulation world frame. Shape is (num_instances, 1, 3). + + This quantity is the position of the rigid bodies' actor frame. + """ + 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 (num_instances, 1, 4). + + This quantity is the orientation of the rigid bodies' actor frame. + """ + 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 (num_instances, 1, 6). + + This quantity contains the linear and angular velocities of the rigid bodies' center of mass frame. + """ + 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 (num_instances, 1, 3). + + This quantity is the linear velocity of the rigid bodies' center of mass frame. + """ + 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 (num_instances, 1, 3). + + This quantity is the angular velocity of the rigid bodies' center of mass frame. + """ + 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 (num_instances, 1, 3). + + This quantity is the linear acceleration of the rigid bodies' center of mass frame. + """ + 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 (num_instances, 1, 3). + + This quantity is the angular acceleration of the rigid bodies' center of mass frame. + """ + return self.body_acc_w[..., 3:6]
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection.html b/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection.html new file mode 100644 index 0000000000..802ab110b9 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection.html @@ -0,0 +1,1066 @@ + + + + + + + + + + + omni.isaac.lab.assets.rigid_object_collection.rigid_object_collection — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.rigid_object_collection.rigid_object_collection 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import re
+import torch
+import weakref
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.kit.app
+import omni.log
+import omni.physics.tensors.impl.api as physx
+import omni.timeline
+from pxr import UsdPhysics
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.math as math_utils
+import omni.isaac.lab.utils.string as string_utils
+
+from ..asset_base import AssetBase
+from .rigid_object_collection_data import RigidObjectCollectionData
+
+if TYPE_CHECKING:
+    from .rigid_object_collection_cfg import RigidObjectCollectionCfg
+
+
+
[文档]class RigidObjectCollection(AssetBase): + """A rigid object collection class. + + This class represents a collection of rigid objects in the simulation, where the state of the rigid objects can be + accessed and modified using a batched ``(env_ids, object_ids)`` API. + + For each rigid body in the collection, 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 bodies. On playing the + simulation, the physics engine will automatically register the rigid bodies and create a corresponding + rigid body handle. This handle can be accessed using the :attr:`root_physx_view` attribute. + + .. note:: + Rigid objects in the collection are uniquely identified via the key of the dictionary + :attr:`~omni.isaac.lab.assets.RigidObjectCollectionCfg.rigid_objects` in :class:`~omni.isaac.lab.assets.RigidObjectCollectionCfg`. + This differs from the class :class:`~omni.isaac.lab.assets.RigidObject`, where a rigid object is identified by + the name of the Xform where the `USD RigidBodyAPI`_ is applied. This would not be possible for the rigid object + collection since the :attr:`~omni.isaac.lab.assets.RigidObjectCollectionCfg.rigid_objects` dictionary could + contain the same rigid object multiple times, leading to ambiguity. + + .. _`USD RigidBodyAPI`: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html + """ + + cfg: RigidObjectCollectionCfg + """Configuration instance for the rigid object collection.""" + +
[文档] def __init__(self, cfg: RigidObjectCollectionCfg): + """Initialize the rigid object collection. + + Args: + cfg: A configuration instance. + """ + # check that the config is valid + cfg.validate() + # store inputs + self.cfg = cfg + # flag for whether the asset is initialized + self._is_initialized = False + for rigid_object_cfg in self.cfg.rigid_objects.values(): + # check if the rigid object path is valid + # note: currently the spawner does not work if there is a regex pattern in the leaf + # For example, if the prim path is "/World/Object_[1,2]" since the spawner will not + # know which prim to spawn. This is a limitation of the spawner and not the asset. + asset_path = rigid_object_cfg.prim_path.split("/")[-1] + asset_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", asset_path) is None + # spawn the asset + if rigid_object_cfg.spawn is not None and not asset_path_is_regex: + rigid_object_cfg.spawn.func( + rigid_object_cfg.prim_path, + rigid_object_cfg.spawn, + translation=rigid_object_cfg.init_state.pos, + orientation=rigid_object_cfg.init_state.rot, + ) + # check that spawn was successful + matching_prims = sim_utils.find_matching_prims(rigid_object_cfg.prim_path) + if len(matching_prims) == 0: + raise RuntimeError(f"Could not find prim with path {rigid_object_cfg.prim_path}.") + + # stores object names + self._object_names_list = [] + + # note: Use weakref on all callbacks to ensure that this object can be deleted when its destructor is called. + # add callbacks for stage play/stop + # The order is set to 10 which is arbitrary but should be lower priority than the default order of 0 + timeline_event_stream = omni.timeline.get_timeline_interface().get_timeline_event_stream() + self._initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.PLAY), + lambda event, obj=weakref.proxy(self): obj._initialize_callback(event), + order=10, + ) + self._invalidate_initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.STOP), + lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event), + order=10, + ) + + self._debug_vis_handle = None
+ + """ + Properties + """ + + @property + def data(self) -> RigidObjectCollectionData: + return self._data + + @property + def num_instances(self) -> int: + """Number of instances of the collection.""" + return self.root_physx_view.count // self.num_objects + + @property + def num_objects(self) -> int: + """Number of objects in the collection. + + This corresponds to the distinct number of rigid bodies in the collection. + """ + return len(self.object_names) + + @property + def object_names(self) -> list[str]: + """Ordered names of objects in the rigid object collection.""" + return self._object_names_list + + @property + def root_physx_view(self) -> physx.RigidBodyView: + """Rigid body view for the rigid body collection (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._root_physx_view + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: torch.Tensor | None = None, object_ids: slice | torch.Tensor | None = None): + """Resets all internal buffers of selected environments and objects. + + Args: + env_ids: The indices of the object to reset. Defaults to None (all instances). + object_ids: The indices of the object to reset. Defaults to None (all objects). + """ + # resolve all indices + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + # reset external wrench + self._external_force_b[env_ids[:, None], object_ids] = 0.0 + self._external_torque_b[env_ids[:, None], object_ids] = 0.0
+ +
[文档] def write_data_to_sim(self): + """Write external wrench to the simulation. + + Note: + We write external wrench to the simulation here since this function is called before the simulation step. + This ensures that the external wrench is applied at every simulation step. + """ + # write external wrench + if self.has_external_wrench: + self.root_physx_view.apply_forces_and_torques_at_position( + force_data=self.reshape_data_to_view(self._external_force_b), + torque_data=self.reshape_data_to_view(self._external_torque_b), + position_data=None, + indices=self._env_obj_ids_to_view_ids(self._ALL_ENV_INDICES, self._ALL_OBJ_INDICES), + is_global=False, + )
+ +
[文档] def update(self, dt: float): + self._data.update(dt)
+ + """ + Operations - Finders. + """ + +
[文档] def find_objects( + self, name_keys: str | Sequence[str], preserve_order: bool = False + ) -> tuple[torch.Tensor, list[str]]: + """Find objects in the collection based on the name keys. + + Please check the :meth:`omni.isaac.lab.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 object names. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple containing the object indices and names. + """ + obj_ids, obj_names = string_utils.resolve_matching_names(name_keys, self.object_names, preserve_order) + return torch.tensor(obj_ids, device=self.device), obj_names
+ + """ + Operations - Write to simulation. + """ + +
[文档] def write_object_state_to_sim( + self, + object_state: torch.Tensor, + env_ids: torch.Tensor | None = None, + object_ids: slice | torch.Tensor | None = None, + ): + """Set the object state over selected environment and object indices into the simulation. + + The object state comprises of the cartesian position, quaternion orientation in (w, x, y, z), and linear + and angular velocity. All the quantities are in the simulation frame. + + Args: + object_state: Object state in simulation frame. Shape is (len(env_ids), len(object_ids), 13). + env_ids: Environment indices. If None, then all indices are used. + object_ids: Object indices. If None, then all indices are used. + """ + # set into simulation + self.write_object_pose_to_sim(object_state[..., :7], env_ids=env_ids, object_ids=object_ids) + self.write_object_velocity_to_sim(object_state[..., 7:], env_ids=env_ids, object_ids=object_ids)
+ +
[文档] def write_object_pose_to_sim( + self, + object_pose: torch.Tensor, + env_ids: torch.Tensor | None = None, + object_ids: slice | torch.Tensor | None = None, + ): + """Set the object pose over selected environment and object indices into the simulation. + + The object pose comprises of the cartesian position and quaternion orientation in (w, x, y, z). + + Args: + object_pose: Object poses in simulation frame. Shape is (len(env_ids), len(object_ids), 7). + env_ids: Environment indices. If None, then all indices are used. + object_ids: Object indices. If None, then all indices are used. + """ + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + # -- object_ids + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + # note: we need to do this here since tensors are not set into simulation until step. + # set into internal buffers + self._data.object_state_w[env_ids[:, None], object_ids, :7] = object_pose.clone() + # convert the quaternion from wxyz to xyzw + poses_xyzw = self._data.object_state_w[..., :7].clone() + poses_xyzw[..., 3:] = math_utils.convert_quat(poses_xyzw[..., 3:], to="xyzw") + # set into simulation + view_ids = self._env_obj_ids_to_view_ids(env_ids, object_ids) + self.root_physx_view.set_transforms(self.reshape_data_to_view(poses_xyzw), indices=view_ids)
+ +
[文档] def write_object_velocity_to_sim( + self, + object_velocity: torch.Tensor, + env_ids: torch.Tensor | None = None, + object_ids: slice | torch.Tensor | None = None, + ): + """Set the object velocity over selected environment and object indices into the simulation. + + Args: + object_velocity: Object velocities in simulation frame. Shape is (len(env_ids), len(object_ids), 6). + env_ids: Environment indices. If None, then all indices are used. + object_ids: Object indices. If None, then all indices are used. + """ + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + # -- object_ids + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + + self._data.object_state_w[env_ids[:, None], object_ids, 7:] = object_velocity.clone() + self._data.object_acc_w[env_ids[:, None], object_ids] = 0.0 + + # set into simulation + view_ids = self._env_obj_ids_to_view_ids(env_ids, object_ids) + self.root_physx_view.set_velocities( + self.reshape_data_to_view(self._data.object_state_w[..., 7:]), indices=view_ids + )
+ + """ + Operations - Setters. + """ + +
[文档] def set_external_force_and_torque( + self, + forces: torch.Tensor, + torques: torch.Tensor, + object_ids: slice | torch.Tensor | None = None, + env_ids: torch.Tensor | None = None, + ): + """Set external force and torque to apply on the objects' bodies in their local frame. + + For many applications, we want to keep the applied external force on rigid bodies constant over a period of + time (for instance, during the policy control). This function allows us to store the external force and torque + into buffers which are then applied to the simulation at every step. + + .. caution:: + If the function is called with empty forces and torques, then this function disables the application + of external wrench to the simulation. + + .. code-block:: python + + # example of disabling external wrench + asset.set_external_force_and_torque(forces=torch.zeros(0, 0, 3), torques=torch.zeros(0, 0, 3)) + + .. note:: + This function does not apply the external wrench to the simulation. It only fills the buffers with + the desired values. To apply the external wrench, call the :meth:`write_data_to_sim` function + right before the simulation step. + + Args: + forces: External forces in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3). + torques: External torques in bodies' local frame. Shape is (len(env_ids), len(object_ids), 3). + object_ids: Object indices to apply external wrench to. Defaults to None (all objects). + env_ids: Environment indices to apply external wrench to. Defaults to None (all instances). + """ + if forces.any() or torques.any(): + self.has_external_wrench = True + # resolve all indices + # -- env_ids + if env_ids is None: + env_ids = self._ALL_ENV_INDICES + # -- object_ids + if object_ids is None: + object_ids = self._ALL_OBJ_INDICES + # set into internal buffers + self._external_force_b[env_ids[:, None], object_ids] = forces + self._external_torque_b[env_ids[:, None], object_ids] = torques + else: + self.has_external_wrench = False
+ + """ + Internal helper. + """ + + def _initialize_impl(self): + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + root_prim_path_exprs = [] + for name, rigid_object_cfg in self.cfg.rigid_objects.items(): + # obtain the first prim in the regex expression (all others are assumed to be a copy of this) + template_prim = sim_utils.find_first_matching_prim(rigid_object_cfg.prim_path) + if template_prim is None: + raise RuntimeError(f"Failed to find prim for expression: '{rigid_object_cfg.prim_path}'.") + template_prim_path = template_prim.GetPath().pathString + + # find rigid root prims + root_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.RigidBodyAPI) + ) + if len(root_prims) == 0: + raise RuntimeError( + f"Failed to find a rigid body when resolving '{rigid_object_cfg.prim_path}'." + " Please ensure that the prim has 'USD RigidBodyAPI' applied." + ) + if len(root_prims) > 1: + raise RuntimeError( + f"Failed to find a single rigid body when resolving '{rigid_object_cfg.prim_path}'." + f" Found multiple '{root_prims}' under '{template_prim_path}'." + " Please ensure that there is only one rigid body in the prim path tree." + ) + + # check that no rigid object has an articulation root API, which decreases simulation performance + articulation_prims = sim_utils.get_all_matching_child_prims( + template_prim_path, predicate=lambda prim: prim.HasAPI(UsdPhysics.ArticulationRootAPI) + ) + if len(articulation_prims) != 0: + if articulation_prims[0].GetAttribute("physxArticulation:articulationEnabled").Get(): + raise RuntimeError( + f"Found an articulation root when resolving '{rigid_object_cfg.prim_path}' in the rigid object" + f" collection. These are located at: '{articulation_prims}' under '{template_prim_path}'." + " Please disable the articulation root in the USD or from code by setting the parameter" + " 'ArticulationRootPropertiesCfg.articulation_enabled' to False in the spawn configuration." + ) + + # resolve root prim back into regex expression + root_prim_path = root_prims[0].GetPath().pathString + root_prim_path_expr = rigid_object_cfg.prim_path + root_prim_path[len(template_prim_path) :] + root_prim_path_exprs.append(root_prim_path_expr.replace(".*", "*")) + + self._object_names_list.append(name) + + # -- object view + self._root_physx_view = self._physics_sim_view.create_rigid_body_view(root_prim_path_exprs) + + # check if the rigid body was created + if self._root_physx_view._backend is None: + raise RuntimeError("Failed to create rigid body collection. Please check PhysX logs.") + + # log information about the rigid body + omni.log.info(f"Number of instances: {self.num_instances}") + omni.log.info(f"Number of distinct objects: {self.num_objects}") + omni.log.info(f"Object names: {self.object_names}") + + # container for data access + self._data = RigidObjectCollectionData(self.root_physx_view, self.num_objects, self.device) + + # create buffers + self._create_buffers() + # process configuration + self._process_cfg() + # update the rigid body data + self.update(0.0) + + def _create_buffers(self): + """Create buffers for storing data.""" + # constants + self._ALL_ENV_INDICES = torch.arange(self.num_instances, dtype=torch.long, device=self.device) + self._ALL_OBJ_INDICES = torch.arange(self.num_objects, dtype=torch.long, device=self.device) + + # external forces and torques + self.has_external_wrench = False + self._external_force_b = torch.zeros((self.num_instances, self.num_objects, 3), device=self.device) + self._external_torque_b = torch.zeros_like(self._external_force_b) + + # set information about rigid body into data + self._data.object_names = self.object_names + self._data.default_mass = self.reshape_view_to_data(self.root_physx_view.get_masses().clone()) + self._data.default_inertia = self.reshape_view_to_data(self.root_physx_view.get_inertias().clone()) + + def _process_cfg(self): + """Post processing of configuration parameters.""" + # default state + # -- object state + default_object_states = [] + for rigid_object_cfg in self.cfg.rigid_objects.values(): + default_object_state = ( + tuple(rigid_object_cfg.init_state.pos) + + tuple(rigid_object_cfg.init_state.rot) + + tuple(rigid_object_cfg.init_state.lin_vel) + + tuple(rigid_object_cfg.init_state.ang_vel) + ) + default_object_state = ( + torch.tensor(default_object_state, dtype=torch.float, device=self.device) + .repeat(self.num_instances, 1) + .unsqueeze(1) + ) + default_object_states.append(default_object_state) + # concatenate the default state for each object + default_object_states = torch.cat(default_object_states, dim=1) + self._data.default_object_state = default_object_states + +
[文档] def reshape_view_to_data(self, data: torch.Tensor) -> torch.Tensor: + """Reshapes and arranges the data coming from the :attr:`root_physx_view` to (num_instances, num_objects, data_size). + + Args: + data: The data coming from the :attr:`root_physx_view`. Shape is (num_instances*num_objects, data_size). + + Returns: + The reshaped data. Shape is (num_instances, num_objects, data_size). + """ + return torch.einsum("ijk -> jik", data.reshape(self.num_objects, self.num_instances, -1))
+ +
[文档] def reshape_data_to_view(self, data: torch.Tensor) -> torch.Tensor: + """Reshapes and arranges the data to the be consistent with data from the :attr:`root_physx_view`. + + Args: + data: The data to be reshaped. Shape is (num_instances, num_objects, data_size). + + Returns: + The reshaped data. Shape is (num_instances*num_objects, data_size). + """ + return torch.einsum("ijk -> jik", data).reshape(self.num_objects * self.num_instances, *data.shape[2:])
+ + def _env_obj_ids_to_view_ids( + self, env_ids: torch.Tensor, object_ids: Sequence[int] | slice | torch.Tensor + ) -> torch.Tensor: + """Converts environment and object indices to indices consistent with data from :attr:`root_physx_view`. + + Args: + env_ids: Environment indices. + object_ids: Object indices. + + Returns: + The view indices. + """ + # the order is env_0/object_0, env_0/object_1, env_0/object_..., env_1/object_0, env_1/object_1, ... + # return a flat tensor of indices + if isinstance(object_ids, slice): + object_ids = self._ALL_OBJ_INDICES + elif isinstance(object_ids, Sequence): + object_ids = torch.tensor(object_ids, device=self.device) + return (object_ids.unsqueeze(1) * self.num_instances + env_ids).flatten() + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._root_physx_view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_cfg.html b/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_cfg.html new file mode 100644 index 0000000000..92058eed77 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_cfg.html @@ -0,0 +1,587 @@ + + + + + + + + + + + omni.isaac.lab.assets.rigid_object_collection.rigid_object_collection_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.rigid_object_collection.rigid_object_collection_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.assets.rigid_object import RigidObjectCfg
+from omni.isaac.lab.utils import configclass
+
+from .rigid_object_collection import RigidObjectCollection
+
+
+
[文档]@configclass +class RigidObjectCollectionCfg: + """Configuration parameters for a rigid object collection.""" + + class_type: type = RigidObjectCollection + """The associated asset class. + + The class should inherit from :class:`omni.isaac.lab.assets.asset_base.AssetBase`. + """ + + rigid_objects: dict[str, RigidObjectCfg] = MISSING + """Dictionary of rigid object configurations to spawn. + + The keys are the names for the objects, which are used as unique identifiers throughout the code. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_data.html b/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_data.html new file mode 100644 index 0000000000..93bdfccd31 --- /dev/null +++ b/_modules/omni/isaac/lab/assets/rigid_object_collection/rigid_object_collection_data.html @@ -0,0 +1,810 @@ + + + + + + + + + + + omni.isaac.lab.assets.rigid_object_collection.rigid_object_collection_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.assets.rigid_object_collection.rigid_object_collection_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+import weakref
+
+import omni.physics.tensors.impl.api as physx
+
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.utils.buffers import TimestampedBuffer
+
+
+
[文档]class RigidObjectCollectionData: + """Data container for a rigid object collection. + + This class contains the data for a rigid object collection in the simulation. The data includes the state of + all the bodies in the collection. The data is stored in the simulation world frame unless otherwise specified. + The data is in the order ``(num_instances, num_objects, data_size)``, where data_size is the size of the data. + + For a rigid body, there are two frames of reference that are used: + + - Actor frame: The frame of reference of the rigid body prim. This typically corresponds to the Xform prim + with the rigid body schema. + - Center of mass frame: The frame of reference of the center of mass of the rigid body. + + Depending on the settings of the simulation, the actor frame and the center of mass frame may be the same. + This needs to be taken into account when interpreting the data. + + The data is lazily updated, meaning that the data is only updated when it is accessed. This is useful + when the data is expensive to compute or retrieve. The data is updated when the timestamp of the buffer + is older than the current simulation timestamp. The timestamp is updated whenever the data is updated. + """ + + def __init__(self, root_physx_view: physx.RigidBodyView, num_objects: int, device: str): + """Initializes the data. + + Args: + root_physx_view: The root rigid body view. + num_objects: The number of objects in the collection. + device: The device used for processing. + """ + # Set the parameters + self.device = device + self.num_objects = num_objects + # Set the root rigid body view + # note: this is stored as a weak reference to avoid circular references between the asset class + # and the data container. This is important to avoid memory leaks. + self._root_physx_view: physx.RigidBodyView = weakref.proxy(root_physx_view) + self.num_instances = self._root_physx_view.count // self.num_objects + + # Set initial time stamp + self._sim_timestamp = 0.0 + + # Obtain global physics sim view + physics_sim_view = physx.create_simulation_view("torch") + physics_sim_view.set_subspace_roots("/") + gravity = physics_sim_view.get_gravity() + # Convert to direction vector + gravity_dir = torch.tensor((gravity[0], gravity[1], gravity[2]), device=self.device) + gravity_dir = math_utils.normalize(gravity_dir.unsqueeze(0)).squeeze(0) + + # Initialize constants + self.GRAVITY_VEC_W = gravity_dir.repeat(self.num_instances, self.num_objects, 1) + self.FORWARD_VEC_B = torch.tensor((1.0, 0.0, 0.0), device=self.device).repeat( + self.num_instances, self.num_objects, 1 + ) + + # Initialize the lazy buffers. + self._object_state_w = TimestampedBuffer() + self._object_acc_w = TimestampedBuffer() + +
[文档] def update(self, dt: float): + """Updates the data for the rigid object collection. + + Args: + dt: The time step for the update. This must be a positive value. + """ + # update the simulation timestamp + self._sim_timestamp += dt
+ + ## + # Names. + ## + + object_names: list[str] = None + """Object names in the order parsed by the simulation view.""" + + ## + # Defaults. + ## + + default_object_state: torch.Tensor = None + """Default object state ``[pos, quat, lin_vel, ang_vel]`` in local environment frame. Shape is (num_instances, num_objects, 13). + + The position and quaternion are of each object's rigid body's actor frame. Meanwhile, the linear and angular velocities are + of the center of mass frame. + """ + + default_mass: torch.Tensor = None + """Default object mass read from the simulation. Shape is (num_instances, num_objects, 1).""" + + default_inertia: torch.Tensor = None + """Default object inertia tensor read from the simulation. Shape is (num_instances, num_objects, 9). + + The inertia is the inertia tensor relative to the center of mass frame. The values are stored in + the order :math:`[I_{xx}, I_{xy}, I_{xz}, I_{yx}, I_{yy}, I_{yz}, I_{zx}, I_{zy}, I_{zz}]`. + """ + + ## + # Properties. + ## + + @property + def object_state_w(self): + """Object state ``[pos, quat, lin_vel, ang_vel]`` in simulation world frame. Shape is (num_instances, num_objects, 13). + + The position and orientation are of the rigid body's actor frame. Meanwhile, the linear and angular + velocities are of the rigid body's center of mass frame. + """ + if self._object_state_w.timestamp < self._sim_timestamp: + # read data from simulation + pose = self._reshape_view_to_data(self._root_physx_view.get_transforms().clone()) + pose[..., 3:7] = math_utils.convert_quat(pose[..., 3:7], to="wxyz") + velocity = self._reshape_view_to_data(self._root_physx_view.get_velocities()) + # set the buffer data and timestamp + self._object_state_w.data = torch.cat((pose, velocity), dim=-1) + self._object_state_w.timestamp = self._sim_timestamp + return self._object_state_w.data + + @property + def object_acc_w(self): + """Acceleration of all objects. Shape is (num_instances, num_objects, 6). + + This quantity is the acceleration of the rigid bodies' center of mass frame. + """ + if self._object_acc_w.timestamp < self._sim_timestamp: + # note: we use finite differencing to compute acceleration + self._object_acc_w.data = self._reshape_view_to_data(self._root_physx_view.get_accelerations().clone()) + self._object_acc_w.timestamp = self._sim_timestamp + return self._object_acc_w.data + + @property + def projected_gravity_b(self): + """Projection of the gravity direction on base frame. Shape is (num_instances, num_objects, 3).""" + return math_utils.quat_rotate_inverse(self.object_quat_w, self.GRAVITY_VEC_W) + + @property + def heading_w(self): + """Yaw heading of the base frame (in radians). Shape is (num_instances, num_objects,). + + Note: + This quantity is computed by assuming that the forward-direction of the base + frame is along x-direction, i.e. :math:`(1, 0, 0)`. + """ + forward_w = math_utils.quat_apply(self.object_quat_w, self.FORWARD_VEC_B) + return torch.atan2(forward_w[..., 1], forward_w[..., 0]) + + ## + # Derived properties. + ## + + @property + def object_pos_w(self) -> torch.Tensor: + """Object position in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the position of the actor frame of the rigid bodies. + """ + return self.object_state_w[..., :3] + + @property + def object_quat_w(self) -> torch.Tensor: + """Object orientation (w, x, y, z) in simulation world frame. Shape is (num_instances, num_objects, 4). + + This quantity is the orientation of the actor frame of the rigid bodies. + """ + return self.object_state_w[..., 3:7] + + @property + def object_vel_w(self) -> torch.Tensor: + """Object velocity in simulation world frame. Shape is (num_instances, num_objects, 6). + + This quantity contains the linear and angular velocities of the rigid bodies' center of mass frame. + """ + return self.object_state_w[..., 7:13] + + @property + def object_lin_vel_w(self) -> torch.Tensor: + """Object linear velocity in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the linear velocity of the rigid bodies' center of mass frame. + """ + return self.object_state_w[..., 7:10] + + @property + def object_ang_vel_w(self) -> torch.Tensor: + """Object angular velocity in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the angular velocity of the rigid bodies' center of mass frame. + """ + return self.object_state_w[..., 10:13] + + @property + def object_lin_vel_b(self) -> torch.Tensor: + """Object linear velocity in base frame. Shape is (num_instances, num_objects, 3). + + This quantity is the linear velocity of the rigid bodies' center of mass frame with respect to the + rigid body's actor frame. + """ + return math_utils.quat_rotate_inverse(self.object_quat_w, self.object_lin_vel_w) + + @property + def object_ang_vel_b(self) -> torch.Tensor: + """Object angular velocity in base world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the angular velocity of the rigid bodies' center of mass frame with respect to the + rigid body's actor frame. + """ + return math_utils.quat_rotate_inverse(self.object_quat_w, self.object_ang_vel_w) + + @property + def object_lin_acc_w(self) -> torch.Tensor: + """Linear acceleration of all bodies in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the linear acceleration of the rigid bodies' center of mass frame. + """ + return self.object_acc_w[..., 0:3] + + @property + def object_ang_acc_w(self) -> torch.Tensor: + """Angular acceleration of all bodies in simulation world frame. Shape is (num_instances, num_objects, 3). + + This quantity is the angular acceleration of the rigid bodies' center of mass frame. + """ + return self.object_acc_w[..., 3:6] + + ## + # Helpers. + ## + + def _reshape_view_to_data(self, data: torch.Tensor) -> torch.Tensor: + """Reshapes and arranges the data from the physics view to (num_instances, num_objects, data_size). + + Args: + data: The data from the physics view. Shape is (num_instances*num_objects, data_size). + + Returns: + The reshaped data. Shape is (num_objects, num_instances, data_size). + """ + return torch.einsum("ijk -> jik", data.reshape(self.num_objects, self.num_instances, -1))
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/controllers/differential_ik.html b/_modules/omni/isaac/lab/controllers/differential_ik.html new file mode 100644 index 0000000000..8b11addb30 --- /dev/null +++ b/_modules/omni/isaac/lab/controllers/differential_ik.html @@ -0,0 +1,799 @@ + + + + + + + + + + + omni.isaac.lab.controllers.differential_ik — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.controllers.differential_ik 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from typing import TYPE_CHECKING
+
+from omni.isaac.lab.utils.math import apply_delta_pose, compute_pose_error
+
+if TYPE_CHECKING:
+    from .differential_ik_cfg import DifferentialIKControllerCfg
+
+
+
[文档]class DifferentialIKController: + r"""Differential inverse kinematics (IK) controller. + + This controller is based on the concept of differential inverse kinematics [1, 2] which is a method for computing + the change in joint positions that yields the desired change in pose. + + .. math:: + + \Delta \mathbf{q} &= \mathbf{J}^{\dagger} \Delta \mathbf{x} \\ + \mathbf{q}_{\text{desired}} &= \mathbf{q}_{\text{current}} + \Delta \mathbf{q} + + where :math:`\mathbf{J}^{\dagger}` is the pseudo-inverse of the Jacobian matrix :math:`\mathbf{J}`, + :math:`\Delta \mathbf{x}` is the desired change in pose, and :math:`\mathbf{q}_{\text{current}}` + is the current joint positions. + + To deal with singularity in Jacobian, the following methods are supported for computing inverse of the Jacobian: + + - "pinv": Moore-Penrose pseudo-inverse + - "svd": Adaptive singular-value decomposition (SVD) + - "trans": Transpose of matrix + - "dls": Damped version of Moore-Penrose pseudo-inverse (also called Levenberg-Marquardt) + + + .. caution:: + The controller does not assume anything about the frames of the current and desired end-effector pose, + or the joint-space velocities. It is up to the user to ensure that these quantities are given + in the correct format. + + Reference: + + 1. `Robot Dynamics Lecture Notes <https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2017/RD_HS2017script.pdf>`_ + by Marco Hutter (ETH Zurich) + 2. `Introduction to Inverse Kinematics <https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf>`_ + by Samuel R. Buss (University of California, San Diego) + + """ + +
[文档] def __init__(self, cfg: DifferentialIKControllerCfg, num_envs: int, device: str): + """Initialize the controller. + + Args: + cfg: The configuration for the controller. + num_envs: The number of environments. + device: The device to use for computations. + """ + # store inputs + self.cfg = cfg + self.num_envs = num_envs + self._device = device + # create buffers + self.ee_pos_des = torch.zeros(self.num_envs, 3, device=self._device) + self.ee_quat_des = torch.zeros(self.num_envs, 4, device=self._device) + # -- input command + self._command = torch.zeros(self.num_envs, self.action_dim, device=self._device)
+ + """ + Properties. + """ + + @property + def action_dim(self) -> int: + """Dimension of the controller's input command.""" + if self.cfg.command_type == "position": + return 3 # (x, y, z) + elif self.cfg.command_type == "pose" and self.cfg.use_relative_mode: + return 6 # (dx, dy, dz, droll, dpitch, dyaw) + else: + return 7 # (x, y, z, qw, qx, qy, qz) + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: torch.Tensor = None): + """Reset the internals. + + Args: + env_ids: The environment indices to reset. If None, then all environments are reset. + """ + pass
+ +
[文档] def set_command( + self, command: torch.Tensor, ee_pos: torch.Tensor | None = None, ee_quat: torch.Tensor | None = None + ): + """Set target end-effector pose command. + + Based on the configured command type and relative mode, the method computes the desired end-effector pose. + It is up to the user to ensure that the command is given in the correct frame. The method only + applies the relative mode if the command type is ``position_rel`` or ``pose_rel``. + + Args: + command: The input command in shape (N, 3) or (N, 6) or (N, 7). + ee_pos: The current end-effector position in shape (N, 3). + This is only needed if the command type is ``position_rel`` or ``pose_rel``. + ee_quat: The current end-effector orientation (w, x, y, z) in shape (N, 4). + This is only needed if the command type is ``position_*`` or ``pose_rel``. + + Raises: + ValueError: If the command type is ``position_*`` and :attr:`ee_quat` is None. + ValueError: If the command type is ``position_rel`` and :attr:`ee_pos` is None. + ValueError: If the command type is ``pose_rel`` and either :attr:`ee_pos` or :attr:`ee_quat` is None. + """ + # store command + self._command[:] = command + # compute the desired end-effector pose + if self.cfg.command_type == "position": + # we need end-effector orientation even though we are in position mode + # this is only needed for display purposes + if ee_quat is None: + raise ValueError("End-effector orientation can not be None for `position_*` command type!") + # compute targets + if self.cfg.use_relative_mode: + if ee_pos is None: + raise ValueError("End-effector position can not be None for `position_rel` command type!") + self.ee_pos_des[:] = ee_pos + self._command + self.ee_quat_des[:] = ee_quat + else: + self.ee_pos_des[:] = self._command + self.ee_quat_des[:] = ee_quat + else: + # compute targets + if self.cfg.use_relative_mode: + if ee_pos is None or ee_quat is None: + raise ValueError( + "Neither end-effector position nor orientation can be None for `pose_rel` command type!" + ) + self.ee_pos_des, self.ee_quat_des = apply_delta_pose(ee_pos, ee_quat, self._command) + else: + self.ee_pos_des = self._command[:, 0:3] + self.ee_quat_des = self._command[:, 3:7]
+ +
[文档] def compute( + self, ee_pos: torch.Tensor, ee_quat: torch.Tensor, jacobian: torch.Tensor, joint_pos: torch.Tensor + ) -> torch.Tensor: + """Computes the target joint positions that will yield the desired end effector pose. + + Args: + ee_pos: The current end-effector position in shape (N, 3). + ee_quat: The current end-effector orientation in shape (N, 4). + jacobian: The geometric jacobian matrix in shape (N, 6, num_joints). + joint_pos: The current joint positions in shape (N, num_joints). + + Returns: + The target joint positions commands in shape (N, num_joints). + """ + # compute the delta in joint-space + if "position" in self.cfg.command_type: + position_error = self.ee_pos_des - ee_pos + jacobian_pos = jacobian[:, 0:3] + delta_joint_pos = self._compute_delta_joint_pos(delta_pose=position_error, jacobian=jacobian_pos) + else: + position_error, axis_angle_error = compute_pose_error( + ee_pos, ee_quat, self.ee_pos_des, self.ee_quat_des, rot_error_type="axis_angle" + ) + pose_error = torch.cat((position_error, axis_angle_error), dim=1) + delta_joint_pos = self._compute_delta_joint_pos(delta_pose=pose_error, jacobian=jacobian) + # return the desired joint positions + return joint_pos + delta_joint_pos
+ + """ + Helper functions. + """ + + def _compute_delta_joint_pos(self, delta_pose: torch.Tensor, jacobian: torch.Tensor) -> torch.Tensor: + """Computes the change in joint position that yields the desired change in pose. + + The method uses the Jacobian mapping from joint-space velocities to end-effector velocities + to compute the delta-change in the joint-space that moves the robot closer to a desired + end-effector position. + + Args: + delta_pose: The desired delta pose in shape (N, 3) or (N, 6). + jacobian: The geometric jacobian matrix in shape (N, 3, num_joints) or (N, 6, num_joints). + + 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"] + # computation + jacobian_pinv = torch.linalg.pinv(jacobian) + delta_joint_pos = k_val * jacobian_pinv @ delta_pose.unsqueeze(-1) + delta_joint_pos = delta_joint_pos.squeeze(-1) + elif self.cfg.ik_method == "svd": # adaptive SVD + # parameters + k_val = self.cfg.ik_params["k_val"] + min_singular_value = self.cfg.ik_params["min_singular_value"] + # computation + # U: 6xd, S: dxd, V: d x num-joint + U, S, Vh = torch.linalg.svd(jacobian) + S_inv = 1.0 / S + S_inv = torch.where(S > min_singular_value, S_inv, torch.zeros_like(S_inv)) + jacobian_pinv = ( + torch.transpose(Vh, dim0=1, dim1=2)[:, :, :6] + @ torch.diag_embed(S_inv) + @ torch.transpose(U, dim0=1, dim1=2) + ) + delta_joint_pos = k_val * jacobian_pinv @ delta_pose.unsqueeze(-1) + delta_joint_pos = delta_joint_pos.squeeze(-1) + elif self.cfg.ik_method == "trans": # Jacobian transpose + # parameters + k_val = self.cfg.ik_params["k_val"] + # computation + jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) + delta_joint_pos = k_val * jacobian_T @ delta_pose.unsqueeze(-1) + delta_joint_pos = delta_joint_pos.squeeze(-1) + elif self.cfg.ik_method == "dls": # damped least squares + # parameters + lambda_val = self.cfg.ik_params["lambda_val"] + # computation + jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) + lambda_matrix = (lambda_val**2) * torch.eye(n=jacobian.shape[1], device=self._device) + delta_joint_pos = ( + jacobian_T @ torch.inverse(jacobian @ jacobian_T + lambda_matrix) @ delta_pose.unsqueeze(-1) + ) + delta_joint_pos = delta_joint_pos.squeeze(-1) + else: + raise ValueError(f"Unsupported inverse-kinematics method: {self.cfg.ik_method}") + + return delta_joint_pos
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/controllers/differential_ik_cfg.html b/_modules/omni/isaac/lab/controllers/differential_ik_cfg.html new file mode 100644 index 0000000000..1497bcaf2a --- /dev/null +++ b/_modules/omni/isaac/lab/controllers/differential_ik_cfg.html @@ -0,0 +1,629 @@ + + + + + + + + + + + omni.isaac.lab.controllers.differential_ik_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.controllers.differential_ik_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+from .differential_ik import DifferentialIKController
+
+
+
[文档]@configclass +class DifferentialIKControllerCfg: + """Configuration for differential inverse kinematics controller.""" + + class_type: type = DifferentialIKController + """The associated controller class.""" + + command_type: Literal["position", "pose"] = MISSING + """Type of task-space command to control the articulation's body. + + If "position", then the controller only controls the position of the articulation's body. + Otherwise, the controller controls the pose of the articulation's body. + """ + + use_relative_mode: bool = False + """Whether to use relative mode for the controller. Defaults to False. + + If True, then the controller treats the input command as a delta change in the position/pose. + Otherwise, the controller treats the input command as the absolute position/pose. + """ + + ik_method: Literal["pinv", "svd", "trans", "dls"] = MISSING + """Method for computing inverse of Jacobian.""" + + ik_params: dict[str, float] | None = None + """Parameters for the inverse-kinematics method. Defaults to None, in which case the default + parameters for the method are used. + + - Moore-Penrose pseudo-inverse ("pinv"): + - "k_val": Scaling of computed delta-joint positions (default: 1.0). + - Adaptive Singular Value Decomposition ("svd"): + - "k_val": Scaling of computed delta-joint positions (default: 1.0). + - "min_singular_value": Single values less than this are suppressed to zero (default: 1e-5). + - Jacobian transpose ("trans"): + - "k_val": Scaling of computed delta-joint positions (default: 1.0). + - Damped Moore-Penrose pseudo-inverse ("dls"): + - "lambda_val": Damping coefficient (default: 0.01). + """ + + def __post_init__(self): + # check valid input + if self.command_type not in ["position", "pose"]: + raise ValueError(f"Unsupported inverse-kinematics command: {self.command_type}.") + if self.ik_method not in ["pinv", "svd", "trans", "dls"]: + raise ValueError(f"Unsupported inverse-kinematics method: {self.ik_method}.") + # default parameters for different inverse kinematics approaches. + default_ik_params = { + "pinv": {"k_val": 1.0}, + "svd": {"k_val": 1.0, "min_singular_value": 1e-5}, + "trans": {"k_val": 1.0}, + "dls": {"lambda_val": 0.01}, + } + # update parameters for IK-method if not provided + ik_params = default_ik_params[self.ik_method].copy() + if self.ik_params is not None: + ik_params.update(self.ik_params) + self.ik_params = ik_params
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/device_base.html b/_modules/omni/isaac/lab/devices/device_base.html new file mode 100644 index 0000000000..4a4960791a --- /dev/null +++ b/_modules/omni/isaac/lab/devices/device_base.html @@ -0,0 +1,610 @@ + + + + + + + + + + + omni.isaac.lab.devices.device_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.device_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Base class for teleoperation interface."""
+
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from typing import Any
+
+
+
[文档]class DeviceBase(ABC): + """An interface class for teleoperation devices.""" + +
[文档] def __init__(self): + """Initialize the teleoperation interface.""" + pass
+ + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + return f"{self.__class__.__name__}" + + """ + Operations + """ + +
[文档] @abstractmethod + def reset(self): + """Reset the internals.""" + raise NotImplementedError
+ +
[文档] @abstractmethod + def add_callback(self, key: Any, func: Callable): + """Add additional functions to bind keyboard. + + Args: + key: The button to check against. + func: The function to call when key is pressed. The callback function should not + take any arguments. + """ + raise NotImplementedError
+ +
[文档] @abstractmethod + def advance(self) -> Any: + """Provides the joystick event state. + + Returns: + The processed output form the joystick. + """ + raise NotImplementedError
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/gamepad/se2_gamepad.html b/_modules/omni/isaac/lab/devices/gamepad/se2_gamepad.html new file mode 100644 index 0000000000..414fa25bcb --- /dev/null +++ b/_modules/omni/isaac/lab/devices/gamepad/se2_gamepad.html @@ -0,0 +1,758 @@ + + + + + + + + + + + omni.isaac.lab.devices.gamepad.se2_gamepad — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.gamepad.se2_gamepad 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Gamepad controller for SE(2) control."""
+
+import numpy as np
+import weakref
+from collections.abc import Callable
+
+import carb
+import omni
+
+from ..device_base import DeviceBase
+
+
+
[文档]class Se2Gamepad(DeviceBase): + r"""A gamepad controller for sending SE(2) commands as velocity commands. + + This class is designed to provide a gamepad controller for mobile base (such as quadrupeds). + It uses the Omniverse gamepad interface to listen to gamepad events and map them to robot's + task-space commands. + + The command comprises of the base linear and angular velocity: :math:`(v_x, v_y, \omega_z)`. + + Key bindings: + ====================== ========================= ======================== + Command Key (+ve axis) Key (-ve axis) + ====================== ========================= ======================== + Move along x-axis left stick up left stick down + Move along y-axis left stick right left stick left + Rotate along z-axis right stick right right stick left + ====================== ========================= ======================== + + .. seealso:: + + The official documentation for the gamepad interface: `Carb Gamepad Interface <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html>`__. + + """ + +
[文档] def __init__( + self, + v_x_sensitivity: float = 1.0, + v_y_sensitivity: float = 1.0, + omega_z_sensitivity: float = 1.0, + dead_zone: float = 0.01, + ): + """Initialize the gamepad layer. + + Args: + v_x_sensitivity: Magnitude of linear velocity along x-direction scaling. Defaults to 1.0. + v_y_sensitivity: Magnitude of linear velocity along y-direction scaling. Defaults to 1.0. + omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0. + dead_zone: Magnitude of dead zone for gamepad. An event value from the gamepad less than + this value will be ignored. Defaults to 0.01. + """ + # turn off simulator gamepad control + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl", False) + # store inputs + self.v_x_sensitivity = v_x_sensitivity + self.v_y_sensitivity = v_y_sensitivity + self.omega_z_sensitivity = omega_z_sensitivity + self.dead_zone = dead_zone + # acquire omniverse interfaces + self._appwindow = omni.appwindow.get_default_app_window() + self._input = carb.input.acquire_input_interface() + self._gamepad = self._appwindow.get_gamepad(0) + # note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called + self._gamepad_sub = self._input.subscribe_to_gamepad_events( + self._gamepad, + lambda event, *args, obj=weakref.proxy(self): obj._on_gamepad_event(event, *args), + ) + # bindings for gamepad to command + self._create_key_bindings() + # command buffers + # When using the gamepad, two values are provided for each axis. + # For example: when the left stick is moved down, there are two evens: `left_stick_down = 0.8` + # and `left_stick_up = 0.0`. If only the value of left_stick_up is used, the value will be 0.0, + # which is not the desired behavior. Therefore, we save both the values into the buffer and use + # the maximum value. + # (positive, negative), (x, y, yaw) + self._base_command_raw = np.zeros([2, 3]) + # dictionary for additional callbacks + self._additional_callbacks = dict()
+ + def __del__(self): + """Unsubscribe from gamepad events.""" + self._input.unsubscribe_from_gamepad_events(self._gamepad, self._gamepad_sub) + self._gamepad_sub = None + + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + msg = f"Gamepad Controller for SE(2): {self.__class__.__name__}\n" + msg += f"\tDevice name: {self._input.get_gamepad_name(self._gamepad)}\n" + msg += "\t----------------------------------------------\n" + msg += "\tMove in X-Y plane: left stick\n" + msg += "\tRotate in Z-axis: right stick\n" + return msg + + """ + Operations + """ + +
[文档] def reset(self): + # default flags + self._base_command_raw.fill(0.0)
+ +
[文档] def add_callback(self, key: carb.input.GamepadInput, func: Callable): + """Add additional functions to bind gamepad. + + A list of available gamepad keys are present in the + `carb documentation <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html>`__. + + Args: + key: The gamepad button to check against. + func: The function to call when key is pressed. The callback function should not + take any arguments. + """ + self._additional_callbacks[key] = func
+ +
[文档] def advance(self) -> np.ndarray: + """Provides the result from gamepad event state. + + Returns: + A 3D array containing the linear (x,y) and angular velocity (z). + """ + return self._resolve_command_buffer(self._base_command_raw)
+ + """ + Internal helpers. + """ + + def _on_gamepad_event(self, event: carb.input.GamepadEvent, *args, **kwargs): + """Subscriber callback to when kit is updated. + + Reference: + https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html + """ + + # check if the event is a button press + cur_val = event.value + if abs(cur_val) < self.dead_zone: + cur_val = 0 + # -- left and right stick + if event.input in self._INPUT_STICK_VALUE_MAPPING: + direction, axis, value = self._INPUT_STICK_VALUE_MAPPING[event.input] + # change the value only if the stick is moved (soft press) + self._base_command_raw[direction, axis] = value * cur_val + + # additional callbacks + if event.input in self._additional_callbacks: + self._additional_callbacks[event.input]() + + # since no error, we are fine :) + return True + + def _create_key_bindings(self): + """Creates default key binding.""" + self._INPUT_STICK_VALUE_MAPPING = { + # forward command + carb.input.GamepadInput.LEFT_STICK_UP: (0, 0, self.v_x_sensitivity), + # backward command + carb.input.GamepadInput.LEFT_STICK_DOWN: (1, 0, self.v_x_sensitivity), + # right command + carb.input.GamepadInput.LEFT_STICK_RIGHT: (0, 1, self.v_y_sensitivity), + # left command + carb.input.GamepadInput.LEFT_STICK_LEFT: (1, 1, self.v_y_sensitivity), + # yaw command (positive) + carb.input.GamepadInput.RIGHT_STICK_RIGHT: (0, 2, self.omega_z_sensitivity), + # yaw command (negative) + carb.input.GamepadInput.RIGHT_STICK_LEFT: (1, 2, self.omega_z_sensitivity), + } + + def _resolve_command_buffer(self, raw_command: np.ndarray) -> np.ndarray: + """Resolves the command buffer. + + Args: + raw_command: The raw command from the gamepad. Shape is (2, 3) + This is a 2D array since gamepad dpad/stick returns two values corresponding to + the positive and negative direction. The first index is the direction (0: positive, 1: negative) + and the second index is value (absolute) of the command. + + Returns: + Resolved command. Shape is (3,) + """ + # compare the positive and negative value decide the sign of the value + # if the positive value is larger, the sign is positive (i.e. False, 0) + # if the negative value is larger, the sign is positive (i.e. True, 1) + command_sign = raw_command[1, :] > raw_command[0, :] + # extract the command value + command = raw_command.max(axis=0) + # apply the sign + # if the sign is positive, the value is already positive. + # if the sign is negative, the value is negative after applying the sign. + command[command_sign] *= -1 + + return command
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/gamepad/se3_gamepad.html b/_modules/omni/isaac/lab/devices/gamepad/se3_gamepad.html new file mode 100644 index 0000000000..bc79806109 --- /dev/null +++ b/_modules/omni/isaac/lab/devices/gamepad/se3_gamepad.html @@ -0,0 +1,803 @@ + + + + + + + + + + + omni.isaac.lab.devices.gamepad.se3_gamepad — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.gamepad.se3_gamepad 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Gamepad controller for SE(3) control."""
+
+import numpy as np
+import weakref
+from collections.abc import Callable
+from scipy.spatial.transform import Rotation
+
+import carb
+import omni
+
+from ..device_base import DeviceBase
+
+
+
[文档]class Se3Gamepad(DeviceBase): + """A gamepad controller for sending SE(3) commands as delta poses and binary command (open/close). + + This class is designed to provide a gamepad controller for a robotic arm with a gripper. + It uses the gamepad interface to listen to gamepad events and map them to the robot's + task-space commands. + + The command comprises of two parts: + + * delta pose: a 6D vector of (x, y, z, roll, pitch, yaw) in meters and radians. + * gripper: a binary command to open or close the gripper. + + Stick and Button bindings: + ============================ ========================= ========================= + Description Stick/Button (+ve axis) Stick/Button (-ve axis) + ============================ ========================= ========================= + Toggle gripper(open/close) X Button X Button + Move along x-axis Left Stick Up Left Stick Down + Move along y-axis Left Stick Left Left Stick Right + Move along z-axis Right Stick Up Right Stick Down + Rotate along x-axis D-Pad Left D-Pad Right + Rotate along y-axis D-Pad Down D-Pad Up + Rotate along z-axis Right Stick Left Right Stick Right + ============================ ========================= ========================= + + .. seealso:: + + The official documentation for the gamepad interface: `Carb Gamepad Interface <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html>`__. + + """ + +
[文档] def __init__(self, pos_sensitivity: float = 1.0, rot_sensitivity: float = 1.6, dead_zone: float = 0.01): + """Initialize the gamepad layer. + + Args: + pos_sensitivity: Magnitude of input position command scaling. Defaults to 1.0. + rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 1.6. + dead_zone: Magnitude of dead zone for gamepad. An event value from the gamepad less than + this value will be ignored. Defaults to 0.01. + """ + # turn off simulator gamepad control + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl", False) + # store inputs + self.pos_sensitivity = pos_sensitivity + self.rot_sensitivity = rot_sensitivity + self.dead_zone = dead_zone + # acquire omniverse interfaces + self._appwindow = omni.appwindow.get_default_app_window() + self._input = carb.input.acquire_input_interface() + self._gamepad = self._appwindow.get_gamepad(0) + # note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called + self._gamepad_sub = self._input.subscribe_to_gamepad_events( + self._gamepad, + lambda event, *args, obj=weakref.proxy(self): obj._on_gamepad_event(event, *args), + ) + # bindings for gamepad to command + self._create_key_bindings() + # command buffers + self._close_gripper = False + # When using the gamepad, two values are provided for each axis. + # For example: when the left stick is moved down, there are two evens: `left_stick_down = 0.8` + # and `left_stick_up = 0.0`. If only the value of left_stick_up is used, the value will be 0.0, + # which is not the desired behavior. Therefore, we save both the values into the buffer and use + # the maximum value. + # (positive, negative), (x, y, z, roll, pitch, yaw) + self._delta_pose_raw = np.zeros([2, 6]) + # dictionary for additional callbacks + self._additional_callbacks = dict()
+ + def __del__(self): + """Unsubscribe from gamepad events.""" + self._input.unsubscribe_from_gamepad_events(self._gamepad, self._gamepad_sub) + self._gamepad_sub = None + + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + msg = f"Gamepad Controller for SE(3): {self.__class__.__name__}\n" + msg += f"\tDevice name: {self._input.get_gamepad_name(self._gamepad)}\n" + msg += "\t----------------------------------------------\n" + msg += "\tToggle gripper (open/close): X\n" + msg += "\tMove arm along x-axis: Left Stick Up/Down\n" + msg += "\tMove arm along y-axis: Left Stick Left/Right\n" + msg += "\tMove arm along z-axis: Right Stick Up/Down\n" + msg += "\tRotate arm along x-axis: D-Pad Right/Left\n" + msg += "\tRotate arm along y-axis: D-Pad Down/Up\n" + msg += "\tRotate arm along z-axis: Right Stick Left/Right\n" + return msg + + """ + Operations + """ + +
[文档] def reset(self): + # default flags + self._close_gripper = False + self._delta_pose_raw.fill(0.0)
+ +
[文档] def add_callback(self, key: carb.input.GamepadInput, func: Callable): + """Add additional functions to bind gamepad. + + A list of available gamepad keys are present in the + `carb documentation <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html>`__. + + Args: + key: The gamepad button to check against. + func: The function to call when key is pressed. The callback function should not + take any arguments. + """ + self._additional_callbacks[key] = func
+ +
[文档] def advance(self) -> tuple[np.ndarray, bool]: + """Provides the result from gamepad event state. + + Returns: + A tuple containing the delta pose command and gripper commands. + """ + # -- resolve position command + delta_pos = self._resolve_command_buffer(self._delta_pose_raw[:, :3]) + # -- resolve rotation command + delta_rot = self._resolve_command_buffer(self._delta_pose_raw[:, 3:]) + # -- convert to rotation vector + rot_vec = Rotation.from_euler("XYZ", delta_rot).as_rotvec() + # return the command and gripper state + return np.concatenate([delta_pos, rot_vec]), self._close_gripper
+ + """ + Internal helpers. + """ + + def _on_gamepad_event(self, event, *args, **kwargs): + """Subscriber callback to when kit is updated. + + Reference: + https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/gamepad.html + """ + # check if the event is a button press + cur_val = event.value + if abs(cur_val) < self.dead_zone: + cur_val = 0 + # -- button + if event.input == carb.input.GamepadInput.X: + # toggle gripper based on the button pressed + if cur_val > 0.5: + self._close_gripper = not self._close_gripper + # -- left and right stick + if event.input in self._INPUT_STICK_VALUE_MAPPING: + direction, axis, value = self._INPUT_STICK_VALUE_MAPPING[event.input] + # change the value only if the stick is moved (soft press) + self._delta_pose_raw[direction, axis] = value * cur_val + # -- dpad (4 arrow buttons on the console) + if event.input in self._INPUT_DPAD_VALUE_MAPPING: + direction, axis, value = self._INPUT_DPAD_VALUE_MAPPING[event.input] + # change the value only if button is pressed on the DPAD + if cur_val > 0.5: + self._delta_pose_raw[direction, axis] = value + self._delta_pose_raw[1 - direction, axis] = 0 + else: + self._delta_pose_raw[:, axis] = 0 + # additional callbacks + if event.input in self._additional_callbacks: + self._additional_callbacks[event.input]() + + # since no error, we are fine :) + return True + + def _create_key_bindings(self): + """Creates default key binding.""" + # map gamepad input to the element in self._delta_pose_raw + # the first index is the direction (0: positive, 1: negative) + # the second index is the axis (0: x, 1: y, 2: z, 3: roll, 4: pitch, 5: yaw) + # the third index is the sensitivity of the command + self._INPUT_STICK_VALUE_MAPPING = { + # forward command + carb.input.GamepadInput.LEFT_STICK_UP: (0, 0, self.pos_sensitivity), + # backward command + carb.input.GamepadInput.LEFT_STICK_DOWN: (1, 0, self.pos_sensitivity), + # right command + carb.input.GamepadInput.LEFT_STICK_RIGHT: (0, 1, self.pos_sensitivity), + # left command + carb.input.GamepadInput.LEFT_STICK_LEFT: (1, 1, self.pos_sensitivity), + # upward command + carb.input.GamepadInput.RIGHT_STICK_UP: (0, 2, self.pos_sensitivity), + # downward command + carb.input.GamepadInput.RIGHT_STICK_DOWN: (1, 2, self.pos_sensitivity), + # yaw command (positive) + carb.input.GamepadInput.RIGHT_STICK_RIGHT: (0, 5, self.rot_sensitivity), + # yaw command (negative) + carb.input.GamepadInput.RIGHT_STICK_LEFT: (1, 5, self.rot_sensitivity), + } + + self._INPUT_DPAD_VALUE_MAPPING = { + # pitch command (positive) + carb.input.GamepadInput.DPAD_UP: (1, 4, self.rot_sensitivity * 0.8), + # pitch command (negative) + carb.input.GamepadInput.DPAD_DOWN: (0, 4, self.rot_sensitivity * 0.8), + # roll command (positive) + carb.input.GamepadInput.DPAD_RIGHT: (1, 3, self.rot_sensitivity * 0.8), + # roll command (negative) + carb.input.GamepadInput.DPAD_LEFT: (0, 3, self.rot_sensitivity * 0.8), + } + + def _resolve_command_buffer(self, raw_command: np.ndarray) -> np.ndarray: + """Resolves the command buffer. + + Args: + raw_command: The raw command from the gamepad. Shape is (2, 3) + This is a 2D array since gamepad dpad/stick returns two values corresponding to + the positive and negative direction. The first index is the direction (0: positive, 1: negative) + and the second index is value (absolute) of the command. + + Returns: + Resolved command. Shape is (3,) + """ + # compare the positive and negative value decide the sign of the value + # if the positive value is larger, the sign is positive (i.e. False, 0) + # if the negative value is larger, the sign is positive (i.e. True, 1) + delta_command_sign = raw_command[1, :] > raw_command[0, :] + # extract the command value + delta_command = raw_command.max(axis=0) + # apply the sign + # if the sign is positive, the value is already positive. + # if the sign is negative, the value is negative after applying the sign. + delta_command[delta_command_sign] *= -1 + + return delta_command
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/keyboard/se2_keyboard.html b/_modules/omni/isaac/lab/devices/keyboard/se2_keyboard.html new file mode 100644 index 0000000000..6ea82e87f4 --- /dev/null +++ b/_modules/omni/isaac/lab/devices/keyboard/se2_keyboard.html @@ -0,0 +1,726 @@ + + + + + + + + + + + omni.isaac.lab.devices.keyboard.se2_keyboard — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.keyboard.se2_keyboard 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Keyboard controller for SE(2) control."""
+
+import numpy as np
+import weakref
+from collections.abc import Callable
+
+import carb
+import omni
+
+from ..device_base import DeviceBase
+
+
+
[文档]class Se2Keyboard(DeviceBase): + r"""A keyboard controller for sending SE(2) commands as velocity commands. + + This class is designed to provide a keyboard controller for mobile base (such as quadrupeds). + It uses the Omniverse keyboard interface to listen to keyboard events and map them to robot's + task-space commands. + + The command comprises of the base linear and angular velocity: :math:`(v_x, v_y, \omega_z)`. + + Key bindings: + ====================== ========================= ======================== + Command Key (+ve axis) Key (-ve axis) + ====================== ========================= ======================== + Move along x-axis Numpad 8 / Arrow Up Numpad 2 / Arrow Down + Move along y-axis Numpad 4 / Arrow Right Numpad 6 / Arrow Left + Rotate along z-axis Numpad 7 / Z Numpad 9 / X + ====================== ========================= ======================== + + .. seealso:: + + The official documentation for the keyboard interface: `Carb Keyboard Interface <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/keyboard.html>`__. + + """ + +
[文档] def __init__(self, v_x_sensitivity: float = 0.8, v_y_sensitivity: float = 0.4, omega_z_sensitivity: float = 1.0): + """Initialize the keyboard layer. + + Args: + v_x_sensitivity: Magnitude of linear velocity along x-direction scaling. Defaults to 0.8. + v_y_sensitivity: Magnitude of linear velocity along y-direction scaling. Defaults to 0.4. + omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0. + """ + # store inputs + self.v_x_sensitivity = v_x_sensitivity + self.v_y_sensitivity = v_y_sensitivity + self.omega_z_sensitivity = omega_z_sensitivity + # acquire omniverse interfaces + self._appwindow = omni.appwindow.get_default_app_window() + self._input = carb.input.acquire_input_interface() + self._keyboard = self._appwindow.get_keyboard() + # note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called + self._keyboard_sub = self._input.subscribe_to_keyboard_events( + self._keyboard, + lambda event, *args, obj=weakref.proxy(self): obj._on_keyboard_event(event, *args), + ) + # bindings for keyboard to command + self._create_key_bindings() + # command buffers + self._base_command = np.zeros(3) + # dictionary for additional callbacks + self._additional_callbacks = dict()
+ + def __del__(self): + """Release the keyboard interface.""" + self._input.unsubscribe_from_keyboard_events(self._keyboard, self._keyboard_sub) + self._keyboard_sub = None + + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + msg = f"Keyboard Controller for SE(2): {self.__class__.__name__}\n" + msg += f"\tKeyboard name: {self._input.get_keyboard_name(self._keyboard)}\n" + msg += "\t----------------------------------------------\n" + msg += "\tReset all commands: L\n" + msg += "\tMove forward (along x-axis): Numpad 8 / Arrow Up\n" + msg += "\tMove backward (along x-axis): Numpad 2 / Arrow Down\n" + msg += "\tMove right (along y-axis): Numpad 4 / Arrow Right\n" + msg += "\tMove left (along y-axis): Numpad 6 / Arrow Left\n" + msg += "\tYaw positively (along z-axis): Numpad 7 / Z\n" + msg += "\tYaw negatively (along z-axis): Numpad 9 / X" + return msg + + """ + Operations + """ + +
[文档] def reset(self): + # default flags + self._base_command.fill(0.0)
+ +
[文档] def add_callback(self, key: str, func: Callable): + """Add additional functions to bind keyboard. + + A list of available keys are present in the + `carb documentation <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/keyboard.html>`__. + + Args: + key: The keyboard button to check against. + func: The function to call when key is pressed. The callback function should not + take any arguments. + """ + self._additional_callbacks[key] = func
+ +
[文档] def advance(self) -> np.ndarray: + """Provides the result from keyboard event state. + + Returns: + 3D array containing the linear (x,y) and angular velocity (z). + """ + return self._base_command
+ + """ + Internal helpers. + """ + + def _on_keyboard_event(self, event, *args, **kwargs): + """Subscriber callback to when kit is updated. + + Reference: + https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/keyboard.html + """ + # apply the command when pressed + if event.type == carb.input.KeyboardEventType.KEY_PRESS: + if event.input.name == "L": + self.reset() + elif event.input.name in self._INPUT_KEY_MAPPING: + self._base_command += self._INPUT_KEY_MAPPING[event.input.name] + # remove the command when un-pressed + if event.type == carb.input.KeyboardEventType.KEY_RELEASE: + if event.input.name in self._INPUT_KEY_MAPPING: + self._base_command -= self._INPUT_KEY_MAPPING[event.input.name] + # additional callbacks + if event.type == carb.input.KeyboardEventType.KEY_PRESS: + if event.input.name in self._additional_callbacks: + self._additional_callbacks[event.input.name]() + + # since no error, we are fine :) + return True + + def _create_key_bindings(self): + """Creates default key binding.""" + self._INPUT_KEY_MAPPING = { + # forward command + "NUMPAD_8": np.asarray([1.0, 0.0, 0.0]) * self.v_x_sensitivity, + "UP": np.asarray([1.0, 0.0, 0.0]) * self.v_x_sensitivity, + # back command + "NUMPAD_2": np.asarray([-1.0, 0.0, 0.0]) * self.v_x_sensitivity, + "DOWN": np.asarray([-1.0, 0.0, 0.0]) * self.v_x_sensitivity, + # right command + "NUMPAD_4": np.asarray([0.0, 1.0, 0.0]) * self.v_y_sensitivity, + "LEFT": np.asarray([0.0, 1.0, 0.0]) * self.v_y_sensitivity, + # left command + "NUMPAD_6": np.asarray([0.0, -1.0, 0.0]) * self.v_y_sensitivity, + "RIGHT": np.asarray([0.0, -1.0, 0.0]) * self.v_y_sensitivity, + # yaw command (positive) + "NUMPAD_7": np.asarray([0.0, 0.0, 1.0]) * self.omega_z_sensitivity, + "Z": np.asarray([0.0, 0.0, 1.0]) * self.omega_z_sensitivity, + # yaw command (negative) + "NUMPAD_9": np.asarray([0.0, 0.0, -1.0]) * self.omega_z_sensitivity, + "X": np.asarray([0.0, 0.0, -1.0]) * self.omega_z_sensitivity, + }
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/keyboard/se3_keyboard.html b/_modules/omni/isaac/lab/devices/keyboard/se3_keyboard.html new file mode 100644 index 0000000000..6385180451 --- /dev/null +++ b/_modules/omni/isaac/lab/devices/keyboard/se3_keyboard.html @@ -0,0 +1,747 @@ + + + + + + + + + + + omni.isaac.lab.devices.keyboard.se3_keyboard — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.keyboard.se3_keyboard 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Keyboard controller for SE(3) control."""
+
+import numpy as np
+import weakref
+from collections.abc import Callable
+from scipy.spatial.transform import Rotation
+
+import carb
+import omni
+
+from ..device_base import DeviceBase
+
+
+
[文档]class Se3Keyboard(DeviceBase): + """A keyboard controller for sending SE(3) commands as delta poses and binary command (open/close). + + This class is designed to provide a keyboard controller for a robotic arm with a gripper. + It uses the Omniverse keyboard interface to listen to keyboard events and map them to robot's + task-space commands. + + The command comprises of two parts: + + * delta pose: a 6D vector of (x, y, z, roll, pitch, yaw) in meters and radians. + * gripper: a binary command to open or close the gripper. + + Key bindings: + ============================== ================= ================= + Description Key (+ve axis) Key (-ve axis) + ============================== ================= ================= + Toggle gripper (open/close) K + Move along x-axis W S + Move along y-axis A D + Move along z-axis Q E + Rotate along x-axis Z X + Rotate along y-axis T G + Rotate along z-axis C V + ============================== ================= ================= + + .. seealso:: + + The official documentation for the keyboard interface: `Carb Keyboard Interface <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/keyboard.html>`__. + + """ + +
[文档] def __init__(self, pos_sensitivity: float = 0.4, rot_sensitivity: float = 0.8): + """Initialize the keyboard layer. + + Args: + pos_sensitivity: Magnitude of input position command scaling. Defaults to 0.05. + rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 0.5. + """ + # store inputs + self.pos_sensitivity = pos_sensitivity + self.rot_sensitivity = rot_sensitivity + # acquire omniverse interfaces + self._appwindow = omni.appwindow.get_default_app_window() + self._input = carb.input.acquire_input_interface() + self._keyboard = self._appwindow.get_keyboard() + # note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called. + self._keyboard_sub = self._input.subscribe_to_keyboard_events( + self._keyboard, + lambda event, *args, obj=weakref.proxy(self): obj._on_keyboard_event(event, *args), + ) + # bindings for keyboard to command + self._create_key_bindings() + # command buffers + self._close_gripper = False + self._delta_pos = np.zeros(3) # (x, y, z) + self._delta_rot = np.zeros(3) # (roll, pitch, yaw) + # dictionary for additional callbacks + self._additional_callbacks = dict()
+ + def __del__(self): + """Release the keyboard interface.""" + self._input.unsubscribe_from_keyboard_events(self._keyboard, self._keyboard_sub) + self._keyboard_sub = None + + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + msg = f"Keyboard Controller for SE(3): {self.__class__.__name__}\n" + msg += f"\tKeyboard name: {self._input.get_keyboard_name(self._keyboard)}\n" + msg += "\t----------------------------------------------\n" + msg += "\tToggle gripper (open/close): K\n" + msg += "\tMove arm along x-axis: W/S\n" + msg += "\tMove arm along y-axis: A/D\n" + msg += "\tMove arm along z-axis: Q/E\n" + msg += "\tRotate arm along x-axis: Z/X\n" + msg += "\tRotate arm along y-axis: T/G\n" + msg += "\tRotate arm along z-axis: C/V" + return msg + + """ + Operations + """ + +
[文档] def reset(self): + # default flags + self._close_gripper = False + self._delta_pos = np.zeros(3) # (x, y, z) + self._delta_rot = np.zeros(3) # (roll, pitch, yaw)
+ +
[文档] def add_callback(self, key: str, func: Callable): + """Add additional functions to bind keyboard. + + A list of available keys are present in the + `carb documentation <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/keyboard.html>`__. + + Args: + key: The keyboard button to check against. + func: The function to call when key is pressed. The callback function should not + take any arguments. + """ + self._additional_callbacks[key] = func
+ +
[文档] def advance(self) -> tuple[np.ndarray, bool]: + """Provides the result from keyboard event state. + + Returns: + A tuple containing the delta pose command and gripper commands. + """ + # convert to rotation vector + rot_vec = Rotation.from_euler("XYZ", self._delta_rot).as_rotvec() + # return the command and gripper state + return np.concatenate([self._delta_pos, rot_vec]), self._close_gripper
+ + """ + Internal helpers. + """ + + def _on_keyboard_event(self, event, *args, **kwargs): + """Subscriber callback to when kit is updated. + + Reference: + https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/input-devices/keyboard.html + """ + # apply the command when pressed + if event.type == carb.input.KeyboardEventType.KEY_PRESS: + if event.input.name == "L": + self.reset() + if event.input.name == "K": + self._close_gripper = not self._close_gripper + elif event.input.name in ["W", "S", "A", "D", "Q", "E"]: + self._delta_pos += self._INPUT_KEY_MAPPING[event.input.name] + elif event.input.name in ["Z", "X", "T", "G", "C", "V"]: + self._delta_rot += self._INPUT_KEY_MAPPING[event.input.name] + # remove the command when un-pressed + if event.type == carb.input.KeyboardEventType.KEY_RELEASE: + if event.input.name in ["W", "S", "A", "D", "Q", "E"]: + self._delta_pos -= self._INPUT_KEY_MAPPING[event.input.name] + elif event.input.name in ["Z", "X", "T", "G", "C", "V"]: + self._delta_rot -= self._INPUT_KEY_MAPPING[event.input.name] + # additional callbacks + if event.type == carb.input.KeyboardEventType.KEY_PRESS: + if event.input.name in self._additional_callbacks: + self._additional_callbacks[event.input.name]() + + # since no error, we are fine :) + return True + + def _create_key_bindings(self): + """Creates default key binding.""" + self._INPUT_KEY_MAPPING = { + # toggle: gripper command + "K": True, + # x-axis (forward) + "W": np.asarray([1.0, 0.0, 0.0]) * self.pos_sensitivity, + "S": np.asarray([-1.0, 0.0, 0.0]) * self.pos_sensitivity, + # y-axis (left-right) + "A": np.asarray([0.0, 1.0, 0.0]) * self.pos_sensitivity, + "D": np.asarray([0.0, -1.0, 0.0]) * self.pos_sensitivity, + # z-axis (up-down) + "Q": np.asarray([0.0, 0.0, 1.0]) * self.pos_sensitivity, + "E": np.asarray([0.0, 0.0, -1.0]) * self.pos_sensitivity, + # roll (around x-axis) + "Z": np.asarray([1.0, 0.0, 0.0]) * self.rot_sensitivity, + "X": np.asarray([-1.0, 0.0, 0.0]) * self.rot_sensitivity, + # pitch (around y-axis) + "T": np.asarray([0.0, 1.0, 0.0]) * self.rot_sensitivity, + "G": np.asarray([0.0, -1.0, 0.0]) * self.rot_sensitivity, + # yaw (around z-axis) + "C": np.asarray([0.0, 0.0, 1.0]) * self.rot_sensitivity, + "V": np.asarray([0.0, 0.0, -1.0]) * self.rot_sensitivity, + }
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/spacemouse/se2_spacemouse.html b/_modules/omni/isaac/lab/devices/spacemouse/se2_spacemouse.html new file mode 100644 index 0000000000..5ee0b85265 --- /dev/null +++ b/_modules/omni/isaac/lab/devices/spacemouse/se2_spacemouse.html @@ -0,0 +1,713 @@ + + + + + + + + + + + omni.isaac.lab.devices.spacemouse.se2_spacemouse — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.spacemouse.se2_spacemouse 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Spacemouse controller for SE(2) control."""
+
+import hid
+import numpy as np
+import threading
+import time
+from collections.abc import Callable
+
+from ..device_base import DeviceBase
+from .utils import convert_buffer
+
+
+
[文档]class Se2SpaceMouse(DeviceBase): + r"""A space-mouse controller for sending SE(2) commands as delta poses. + + This class implements a space-mouse controller to provide commands to mobile base. + It uses the `HID-API`_ which interfaces with USD and Bluetooth HID-class devices across multiple platforms. + + The command comprises of the base linear and angular velocity: :math:`(v_x, v_y, \omega_z)`. + + Note: + The interface finds and uses the first supported device connected to the computer. + + Currently tested for following devices: + + - SpaceMouse Compact: https://3dconnexion.com/de/product/spacemouse-compact/ + + .. _HID-API: https://github.com/libusb/hidapi + + """ + +
[文档] def __init__(self, v_x_sensitivity: float = 0.8, v_y_sensitivity: float = 0.4, omega_z_sensitivity: float = 1.0): + """Initialize the spacemouse layer. + + Args: + v_x_sensitivity: Magnitude of linear velocity along x-direction scaling. Defaults to 0.8. + v_y_sensitivity: Magnitude of linear velocity along y-direction scaling. Defaults to 0.4. + omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0. + """ + # store inputs + self.v_x_sensitivity = v_x_sensitivity + self.v_y_sensitivity = v_y_sensitivity + self.omega_z_sensitivity = omega_z_sensitivity + # acquire device interface + self._device = hid.device() + self._find_device() + # command buffers + self._base_command = np.zeros(3) + # dictionary for additional callbacks + self._additional_callbacks = dict() + # run a thread for listening to device updates + self._thread = threading.Thread(target=self._run_device) + self._thread.daemon = True + self._thread.start()
+ + def __del__(self): + """Destructor for the class.""" + self._thread.join() + + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + msg = f"Spacemouse Controller for SE(2): {self.__class__.__name__}\n" + msg += f"\tManufacturer: {self._device.get_manufacturer_string()}\n" + msg += f"\tProduct: {self._device.get_product_string()}\n" + msg += "\t----------------------------------------------\n" + msg += "\tRight button: reset command\n" + msg += "\tMove mouse laterally: move base horizontally in x-y plane\n" + msg += "\tTwist mouse about z-axis: yaw base about a corresponding axis" + return msg + + """ + Operations + """ + +
[文档] def reset(self): + # default flags + self._base_command.fill(0.0)
+ +
[文档] def add_callback(self, key: str, func: Callable): + # check keys supported by callback + if key not in ["L", "R"]: + raise ValueError(f"Only left (L) and right (R) buttons supported. Provided: {key}.") + # TODO: Improve this to allow multiple buttons on same key. + self._additional_callbacks[key] = func
+ +
[文档] def advance(self) -> np.ndarray: + """Provides the result from spacemouse event state. + + Returns: + A 3D array containing the linear (x,y) and angular velocity (z). + """ + return self._base_command
+ + """ + Internal helpers. + """ + + def _find_device(self): + """Find the device connected to computer.""" + found = False + # implement a timeout for device search + for _ in range(5): + for device in hid.enumerate(): + if device["product_string"] == "SpaceMouse Compact": + # set found flag + found = True + vendor_id = device["vendor_id"] + product_id = device["product_id"] + # connect to the device + self._device.open(vendor_id, product_id) + # check if device found + if not found: + time.sleep(1.0) + else: + break + # no device found: return false + if not found: + raise OSError("No device found by SpaceMouse. Is the device connected?") + + def _run_device(self): + """Listener thread that keeps pulling new messages.""" + # keep running + while True: + # read the device data + data = self._device.read(13) + if data is not None: + # readings from 6-DoF sensor + if data[0] == 1: + # along y-axis + self._base_command[1] = self.v_y_sensitivity * convert_buffer(data[1], data[2]) + # along x-axis + self._base_command[0] = self.v_x_sensitivity * convert_buffer(data[3], data[4]) + elif data[0] == 2: + # along z-axis + self._base_command[2] = self.omega_z_sensitivity * convert_buffer(data[3], data[4]) + # readings from the side buttons + elif data[0] == 3: + # press left button + if data[1] == 1: + # additional callbacks + if "L" in self._additional_callbacks: + self._additional_callbacks["L"] + # right button is for reset + if data[1] == 2: + # reset layer + self.reset() + # additional callbacks + if "R" in self._additional_callbacks: + self._additional_callbacks["R"]
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/devices/spacemouse/se3_spacemouse.html b/_modules/omni/isaac/lab/devices/spacemouse/se3_spacemouse.html new file mode 100644 index 0000000000..42ef22100c --- /dev/null +++ b/_modules/omni/isaac/lab/devices/spacemouse/se3_spacemouse.html @@ -0,0 +1,730 @@ + + + + + + + + + + + omni.isaac.lab.devices.spacemouse.se3_spacemouse — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.devices.spacemouse.se3_spacemouse 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Spacemouse controller for SE(3) control."""
+
+import hid
+import numpy as np
+import threading
+import time
+from collections.abc import Callable
+from scipy.spatial.transform import Rotation
+
+from ..device_base import DeviceBase
+from .utils import convert_buffer
+
+
+
[文档]class Se3SpaceMouse(DeviceBase): + """A space-mouse controller for sending SE(3) commands as delta poses. + + This class implements a space-mouse controller to provide commands to a robotic arm with a gripper. + It uses the `HID-API`_ which interfaces with USD and Bluetooth HID-class devices across multiple platforms [1]. + + The command comprises of two parts: + + * delta pose: a 6D vector of (x, y, z, roll, pitch, yaw) in meters and radians. + * gripper: a binary command to open or close the gripper. + + Note: + The interface finds and uses the first supported device connected to the computer. + + Currently tested for following devices: + + - SpaceMouse Compact: https://3dconnexion.com/de/product/spacemouse-compact/ + + .. _HID-API: https://github.com/libusb/hidapi + + """ + +
[文档] def __init__(self, pos_sensitivity: float = 0.4, rot_sensitivity: float = 0.8): + """Initialize the space-mouse layer. + + Args: + pos_sensitivity: Magnitude of input position command scaling. Defaults to 0.4. + rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 0.8. + """ + # store inputs + self.pos_sensitivity = pos_sensitivity + self.rot_sensitivity = rot_sensitivity + # acquire device interface + self._device = hid.device() + self._find_device() + # read rotations + self._read_rotation = False + + # command buffers + self._close_gripper = False + self._delta_pos = np.zeros(3) # (x, y, z) + self._delta_rot = np.zeros(3) # (roll, pitch, yaw) + # dictionary for additional callbacks + self._additional_callbacks = dict() + # run a thread for listening to device updates + self._thread = threading.Thread(target=self._run_device) + self._thread.daemon = True + self._thread.start()
+ + def __del__(self): + """Destructor for the class.""" + self._thread.join() + + def __str__(self) -> str: + """Returns: A string containing the information of joystick.""" + msg = f"Spacemouse Controller for SE(3): {self.__class__.__name__}\n" + msg += f"\tManufacturer: {self._device.get_manufacturer_string()}\n" + msg += f"\tProduct: {self._device.get_product_string()}\n" + msg += "\t----------------------------------------------\n" + msg += "\tRight button: reset command\n" + msg += "\tLeft button: toggle gripper command (open/close)\n" + msg += "\tMove mouse laterally: move arm horizontally in x-y plane\n" + msg += "\tMove mouse vertically: move arm vertically\n" + msg += "\tTwist mouse about an axis: rotate arm about a corresponding axis" + return msg + + """ + Operations + """ + +
[文档] def reset(self): + # default flags + self._close_gripper = False + self._delta_pos = np.zeros(3) # (x, y, z) + self._delta_rot = np.zeros(3) # (roll, pitch, yaw)
+ +
[文档] def add_callback(self, key: str, func: Callable): + # check keys supported by callback + if key not in ["L", "R"]: + raise ValueError(f"Only left (L) and right (R) buttons supported. Provided: {key}.") + # TODO: Improve this to allow multiple buttons on same key. + self._additional_callbacks[key] = func
+ +
[文档] def advance(self) -> tuple[np.ndarray, bool]: + """Provides the result from spacemouse event state. + + Returns: + A tuple containing the delta pose command and gripper commands. + """ + rot_vec = Rotation.from_euler("XYZ", self._delta_rot).as_rotvec() + # if new command received, reset event flag to False until keyboard updated. + return np.concatenate([self._delta_pos, rot_vec]), self._close_gripper
+ + """ + Internal helpers. + """ + + def _find_device(self): + """Find the device connected to computer.""" + found = False + # implement a timeout for device search + for _ in range(5): + for device in hid.enumerate(): + if device["product_string"] == "SpaceMouse Compact": + # set found flag + found = True + vendor_id = device["vendor_id"] + product_id = device["product_id"] + # connect to the device + self._device.open(vendor_id, product_id) + # check if device found + if not found: + time.sleep(1.0) + else: + break + # no device found: return false + if not found: + raise OSError("No device found by SpaceMouse. Is the device connected?") + + def _run_device(self): + """Listener thread that keeps pulling new messages.""" + # keep running + while True: + # read the device data + data = self._device.read(7) + if data is not None: + # readings from 6-DoF sensor + if data[0] == 1: + self._delta_pos[1] = self.pos_sensitivity * convert_buffer(data[1], data[2]) + self._delta_pos[0] = self.pos_sensitivity * convert_buffer(data[3], data[4]) + self._delta_pos[2] = self.pos_sensitivity * convert_buffer(data[5], data[6]) * -1.0 + elif data[0] == 2 and not self._read_rotation: + self._delta_rot[1] = self.rot_sensitivity * convert_buffer(data[1], data[2]) + self._delta_rot[0] = self.rot_sensitivity * convert_buffer(data[3], data[4]) + self._delta_rot[2] = self.rot_sensitivity * convert_buffer(data[5], data[6]) + # readings from the side buttons + elif data[0] == 3: + # press left button + if data[1] == 1: + # close gripper + self._close_gripper = not self._close_gripper + # additional callbacks + if "L" in self._additional_callbacks: + self._additional_callbacks["L"] + # right button is for reset + if data[1] == 2: + # reset layer + self.reset() + # additional callbacks + if "R" in self._additional_callbacks: + self._additional_callbacks["R"] + if data[1] == 3: + self._read_rotation = not self._read_rotation
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/common.html b/_modules/omni/isaac/lab/envs/common.html new file mode 100644 index 0000000000..f28e7c95ed --- /dev/null +++ b/_modules/omni/isaac/lab/envs/common.html @@ -0,0 +1,698 @@ + + + + + + + + + + + omni.isaac.lab.envs.common — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.common 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import gymnasium as gym
+import torch
+from typing import Dict, Literal, TypeVar
+
+from omni.isaac.lab.utils import configclass
+
+##
+# Configuration.
+##
+
+
+
[文档]@configclass +class ViewerCfg: + """Configuration of the scene viewport camera.""" + + eye: tuple[float, float, float] = (7.5, 7.5, 7.5) + """Initial camera position (in m). Default is (7.5, 7.5, 7.5).""" + + lookat: tuple[float, float, float] = (0.0, 0.0, 0.0) + """Initial camera target position (in m). Default is (0.0, 0.0, 0.0).""" + + cam_prim_path: str = "/OmniverseKit_Persp" + """The camera prim path to record images from. Default is "/OmniverseKit_Persp", + which is the default camera in the viewport. + """ + + resolution: tuple[int, int] = (1280, 720) + """The resolution (width, height) of the camera specified using :attr:`cam_prim_path`. + Default is (1280, 720). + """ + + origin_type: Literal["world", "env", "asset_root"] = "world" + """The frame in which the camera position (eye) and target (lookat) are defined in. Default is "world". + + Available options are: + + * ``"world"``: The origin of the world. + * ``"env"``: The origin of the environment defined by :attr:`env_index`. + * ``"asset_root"``: The center of the asset defined by :attr:`asset_name` in environment :attr:`env_index`. + """ + + env_index: int = 0 + """The environment index for frame origin. Default is 0. + + This quantity is only effective if :attr:`origin` is set to "env" or "asset_root". + """ + + asset_name: str | None = None + """The asset name in the interactive scene for the frame origin. Default is None. + + This quantity is only effective if :attr:`origin` is set to "asset_root". + """
+ + +## +# Types. +## + +SpaceType = TypeVar("SpaceType", gym.spaces.Space, int, set, tuple, list, dict) +"""A sentinel object to indicate a valid space type to specify states, observations and actions.""" + +VecEnvObs = Dict[str, torch.Tensor | Dict[str, torch.Tensor]] +"""Observation returned by the environment. + +The observations are stored in a dictionary. The keys are the group to which the observations belong. +This is useful for various setups such as reinforcement learning with asymmetric actor-critic or +multi-agent learning. For non-learning paradigms, this may include observations for different components +of a system. + +Within each group, the observations can be stored either as a dictionary with keys as the names of each +observation term in the group, or a single tensor obtained from concatenating all the observation terms. +For example, for asymmetric actor-critic, the observation for the actor and the critic can be accessed +using the keys ``"policy"`` and ``"critic"`` respectively. + +Note: + By default, most learning frameworks deal with default and privileged observations in different ways. + This handling must be taken care of by the wrapper around the :class:`ManagerBasedRLEnv` instance. + + For included frameworks (RSL-RL, RL-Games, skrl), the observations must have the key "policy". In case, + the key "critic" is also present, then the critic observations are taken from the "critic" group. + Otherwise, they are the same as the "policy" group. + +""" + +VecEnvStepReturn = tuple[VecEnvObs, torch.Tensor, torch.Tensor, torch.Tensor, dict] +"""The environment signals processed at the end of each step. + +The tuple contains batched information for each sub-environment. The information is stored in the following order: + +1. **Observations**: The observations from the environment. +2. **Rewards**: The rewards from the environment. +3. **Terminated Dones**: Whether the environment reached a terminal state, such as task success or robot falling etc. +4. **Timeout Dones**: Whether the environment reached a timeout state, such as end of max episode length. +5. **Extras**: A dictionary containing additional information from the environment. +""" + +AgentID = TypeVar("AgentID") +"""Unique identifier for an agent within a multi-agent environment. + +The identifier has to be an immutable object, typically a string (e.g.: ``"agent_0"``). +""" + +ObsType = TypeVar("ObsType", torch.Tensor, Dict[str, torch.Tensor]) +"""A sentinel object to indicate the data type of the observation. +""" + +ActionType = TypeVar("ActionType", torch.Tensor, Dict[str, torch.Tensor]) +"""A sentinel object to indicate the data type of the action. +""" + +StateType = TypeVar("StateType", torch.Tensor, dict) +"""A sentinel object to indicate the data type of the state. +""" + +EnvStepReturn = tuple[ + Dict[AgentID, ObsType], + Dict[AgentID, torch.Tensor], + Dict[AgentID, torch.Tensor], + Dict[AgentID, torch.Tensor], + Dict[AgentID, dict], +] +"""The environment signals processed at the end of each step. + +The tuple contains batched information for each sub-environment (keyed by the agent ID). +The information is stored in the following order: + +1. **Observations**: The observations from the environment. +2. **Rewards**: The rewards from the environment. +3. **Terminated Dones**: Whether the environment reached a terminal state, such as task success or robot falling etc. +4. **Timeout Dones**: Whether the environment reached a timeout state, such as end of max episode length. +5. **Extras**: A dictionary containing additional information from the environment. +""" +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/direct_marl_env.html b/_modules/omni/isaac/lab/envs/direct_marl_env.html new file mode 100644 index 0000000000..d2a9e4af01 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/direct_marl_env.html @@ -0,0 +1,1284 @@ + + + + + + + + + + + omni.isaac.lab.envs.direct_marl_env — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.direct_marl_env 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import builtins
+import gymnasium as gym
+import inspect
+import math
+import numpy as np
+import torch
+import weakref
+from abc import abstractmethod
+from collections.abc import Sequence
+from dataclasses import MISSING
+from typing import Any, ClassVar
+
+import omni.isaac.core.utils.torch as torch_utils
+import omni.kit.app
+import omni.log
+from omni.isaac.version import get_version
+
+from omni.isaac.lab.managers import EventManager
+from omni.isaac.lab.scene import InteractiveScene
+from omni.isaac.lab.sim import SimulationContext
+from omni.isaac.lab.utils.noise import NoiseModel
+from omni.isaac.lab.utils.timer import Timer
+
+from .common import ActionType, AgentID, EnvStepReturn, ObsType, StateType
+from .direct_marl_env_cfg import DirectMARLEnvCfg
+from .ui import ViewportCameraController
+from .utils.spaces import sample_space, spec_to_gym_space
+
+
+
[文档]class DirectMARLEnv: + """The superclass for the direct workflow to design multi-agent environments. + + This class implements the core functionality for multi-agent reinforcement learning (MARL) + environments. It is designed to be used with any RL library. The class is designed + to be used with vectorized environments, i.e., the environment is expected to be run + in parallel with multiple sub-environments. + + The design of this class is based on the PettingZoo Parallel API. + While the environment itself is implemented as a vectorized environment, we do not + inherit from :class:`pettingzoo.ParallelEnv` or :class:`gym.vector.VectorEnv`. This is mainly + because the class adds various attributes and methods that are inconsistent with them. + + Note: + For vectorized environments, it is recommended to **only** call the :meth:`reset` + method once before the first call to :meth:`step`, i.e. after the environment is created. + After that, the :meth:`step` function handles the reset of terminated sub-environments. + This is because the simulator does not support resetting individual sub-environments + in a vectorized environment. + + """ + + metadata: ClassVar[dict[str, Any]] = { + "render_modes": [None, "human", "rgb_array"], + "isaac_sim_version": get_version(), + } + """Metadata for the environment.""" + +
[文档] def __init__(self, cfg: DirectMARLEnvCfg, render_mode: str | None = None, **kwargs): + """Initialize the environment. + + Args: + cfg: The configuration object for the environment. + render_mode: The render mode for the environment. Defaults to None, which + is similar to ``"human"``. + + Raises: + RuntimeError: If a simulation context already exists. The environment must always create one + since it configures the simulation context and controls the simulation. + """ + # check that the config is valid + cfg.validate() + # store inputs to class + self.cfg = cfg + # store the render mode + self.render_mode = render_mode + # initialize internal variables + self._is_closed = False + + # set the seed for the environment + if self.cfg.seed is not None: + self.cfg.seed = self.seed(self.cfg.seed) + else: + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + + # create a simulation context to control the simulator + if SimulationContext.instance() is None: + self.sim: SimulationContext = SimulationContext(self.cfg.sim) + else: + raise RuntimeError("Simulation context already exists. Cannot create a new one.") + + # print useful information + print("[INFO]: Base environment:") + print(f"\tEnvironment device : {self.device}") + print(f"\tEnvironment seed : {self.cfg.seed}") + print(f"\tPhysics step-size : {self.physics_dt}") + print(f"\tRendering step-size : {self.physics_dt * self.cfg.sim.render_interval}") + print(f"\tEnvironment step-size : {self.step_dt}") + + if self.cfg.sim.render_interval < self.cfg.decimation: + msg = ( + f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step." + "If this is not intended, set the render interval to be equal to the decimation." + ) + omni.log.warn(msg) + + # generate scene + with Timer("[INFO]: Time taken for scene creation", "scene_creation"): + self.scene = InteractiveScene(self.cfg.scene) + self._setup_scene() + print("[INFO]: Scene manager: ", self.scene) + + # set up camera viewport controller + # viewport is not available in other rendering modes so the function will throw a warning + # FIXME: This needs to be fixed in the future when we unify the UI functionalities even for + # non-rendering modes. + if self.sim.render_mode >= self.sim.RenderMode.PARTIAL_RENDERING: + self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer) + else: + self.viewport_camera_controller = None + + # play the simulator to activate physics handles + # note: this activates the physics simulation view that exposes TensorAPIs + # note: when started in extension mode, first call sim.reset_async() and then initialize the managers + if builtins.ISAAC_LAUNCHED_FROM_TERMINAL is False: + print("[INFO]: Starting the simulation. This may take a few seconds. Please wait...") + with Timer("[INFO]: Time taken for simulation start", "simulation_start"): + self.sim.reset() + + # -- event manager used for randomization + if self.cfg.events: + self.event_manager = EventManager(self.cfg.events, self) + print("[INFO] Event Manager: ", self.event_manager) + + # make sure torch is running on the correct device + if "cuda" in self.device: + torch.cuda.set_device(self.device) + + # check if debug visualization is has been implemented by the environment + source_code = inspect.getsource(self._set_debug_vis_impl) + self.has_debug_vis_implementation = "NotImplementedError" not in source_code + self._debug_vis_handle = None + + # extend UI elements + # we need to do this here after all the managers are initialized + # this is because they dictate the sensors and commands right now + if self.sim.has_gui() and self.cfg.ui_window_class_type is not None: + self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") + else: + # if no window, then we don't need to store the window + self._window = None + + # allocate dictionary to store metrics + self.extras = {agent: {} for agent in self.cfg.possible_agents} + + # initialize data and constants + # -- counter for simulation steps + self._sim_step_counter = 0 + # -- counter for curriculum + self.common_step_counter = 0 + # -- init buffers + self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) + self.reset_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.sim.device) + + # setup the observation, state and action spaces + self._configure_env_spaces() + + # setup noise cfg for adding action and observation noise + if self.cfg.action_noise_model: + self._action_noise_model: dict[AgentID, NoiseModel] = { + agent: noise_model.class_type(self.num_envs, noise_model, self.device) + for agent, noise_model in self.cfg.action_noise_model.items() + if noise_model is not None + } + if self.cfg.observation_noise_model: + self._observation_noise_model: dict[AgentID, NoiseModel] = { + agent: noise_model.class_type(self.num_envs, noise_model, self.device) + for agent, noise_model in self.cfg.observation_noise_model.items() + if noise_model is not None + } + + # perform events at the start of the simulation + if self.cfg.events: + if "startup" in self.event_manager.available_modes: + self.event_manager.apply(mode="startup") + + # print the environment information + print("[INFO]: Completed setting up the environment...")
+ + def __del__(self): + """Cleanup for the environment.""" + self.close() + + """ + Properties. + """ + + @property + def num_envs(self) -> int: + """The number of instances of the environment that are running.""" + return self.scene.num_envs + + @property + def num_agents(self) -> int: + """Number of current agents. + + The number of current agents may change as the environment progresses (e.g.: agents can be added or removed). + """ + return len(self.agents) + + @property + def max_num_agents(self) -> int: + """Number of all possible agents the environment can generate. + + This value remains constant as the environment progresses. + """ + return len(self.possible_agents) + + @property + def unwrapped(self) -> DirectMARLEnv: + """Get the unwrapped environment underneath all the layers of wrappers.""" + return self + + @property + def physics_dt(self) -> float: + """The physics time-step (in s). + + This is the lowest time-decimation at which the simulation is happening. + """ + return self.cfg.sim.dt + + @property + def step_dt(self) -> float: + """The environment stepping time-step (in s). + + This is the time-step at which the environment steps forward. + """ + return self.cfg.sim.dt * self.cfg.decimation + + @property + def device(self): + """The device on which the environment is running.""" + return self.sim.device + + @property + def max_episode_length_s(self) -> float: + """Maximum episode length in seconds.""" + return self.cfg.episode_length_s + + @property + def max_episode_length(self): + """The maximum episode length in steps adjusted from s.""" + return math.ceil(self.max_episode_length_s / (self.cfg.sim.dt * self.cfg.decimation)) + + """ + Space methods + """ + +
[文档] def observation_space(self, agent: AgentID) -> gym.Space: + """Get the observation space for the specified agent. + + Returns: + The agent's observation space. + """ + return self.observation_spaces[agent]
+ +
[文档] def action_space(self, agent: AgentID) -> gym.Space: + """Get the action space for the specified agent. + + Returns: + The agent's action space. + """ + return self.action_spaces[agent]
+ + """ + Operations. + """ + +
[文档] def reset( + self, seed: int | None = None, options: dict[str, Any] | None = None + ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]: + """Resets all the environments and returns observations. + + Args: + seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + options: Additional information to specify how the environment is reset. Defaults to None. + + Note: + This argument is used for compatibility with Gymnasium environment definition. + + Returns: + A tuple containing the observations and extras (keyed by the agent ID). + """ + # set the seed + if seed is not None: + self.seed(seed) + + # reset state of scene + indices = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + self._reset_idx(indices) + + # update observations and the list of current agents (sorted as in possible_agents) + self.obs_dict = self._get_observations() + self.agents = [agent for agent in self.possible_agents if agent in self.obs_dict] + + # return observations + return self.obs_dict, self.extras
+ +
[文档] def step(self, actions: dict[AgentID, ActionType]) -> EnvStepReturn: + """Execute one time-step of the environment's dynamics. + + 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:`DirectMARLEnvCfg.decimation` (number of simulation steps per environment step) + and the :attr:`DirectMARLEnvCfg.sim.physics_dt` (physics time-step). Based on these parameters, the environment + time-step is computed as the product of the two. + + This function performs the following steps: + + 1. Pre-process the actions before stepping through the physics. + 2. Apply the actions to the simulator and step through the physics in a decimated manner. + 3. Compute the reward and done signals. + 4. Reset environments that have terminated or reached the maximum episode length. + 5. Apply interval events if they are enabled. + 6. Compute observations. + + Args: + actions: The actions to apply on the environment (keyed by the agent ID). + Shape of individual tensors is (num_envs, action_dim). + + Returns: + A tuple containing the observations, rewards, resets (terminated and truncated) and extras (keyed by the agent ID). + """ + actions = {agent: action.to(self.device) for agent, action in actions.items()} + + # add action noise + if self.cfg.action_noise_model: + for agent, action in actions.items(): + if agent in self._action_noise_model: + actions[agent] = self._action_noise_model[agent].apply(action) + # process actions + self._pre_physics_step(actions) + + # check if we need to do rendering within the physics loop + # note: checked here once to avoid multiple checks within the loop + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() + + # perform physics stepping + for _ in range(self.cfg.decimation): + self._sim_step_counter += 1 + # set actions into buffers + self._apply_action() + # set actions into simulator + self.scene.write_data_to_sim() + # simulate + self.sim.step(render=False) + # render between steps only if the GUI or an RTX sensor needs it + # note: we assume the render interval to be the shortest accepted rendering interval. + # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: + self.sim.render() + # update buffers at sim dt + self.scene.update(dt=self.physics_dt) + + # post-step: + # -- update env counters (used for curriculum generation) + self.episode_length_buf += 1 # step in current episode (per env) + self.common_step_counter += 1 # total step (common for all envs) + + self.terminated_dict, self.time_out_dict = self._get_dones() + self.reset_buf[:] = math.prod(self.terminated_dict.values()) | math.prod(self.time_out_dict.values()) + self.reward_dict = self._get_rewards() + + # -- reset envs that terminated/timed-out and log the episode information + reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) + if len(reset_env_ids) > 0: + self._reset_idx(reset_env_ids) + + # post-step: step interval event + if self.cfg.events: + if "interval" in self.event_manager.available_modes: + self.event_manager.apply(mode="interval", dt=self.step_dt) + + # update observations and the list of current agents (sorted as in possible_agents) + self.obs_dict = self._get_observations() + self.agents = [agent for agent in self.possible_agents if agent in self.obs_dict] + + # add observation noise + # note: we apply no noise to the state space (since it is used for centralized training or critic networks) + if self.cfg.observation_noise_model: + for agent, obs in self.obs_dict.items(): + if agent in self._observation_noise_model: + self.obs_dict[agent] = self._observation_noise_model[agent].apply(obs) + + # return observations, rewards, resets and extras + return self.obs_dict, self.reward_dict, self.terminated_dict, self.time_out_dict, self.extras
+ +
[文档] def state(self) -> StateType | None: + """Returns the state for the environment. + + The state-space is used for centralized training or asymmetric actor-critic architectures. It is configured + using the :attr:`DirectMARLEnvCfg.state_space` parameter. + + Returns: + The states for the environment, or None if :attr:`DirectMARLEnvCfg.state_space` parameter is zero. + """ + if not self.cfg.state_space: + return None + # concatenate and return the observations as state + # FIXME: This implementation assumes the spaces are fundamental ones. Fix it to support composite spaces + if isinstance(self.cfg.state_space, int) and self.cfg.state_space < 0: + self.state_buf = torch.cat( + [self.obs_dict[agent].reshape(self.num_envs, -1) for agent in self.cfg.possible_agents], dim=-1 + ) + # compute and return custom environment state + else: + self.state_buf = self._get_states() + return self.state_buf
+ +
[文档] @staticmethod + def seed(seed: int = -1) -> int: + """Set the seed for the environment. + + Args: + seed: The seed for random generator. Defaults to -1. + + Returns: + The seed used for random generator. + """ + # set seed for replicator + try: + import omni.replicator.core as rep + + rep.set_global_seed(seed) + except ModuleNotFoundError: + pass + # set seed for torch and other libraries + return torch_utils.set_seed(seed)
+ +
[文档] def render(self, recompute: bool = False) -> np.ndarray | None: + """Run rendering without stepping through the physics. + + By convention, if mode is: + + - **human**: Render to the current display and return nothing. Usually for human consumption. + - **rgb_array**: Return an numpy.ndarray with shape (x, y, 3), representing RGB values for an + x-by-y pixel image, suitable for turning into a video. + + Args: + recompute: Whether to force a render even if the simulator has already rendered the scene. + Defaults to False. + + Returns: + The rendered image as a numpy array if mode is "rgb_array". Otherwise, returns None. + + Raises: + RuntimeError: If mode is set to "rgb_data" and simulation render mode does not support it. + In this case, the simulation render mode must be set to ``RenderMode.PARTIAL_RENDERING`` + or ``RenderMode.FULL_RENDERING``. + NotImplementedError: If an unsupported rendering mode is specified. + """ + # run a rendering step of the simulator + # if we have rtx sensors, we do not need to render again sin + if not self.sim.has_rtx_sensors() and not recompute: + self.sim.render() + # decide the rendering mode + if self.render_mode == "human" or self.render_mode is None: + return None + elif self.render_mode == "rgb_array": + # check that if any render could have happened + if self.sim.render_mode.value < self.sim.RenderMode.PARTIAL_RENDERING.value: + raise RuntimeError( + f"Cannot render '{self.render_mode}' when the simulation render mode is" + f" '{self.sim.render_mode.name}'. Please set the simulation render mode to:" + f"'{self.sim.RenderMode.PARTIAL_RENDERING.name}' or '{self.sim.RenderMode.FULL_RENDERING.name}'." + " If running headless, make sure --enable_cameras is set." + ) + # create the annotator if it does not exist + if not hasattr(self, "_rgb_annotator"): + import omni.replicator.core as rep + + # create render product + self._render_product = rep.create.render_product( + self.cfg.viewer.cam_prim_path, self.cfg.viewer.resolution + ) + # create rgb annotator -- used to read data from the render product + self._rgb_annotator = rep.AnnotatorRegistry.get_annotator("rgb", device="cpu") + self._rgb_annotator.attach([self._render_product]) + # obtain the rgb data + rgb_data = self._rgb_annotator.get_data() + # convert to numpy array + rgb_data = np.frombuffer(rgb_data, dtype=np.uint8).reshape(*rgb_data.shape) + # return the rgb data + # note: initially the renderer is warming up and returns empty data + if rgb_data.size == 0: + return np.zeros((self.cfg.viewer.resolution[1], self.cfg.viewer.resolution[0], 3), dtype=np.uint8) + else: + return rgb_data[:, :, :3] + else: + raise NotImplementedError( + f"Render mode '{self.render_mode}' is not supported. Please use: {self.metadata['render_modes']}." + )
+ +
[文档] def close(self): + """Cleanup for the environment.""" + if not self._is_closed: + # close entities related to the environment + # note: this is order-sensitive to avoid any dangling references + if self.cfg.events: + del self.event_manager + del self.scene + if self.viewport_camera_controller is not None: + del self.viewport_camera_controller + # clear callbacks and instance + self.sim.clear_all_callbacks() + self.sim.clear_instance() + # destroy the window + if self._window is not None: + self._window = None + # update closing status + self._is_closed = True
+ + """ + Operations - Debug Visualization. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Toggles the environment debug visualization. + + Args: + debug_vis: Whether to visualize the environment debug visualization. + + Returns: + Whether the debug visualization was successfully set. False if the environment + does not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_debug_vis_implementation: + return False + # toggle debug visualization objects + self._set_debug_vis_impl(debug_vis) + # toggle debug visualization handles + if debug_vis: + # create a subscriber for the post update event if it doesn't exist + if self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + # return success + return True
+ + """ + Helper functions. + """ + + def _configure_env_spaces(self): + """Configure the spaces for the environment.""" + self.agents = self.cfg.possible_agents + self.possible_agents = self.cfg.possible_agents + + # show deprecation message and overwrite configuration + if self.cfg.num_actions is not None: + omni.log.warn("DirectMARLEnvCfg.num_actions is deprecated. Use DirectMARLEnvCfg.action_spaces instead.") + if isinstance(self.cfg.action_spaces, type(MISSING)): + self.cfg.action_spaces = self.cfg.num_actions + if self.cfg.num_observations is not None: + omni.log.warn( + "DirectMARLEnvCfg.num_observations is deprecated. Use DirectMARLEnvCfg.observation_spaces instead." + ) + if isinstance(self.cfg.observation_spaces, type(MISSING)): + self.cfg.observation_spaces = self.cfg.num_observations + if self.cfg.num_states is not None: + omni.log.warn("DirectMARLEnvCfg.num_states is deprecated. Use DirectMARLEnvCfg.state_space instead.") + if isinstance(self.cfg.state_space, type(MISSING)): + self.cfg.state_space = self.cfg.num_states + + # set up observation and action spaces + self.observation_spaces = { + agent: spec_to_gym_space(self.cfg.observation_spaces[agent]) for agent in self.cfg.possible_agents + } + self.action_spaces = { + agent: spec_to_gym_space(self.cfg.action_spaces[agent]) for agent in self.cfg.possible_agents + } + + # set up state space + if not self.cfg.state_space: + self.state_space = None + if isinstance(self.cfg.state_space, int) and self.cfg.state_space < 0: + self.state_space = gym.spaces.flatten_space( + gym.spaces.Tuple([self.observation_spaces[agent] for agent in self.cfg.possible_agents]) + ) + else: + self.state_space = spec_to_gym_space(self.cfg.state_space) + + # instantiate actions (needed for tasks for which the observations computation is dependent on the actions) + self.actions = { + agent: sample_space(self.action_spaces[agent], self.sim.device, batch_size=self.num_envs, fill_value=0) + for agent in self.cfg.possible_agents + } + + def _reset_idx(self, env_ids: Sequence[int]): + """Reset environments based on specified indices. + + Args: + env_ids: List of environment ids which must be reset + """ + self.scene.reset(env_ids) + + # apply events such as randomization for environments that need a reset + if self.cfg.events: + if "reset" in self.event_manager.available_modes: + env_step_count = self._sim_step_counter // self.cfg.decimation + self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) + + # reset noise models + if self.cfg.action_noise_model: + for noise_model in self._action_noise_model.values(): + noise_model.reset(env_ids) + if self.cfg.observation_noise_model: + for noise_model in self._observation_noise_model.values(): + noise_model.reset(env_ids) + + # reset the episode length buffer + self.episode_length_buf[env_ids] = 0 + + """ + Implementation-specific functions. + """ + + def _setup_scene(self): + """Setup the scene for the environment. + + This function is responsible for creating the scene objects and setting up the scene for the environment. + The scene creation can happen through :class:`omni.isaac.lab.scene.InteractiveSceneCfg` or through + directly creating the scene objects and registering them with the scene manager. + + We leave the implementation of this function to the derived classes. If the environment does not require + any explicit scene setup, the function can be left empty. + """ + pass + + @abstractmethod + def _pre_physics_step(self, actions: dict[AgentID, ActionType]): + """Pre-process actions before stepping through the physics. + + This function is responsible for pre-processing the actions before stepping through the physics. + It is called before the physics stepping (which is decimated). + + Args: + actions: The actions to apply on the environment (keyed by the agent ID). + Shape of individual tensors is (num_envs, action_dim). + """ + raise NotImplementedError(f"Please implement the '_pre_physics_step' method for {self.__class__.__name__}.") + + @abstractmethod + def _apply_action(self): + """Apply actions to the simulator. + + This function is responsible for applying the actions to the simulator. It is called at each + physics time-step. + """ + raise NotImplementedError(f"Please implement the '_apply_action' method for {self.__class__.__name__}.") + + @abstractmethod + def _get_observations(self) -> dict[AgentID, ObsType]: + """Compute and return the observations for the environment. + + Returns: + The observations for the environment (keyed by the agent ID). + """ + raise NotImplementedError(f"Please implement the '_get_observations' method for {self.__class__.__name__}.") + + @abstractmethod + def _get_states(self) -> StateType: + """Compute and return the states for the environment. + + This method is only called (and therefore has to be implemented) when the :attr:`DirectMARLEnvCfg.state_space` + parameter is not a number less than or equal to zero. + + Returns: + The states for the environment. + """ + raise NotImplementedError(f"Please implement the '_get_states' method for {self.__class__.__name__}.") + + @abstractmethod + def _get_rewards(self) -> dict[AgentID, torch.Tensor]: + """Compute and return the rewards for the environment. + + Returns: + The rewards for the environment (keyed by the agent ID). + Shape of individual tensors is (num_envs,). + """ + raise NotImplementedError(f"Please implement the '_get_rewards' method for {self.__class__.__name__}.") + + @abstractmethod + def _get_dones(self) -> tuple[dict[AgentID, torch.Tensor], dict[AgentID, torch.Tensor]]: + """Compute and return the done flags for the environment. + + Returns: + A tuple containing the done flags for termination and time-out (keyed by the agent ID). + Shape of individual tensors is (num_envs,). + """ + raise NotImplementedError(f"Please implement the '_get_dones' method for {self.__class__.__name__}.") + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects. + + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.")
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/direct_marl_env_cfg.html b/_modules/omni/isaac/lab/envs/direct_marl_env_cfg.html new file mode 100644 index 0000000000..811a62fd8d --- /dev/null +++ b/_modules/omni/isaac/lab/envs/direct_marl_env_cfg.html @@ -0,0 +1,782 @@ + + + + + + + + + + + omni.isaac.lab.envs.direct_marl_env_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.direct_marl_env_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.scene import InteractiveSceneCfg
+from omni.isaac.lab.sim import SimulationCfg
+from omni.isaac.lab.utils import configclass
+from omni.isaac.lab.utils.noise import NoiseModelCfg
+
+from .common import AgentID, SpaceType, ViewerCfg
+from .ui import BaseEnvWindow
+
+
+
[文档]@configclass +class DirectMARLEnvCfg: + """Configuration for a MARL environment defined with the direct workflow. + + Please refer to the :class:`omni.isaac.lab.envs.direct_marl_env.DirectMARLEnv` class for more details. + """ + + # simulation settings + viewer: ViewerCfg = ViewerCfg() + """Viewer configuration. Default is ViewerCfg().""" + + sim: SimulationCfg = SimulationCfg() + """Physics simulation configuration. Default is SimulationCfg().""" + + # ui settings + ui_window_class_type: type | None = BaseEnvWindow + """The class type of the UI window. Default is None. + + If None, then no UI window is created. + + Note: + If you want to make your own UI window, you can create a class that inherits from + from :class:`omni.isaac.lab.envs.ui.base_env_window.BaseEnvWindow`. Then, you can set + this attribute to your class type. + """ + + # general settings + seed: int | None = None + """The seed for the random number generator. Defaults to None, in which case the seed is not set. + + Note: + The seed is set at the beginning of the environment initialization. This ensures that the environment + creation is deterministic and behaves similarly across different runs. + """ + + decimation: int = MISSING + """Number of control action updates @ sim dt per policy dt. + + For instance, if the simulation dt is 0.01s and the policy dt is 0.1s, then the decimation is 10. + This means that the control action is updated every 10 simulation steps. + """ + + is_finite_horizon: bool = False + """Whether the learning task is treated as a finite or infinite horizon problem for the agent. + Defaults to False, which means the task is treated as an infinite horizon problem. + + This flag handles the subtleties of finite and infinite horizon tasks: + + * **Finite horizon**: no penalty or bootstrapping value is required by the the agent for + running out of time. However, the environment still needs to terminate the episode after the + time limit is reached. + * **Infinite horizon**: the agent needs to bootstrap the value of the state at the end of the episode. + This is done by sending a time-limit (or truncated) done signal to the agent, which triggers this + bootstrapping calculation. + + If True, then the environment is treated as a finite horizon problem and no time-out (or truncated) done signal + is sent to the agent. If False, then the environment is treated as an infinite horizon problem and a time-out + (or truncated) done signal is sent to the agent. + + Note: + The base :class:`ManagerBasedRLEnv` class does not use this flag directly. It is used by the environment + wrappers to determine what type of done signal to send to the corresponding learning agent. + """ + + episode_length_s: float = MISSING + """Duration of an episode (in seconds). + + Based on the decimation rate and physics time step, the episode length is calculated as: + + .. code-block:: python + + episode_length_steps = ceil(episode_length_s / (decimation_rate * physics_time_step)) + + For example, if the decimation rate is 10, the physics time step is 0.01, and the episode length is 10 seconds, + then the episode length in steps is 100. + """ + + # environment settings + scene: InteractiveSceneCfg = MISSING + """Scene settings. + + Please refer to the :class:`omni.isaac.lab.scene.InteractiveSceneCfg` class for more details. + """ + + events: object = None + """Event settings. Defaults to None, in which case no events are applied through the event manager. + + Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details. + """ + + observation_spaces: dict[AgentID, SpaceType] = MISSING + """Observation space definition for each agent. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_observations: dict[AgentID, int] | None = None + """The dimension of the observation space for each agent. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectMARLEnvCfg.observation_spaces` instead. + """ + + state_space: SpaceType = MISSING + """State space definition. + + The following values are supported: + + * -1: All the observations from the different agents are automatically concatenated. + * 0: No state-space will be constructed (`state_space` is None). + This is useful to save computational resources when the algorithm to be trained does not need it. + * greater than 0: Custom state-space dimension to be provided by the task implementation. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_states: int | None = None + """The dimension of the state space from each environment instance. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectMARLEnvCfg.state_space` instead. + """ + + observation_noise_model: dict[AgentID, NoiseModelCfg | None] | None = None + """The noise model to apply to the computed observations from the environment. Default is None, which means no noise is added. + + Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. + """ + + action_spaces: dict[AgentID, SpaceType] = MISSING + """Action space definition for each agent. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_actions: dict[AgentID, int] | None = None + """The dimension of the action space for each agent. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectMARLEnvCfg.action_spaces` instead. + """ + + action_noise_model: dict[AgentID, NoiseModelCfg | None] | None = None + """The noise model applied to the actions provided to the environment. Default is None, which means no noise is added. + + Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. + """ + + possible_agents: list[AgentID] = MISSING + """A list of all possible agents the environment could generate. + + The contents of the list cannot be modified during the entire training process. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/direct_rl_env.html b/_modules/omni/isaac/lab/envs/direct_rl_env.html new file mode 100644 index 0000000000..b16c03e0a6 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/direct_rl_env.html @@ -0,0 +1,1213 @@ + + + + + + + + + + + omni.isaac.lab.envs.direct_rl_env — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.direct_rl_env 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import builtins
+import gymnasium as gym
+import inspect
+import math
+import numpy as np
+import torch
+import weakref
+from abc import abstractmethod
+from collections.abc import Sequence
+from dataclasses import MISSING
+from typing import Any, ClassVar
+
+import omni.isaac.core.utils.torch as torch_utils
+import omni.kit.app
+import omni.log
+from omni.isaac.version import get_version
+
+from omni.isaac.lab.managers import EventManager
+from omni.isaac.lab.scene import InteractiveScene
+from omni.isaac.lab.sim import SimulationContext
+from omni.isaac.lab.utils.noise import NoiseModel
+from omni.isaac.lab.utils.timer import Timer
+
+from .common import VecEnvObs, VecEnvStepReturn
+from .direct_rl_env_cfg import DirectRLEnvCfg
+from .ui import ViewportCameraController
+from .utils.spaces import sample_space, spec_to_gym_space
+
+
+
[文档]class DirectRLEnv(gym.Env): + """The superclass for the direct workflow to design environments. + + This class implements the core functionality for reinforcement learning (RL) + environments. It is designed to be used with any RL library. The class is designed + to be used with vectorized environments, i.e., the environment is expected to be run + in parallel with multiple sub-environments. + + While the environment itself is implemented as a vectorized environment, we do not + inherit from :class:`gym.vector.VectorEnv`. This is mainly because the class adds + various methods (for wait and asynchronous updates) which are not required. + Additionally, each RL library typically has its own definition for a vectorized + environment. Thus, to reduce complexity, we directly use the :class:`gym.Env` over + here and leave it up to library-defined wrappers to take care of wrapping this + environment for their agents. + + Note: + For vectorized environments, it is recommended to **only** call the :meth:`reset` + method once before the first call to :meth:`step`, i.e. after the environment is created. + After that, the :meth:`step` function handles the reset of terminated sub-environments. + This is because the simulator does not support resetting individual sub-environments + in a vectorized environment. + + """ + + is_vector_env: ClassVar[bool] = True + """Whether the environment is a vectorized environment.""" + metadata: ClassVar[dict[str, Any]] = { + "render_modes": [None, "human", "rgb_array"], + "isaac_sim_version": get_version(), + } + """Metadata for the environment.""" + +
[文档] def __init__(self, cfg: DirectRLEnvCfg, render_mode: str | None = None, **kwargs): + """Initialize the environment. + + Args: + cfg: The configuration object for the environment. + render_mode: The render mode for the environment. Defaults to None, which + is similar to ``"human"``. + + Raises: + RuntimeError: If a simulation context already exists. The environment must always create one + since it configures the simulation context and controls the simulation. + """ + # check that the config is valid + cfg.validate() + # store inputs to class + self.cfg = cfg + # store the render mode + self.render_mode = render_mode + # initialize internal variables + self._is_closed = False + + # set the seed for the environment + if self.cfg.seed is not None: + self.cfg.seed = self.seed(self.cfg.seed) + else: + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + + # create a simulation context to control the simulator + if SimulationContext.instance() is None: + self.sim: SimulationContext = SimulationContext(self.cfg.sim) + else: + raise RuntimeError("Simulation context already exists. Cannot create a new one.") + + # print useful information + print("[INFO]: Base environment:") + print(f"\tEnvironment device : {self.device}") + print(f"\tEnvironment seed : {self.cfg.seed}") + print(f"\tPhysics step-size : {self.physics_dt}") + print(f"\tRendering step-size : {self.physics_dt * self.cfg.sim.render_interval}") + print(f"\tEnvironment step-size : {self.step_dt}") + + if self.cfg.sim.render_interval < self.cfg.decimation: + msg = ( + f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step." + "If this is not intended, set the render interval to be equal to the decimation." + ) + omni.log.warn(msg) + + # generate scene + with Timer("[INFO]: Time taken for scene creation", "scene_creation"): + self.scene = InteractiveScene(self.cfg.scene) + self._setup_scene() + print("[INFO]: Scene manager: ", self.scene) + + # set up camera viewport controller + # viewport is not available in other rendering modes so the function will throw a warning + # FIXME: This needs to be fixed in the future when we unify the UI functionalities even for + # non-rendering modes. + if self.sim.render_mode >= self.sim.RenderMode.PARTIAL_RENDERING: + self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer) + else: + self.viewport_camera_controller = None + + # play the simulator to activate physics handles + # note: this activates the physics simulation view that exposes TensorAPIs + # note: when started in extension mode, first call sim.reset_async() and then initialize the managers + if builtins.ISAAC_LAUNCHED_FROM_TERMINAL is False: + print("[INFO]: Starting the simulation. This may take a few seconds. Please wait...") + with Timer("[INFO]: Time taken for simulation start", "simulation_start"): + self.sim.reset() + + # -- event manager used for randomization + if self.cfg.events: + self.event_manager = EventManager(self.cfg.events, self) + print("[INFO] Event Manager: ", self.event_manager) + + # make sure torch is running on the correct device + if "cuda" in self.device: + torch.cuda.set_device(self.device) + + # check if debug visualization is has been implemented by the environment + source_code = inspect.getsource(self._set_debug_vis_impl) + self.has_debug_vis_implementation = "NotImplementedError" not in source_code + self._debug_vis_handle = None + + # extend UI elements + # we need to do this here after all the managers are initialized + # this is because they dictate the sensors and commands right now + if self.sim.has_gui() and self.cfg.ui_window_class_type is not None: + self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") + else: + # if no window, then we don't need to store the window + self._window = None + + # allocate dictionary to store metrics + self.extras = {} + + # initialize data and constants + # -- counter for simulation steps + self._sim_step_counter = 0 + # -- counter for curriculum + self.common_step_counter = 0 + # -- init buffers + self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) + self.reset_terminated = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) + self.reset_time_outs = torch.zeros_like(self.reset_terminated) + self.reset_buf = torch.zeros(self.num_envs, dtype=torch.bool, device=self.sim.device) + + # setup the action and observation spaces for Gym + self._configure_gym_env_spaces() + + # setup noise cfg for adding action and observation noise + if self.cfg.action_noise_model: + self._action_noise_model: NoiseModel = self.cfg.action_noise_model.class_type( + self.cfg.action_noise_model, num_envs=self.num_envs, device=self.device + ) + if self.cfg.observation_noise_model: + self._observation_noise_model: NoiseModel = self.cfg.observation_noise_model.class_type( + self.cfg.observation_noise_model, num_envs=self.num_envs, device=self.device + ) + + # perform events at the start of the simulation + if self.cfg.events: + if "startup" in self.event_manager.available_modes: + self.event_manager.apply(mode="startup") + + # -- set the framerate of the gym video recorder wrapper so that the playback speed of the produced video matches the simulation + self.metadata["render_fps"] = 1 / self.step_dt + + # print the environment information + print("[INFO]: Completed setting up the environment...")
+ + def __del__(self): + """Cleanup for the environment.""" + self.close() + + """ + Properties. + """ + + @property + def num_envs(self) -> int: + """The number of instances of the environment that are running.""" + return self.scene.num_envs + + @property + def physics_dt(self) -> float: + """The physics time-step (in s). + + This is the lowest time-decimation at which the simulation is happening. + """ + return self.cfg.sim.dt + + @property + def step_dt(self) -> float: + """The environment stepping time-step (in s). + + This is the time-step at which the environment steps forward. + """ + return self.cfg.sim.dt * self.cfg.decimation + + @property + def device(self): + """The device on which the environment is running.""" + return self.sim.device + + @property + def max_episode_length_s(self) -> float: + """Maximum episode length in seconds.""" + return self.cfg.episode_length_s + + @property + def max_episode_length(self): + """The maximum episode length in steps adjusted from s.""" + return math.ceil(self.max_episode_length_s / (self.cfg.sim.dt * self.cfg.decimation)) + + """ + Operations. + """ + +
[文档] def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) -> tuple[VecEnvObs, dict]: + """Resets all the environments and returns observations. + + This function calls the :meth:`_reset_idx` function to reset all the environments. + However, certain operations, such as procedural terrain generation, that happened during initialization + are not repeated. + + Args: + seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + options: Additional information to specify how the environment is reset. Defaults to None. + + Note: + This argument is used for compatibility with Gymnasium environment definition. + + Returns: + A tuple containing the observations and extras. + """ + # set the seed + if seed is not None: + self.seed(seed) + + # reset state of scene + indices = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + self._reset_idx(indices) + + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # return observations + return self._get_observations(), self.extras
+ +
[文档] def step(self, action: torch.Tensor) -> VecEnvStepReturn: + """Execute one time-step of the environment's dynamics. + + 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:`DirectRLEnvCfg.decimation` (number of simulation steps per environment step) + and the :attr:`DirectRLEnvCfg.sim.physics_dt` (physics time-step). Based on these parameters, the environment + time-step is computed as the product of the two. + + This function performs the following steps: + + 1. Pre-process the actions before stepping through the physics. + 2. Apply the actions to the simulator and step through the physics in a decimated manner. + 3. Compute the reward and done signals. + 4. Reset environments that have terminated or reached the maximum episode length. + 5. Apply interval events if they are enabled. + 6. Compute observations. + + Args: + 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. + """ + action = action.to(self.device) + # add action noise + if self.cfg.action_noise_model: + action = self._action_noise_model.apply(action) + + # process actions + self._pre_physics_step(action) + + # check if we need to do rendering within the physics loop + # note: checked here once to avoid multiple checks within the loop + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() + + # perform physics stepping + for _ in range(self.cfg.decimation): + self._sim_step_counter += 1 + # set actions into buffers + self._apply_action() + # set actions into simulator + self.scene.write_data_to_sim() + # simulate + self.sim.step(render=False) + # render between steps only if the GUI or an RTX sensor needs it + # note: we assume the render interval to be the shortest accepted rendering interval. + # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: + self.sim.render() + # update buffers at sim dt + self.scene.update(dt=self.physics_dt) + + # post-step: + # -- update env counters (used for curriculum generation) + self.episode_length_buf += 1 # step in current episode (per env) + self.common_step_counter += 1 # total step (common for all envs) + + self.reset_terminated[:], self.reset_time_outs[:] = self._get_dones() + self.reset_buf = self.reset_terminated | self.reset_time_outs + self.reward_buf = self._get_rewards() + + # -- reset envs that terminated/timed-out and log the episode information + reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) + if len(reset_env_ids) > 0: + self._reset_idx(reset_env_ids) + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # post-step: step interval event + if self.cfg.events: + if "interval" in self.event_manager.available_modes: + self.event_manager.apply(mode="interval", dt=self.step_dt) + + # update observations + self.obs_buf = self._get_observations() + + # add observation noise + # note: we apply no noise to the state space (since it is used for critic networks) + if self.cfg.observation_noise_model: + self.obs_buf["policy"] = self._observation_noise_model.apply(self.obs_buf["policy"]) + + # return observations, rewards, resets and extras + return self.obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras
+ +
[文档] @staticmethod + def seed(seed: int = -1) -> int: + """Set the seed for the environment. + + Args: + seed: The seed for random generator. Defaults to -1. + + Returns: + The seed used for random generator. + """ + # set seed for replicator + try: + import omni.replicator.core as rep + + rep.set_global_seed(seed) + except ModuleNotFoundError: + pass + # set seed for torch and other libraries + return torch_utils.set_seed(seed)
+ +
[文档] def render(self, recompute: bool = False) -> np.ndarray | None: + """Run rendering without stepping through the physics. + + By convention, if mode is: + + - **human**: Render to the current display and return nothing. Usually for human consumption. + - **rgb_array**: Return an numpy.ndarray with shape (x, y, 3), representing RGB values for an + x-by-y pixel image, suitable for turning into a video. + + Args: + recompute: Whether to force a render even if the simulator has already rendered the scene. + Defaults to False. + + Returns: + The rendered image as a numpy array if mode is "rgb_array". Otherwise, returns None. + + Raises: + RuntimeError: If mode is set to "rgb_data" and simulation render mode does not support it. + In this case, the simulation render mode must be set to ``RenderMode.PARTIAL_RENDERING`` + or ``RenderMode.FULL_RENDERING``. + NotImplementedError: If an unsupported rendering mode is specified. + """ + # run a rendering step of the simulator + # if we have rtx sensors, we do not need to render again sin + if not self.sim.has_rtx_sensors() and not recompute: + self.sim.render() + # decide the rendering mode + if self.render_mode == "human" or self.render_mode is None: + return None + elif self.render_mode == "rgb_array": + # check that if any render could have happened + if self.sim.render_mode.value < self.sim.RenderMode.PARTIAL_RENDERING.value: + raise RuntimeError( + f"Cannot render '{self.render_mode}' when the simulation render mode is" + f" '{self.sim.render_mode.name}'. Please set the simulation render mode to:" + f"'{self.sim.RenderMode.PARTIAL_RENDERING.name}' or '{self.sim.RenderMode.FULL_RENDERING.name}'." + " If running headless, make sure --enable_cameras is set." + ) + # create the annotator if it does not exist + if not hasattr(self, "_rgb_annotator"): + import omni.replicator.core as rep + + # create render product + self._render_product = rep.create.render_product( + self.cfg.viewer.cam_prim_path, self.cfg.viewer.resolution + ) + # create rgb annotator -- used to read data from the render product + self._rgb_annotator = rep.AnnotatorRegistry.get_annotator("rgb", device="cpu") + self._rgb_annotator.attach([self._render_product]) + # obtain the rgb data + rgb_data = self._rgb_annotator.get_data() + # convert to numpy array + rgb_data = np.frombuffer(rgb_data, dtype=np.uint8).reshape(*rgb_data.shape) + # return the rgb data + # note: initially the renerer is warming up and returns empty data + if rgb_data.size == 0: + return np.zeros((self.cfg.viewer.resolution[1], self.cfg.viewer.resolution[0], 3), dtype=np.uint8) + else: + return rgb_data[:, :, :3] + else: + raise NotImplementedError( + f"Render mode '{self.render_mode}' is not supported. Please use: {self.metadata['render_modes']}." + )
+ +
[文档] def close(self): + """Cleanup for the environment.""" + if not self._is_closed: + # close entities related to the environment + # note: this is order-sensitive to avoid any dangling references + if self.cfg.events: + del self.event_manager + del self.scene + if self.viewport_camera_controller is not None: + del self.viewport_camera_controller + # clear callbacks and instance + self.sim.clear_all_callbacks() + self.sim.clear_instance() + # destroy the window + if self._window is not None: + self._window = None + # update closing status + self._is_closed = True
+ + """ + Operations - Debug Visualization. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Toggles the environment debug visualization. + + Args: + debug_vis: Whether to visualize the environment debug visualization. + + Returns: + Whether the debug visualization was successfully set. False if the environment + does not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_debug_vis_implementation: + return False + # toggle debug visualization objects + self._set_debug_vis_impl(debug_vis) + # toggle debug visualization handles + if debug_vis: + # create a subscriber for the post update event if it doesn't exist + if self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + # return success + return True
+ + """ + Helper functions. + """ + + def _configure_gym_env_spaces(self): + """Configure the action and observation spaces for the Gym environment.""" + # show deprecation message and overwrite configuration + if self.cfg.num_actions is not None: + omni.log.warn("DirectRLEnvCfg.num_actions is deprecated. Use DirectRLEnvCfg.action_space instead.") + if isinstance(self.cfg.action_space, type(MISSING)): + self.cfg.action_space = self.cfg.num_actions + if self.cfg.num_observations is not None: + omni.log.warn( + "DirectRLEnvCfg.num_observations is deprecated. Use DirectRLEnvCfg.observation_space instead." + ) + if isinstance(self.cfg.observation_space, type(MISSING)): + self.cfg.observation_space = self.cfg.num_observations + if self.cfg.num_states is not None: + omni.log.warn("DirectRLEnvCfg.num_states is deprecated. Use DirectRLEnvCfg.state_space instead.") + if isinstance(self.cfg.state_space, type(MISSING)): + self.cfg.state_space = self.cfg.num_states + + # set up spaces + self.single_observation_space = gym.spaces.Dict() + self.single_observation_space["policy"] = spec_to_gym_space(self.cfg.observation_space) + self.single_action_space = spec_to_gym_space(self.cfg.action_space) + + # batch the spaces for vectorized environments + self.observation_space = gym.vector.utils.batch_space(self.single_observation_space["policy"], self.num_envs) + self.action_space = gym.vector.utils.batch_space(self.single_action_space, self.num_envs) + + # optional state space for asymmetric actor-critic architectures + self.state_space = None + if self.cfg.state_space: + self.single_observation_space["critic"] = spec_to_gym_space(self.cfg.state_space) + self.state_space = gym.vector.utils.batch_space(self.single_observation_space["critic"], self.num_envs) + + # instantiate actions (needed for tasks for which the observations computation is dependent on the actions) + self.actions = sample_space(self.single_action_space, self.sim.device, batch_size=self.num_envs, fill_value=0) + + def _reset_idx(self, env_ids: Sequence[int]): + """Reset environments based on specified indices. + + Args: + env_ids: List of environment ids which must be reset + """ + self.scene.reset(env_ids) + + # apply events such as randomization for environments that need a reset + if self.cfg.events: + if "reset" in self.event_manager.available_modes: + env_step_count = self._sim_step_counter // self.cfg.decimation + self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) + + # reset noise models + if self.cfg.action_noise_model: + self._action_noise_model.reset(env_ids) + if self.cfg.observation_noise_model: + self._observation_noise_model.reset(env_ids) + + # reset the episode length buffer + self.episode_length_buf[env_ids] = 0 + + """ + Implementation-specific functions. + """ + + def _setup_scene(self): + """Setup the scene for the environment. + + This function is responsible for creating the scene objects and setting up the scene for the environment. + The scene creation can happen through :class:`omni.isaac.lab.scene.InteractiveSceneCfg` or through + directly creating the scene objects and registering them with the scene manager. + + We leave the implementation of this function to the derived classes. If the environment does not require + any explicit scene setup, the function can be left empty. + """ + pass + + @abstractmethod + def _pre_physics_step(self, actions: torch.Tensor): + """Pre-process actions before stepping through the physics. + + This function is responsible for pre-processing the actions before stepping through the physics. + It is called before the physics stepping (which is decimated). + + Args: + actions: The actions to apply on the environment. Shape is (num_envs, action_dim). + """ + raise NotImplementedError(f"Please implement the '_pre_physics_step' method for {self.__class__.__name__}.") + + @abstractmethod + def _apply_action(self): + """Apply actions to the simulator. + + This function is responsible for applying the actions to the simulator. It is called at each + physics time-step. + """ + raise NotImplementedError(f"Please implement the '_apply_action' method for {self.__class__.__name__}.") + + @abstractmethod + def _get_observations(self) -> VecEnvObs: + """Compute and return the observations for the environment. + + Returns: + The observations for the environment. + """ + raise NotImplementedError(f"Please implement the '_get_observations' method for {self.__class__.__name__}.") + + def _get_states(self) -> VecEnvObs | None: + """Compute and return the states for the environment. + + The state-space is used for asymmetric actor-critic architectures. It is configured + using the :attr:`DirectRLEnvCfg.state_space` parameter. + + Returns: + The states for the environment. If the environment does not have a state-space, the function + returns a None. + """ + return None # noqa: R501 + + @abstractmethod + def _get_rewards(self) -> torch.Tensor: + """Compute and return the rewards for the environment. + + Returns: + The rewards for the environment. Shape is (num_envs,). + """ + raise NotImplementedError(f"Please implement the '_get_rewards' method for {self.__class__.__name__}.") + + @abstractmethod + def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: + """Compute and return the done flags for the environment. + + Returns: + A tuple containing the done flags for termination and time-out. + Shape of individual tensors is (num_envs,). + """ + raise NotImplementedError(f"Please implement the '_get_dones' method for {self.__class__.__name__}.") + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects. + + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.")
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/direct_rl_env_cfg.html b/_modules/omni/isaac/lab/envs/direct_rl_env_cfg.html new file mode 100644 index 0000000000..3f61a99bd8 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/direct_rl_env_cfg.html @@ -0,0 +1,783 @@ + + + + + + + + + + + omni.isaac.lab.envs.direct_rl_env_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.direct_rl_env_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.scene import InteractiveSceneCfg
+from omni.isaac.lab.sim import SimulationCfg
+from omni.isaac.lab.utils import configclass
+from omni.isaac.lab.utils.noise import NoiseModelCfg
+
+from .common import SpaceType, ViewerCfg
+from .ui import BaseEnvWindow
+
+
+
[文档]@configclass +class DirectRLEnvCfg: + """Configuration for an RL environment defined with the direct workflow. + + Please refer to the :class:`omni.isaac.lab.envs.direct_rl_env.DirectRLEnv` class for more details. + """ + + # simulation settings + viewer: ViewerCfg = ViewerCfg() + """Viewer configuration. Default is ViewerCfg().""" + + sim: SimulationCfg = SimulationCfg() + """Physics simulation configuration. Default is SimulationCfg().""" + + # ui settings + ui_window_class_type: type | None = BaseEnvWindow + """The class type of the UI window. Default is None. + + If None, then no UI window is created. + + Note: + If you want to make your own UI window, you can create a class that inherits from + from :class:`omni.isaac.lab.envs.ui.base_env_window.BaseEnvWindow`. Then, you can set + this attribute to your class type. + """ + + # general settings + seed: int | None = None + """The seed for the random number generator. Defaults to None, in which case the seed is not set. + + Note: + The seed is set at the beginning of the environment initialization. This ensures that the environment + creation is deterministic and behaves similarly across different runs. + """ + + decimation: int = MISSING + """Number of control action updates @ sim dt per policy dt. + + For instance, if the simulation dt is 0.01s and the policy dt is 0.1s, then the decimation is 10. + This means that the control action is updated every 10 simulation steps. + """ + + is_finite_horizon: bool = False + """Whether the learning task is treated as a finite or infinite horizon problem for the agent. + Defaults to False, which means the task is treated as an infinite horizon problem. + + This flag handles the subtleties of finite and infinite horizon tasks: + + * **Finite horizon**: no penalty or bootstrapping value is required by the the agent for + running out of time. However, the environment still needs to terminate the episode after the + time limit is reached. + * **Infinite horizon**: the agent needs to bootstrap the value of the state at the end of the episode. + This is done by sending a time-limit (or truncated) done signal to the agent, which triggers this + bootstrapping calculation. + + If True, then the environment is treated as a finite horizon problem and no time-out (or truncated) done signal + is sent to the agent. If False, then the environment is treated as an infinite horizon problem and a time-out + (or truncated) done signal is sent to the agent. + + Note: + The base :class:`ManagerBasedRLEnv` class does not use this flag directly. It is used by the environment + wrappers to determine what type of done signal to send to the corresponding learning agent. + """ + + episode_length_s: float = MISSING + """Duration of an episode (in seconds). + + Based on the decimation rate and physics time step, the episode length is calculated as: + + .. code-block:: python + + episode_length_steps = ceil(episode_length_s / (decimation_rate * physics_time_step)) + + For example, if the decimation rate is 10, the physics time step is 0.01, and the episode length is 10 seconds, + then the episode length in steps is 100. + """ + + # environment settings + scene: InteractiveSceneCfg = MISSING + """Scene settings. + + Please refer to the :class:`omni.isaac.lab.scene.InteractiveSceneCfg` class for more details. + """ + + events: object | None = None + """Event settings. Defaults to None, in which case no events are applied through the event manager. + + Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details. + """ + + observation_space: SpaceType = MISSING + """Observation space definition. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_observations: int | None = None + """The dimension of the observation space from each environment instance. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectRLEnvCfg.observation_space` instead. + """ + + state_space: SpaceType | None = None + """State space definition. + + This is useful for asymmetric actor-critic and defines the observation space for the critic. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_states: int | None = None + """The dimension of the state-space from each environment instance. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectRLEnvCfg.state_space` instead. + """ + + observation_noise_model: NoiseModelCfg | None = None + """The noise model to apply to the computed observations from the environment. Default is None, which means no noise is added. + + Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. + """ + + action_space: SpaceType = MISSING + """Action space definition. + + The space can be defined either using Gymnasium :py:mod:`~gymnasium.spaces` (when a more detailed + specification of the space is desired) or basic Python data types (for simplicity). + + .. list-table:: + :header-rows: 1 + + * - Gymnasium space + - Python data type + * - :class:`~gymnasium.spaces.Box` + - Integer or list of integers (e.g.: ``7``, ``[64, 64, 3]``) + * - :class:`~gymnasium.spaces.Discrete` + - Single-element set (e.g.: ``{2}``) + * - :class:`~gymnasium.spaces.MultiDiscrete` + - List of single-element sets (e.g.: ``[{2}, {5}]``) + * - :class:`~gymnasium.spaces.Dict` + - Dictionary (e.g.: ``{"joints": 7, "rgb": [64, 64, 3], "gripper": {2}}``) + * - :class:`~gymnasium.spaces.Tuple` + - Tuple (e.g.: ``(7, [64, 64, 3], {2})``) + """ + + num_actions: int | None = None + """The dimension of the action space for each environment. + + .. warning:: + + This attribute is deprecated. Use :attr:`~omni.isaac.lab.envs.DirectRLEnvCfg.action_space` instead. + """ + + action_noise_model: NoiseModelCfg | None = None + """The noise model applied to the actions provided to the environment. Default is None, which means no noise is added. + + Please refer to the :class:`omni.isaac.lab.utils.noise.NoiseModel` class for more details. + """ + + rerender_on_reset: bool = False + """Whether a render step is performed again after at least one environment has been reset. + Defaults to False, which means no render step will be performed after reset. + + * When this is False, data collected from sensors after performing reset will be stale and will not reflect the + latest states in simulation caused by the reset. + * When this is True, an extra render step will be performed to update the sensor data + to reflect the latest states from the reset. This comes at a cost of performance as an additional render + step will be performed after each time an environment is reset. + + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/manager_based_env.html b/_modules/omni/isaac/lab/envs/manager_based_env.html new file mode 100644 index 0000000000..f0f9e8c540 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/manager_based_env.html @@ -0,0 +1,936 @@ + + + + + + + + + + + omni.isaac.lab.envs.manager_based_env — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.manager_based_env 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import builtins
+import torch
+from collections.abc import Sequence
+from typing import Any
+
+import omni.isaac.core.utils.torch as torch_utils
+import omni.log
+
+from omni.isaac.lab.managers import ActionManager, EventManager, ObservationManager
+from omni.isaac.lab.scene import InteractiveScene
+from omni.isaac.lab.sim import SimulationContext
+from omni.isaac.lab.utils.timer import Timer
+
+from .common import VecEnvObs
+from .manager_based_env_cfg import ManagerBasedEnvCfg
+from .ui import ViewportCameraController
+
+
+
[文档]class ManagerBasedEnv: + """The base environment encapsulates the simulation scene and the environment managers for the manager-based workflow. + + While a simulation scene or world comprises of different components such as the robots, objects, + and sensors (cameras, lidars, etc.), the environment is a higher level abstraction + that provides an interface for interacting with the simulation. The environment is comprised of + the following components: + + * **Scene**: The scene manager that creates and manages the virtual world in which the robot operates. + This includes defining the robot, static and dynamic objects, sensors, etc. + * **Observation Manager**: The observation manager that generates observations from the current simulation + state and the data gathered from the sensors. These observations may include privileged information + that is not available to the robot in the real world. Additionally, user-defined terms can be added + to process the observations and generate custom observations. For example, using a network to embed + high-dimensional observations into a lower-dimensional space. + * **Action Manager**: The action manager that processes the raw actions sent to the environment and + converts them to low-level commands that are sent to the simulation. It can be configured to accept + raw actions at different levels of abstraction. For example, in case of a robotic arm, the raw actions + can be joint torques, joint positions, or end-effector poses. Similarly for a mobile base, it can be + the joint torques, or the desired velocity of the floating base. + * **Event Manager**: The event manager orchestrates operations triggered based on simulation events. + This includes resetting the scene to a default state, applying random pushes to the robot at different intervals + of time, or randomizing properties such as mass and friction coefficients. This is useful for training + and evaluating the robot in a variety of scenarios. + + The environment provides a unified interface for interacting with the simulation. However, it does not + include task-specific quantities such as the reward function, or the termination conditions. These + quantities are often specific to defining Markov Decision Processes (MDPs) while the base environment + is agnostic to the MDP definition. + + The environment steps forward in time at a fixed time-step. 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:`ManagerBasedEnvCfg.decimation` (number of simulation steps per environment step) + and the :attr:`ManagerBasedEnvCfg.sim.dt` (physics time-step) parameters. Based on these parameters, the + environment time-step is computed as the product of the two. The two time-steps can be obtained by + querying the :attr:`physics_dt` and the :attr:`step_dt` properties respectively. + """ + +
[文档] def __init__(self, cfg: ManagerBasedEnvCfg): + """Initialize the environment. + + Args: + cfg: The configuration object for the environment. + + Raises: + RuntimeError: If a simulation context already exists. The environment must always create one + since it configures the simulation context and controls the simulation. + """ + # check that the config is valid + cfg.validate() + # store inputs to class + self.cfg = cfg + # initialize internal variables + self._is_closed = False + + # set the seed for the environment + if self.cfg.seed is not None: + self.cfg.seed = self.seed(self.cfg.seed) + else: + omni.log.warn("Seed not set for the environment. The environment creation may not be deterministic.") + + # create a simulation context to control the simulator + if SimulationContext.instance() is None: + # the type-annotation is required to avoid a type-checking error + # since it gets confused with Isaac Sim's SimulationContext class + self.sim: SimulationContext = SimulationContext(self.cfg.sim) + else: + # simulation context should only be created before the environment + # when in extension mode + if not builtins.ISAAC_LAUNCHED_FROM_TERMINAL: + raise RuntimeError("Simulation context already exists. Cannot create a new one.") + self.sim: SimulationContext = SimulationContext.instance() + + # print useful information + print("[INFO]: Base environment:") + print(f"\tEnvironment device : {self.device}") + print(f"\tEnvironment seed : {self.cfg.seed}") + print(f"\tPhysics step-size : {self.physics_dt}") + print(f"\tRendering step-size : {self.physics_dt * self.cfg.sim.render_interval}") + print(f"\tEnvironment step-size : {self.step_dt}") + + if self.cfg.sim.render_interval < self.cfg.decimation: + msg = ( + f"The render interval ({self.cfg.sim.render_interval}) is smaller than the decimation " + f"({self.cfg.decimation}). Multiple render calls will happen for each environment step. " + "If this is not intended, set the render interval to be equal to the decimation." + ) + omni.log.warn(msg) + + # counter for simulation steps + self._sim_step_counter = 0 + + # generate scene + with Timer("[INFO]: Time taken for scene creation", "scene_creation"): + self.scene = InteractiveScene(self.cfg.scene) + print("[INFO]: Scene manager: ", self.scene) + + # set up camera viewport controller + # viewport is not available in other rendering modes so the function will throw a warning + # FIXME: This needs to be fixed in the future when we unify the UI functionalities even for + # non-rendering modes. + if self.sim.render_mode >= self.sim.RenderMode.PARTIAL_RENDERING: + self.viewport_camera_controller = ViewportCameraController(self, self.cfg.viewer) + else: + self.viewport_camera_controller = None + + # play the simulator to activate physics handles + # note: this activates the physics simulation view that exposes TensorAPIs + # note: when started in extension mode, first call sim.reset_async() and then initialize the managers + if builtins.ISAAC_LAUNCHED_FROM_TERMINAL is False: + print("[INFO]: Starting the simulation. This may take a few seconds. Please wait...") + with Timer("[INFO]: Time taken for simulation start", "simulation_start"): + self.sim.reset() + # add timeline event to load managers + self.load_managers() + + # make sure torch is running on the correct device + if "cuda" in self.device: + torch.cuda.set_device(self.device) + + # extend UI elements + # we need to do this here after all the managers are initialized + # this is because they dictate the sensors and commands right now + if self.sim.has_gui() and self.cfg.ui_window_class_type is not None: + self._window = self.cfg.ui_window_class_type(self, window_name="IsaacLab") + else: + # if no window, then we don't need to store the window + self._window = None + + # allocate dictionary to store metrics + self.extras = {}
+ + def __del__(self): + """Cleanup for the environment.""" + self.close() + + """ + Properties. + """ + + @property + def num_envs(self) -> int: + """The number of instances of the environment that are running.""" + return self.scene.num_envs + + @property + def physics_dt(self) -> float: + """The physics time-step (in s). + + This is the lowest time-decimation at which the simulation is happening. + """ + return self.cfg.sim.dt + + @property + def step_dt(self) -> float: + """The environment stepping time-step (in s). + + This is the time-step at which the environment steps forward. + """ + return self.cfg.sim.dt * self.cfg.decimation + + @property + def device(self): + """The device on which the environment is running.""" + return self.sim.device + + """ + Operations - Setup. + """ + +
[文档] def load_managers(self): + """Load the managers for the environment. + + This function is responsible for creating the various managers (action, observation, + events, 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 + self.action_manager = ActionManager(self.cfg.actions, self) + print("[INFO] Action Manager: ", self.action_manager) + # -- observation manager + self.observation_manager = ObservationManager(self.cfg.observations, self) + print("[INFO] Observation Manager:", self.observation_manager) + # -- event manager + self.event_manager = EventManager(self.cfg.events, self) + print("[INFO] Event Manager: ", self.event_manager) + + # perform events at the start of the simulation + # in-case a child implementation creates other managers, the randomization should happen + # when all the other managers are created + if self.__class__ == ManagerBasedEnv and "startup" in self.event_manager.available_modes: + self.event_manager.apply(mode="startup")
+ + """ + Operations - MDP. + """ + +
[文档] def reset(self, seed: int | None = None, options: dict[str, Any] | None = None) -> tuple[VecEnvObs, dict]: + """Resets all the environments and returns observations. + + This function calls the :meth:`_reset_idx` function to reset all the environments. + However, certain operations, such as procedural terrain generation, that happened during initialization + are not repeated. + + Args: + seed: The seed to use for randomization. Defaults to None, in which case the seed is not set. + options: Additional information to specify how the environment is reset. Defaults to None. + + Note: + This argument is used for compatibility with Gymnasium environment definition. + + Returns: + A tuple containing the observations and extras. + """ + # set the seed + if seed is not None: + self.seed(seed) + + # reset state of scene + indices = torch.arange(self.num_envs, dtype=torch.int64, device=self.device) + self._reset_idx(indices) + + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # return observations + return self.observation_manager.compute(), self.extras
+ +
[文档] def step(self, action: torch.Tensor) -> tuple[VecEnvObs, dict]: + """Execute one time-step of the environment's dynamics. + + 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:`ManagerBasedEnvCfg.decimation` (number of + simulation steps per environment step) and the :attr:`ManagerBasedEnvCfg.sim.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). + + Returns: + A tuple containing the observations and extras. + """ + # process actions + self.action_manager.process_action(action.to(self.device)) + + # check if we need to do rendering within the physics loop + # note: checked here once to avoid multiple checks within the loop + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() + + # perform physics stepping + for _ in range(self.cfg.decimation): + self._sim_step_counter += 1 + # set actions into buffers + self.action_manager.apply_action() + # set actions into simulator + self.scene.write_data_to_sim() + # simulate + self.sim.step(render=False) + # render between steps only if the GUI or an RTX sensor needs it + # note: we assume the render interval to be the shortest accepted rendering interval. + # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: + self.sim.render() + # update buffers at sim dt + self.scene.update(dt=self.physics_dt) + + # post-step: step interval event + if "interval" in self.event_manager.available_modes: + self.event_manager.apply(mode="interval", dt=self.step_dt) + + # return observations and extras + return self.observation_manager.compute(), self.extras
+ +
[文档] @staticmethod + def seed(seed: int = -1) -> int: + """Set the seed for the environment. + + Args: + seed: The seed for random generator. Defaults to -1. + + Returns: + The seed used for random generator. + """ + # set seed for replicator + try: + import omni.replicator.core as rep + + rep.set_global_seed(seed) + except ModuleNotFoundError: + pass + # set seed for torch and other libraries + return torch_utils.set_seed(seed)
+ +
[文档] def close(self): + """Cleanup for the environment.""" + if not self._is_closed: + # destructor is order-sensitive + del self.viewport_camera_controller + del self.action_manager + del self.observation_manager + del self.event_manager + del self.scene + # clear callbacks and instance + self.sim.clear_all_callbacks() + self.sim.clear_instance() + # destroy the window + if self._window is not None: + self._window = None + # update closing status + self._is_closed = True
+ + """ + Helper functions. + """ + + def _reset_idx(self, env_ids: Sequence[int]): + """Reset environments based on specified indices. + + Args: + env_ids: List of environment ids which must be reset + """ + # reset the internal buffers of the scene elements + self.scene.reset(env_ids) + + # apply events such as randomization for environments that need a reset + if "reset" in self.event_manager.available_modes: + env_step_count = self._sim_step_counter // self.cfg.decimation + self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) + + # iterate over all managers and reset them + # this returns a dictionary of information which is stored in the extras + # note: This is order-sensitive! Certain things need be reset before others. + self.extras["log"] = dict() + # -- observation manager + info = self.observation_manager.reset(env_ids) + self.extras["log"].update(info) + # -- action manager + info = self.action_manager.reset(env_ids) + self.extras["log"].update(info) + # -- event manager + info = self.event_manager.reset(env_ids) + self.extras["log"].update(info)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/manager_based_env_cfg.html b/_modules/omni/isaac/lab/envs/manager_based_env_cfg.html new file mode 100644 index 0000000000..32dec9cfba --- /dev/null +++ b/_modules/omni/isaac/lab/envs/manager_based_env_cfg.html @@ -0,0 +1,668 @@ + + + + + + + + + + + omni.isaac.lab.envs.manager_based_env_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.manager_based_env_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Base configuration of the environment.
+
+This module defines the general configuration of the environment. It includes parameters for
+configuring the environment instances, viewer settings, and simulation parameters.
+"""
+
+from dataclasses import MISSING
+
+import omni.isaac.lab.envs.mdp as mdp
+from omni.isaac.lab.managers import EventTermCfg as EventTerm
+from omni.isaac.lab.scene import InteractiveSceneCfg
+from omni.isaac.lab.sim import SimulationCfg
+from omni.isaac.lab.utils import configclass
+
+from .common import ViewerCfg
+from .ui import BaseEnvWindow
+
+
+@configclass
+class DefaultEventManagerCfg:
+    """Configuration of the default event manager.
+
+    This manager is used to reset the scene to a default state. The default state is specified
+    by the scene configuration.
+    """
+
+    reset_scene_to_default = EventTerm(func=mdp.reset_scene_to_default, mode="reset")
+
+
+
[文档]@configclass +class ManagerBasedEnvCfg: + """Base configuration of the environment.""" + + # simulation settings + viewer: ViewerCfg = ViewerCfg() + """Viewer configuration. Default is ViewerCfg().""" + + sim: SimulationCfg = SimulationCfg() + """Physics simulation configuration. Default is SimulationCfg().""" + + # ui settings + ui_window_class_type: type | None = BaseEnvWindow + """The class type of the UI window. Default is None. + + If None, then no UI window is created. + + Note: + If you want to make your own UI window, you can create a class that inherits from + from :class:`omni.isaac.lab.envs.ui.base_env_window.BaseEnvWindow`. Then, you can set + this attribute to your class type. + """ + + # general settings + seed: int | None = None + """The seed for the random number generator. Defaults to None, in which case the seed is not set. + + Note: + The seed is set at the beginning of the environment initialization. This ensures that the environment + creation is deterministic and behaves similarly across different runs. + """ + + decimation: int = MISSING + """Number of control action updates @ sim dt per policy dt. + + For instance, if the simulation dt is 0.01s and the policy dt is 0.1s, then the decimation is 10. + This means that the control action is updated every 10 simulation steps. + """ + + # environment settings + scene: InteractiveSceneCfg = MISSING + """Scene settings. + + Please refer to the :class:`omni.isaac.lab.scene.InteractiveSceneCfg` class for more details. + """ + + observations: object = MISSING + """Observation space settings. + + Please refer to the :class:`omni.isaac.lab.managers.ObservationManager` class for more details. + """ + + actions: object = MISSING + """Action space settings. + + Please refer to the :class:`omni.isaac.lab.managers.ActionManager` class for more details. + """ + + events: object = DefaultEventManagerCfg() + """Event settings. Defaults to the basic configuration that resets the scene to its default state. + + Please refer to the :class:`omni.isaac.lab.managers.EventManager` class for more details. + """ + + rerender_on_reset: bool = False + """Whether a render step is performed again after at least one environment has been reset. + Defaults to False, which means no render step will be performed after reset. + + * When this is False, data collected from sensors after performing reset will be stale and will not reflect the + latest states in simulation caused by the reset. + * When this is True, an extra render step will be performed to update the sensor data + to reflect the latest states from the reset. This comes at a cost of performance as an additional render + step will be performed after each time an environment is reset. + + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/manager_based_rl_env.html b/_modules/omni/isaac/lab/envs/manager_based_rl_env.html new file mode 100644 index 0000000000..5a517fa491 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/manager_based_rl_env.html @@ -0,0 +1,917 @@ + + + + + + + + + + + omni.isaac.lab.envs.manager_based_rl_env — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.manager_based_rl_env 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# needed to import for allowing type-hinting: np.ndarray | None
+from __future__ import annotations
+
+import gymnasium as gym
+import math
+import numpy as np
+import torch
+from collections.abc import Sequence
+from typing import Any, ClassVar
+
+from omni.isaac.version import get_version
+
+from omni.isaac.lab.managers import CommandManager, CurriculumManager, RewardManager, TerminationManager
+
+from .common import VecEnvStepReturn
+from .manager_based_env import ManagerBasedEnv
+from .manager_based_rl_env_cfg import ManagerBasedRLEnvCfg
+
+
+
[文档]class ManagerBasedRLEnv(ManagerBasedEnv, gym.Env): + """The superclass for the manager-based workflow reinforcement learning-based environments. + + This class inherits from :class:`ManagerBasedEnv` and implements the core functionality for + reinforcement learning-based environments. It is designed to be used with any RL + library. The class is designed to be used with vectorized environments, i.e., the + environment is expected to be run in parallel with multiple sub-environments. The + number of sub-environments is specified using the ``num_envs``. + + Each observation from the environment is a batch of observations for each sub- + environments. The method :meth:`step` is also expected to receive a batch of actions + for each sub-environment. + + While the environment itself is implemented as a vectorized environment, we do not + inherit from :class:`gym.vector.VectorEnv`. This is mainly because the class adds + various methods (for wait and asynchronous updates) which are not required. + Additionally, each RL library typically has its own definition for a vectorized + environment. Thus, to reduce complexity, we directly use the :class:`gym.Env` over + here and leave it up to library-defined wrappers to take care of wrapping this + environment for their agents. + + Note: + For vectorized environments, it is recommended to **only** call the :meth:`reset` + method once before the first call to :meth:`step`, i.e. after the environment is created. + After that, the :meth:`step` function handles the reset of terminated sub-environments. + This is because the simulator does not support resetting individual sub-environments + in a vectorized environment. + + """ + + is_vector_env: ClassVar[bool] = True + """Whether the environment is a vectorized environment.""" + metadata: ClassVar[dict[str, Any]] = { + "render_modes": [None, "human", "rgb_array"], + "isaac_sim_version": get_version(), + } + """Metadata for the environment.""" + + cfg: ManagerBasedRLEnvCfg + """Configuration for the environment.""" + +
[文档] def __init__(self, cfg: ManagerBasedRLEnvCfg, render_mode: str | None = None, **kwargs): + """Initialize the environment. + + Args: + cfg: The configuration for the environment. + render_mode: The render mode for the environment. Defaults to None, which + is similar to ``"human"``. + """ + # initialize the base class to setup the scene. + super().__init__(cfg=cfg) + # store the render mode + self.render_mode = render_mode + + # initialize data and constants + # -- counter for curriculum + self.common_step_counter = 0 + # -- init buffers + self.episode_length_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) + # -- set the framerate of the gym video recorder wrapper so that the playback speed of the produced video matches the simulation + self.metadata["render_fps"] = 1 / self.step_dt + + print("[INFO]: Completed setting up the environment...")
+ + """ + Properties. + """ + + @property + def max_episode_length_s(self) -> float: + """Maximum episode length in seconds.""" + return self.cfg.episode_length_s + + @property + def max_episode_length(self) -> int: + """Maximum episode length in environment steps.""" + return math.ceil(self.max_episode_length_s / self.step_dt) + + """ + Operations - Setup. + """ + +
[文档] def load_managers(self): + # note: this order is important since observation manager needs to know the command and action managers + # and the reward manager needs to know the termination manager + # -- command manager + self.command_manager: CommandManager = CommandManager(self.cfg.commands, self) + print("[INFO] Command Manager: ", self.command_manager) + + # call the parent class to load the managers for observations and actions. + super().load_managers() + + # prepare the managers + # -- termination manager + self.termination_manager = TerminationManager(self.cfg.terminations, self) + print("[INFO] Termination Manager: ", self.termination_manager) + # -- reward manager + self.reward_manager = RewardManager(self.cfg.rewards, self) + print("[INFO] Reward Manager: ", self.reward_manager) + # -- curriculum manager + self.curriculum_manager = CurriculumManager(self.cfg.curriculum, self) + print("[INFO] Curriculum Manager: ", self.curriculum_manager) + + # setup the action and observation spaces for Gym + self._configure_gym_env_spaces() + + # perform events at the start of the simulation + if "startup" in self.event_manager.available_modes: + self.event_manager.apply(mode="startup")
+ + """ + Operations - MDP + """ + +
[文档] def step(self, action: torch.Tensor) -> VecEnvStepReturn: + """Execute one time-step of the environment's dynamics and reset terminated environments. + + Unlike the :class:`ManagerBasedEnv.step` class, the function performs the following operations: + + 1. Process the actions. + 2. Perform physics stepping. + 3. Perform rendering if gui is enabled. + 4. Update the environment counters and compute the rewards and terminations. + 5. Reset the environments that terminated. + 6. Compute the observations. + 7. Return the observations, rewards, resets and extras. + + Args: + 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. + """ + # process actions + self.action_manager.process_action(action.to(self.device)) + + # check if we need to do rendering within the physics loop + # note: checked here once to avoid multiple checks within the loop + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() + + # perform physics stepping + for _ in range(self.cfg.decimation): + self._sim_step_counter += 1 + # set actions into buffers + self.action_manager.apply_action() + # set actions into simulator + self.scene.write_data_to_sim() + # simulate + self.sim.step(render=False) + # render between steps only if the GUI or an RTX sensor needs it + # note: we assume the render interval to be the shortest accepted rendering interval. + # If a camera needs rendering at a faster frequency, this will lead to unexpected behavior. + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: + self.sim.render() + # update buffers at sim dt + self.scene.update(dt=self.physics_dt) + + # post-step: + # -- update env counters (used for curriculum generation) + self.episode_length_buf += 1 # step in current episode (per env) + self.common_step_counter += 1 # total step (common for all envs) + # -- check terminations + self.reset_buf = self.termination_manager.compute() + self.reset_terminated = self.termination_manager.terminated + self.reset_time_outs = self.termination_manager.time_outs + # -- reward computation + self.reward_buf = self.reward_manager.compute(dt=self.step_dt) + + # -- reset envs that terminated/timed-out and log the episode information + reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) + if len(reset_env_ids) > 0: + self._reset_idx(reset_env_ids) + # if sensors are added to the scene, make sure we render to reflect changes in reset + if self.sim.has_rtx_sensors() and self.cfg.rerender_on_reset: + self.sim.render() + + # -- update command + self.command_manager.compute(dt=self.step_dt) + # -- step interval events + if "interval" in self.event_manager.available_modes: + self.event_manager.apply(mode="interval", dt=self.step_dt) + # -- compute observations + # note: done after reset to get the correct observations for reset envs + self.obs_buf = self.observation_manager.compute() + + # return observations, rewards, resets and extras + return self.obs_buf, self.reward_buf, self.reset_terminated, self.reset_time_outs, self.extras
+ +
[文档] def render(self, recompute: bool = False) -> np.ndarray | None: + """Run rendering without stepping through the physics. + + By convention, if mode is: + + - **human**: Render to the current display and return nothing. Usually for human consumption. + - **rgb_array**: Return an numpy.ndarray with shape (x, y, 3), representing RGB values for an + x-by-y pixel image, suitable for turning into a video. + + Args: + recompute: Whether to force a render even if the simulator has already rendered the scene. + Defaults to False. + + Returns: + The rendered image as a numpy array if mode is "rgb_array". Otherwise, returns None. + + Raises: + RuntimeError: If mode is set to "rgb_data" and simulation render mode does not support it. + In this case, the simulation render mode must be set to ``RenderMode.PARTIAL_RENDERING`` + or ``RenderMode.FULL_RENDERING``. + NotImplementedError: If an unsupported rendering mode is specified. + """ + # run a rendering step of the simulator + # if we have rtx sensors, we do not need to render again sin + if not self.sim.has_rtx_sensors() and not recompute: + self.sim.render() + # decide the rendering mode + if self.render_mode == "human" or self.render_mode is None: + return None + elif self.render_mode == "rgb_array": + # check that if any render could have happened + if self.sim.render_mode.value < self.sim.RenderMode.PARTIAL_RENDERING.value: + raise RuntimeError( + f"Cannot render '{self.render_mode}' when the simulation render mode is" + f" '{self.sim.render_mode.name}'. Please set the simulation render mode to:" + f"'{self.sim.RenderMode.PARTIAL_RENDERING.name}' or '{self.sim.RenderMode.FULL_RENDERING.name}'." + " If running headless, make sure --enable_cameras is set." + ) + # create the annotator if it does not exist + if not hasattr(self, "_rgb_annotator"): + import omni.replicator.core as rep + + # create render product + self._render_product = rep.create.render_product( + self.cfg.viewer.cam_prim_path, self.cfg.viewer.resolution + ) + # create rgb annotator -- used to read data from the render product + self._rgb_annotator = rep.AnnotatorRegistry.get_annotator("rgb", device="cpu") + self._rgb_annotator.attach([self._render_product]) + # obtain the rgb data + rgb_data = self._rgb_annotator.get_data() + # convert to numpy array + rgb_data = np.frombuffer(rgb_data, dtype=np.uint8).reshape(*rgb_data.shape) + # return the rgb data + # note: initially the renerer is warming up and returns empty data + if rgb_data.size == 0: + return np.zeros((self.cfg.viewer.resolution[1], self.cfg.viewer.resolution[0], 3), dtype=np.uint8) + else: + return rgb_data[:, :, :3] + else: + raise NotImplementedError( + f"Render mode '{self.render_mode}' is not supported. Please use: {self.metadata['render_modes']}." + )
+ +
[文档] def close(self): + if not self._is_closed: + # destructor is order-sensitive + del self.command_manager + del self.reward_manager + del self.termination_manager + del self.curriculum_manager + # call the parent class to close the environment + super().close()
+ + """ + Helper functions. + """ + + def _configure_gym_env_spaces(self): + """Configure the action and observation spaces for the Gym environment.""" + # observation space (unbounded since we don't impose any limits) + self.single_observation_space = gym.spaces.Dict() + for group_name, group_term_names in self.observation_manager.active_terms.items(): + # extract quantities about the group + has_concatenated_obs = self.observation_manager.group_obs_concatenate[group_name] + group_dim = self.observation_manager.group_obs_dim[group_name] + # check if group is concatenated or not + # if not concatenated, then we need to add each term separately as a dictionary + if has_concatenated_obs: + self.single_observation_space[group_name] = gym.spaces.Box(low=-np.inf, high=np.inf, shape=group_dim) + else: + self.single_observation_space[group_name] = gym.spaces.Dict({ + term_name: gym.spaces.Box(low=-np.inf, high=np.inf, shape=term_dim) + for term_name, term_dim in zip(group_term_names, group_dim) + }) + # action space (unbounded since we don't impose any limits) + action_dim = sum(self.action_manager.action_term_dim) + self.single_action_space = gym.spaces.Box(low=-np.inf, high=np.inf, shape=(action_dim,)) + + # batch the spaces for vectorized environments + self.observation_space = gym.vector.utils.batch_space(self.single_observation_space, self.num_envs) + self.action_space = gym.vector.utils.batch_space(self.single_action_space, self.num_envs) + + def _reset_idx(self, env_ids: Sequence[int]): + """Reset environments based on specified indices. + + Args: + env_ids: List of environment ids which must be reset + """ + # update the curriculum for environments that need a reset + self.curriculum_manager.compute(env_ids=env_ids) + # reset the internal buffers of the scene elements + self.scene.reset(env_ids) + # apply events such as randomizations for environments that need a reset + if "reset" in self.event_manager.available_modes: + env_step_count = self._sim_step_counter // self.cfg.decimation + self.event_manager.apply(mode="reset", env_ids=env_ids, global_env_step_count=env_step_count) + + # iterate over all managers and reset them + # this returns a dictionary of information which is stored in the extras + # note: This is order-sensitive! Certain things need be reset before others. + self.extras["log"] = dict() + # -- observation manager + info = self.observation_manager.reset(env_ids) + self.extras["log"].update(info) + # -- action manager + info = self.action_manager.reset(env_ids) + self.extras["log"].update(info) + # -- rewards manager + info = self.reward_manager.reset(env_ids) + self.extras["log"].update(info) + # -- curriculum manager + info = self.curriculum_manager.reset(env_ids) + self.extras["log"].update(info) + # -- command manager + info = self.command_manager.reset(env_ids) + self.extras["log"].update(info) + # -- event manager + info = self.event_manager.reset(env_ids) + self.extras["log"].update(info) + # -- termination manager + info = self.termination_manager.reset(env_ids) + self.extras["log"].update(info) + + # reset the episode length buffer + self.episode_length_buf[env_ids] = 0
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/manager_based_rl_env_cfg.html b/_modules/omni/isaac/lab/envs/manager_based_rl_env_cfg.html new file mode 100644 index 0000000000..5ac210c63e --- /dev/null +++ b/_modules/omni/isaac/lab/envs/manager_based_rl_env_cfg.html @@ -0,0 +1,639 @@ + + + + + + + + + + + omni.isaac.lab.envs.manager_based_rl_env_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.manager_based_rl_env_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.utils import configclass
+
+from .manager_based_env_cfg import ManagerBasedEnvCfg
+from .ui import ManagerBasedRLEnvWindow
+
+
+
[文档]@configclass +class ManagerBasedRLEnvCfg(ManagerBasedEnvCfg): + """Configuration for a reinforcement learning environment with the manager-based workflow.""" + + # ui settings + ui_window_class_type: type | None = ManagerBasedRLEnvWindow + + # general settings + is_finite_horizon: bool = False + """Whether the learning task is treated as a finite or infinite horizon problem for the agent. + Defaults to False, which means the task is treated as an infinite horizon problem. + + This flag handles the subtleties of finite and infinite horizon tasks: + + * **Finite horizon**: no penalty or bootstrapping value is required by the the agent for + running out of time. However, the environment still needs to terminate the episode after the + time limit is reached. + * **Infinite horizon**: the agent needs to bootstrap the value of the state at the end of the episode. + This is done by sending a time-limit (or truncated) done signal to the agent, which triggers this + bootstrapping calculation. + + If True, then the environment is treated as a finite horizon problem and no time-out (or truncated) done signal + is sent to the agent. If False, then the environment is treated as an infinite horizon problem and a time-out + (or truncated) done signal is sent to the agent. + + Note: + The base :class:`ManagerBasedRLEnv` class does not use this flag directly. It is used by the environment + wrappers to determine what type of done signal to send to the corresponding learning agent. + """ + + episode_length_s: float = MISSING + """Duration of an episode (in seconds). + + Based on the decimation rate and physics time step, the episode length is calculated as: + + .. code-block:: python + + episode_length_steps = ceil(episode_length_s / (decimation_rate * physics_time_step)) + + For example, if the decimation rate is 10, the physics time step is 0.01, and the episode length is 10 seconds, + then the episode length in steps is 100. + """ + + # environment settings + rewards: object = MISSING + """Reward settings. + + Please refer to the :class:`omni.isaac.lab.managers.RewardManager` class for more details. + """ + + terminations: object = MISSING + """Termination settings. + + Please refer to the :class:`omni.isaac.lab.managers.TerminationManager` class for more details. + """ + + curriculum: object | None = None + """Curriculum settings. Defaults to None, in which case no curriculum is applied. + + Please refer to the :class:`omni.isaac.lab.managers.CurriculumManager` class for more details. + """ + + commands: object | None = None + """Command settings. Defaults to None, in which case no commands are generated. + + Please refer to the :class:`omni.isaac.lab.managers.CommandManager` class for more details. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/actions/actions_cfg.html b/_modules/omni/isaac/lab/envs/mdp/actions/actions_cfg.html new file mode 100644 index 0000000000..8327b57b1f --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/actions/actions_cfg.html @@ -0,0 +1,809 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.actions.actions_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.actions.actions_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.controllers import DifferentialIKControllerCfg
+from omni.isaac.lab.managers.action_manager import ActionTerm, ActionTermCfg
+from omni.isaac.lab.utils import configclass
+
+from . import binary_joint_actions, joint_actions, joint_actions_to_limits, non_holonomic_actions, task_space_actions
+
+##
+# Joint actions.
+##
+
+
+
[文档]@configclass +class JointActionCfg(ActionTermCfg): + """Configuration for the base joint action term. + + See :class:`JointAction` for more details. + """ + + joint_names: list[str] = MISSING + """List of joint names or regex expressions that the action will be mapped to.""" + scale: float | dict[str, float] = 1.0 + """Scale factor for the action (float or dict of regex expressions). Defaults to 1.0.""" + offset: float | dict[str, float] = 0.0 + """Offset factor for the action (float or dict of regex expressions). Defaults to 0.0.""" + preserve_order: bool = False + """Whether to preserve the order of the joint names in the action output. Defaults to False."""
+ + +
[文档]@configclass +class JointPositionActionCfg(JointActionCfg): + """Configuration for the joint position action term. + + See :class:`JointPositionAction` for more details. + """ + + class_type: type[ActionTerm] = joint_actions.JointPositionAction + + use_default_offset: bool = True + """Whether to use default joint positions configured in the articulation asset as offset. + Defaults to True. + + If True, this flag results in overwriting the values of :attr:`offset` to the default joint positions + from the articulation asset. + """
+ + +
[文档]@configclass +class RelativeJointPositionActionCfg(JointActionCfg): + """Configuration for the relative joint position action term. + + See :class:`RelativeJointPositionAction` for more details. + """ + + class_type: type[ActionTerm] = joint_actions.RelativeJointPositionAction + + use_zero_offset: bool = True + """Whether to ignore the offset defined in articulation asset. Defaults to True. + + If True, this flag results in overwriting the values of :attr:`offset` to zero. + """
+ + +
[文档]@configclass +class JointVelocityActionCfg(JointActionCfg): + """Configuration for the joint velocity action term. + + See :class:`JointVelocityAction` for more details. + """ + + class_type: type[ActionTerm] = joint_actions.JointVelocityAction + + use_default_offset: bool = True + """Whether to use default joint velocities configured in the articulation asset as offset. + Defaults to True. + + This overrides the settings from :attr:`offset` if set to True. + """
+ + +
[文档]@configclass +class JointEffortActionCfg(JointActionCfg): + """Configuration for the joint effort action term. + + See :class:`JointEffortAction` for more details. + """ + + class_type: type[ActionTerm] = joint_actions.JointEffortAction
+ + +## +# Joint actions rescaled to limits. +## + + +
[文档]@configclass +class JointPositionToLimitsActionCfg(ActionTermCfg): + """Configuration for the bounded joint position action term. + + See :class:`JointPositionWithinLimitsAction` for more details. + """ + + class_type: type[ActionTerm] = joint_actions_to_limits.JointPositionToLimitsAction + + joint_names: list[str] = MISSING + """List of joint names or regex expressions that the action will be mapped to.""" + + scale: float | dict[str, float] = 1.0 + """Scale factor for the action (float or dict of regex expressions). Defaults to 1.0.""" + + rescale_to_limits: bool = True + """Whether to rescale the action to the joint limits. Defaults to True. + + If True, the input actions are rescaled to the joint limits, i.e., the action value in + the range [-1, 1] corresponds to the joint lower and upper limits respectively. + + Note: + This operation is performed after applying the scale factor. + """
+ + +
[文档]@configclass +class EMAJointPositionToLimitsActionCfg(JointPositionToLimitsActionCfg): + """Configuration for the exponential moving average (EMA) joint position action term. + + See :class:`EMAJointPositionToLimitsAction` for more details. + """ + + class_type: type[ActionTerm] = joint_actions_to_limits.EMAJointPositionToLimitsAction + + alpha: float | dict[str, float] = 1.0 + """The weight for the moving average (float or dict of regex expressions). Defaults to 1.0. + + If set to 1.0, the processed action is applied directly without any moving average window. + """
+ + +## +# Gripper actions. +## + + +
[文档]@configclass +class BinaryJointActionCfg(ActionTermCfg): + """Configuration for the base binary joint action term. + + See :class:`BinaryJointAction` for more details. + """ + + joint_names: list[str] = MISSING + """List of joint names or regex expressions that the action will be mapped to.""" + open_command_expr: dict[str, float] = MISSING + """The joint command to move to *open* configuration.""" + close_command_expr: dict[str, float] = MISSING + """The joint command to move to *close* configuration."""
+ + +
[文档]@configclass +class BinaryJointPositionActionCfg(BinaryJointActionCfg): + """Configuration for the binary joint position action term. + + See :class:`BinaryJointPositionAction` for more details. + """ + + class_type: type[ActionTerm] = binary_joint_actions.BinaryJointPositionAction
+ + +
[文档]@configclass +class BinaryJointVelocityActionCfg(BinaryJointActionCfg): + """Configuration for the binary joint velocity action term. + + See :class:`BinaryJointVelocityAction` for more details. + """ + + class_type: type[ActionTerm] = binary_joint_actions.BinaryJointVelocityAction
+ + +## +# Non-holonomic actions. +## + + +
[文档]@configclass +class NonHolonomicActionCfg(ActionTermCfg): + """Configuration for the non-holonomic action term with dummy joints at the base. + + See :class:`NonHolonomicAction` for more details. + """ + + class_type: type[ActionTerm] = non_holonomic_actions.NonHolonomicAction + + body_name: str = MISSING + """Name of the body which has the dummy mechanism connected to.""" + x_joint_name: str = MISSING + """The dummy joint name in the x direction.""" + y_joint_name: str = MISSING + """The dummy joint name in the y direction.""" + yaw_joint_name: str = MISSING + """The dummy joint name in the yaw direction.""" + scale: tuple[float, float] = (1.0, 1.0) + """Scale factor for the action. Defaults to (1.0, 1.0).""" + offset: tuple[float, float] = (0.0, 0.0) + """Offset factor for the action. Defaults to (0.0, 0.0)."""
+ + +## +# Task-space Actions. +## + + +
[文档]@configclass +class DifferentialInverseKinematicsActionCfg(ActionTermCfg): + """Configuration for inverse differential kinematics action term. + + See :class:`DifferentialInverseKinematicsAction` for more details. + """ + +
[文档] @configclass + class OffsetCfg: + """The offset pose from parent frame to child frame. + + On many robots, end-effector frames are fictitious frames that do not have a corresponding + rigid body. In such cases, it is easier to define this transform w.r.t. their parent rigid body. + For instance, for the Franka Emika arm, the end-effector is defined at an offset to the the + "panda_hand" frame. + """ + + 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)."""
+ + class_type: type[ActionTerm] = task_space_actions.DifferentialInverseKinematicsAction + + joint_names: list[str] = MISSING + """List of joint names or regex expressions that the action will be mapped to.""" + body_name: str = MISSING + """Name of the body or frame for which IK is performed.""" + body_offset: OffsetCfg | None = None + """Offset of target frame w.r.t. to the body frame. Defaults to None, in which case no offset is applied.""" + scale: float | tuple[float, ...] = 1.0 + """Scale factor for the action. Defaults to 1.0.""" + controller: DifferentialIKControllerCfg = MISSING + """The configuration for the differential IK controller."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/commands/commands_cfg.html b/_modules/omni/isaac/lab/envs/mdp/commands/commands_cfg.html new file mode 100644 index 0000000000..0efa3c0861 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/commands/commands_cfg.html @@ -0,0 +1,807 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.commands.commands_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.commands.commands_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import math
+from dataclasses import MISSING
+
+from omni.isaac.lab.managers import CommandTermCfg
+from omni.isaac.lab.markers import VisualizationMarkersCfg
+from omni.isaac.lab.markers.config import BLUE_ARROW_X_MARKER_CFG, FRAME_MARKER_CFG, GREEN_ARROW_X_MARKER_CFG
+from omni.isaac.lab.utils import configclass
+
+from .null_command import NullCommand
+from .pose_2d_command import TerrainBasedPose2dCommand, UniformPose2dCommand
+from .pose_command import UniformPoseCommand
+from .velocity_command import NormalVelocityCommand, UniformVelocityCommand
+
+
+
[文档]@configclass +class NullCommandCfg(CommandTermCfg): + """Configuration for the null command generator.""" + + class_type: type = NullCommand + + def __post_init__(self): + """Post initialization.""" + # set the resampling time range to infinity to avoid resampling + self.resampling_time_range = (math.inf, math.inf)
+ + +
[文档]@configclass +class UniformVelocityCommandCfg(CommandTermCfg): + """Configuration for the uniform velocity command generator.""" + + class_type: type = UniformVelocityCommand + + asset_name: str = MISSING + """Name of the asset in the environment for which the commands are generated.""" + + heading_command: bool = False + """Whether to use heading command or angular velocity command. Defaults to False. + + If True, the angular velocity command is computed from the heading error, where the + target heading is sampled uniformly from provided range. Otherwise, the angular velocity + command is sampled uniformly from provided range. + """ + + heading_control_stiffness: float = 1.0 + """Scale factor to convert the heading error to angular velocity command. Defaults to 1.0.""" + + rel_standing_envs: float = 0.0 + """The sampled probability of environments that should be standing still. Defaults to 0.0.""" + + rel_heading_envs: float = 1.0 + """The sampled probability of environments where the robots follow the heading-based angular velocity command + (the others follow the sampled angular velocity command). Defaults to 1.0. + + This parameter is only used if :attr:`heading_command` is True. + """ + +
[文档] @configclass + class Ranges: + """Uniform distribution ranges for the velocity commands.""" + + lin_vel_x: tuple[float, float] = MISSING + """Range for the linear-x velocity command (in m/s).""" + + lin_vel_y: tuple[float, float] = MISSING + """Range for the linear-y velocity command (in m/s).""" + + ang_vel_z: tuple[float, float] = MISSING + """Range for the angular-z velocity command (in rad/s).""" + + heading: tuple[float, float] | None = None + """Range for the heading command (in rad). Defaults to None. + + This parameter is only used if :attr:`~UniformVelocityCommandCfg.heading_command` is True. + """
+ + ranges: Ranges = MISSING + """Distribution ranges for the velocity commands.""" + + goal_vel_visualizer_cfg: VisualizationMarkersCfg = GREEN_ARROW_X_MARKER_CFG.replace( + prim_path="/Visuals/Command/velocity_goal" + ) + """The configuration for the goal velocity visualization marker. Defaults to GREEN_ARROW_X_MARKER_CFG.""" + + current_vel_visualizer_cfg: VisualizationMarkersCfg = BLUE_ARROW_X_MARKER_CFG.replace( + prim_path="/Visuals/Command/velocity_current" + ) + """The configuration for the current velocity visualization marker. Defaults to BLUE_ARROW_X_MARKER_CFG.""" + + # Set the scale of the visualization markers to (0.5, 0.5, 0.5) + goal_vel_visualizer_cfg.markers["arrow"].scale = (0.5, 0.5, 0.5) + current_vel_visualizer_cfg.markers["arrow"].scale = (0.5, 0.5, 0.5)
+ + +
[文档]@configclass +class NormalVelocityCommandCfg(UniformVelocityCommandCfg): + """Configuration for the normal velocity command generator.""" + + class_type: type = NormalVelocityCommand + heading_command: bool = False # --> we don't use heading command for normal velocity command. + +
[文档] @configclass + class Ranges: + """Normal distribution ranges for the velocity commands.""" + + mean_vel: tuple[float, float, float] = MISSING + """Mean velocity for the normal distribution (in m/s). + + The tuple contains the mean linear-x, linear-y, and angular-z velocity. + """ + + std_vel: tuple[float, float, float] = MISSING + """Standard deviation for the normal distribution (in m/s). + + The tuple contains the standard deviation linear-x, linear-y, and angular-z velocity. + """ + + zero_prob: tuple[float, float, float] = MISSING + """Probability of zero velocity for the normal distribution. + + The tuple contains the probability of zero linear-x, linear-y, and angular-z velocity. + """
+ + ranges: Ranges = MISSING + """Distribution ranges for the velocity commands."""
+ + +
[文档]@configclass +class UniformPoseCommandCfg(CommandTermCfg): + """Configuration for uniform pose command generator.""" + + class_type: type = UniformPoseCommand + + asset_name: str = MISSING + """Name of the asset in the environment for which the commands are generated.""" + + body_name: str = MISSING + """Name of the body in the asset for which the commands are generated.""" + + make_quat_unique: bool = False + """Whether to make the quaternion unique or not. Defaults to False. + + If True, the quaternion is made unique by ensuring the real part is positive. + """ + +
[文档] @configclass + class Ranges: + """Uniform distribution ranges for the pose commands.""" + + pos_x: tuple[float, float] = MISSING + """Range for the x position (in m).""" + + pos_y: tuple[float, float] = MISSING + """Range for the y position (in m).""" + + pos_z: tuple[float, float] = MISSING + """Range for the z position (in m).""" + + roll: tuple[float, float] = MISSING + """Range for the roll angle (in rad).""" + + pitch: tuple[float, float] = MISSING + """Range for the pitch angle (in rad).""" + + yaw: tuple[float, float] = MISSING + """Range for the yaw angle (in rad)."""
+ + ranges: Ranges = MISSING + """Ranges for the commands.""" + + goal_pose_visualizer_cfg: VisualizationMarkersCfg = FRAME_MARKER_CFG.replace(prim_path="/Visuals/Command/goal_pose") + """The configuration for the goal pose visualization marker. Defaults to FRAME_MARKER_CFG.""" + + current_pose_visualizer_cfg: VisualizationMarkersCfg = FRAME_MARKER_CFG.replace( + prim_path="/Visuals/Command/body_pose" + ) + """The configuration for the current pose visualization marker. Defaults to FRAME_MARKER_CFG.""" + + # Set the scale of the visualization markers to (0.1, 0.1, 0.1) + goal_pose_visualizer_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) + current_pose_visualizer_cfg.markers["frame"].scale = (0.1, 0.1, 0.1)
+ + +
[文档]@configclass +class UniformPose2dCommandCfg(CommandTermCfg): + """Configuration for the uniform 2D-pose command generator.""" + + class_type: type = UniformPose2dCommand + + asset_name: str = MISSING + """Name of the asset in the environment for which the commands are generated.""" + + simple_heading: bool = MISSING + """Whether to use simple heading or not. + + If True, the heading is in the direction of the target position. + """ + +
[文档] @configclass + class Ranges: + """Uniform distribution ranges for the position commands.""" + + pos_x: tuple[float, float] = MISSING + """Range for the x position (in m).""" + + pos_y: tuple[float, float] = MISSING + """Range for the y position (in m).""" + + heading: tuple[float, float] = MISSING + """Heading range for the position commands (in rad). + + Used only if :attr:`simple_heading` is False. + """
+ + ranges: Ranges = MISSING + """Distribution ranges for the position commands.""" + + goal_pose_visualizer_cfg: VisualizationMarkersCfg = GREEN_ARROW_X_MARKER_CFG.replace( + prim_path="/Visuals/Command/pose_goal" + ) + """The configuration for the goal pose visualization marker. Defaults to GREEN_ARROW_X_MARKER_CFG.""" + + # Set the scale of the visualization markers to (0.2, 0.2, 0.8) + goal_pose_visualizer_cfg.markers["arrow"].scale = (0.2, 0.2, 0.8)
+ + +
[文档]@configclass +class TerrainBasedPose2dCommandCfg(UniformPose2dCommandCfg): + """Configuration for the terrain-based position command generator.""" + + class_type = TerrainBasedPose2dCommand + +
[文档] @configclass + class Ranges: + """Uniform distribution ranges for the position commands.""" + + heading: tuple[float, float] = MISSING + """Heading range for the position commands (in rad). + + Used only if :attr:`simple_heading` is False. + """
+ + ranges: Ranges = MISSING + """Distribution ranges for the sampled commands."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/curriculums.html b/_modules/omni/isaac/lab/envs/mdp/curriculums.html new file mode 100644 index 0000000000..66798b0abf --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/curriculums.html @@ -0,0 +1,595 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.curriculums — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.curriculums 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Common functions that can be used to create curriculum for the learning environment.
+
+The functions can be passed to the :class:`omni.isaac.lab.managers.CurriculumTermCfg` object to enable
+the curriculum introduced by the function.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+
+
+
[文档]def modify_reward_weight(env: ManagerBasedRLEnv, env_ids: Sequence[int], term_name: str, weight: float, num_steps: int): + """Curriculum that modifies a reward weight a given number of steps. + + Args: + env: The learning environment. + env_ids: Not used since all environments are affected. + term_name: The name of the reward term. + weight: The weight of the reward term. + num_steps: The number of steps after which the change should be applied. + """ + if env.common_step_counter > num_steps: + # obtain term settings + term_cfg = env.reward_manager.get_term_cfg(term_name) + # update term settings + term_cfg.weight = weight + env.reward_manager.set_term_cfg(term_name, term_cfg)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/events.html b/_modules/omni/isaac/lab/envs/mdp/events.html new file mode 100644 index 0000000000..44e26dbabf --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/events.html @@ -0,0 +1,1563 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.events — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.events 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Common functions that can be used to enable different events.
+
+Events include anything related to altering the simulation state. This includes changing the physics
+materials, applying external forces, and resetting the state of the asset.
+
+The functions can be passed to the :class:`omni.isaac.lab.managers.EventTermCfg` object to enable
+the event introduced by the function.
+"""
+
+from __future__ import annotations
+
+import torch
+from typing import TYPE_CHECKING, Literal
+
+import carb
+import omni.physics.tensors.impl.api as physx
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.actuators import ImplicitActuator
+from omni.isaac.lab.assets import Articulation, DeformableObject, RigidObject
+from omni.isaac.lab.managers import EventTermCfg, ManagerTermBase, SceneEntityCfg
+from omni.isaac.lab.terrains import TerrainImporter
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedEnv
+
+
+
[文档]class randomize_rigid_body_material(ManagerTermBase): + """Randomize the physics materials on all geometries of the asset. + + This function creates a set of physics materials with random static friction, dynamic friction, and restitution + values. The number of materials is specified by ``num_buckets``. The materials are generated by sampling + 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 (num_instances, max_num_shapes) where ``num_instances`` + is the number of assets spawned and ``max_num_shapes`` is the maximum number of shapes in the asset (over + all bodies). The integer values are used as indices to select the material properties from the + material buckets. + + If the flag ``make_consistent`` is set to ``True``, the dynamic friction is set to be less than or equal to + the static friction. This obeys the physics constraint on friction values. However, it may not always be + essential for the application. Thus, the flag is set to ``False`` by default. + + .. attention:: + This function uses CPU tensors to assign the material properties. It is recommended to use this function + only during the initialization of the environment. Otherwise, it may lead to a significant performance + overhead. + + .. note:: + PhysX only allows 64000 unique physics materials in the scene. If the number of materials exceeds this + limit, the simulation will crash. Due to this reason, we sample the materials only once during initialization. + Afterwards, these materials are randomly assigned to the geometries of the asset. + """ + +
[文档] def __init__(self, cfg: EventTermCfg, env: ManagerBasedEnv): + """Initialize the term. + + Args: + cfg: The configuration of the event term. + env: The environment instance. + + Raises: + ValueError: If the asset is not a RigidObject or an Articulation. + """ + super().__init__(cfg, env) + + # extract the used quantities (to enable type-hinting) + self.asset_cfg: SceneEntityCfg = cfg.params["asset_cfg"] + self.asset: RigidObject | Articulation = env.scene[self.asset_cfg.name] + + if not isinstance(self.asset, (RigidObject, Articulation)): + raise ValueError( + f"Randomization term 'randomize_rigid_body_material' not supported for asset: '{self.asset_cfg.name}'" + f" with type: '{type(self.asset)}'." + ) + + # obtain number of shapes per body (needed for indexing the material properties correctly) + # note: this is a workaround since the Articulation does not provide a direct way to obtain the number of shapes + # per body. We use the physics simulation view to obtain the number of shapes per body. + if isinstance(self.asset, Articulation) and self.asset_cfg.body_ids != slice(None): + self.num_shapes_per_body = [] + for link_path in self.asset.root_physx_view.link_paths[0]: + link_physx_view = self.asset._physics_sim_view.create_rigid_body_view(link_path) # type: ignore + self.num_shapes_per_body.append(link_physx_view.max_shapes) + # ensure the parsing is correct + num_shapes = sum(self.num_shapes_per_body) + expected_shapes = self.asset.root_physx_view.max_shapes + if num_shapes != expected_shapes: + raise ValueError( + "Randomization term 'randomize_rigid_body_material' failed to parse the number of shapes per body." + f" Expected total shapes: {expected_shapes}, but got: {num_shapes}." + ) + else: + # in this case, we don't need to do special indexing + self.num_shapes_per_body = None + + # obtain parameters for sampling friction and restitution values + static_friction_range = cfg.params.get("static_friction_range", (1.0, 1.0)) + dynamic_friction_range = cfg.params.get("dynamic_friction_range", (1.0, 1.0)) + restitution_range = cfg.params.get("restitution_range", (0.0, 0.0)) + num_buckets = int(cfg.params.get("num_buckets", 1)) + + # sample material properties from the given ranges + # note: we only sample the materials once during initialization + # afterwards these are randomly assigned to the geometries of the asset + range_list = [static_friction_range, dynamic_friction_range, restitution_range] + ranges = torch.tensor(range_list, device="cpu") + self.material_buckets = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (num_buckets, 3), device="cpu") + + # ensure dynamic friction is always less than static friction + make_consistent = cfg.params.get("make_consistent", False) + if make_consistent: + self.material_buckets[:, 1] = torch.min(self.material_buckets[:, 0], self.material_buckets[:, 1])
+ + def __call__( + self, + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + static_friction_range: tuple[float, float], + dynamic_friction_range: tuple[float, float], + restitution_range: tuple[float, float], + num_buckets: int, + asset_cfg: SceneEntityCfg, + make_consistent: bool = False, + ): + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device="cpu") + else: + env_ids = env_ids.cpu() + + # randomly assign material IDs to the geometries + total_num_shapes = self.asset.root_physx_view.max_shapes + bucket_ids = torch.randint(0, num_buckets, (len(env_ids), total_num_shapes), device="cpu") + material_samples = self.material_buckets[bucket_ids] + + # retrieve material buffer from the physics simulation + materials = self.asset.root_physx_view.get_material_properties() + + # update material buffer with new samples + if self.num_shapes_per_body is not None: + # sample material properties from the given ranges + for body_id in self.asset_cfg.body_ids: + # obtain indices of shapes for the body + start_idx = sum(self.num_shapes_per_body[:body_id]) + end_idx = start_idx + self.num_shapes_per_body[body_id] + # assign the new materials + # material samples are of shape: num_env_ids x total_num_shapes x 3 + materials[env_ids, start_idx:end_idx] = material_samples[:, start_idx:end_idx] + else: + # assign all the materials + materials[env_ids] = material_samples[:] + + # apply to simulation + self.asset.root_physx_view.set_material_properties(materials, env_ids)
+ + +
[文档]def randomize_rigid_body_mass( + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + asset_cfg: SceneEntityCfg, + mass_distribution_params: tuple[float, float], + operation: Literal["add", "scale", "abs"], + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", + recompute_inertia: bool = True, +): + """Randomize the mass of the bodies by adding, scaling, or setting random values. + + This function allows randomizing the mass of the bodies of the asset. The function samples random values from the + given distribution parameters and adds, scales, or sets the values into the physics simulation based on the operation. + + If the ``recompute_inertia`` flag is set to ``True``, the function recomputes the inertia tensor of the bodies + after setting the mass. This is useful when the mass is changed significantly, as the inertia tensor depends + on the mass. It assumes the body is a uniform density object. If the body is not a uniform density object, + the inertia tensor may not be accurate. + + .. tip:: + This function uses CPU tensors to assign the body masses. It is recommended to use this function + only during the initialization of the environment. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device="cpu") + else: + env_ids = env_ids.cpu() + + # resolve body indices + if asset_cfg.body_ids == slice(None): + body_ids = torch.arange(asset.num_bodies, dtype=torch.int, device="cpu") + else: + body_ids = torch.tensor(asset_cfg.body_ids, dtype=torch.int, device="cpu") + + # get the current masses of the bodies (num_assets, num_bodies) + masses = asset.root_physx_view.get_masses() + + # apply randomization on default values + # this is to make sure when calling the function multiple times, the randomization is applied on the + # default values and not the previously randomized values + masses[env_ids[:, None], body_ids] = asset.data.default_mass[env_ids[:, None], body_ids].clone() + + # sample from the given range + # note: we modify the masses in-place for all environments + # however, the setter takes care that only the masses of the specified environments are modified + masses = _randomize_prop_by_op( + masses, mass_distribution_params, env_ids, body_ids, operation=operation, distribution=distribution + ) + + # set the mass into the physics simulation + asset.root_physx_view.set_masses(masses, env_ids) + + # recompute inertia tensors if needed + if recompute_inertia: + # compute the ratios of the new masses to the initial masses + ratios = masses[env_ids[:, None], body_ids] / asset.data.default_mass[env_ids[:, None], body_ids] + # scale the inertia tensors by the the ratios + # since mass randomization is done on default values, we can use the default inertia tensors + inertias = asset.root_physx_view.get_inertias() + if isinstance(asset, Articulation): + # inertia has shape: (num_envs, num_bodies, 9) for articulation + inertias[env_ids[:, None], body_ids] = ( + asset.data.default_inertia[env_ids[:, None], body_ids] * ratios[..., None] + ) + else: + # inertia has shape: (num_envs, 9) for rigid object + inertias[env_ids] = asset.data.default_inertia[env_ids] * ratios + # set the inertia tensors into the physics simulation + asset.root_physx_view.set_inertias(inertias, env_ids)
+ + +
[文档]def randomize_physics_scene_gravity( + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + gravity_distribution_params: tuple[list[float], list[float]], + operation: Literal["add", "scale", "abs"], + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", +): + """Randomize gravity by adding, scaling, or setting random values. + + This function allows randomizing gravity of the physics scene. The function samples random values from the + given distribution parameters and adds, scales, or sets the values into the physics simulation based on the + operation. + + The distribution parameters are lists of two elements each, representing the lower and upper bounds of the + distribution for the x, y, and z components of the gravity vector. The function samples random values for each + component independently. + + .. attention:: + This function applied the same gravity for all the environments. + + .. tip:: + This function uses CPU tensors to assign gravity. + """ + # get the current gravity + gravity = torch.tensor(env.sim.cfg.gravity, device="cpu").unsqueeze(0) + dist_param_0 = torch.tensor(gravity_distribution_params[0], device="cpu") + dist_param_1 = torch.tensor(gravity_distribution_params[1], device="cpu") + gravity = _randomize_prop_by_op( + gravity, + (dist_param_0, dist_param_1), + None, + slice(None), + operation=operation, + distribution=distribution, + ) + # unbatch the gravity tensor into a list + gravity = gravity[0].tolist() + + # set the gravity into the physics simulation + physics_sim_view: physx.SimulationView = sim_utils.SimulationContext.instance().physics_sim_view + physics_sim_view.set_gravity(carb.Float3(*gravity))
+ + +
[文档]def randomize_actuator_gains( + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + asset_cfg: SceneEntityCfg, + stiffness_distribution_params: tuple[float, float] | None = None, + damping_distribution_params: tuple[float, float] | None = None, + operation: Literal["add", "scale", "abs"] = "abs", + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", +): + """Randomize the actuator gains in an articulation by adding, scaling, or setting random values. + + This function allows randomizing the actuator stiffness and damping gains. + + The function samples random values from the given distribution parameters and applies the operation to the joint properties. + It then sets the values into the actuator models. If the distribution parameters are not provided for a particular property, + the function does not modify the property. + + .. tip:: + For implicit actuators, this function uses CPU tensors to assign the actuator gains into the simulation. + In such cases, it is recommended to use this function only during the initialization of the environment. + """ + # Extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + + # Resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device=asset.device) + + def randomize(data: torch.Tensor, params: tuple[float, float]) -> torch.Tensor: + return _randomize_prop_by_op( + data, params, dim_0_ids=None, dim_1_ids=actuator_indices, operation=operation, distribution=distribution + ) + + # Loop through actuators and randomize gains + for actuator in asset.actuators.values(): + if isinstance(asset_cfg.joint_ids, slice): + # we take all the joints of the actuator + actuator_indices = slice(None) + if isinstance(actuator.joint_indices, slice): + global_indices = slice(None) + else: + global_indices = torch.tensor(actuator.joint_indices, device=asset.device) + elif isinstance(actuator.joint_indices, slice): + # we take the joints defined in the asset config + global_indices = actuator_indices = torch.tensor(asset_cfg.joint_ids, device=asset.device) + else: + # we take the intersection of the actuator joints and the asset config joints + actuator_joint_indices = torch.tensor(actuator.joint_indices, device=asset.device) + asset_joint_ids = torch.tensor(asset_cfg.joint_ids, device=asset.device) + # the indices of the joints in the actuator that have to be randomized + actuator_indices = torch.nonzero(torch.isin(actuator_joint_indices, asset_joint_ids)).view(-1) + if len(actuator_indices) == 0: + continue + # maps actuator indices that have to be randomized to global joint indices + global_indices = actuator_joint_indices[actuator_indices] + # Randomize stiffness + if stiffness_distribution_params is not None: + stiffness = actuator.stiffness[env_ids].clone() + stiffness[:, actuator_indices] = asset.data.default_joint_stiffness[env_ids][:, global_indices].clone() + randomize(stiffness, stiffness_distribution_params) + actuator.stiffness[env_ids] = stiffness + if isinstance(actuator, ImplicitActuator): + asset.write_joint_stiffness_to_sim(stiffness, joint_ids=actuator.joint_indices, env_ids=env_ids) + # Randomize damping + if damping_distribution_params is not None: + damping = actuator.damping[env_ids].clone() + damping[:, actuator_indices] = asset.data.default_joint_damping[env_ids][:, global_indices].clone() + randomize(damping, damping_distribution_params) + actuator.damping[env_ids] = damping + if isinstance(actuator, ImplicitActuator): + asset.write_joint_damping_to_sim(damping, joint_ids=actuator.joint_indices, env_ids=env_ids)
+ + +
[文档]def randomize_joint_parameters( + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + asset_cfg: SceneEntityCfg, + friction_distribution_params: tuple[float, float] | None = None, + armature_distribution_params: tuple[float, float] | None = None, + lower_limit_distribution_params: tuple[float, float] | None = None, + upper_limit_distribution_params: tuple[float, float] | None = None, + operation: Literal["add", "scale", "abs"] = "abs", + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", +): + """Randomize the joint parameters of an articulation by adding, scaling, or setting random values. + + This function allows randomizing the joint parameters of the asset. + These correspond to the physics engine joint properties that affect the joint behavior. + + The function samples random values from the given distribution parameters and applies the operation to the joint properties. + It then sets the values into the physics simulation. If the distribution parameters are not provided for a + particular property, the function does not modify the property. + + .. tip:: + This function uses CPU tensors to assign the joint properties. It is recommended to use this function + only during the initialization of the environment. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device=asset.device) + + # resolve joint indices + if asset_cfg.joint_ids == slice(None): + joint_ids = slice(None) # for optimization purposes + else: + joint_ids = torch.tensor(asset_cfg.joint_ids, dtype=torch.int, device=asset.device) + + # sample joint properties from the given ranges and set into the physics simulation + # -- friction + if friction_distribution_params is not None: + friction = asset.data.default_joint_friction.to(asset.device).clone() + friction = _randomize_prop_by_op( + friction, friction_distribution_params, env_ids, joint_ids, operation=operation, distribution=distribution + )[env_ids][:, joint_ids] + asset.write_joint_friction_to_sim(friction, joint_ids=joint_ids, env_ids=env_ids) + # -- armature + if armature_distribution_params is not None: + armature = asset.data.default_joint_armature.to(asset.device).clone() + armature = _randomize_prop_by_op( + armature, armature_distribution_params, env_ids, joint_ids, operation=operation, distribution=distribution + )[env_ids][:, joint_ids] + asset.write_joint_armature_to_sim(armature, joint_ids=joint_ids, env_ids=env_ids) + # -- dof limits + if lower_limit_distribution_params is not None or upper_limit_distribution_params is not None: + dof_limits = asset.data.default_joint_limits.to(asset.device).clone() + if lower_limit_distribution_params is not None: + lower_limits = dof_limits[..., 0] + lower_limits = _randomize_prop_by_op( + lower_limits, + lower_limit_distribution_params, + env_ids, + joint_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, joint_ids] + dof_limits[env_ids[:, None], joint_ids, 0] = lower_limits + if upper_limit_distribution_params is not None: + upper_limits = dof_limits[..., 1] + upper_limits = _randomize_prop_by_op( + upper_limits, + upper_limit_distribution_params, + env_ids, + joint_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, joint_ids] + dof_limits[env_ids[:, None], joint_ids, 1] = upper_limits + if (dof_limits[env_ids[:, None], joint_ids, 0] > dof_limits[env_ids[:, None], joint_ids, 1]).any(): + raise ValueError( + "Randomization term 'randomize_joint_parameters' is setting lower joint limits that are greater than" + " upper joint limits." + ) + + asset.write_joint_limits_to_sim(dof_limits[env_ids][:, joint_ids], joint_ids=joint_ids, env_ids=env_ids)
+ + +
[文档]def randomize_fixed_tendon_parameters( + env: ManagerBasedEnv, + env_ids: torch.Tensor | None, + asset_cfg: SceneEntityCfg, + stiffness_distribution_params: tuple[float, float] | None = None, + damping_distribution_params: tuple[float, float] | None = None, + limit_stiffness_distribution_params: tuple[float, float] | None = None, + lower_limit_distribution_params: tuple[float, float] | None = None, + upper_limit_distribution_params: tuple[float, float] | None = None, + rest_length_distribution_params: tuple[float, float] | None = None, + offset_distribution_params: tuple[float, float] | None = None, + operation: Literal["add", "scale", "abs"] = "abs", + distribution: Literal["uniform", "log_uniform", "gaussian"] = "uniform", +): + """Randomize the fixed tendon parameters of an articulation by adding, scaling, or setting random values. + + This function allows randomizing the fixed tendon parameters of the asset. + These correspond to the physics engine tendon properties that affect the joint behavior. + + The function samples random values from the given distribution parameters and applies the operation to the tendon properties. + It then sets the values into the physics simulation. If the distribution parameters are not provided for a + particular property, the function does not modify the property. + + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device=asset.device) + + # resolve joint indices + if asset_cfg.fixed_tendon_ids == slice(None): + fixed_tendon_ids = slice(None) # for optimization purposes + else: + fixed_tendon_ids = torch.tensor(asset_cfg.fixed_tendon_ids, dtype=torch.int, device=asset.device) + + # sample tendon properties from the given ranges and set into the physics simulation + # -- stiffness + if stiffness_distribution_params is not None: + stiffness = asset.data.default_fixed_tendon_stiffness.clone() + stiffness = _randomize_prop_by_op( + stiffness, + stiffness_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + asset.set_fixed_tendon_stiffness(stiffness, fixed_tendon_ids, env_ids) + # -- damping + if damping_distribution_params is not None: + damping = asset.data.default_fixed_tendon_damping.clone() + damping = _randomize_prop_by_op( + damping, + damping_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + asset.set_fixed_tendon_damping(damping, fixed_tendon_ids, env_ids) + # -- limit stiffness + if limit_stiffness_distribution_params is not None: + limit_stiffness = asset.data.default_fixed_tendon_limit_stiffness.clone() + limit_stiffness = _randomize_prop_by_op( + limit_stiffness, + limit_stiffness_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + asset.set_fixed_tendon_limit_stiffness(limit_stiffness, fixed_tendon_ids, env_ids) + # -- limits + if lower_limit_distribution_params is not None or upper_limit_distribution_params is not None: + limit = asset.data.default_fixed_tendon_limit.clone() + # -- lower limit + if lower_limit_distribution_params is not None: + lower_limit = limit[..., 0] + lower_limit = _randomize_prop_by_op( + lower_limit, + lower_limit_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + limit[env_ids[:, None], fixed_tendon_ids, 0] = lower_limit + # -- upper limit + if upper_limit_distribution_params is not None: + upper_limit = limit[..., 1] + upper_limit = _randomize_prop_by_op( + upper_limit, + upper_limit_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + limit[env_ids[:, None], fixed_tendon_ids, 1] = upper_limit + if (limit[env_ids[:, None], fixed_tendon_ids, 0] > limit[env_ids[:, None], fixed_tendon_ids, 1]).any(): + raise ValueError( + "Randomization term 'randomize_fixed_tendon_parameters' is setting lower tendon limits that are greater" + " than upper tendon limits." + ) + asset.set_fixed_tendon_limit(limit, fixed_tendon_ids, env_ids) + # -- rest length + if rest_length_distribution_params is not None: + rest_length = asset.data.default_fixed_tendon_rest_length.clone() + rest_length = _randomize_prop_by_op( + rest_length, + rest_length_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + asset.set_fixed_tendon_rest_length(rest_length, fixed_tendon_ids, env_ids) + # -- offset + if offset_distribution_params is not None: + offset = asset.data.default_fixed_tendon_offset.clone() + offset = _randomize_prop_by_op( + offset, + offset_distribution_params, + env_ids, + fixed_tendon_ids, + operation=operation, + distribution=distribution, + )[env_ids][:, fixed_tendon_ids] + asset.set_fixed_tendon_offset(offset, fixed_tendon_ids, env_ids) + + asset.write_fixed_tendon_properties_to_sim(fixed_tendon_ids, env_ids)
+ + +
[文档]def apply_external_force_torque( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + force_range: tuple[float, float], + torque_range: tuple[float, float], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Randomize the external forces and torques applied to the bodies. + + This function creates a set of random forces and torques sampled from the given ranges. The number of forces + and torques is equal to the number of bodies times the number of environments. The forces and torques are + applied to the bodies by calling ``asset.set_external_force_and_torque``. The forces and torques are only + applied when ``asset.write_data_to_sim()`` is called in the environment. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + # resolve environment ids + if env_ids is None: + env_ids = torch.arange(env.scene.num_envs, device=asset.device) + # resolve number of bodies + num_bodies = len(asset_cfg.body_ids) if isinstance(asset_cfg.body_ids, list) else asset.num_bodies + + # sample random forces and torques + size = (len(env_ids), num_bodies, 3) + forces = math_utils.sample_uniform(*force_range, size, asset.device) + torques = math_utils.sample_uniform(*torque_range, size, asset.device) + # set the forces and torques into the buffers + # note: these are only applied when you call: `asset.write_data_to_sim()` + asset.set_external_force_and_torque(forces, torques, env_ids=env_ids, body_ids=asset_cfg.body_ids)
+ + +
[文档]def push_by_setting_velocity( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + velocity_range: dict[str, tuple[float, float]], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Push the asset by setting the root velocity to a random value within the given ranges. + + This creates an effect similar to pushing the asset with a random impulse that changes the asset's velocity. + It samples the root velocity from the given ranges and sets the velocity into the physics simulation. + + The function takes a dictionary of velocity ranges for each axis and rotation. The keys of the dictionary + are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. The values are tuples of the form ``(min, max)``. + If the dictionary does not contain a key, the velocity is set to zero for that axis. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + + # velocities + vel_w = asset.data.root_vel_w[env_ids] + # sample random velocities + range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=asset.device) + vel_w[:] = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], vel_w.shape, device=asset.device) + # set the velocities into the physics simulation + asset.write_root_velocity_to_sim(vel_w, env_ids=env_ids)
+ + +
[文档]def reset_root_state_uniform( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + pose_range: dict[str, tuple[float, float]], + velocity_range: dict[str, tuple[float, float]], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Reset the asset root state to a random position and velocity uniformly within the given ranges. + + This function randomizes the root position and velocity of the asset. + + * It samples the root position from the given ranges and adds them to the default root position, before setting + them into the physics simulation. + * It samples the root orientation from the given ranges and sets them into the physics simulation. + * It samples the root velocity from the given ranges and sets them into the physics simulation. + + The function takes a dictionary of pose and velocity ranges for each axis and rotation. The keys of the + dictionary are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. The values are tuples of the form + ``(min, max)``. If the dictionary does not contain a key, the position or velocity is set to zero for that axis. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + # get default root state + root_states = asset.data.default_root_state[env_ids].clone() + + # poses + range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=asset.device) + + positions = root_states[:, 0:3] + env.scene.env_origins[env_ids] + rand_samples[:, 0:3] + orientations_delta = math_utils.quat_from_euler_xyz(rand_samples[:, 3], rand_samples[:, 4], rand_samples[:, 5]) + orientations = math_utils.quat_mul(root_states[:, 3:7], orientations_delta) + # velocities + range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=asset.device) + + velocities = root_states[:, 7:13] + rand_samples + + # set into the physics simulation + asset.write_root_pose_to_sim(torch.cat([positions, orientations], dim=-1), env_ids=env_ids) + asset.write_root_velocity_to_sim(velocities, env_ids=env_ids)
+ + +
[文档]def reset_root_state_with_random_orientation( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + pose_range: dict[str, tuple[float, float]], + velocity_range: dict[str, tuple[float, float]], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Reset the asset root position and velocities sampled randomly within the given ranges + and the asset root orientation sampled randomly from the SO(3). + + This function randomizes the root position and velocity of the asset. + + * It samples the root position from the given ranges and adds them to the default root position, before setting + them into the physics simulation. + * It samples the root orientation uniformly from the SO(3) and sets them into the physics simulation. + * It samples the root velocity from the given ranges and sets them into the physics simulation. + + The function takes a dictionary of position and velocity ranges for each axis and rotation: + + * :attr:`pose_range` - a dictionary of position ranges for each axis. The keys of the dictionary are ``x``, + ``y``, and ``z``. The orientation is sampled uniformly from the SO(3). + * :attr:`velocity_range` - a dictionary of velocity ranges for each axis and rotation. The keys of the dictionary + are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. + + The values are tuples of the form ``(min, max)``. If the dictionary does not contain a particular key, + the position is set to zero for that axis. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + # get default root state + root_states = asset.data.default_root_state[env_ids].clone() + + # poses + range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 3), device=asset.device) + + positions = root_states[:, 0:3] + env.scene.env_origins[env_ids] + rand_samples + orientations = math_utils.random_orientation(len(env_ids), device=asset.device) + + # velocities + range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=asset.device) + + velocities = root_states[:, 7:13] + rand_samples + + # set into the physics simulation + asset.write_root_pose_to_sim(torch.cat([positions, orientations], dim=-1), env_ids=env_ids) + asset.write_root_velocity_to_sim(velocities, env_ids=env_ids)
+ + +
[文档]def reset_root_state_from_terrain( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + pose_range: dict[str, tuple[float, float]], + velocity_range: dict[str, tuple[float, float]], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Reset the asset root state by sampling a random valid pose from the terrain. + + This function samples a random valid pose(based on flat patches) from the terrain and sets the root state + of the asset to this position. The function also samples random velocities from the given ranges and sets them + into the physics simulation. + + The function takes a dictionary of position and velocity ranges for each axis and rotation: + + * :attr:`pose_range` - a dictionary of pose ranges for each axis. The keys of the dictionary are ``roll``, + ``pitch``, and ``yaw``. The position is sampled from the flat patches of the terrain. + * :attr:`velocity_range` - a dictionary of velocity ranges for each axis and rotation. The keys of the dictionary + are ``x``, ``y``, ``z``, ``roll``, ``pitch``, and ``yaw``. + + The values are tuples of the form ``(min, max)``. If the dictionary does not contain a particular key, + the position is set to zero for that axis. + + Note: + The function expects the terrain to have valid flat patches under the key "init_pos". The flat patches + are used to sample the random pose for the robot. + + Raises: + ValueError: If the terrain does not have valid flat patches under the key "init_pos". + """ + # access the used quantities (to enable type-hinting) + asset: RigidObject | Articulation = env.scene[asset_cfg.name] + terrain: TerrainImporter = env.scene.terrain + + # obtain all flat patches corresponding to the valid poses + valid_positions: torch.Tensor = terrain.flat_patches.get("init_pos") + if valid_positions is None: + raise ValueError( + "The event term 'reset_root_state_from_terrain' requires valid flat patches under 'init_pos'." + f" Found: {list(terrain.flat_patches.keys())}" + ) + + # sample random valid poses + ids = torch.randint(0, valid_positions.shape[2], size=(len(env_ids),), device=env.device) + positions = valid_positions[terrain.terrain_levels[env_ids], terrain.terrain_types[env_ids], ids] + positions += asset.data.default_root_state[env_ids, :3] + + # sample random orientations + range_list = [pose_range.get(key, (0.0, 0.0)) for key in ["roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 3), device=asset.device) + + # convert to quaternions + orientations = math_utils.quat_from_euler_xyz(rand_samples[:, 0], rand_samples[:, 1], rand_samples[:, 2]) + + # sample random velocities + range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z", "roll", "pitch", "yaw"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 6), device=asset.device) + + velocities = asset.data.default_root_state[:, 7:13] + rand_samples + + # set into the physics simulation + asset.write_root_pose_to_sim(torch.cat([positions, orientations], dim=-1), env_ids=env_ids) + asset.write_root_velocity_to_sim(velocities, env_ids=env_ids)
+ + +
[文档]def reset_joints_by_scale( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + position_range: tuple[float, float], + velocity_range: tuple[float, float], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Reset the robot joints by scaling the default position and velocity by the given ranges. + + This function samples random values from the given ranges and scales the default joint positions and velocities + by these values. The scaled values are then set into the physics simulation. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # get default joint state + joint_pos = asset.data.default_joint_pos[env_ids].clone() + joint_vel = asset.data.default_joint_vel[env_ids].clone() + + # scale these values randomly + joint_pos *= math_utils.sample_uniform(*position_range, joint_pos.shape, joint_pos.device) + joint_vel *= math_utils.sample_uniform(*velocity_range, joint_vel.shape, joint_vel.device) + + # clamp joint pos to limits + joint_pos_limits = asset.data.soft_joint_pos_limits[env_ids] + joint_pos = joint_pos.clamp_(joint_pos_limits[..., 0], joint_pos_limits[..., 1]) + # clamp joint vel to limits + joint_vel_limits = asset.data.soft_joint_vel_limits[env_ids] + joint_vel = joint_vel.clamp_(-joint_vel_limits, joint_vel_limits) + + # set into the physics simulation + asset.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids)
+ + +
[文档]def reset_joints_by_offset( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + position_range: tuple[float, float], + velocity_range: tuple[float, float], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Reset the robot joints with offsets around the default position and velocity by the given ranges. + + This function samples random values from the given ranges and biases the default joint positions and velocities + by these values. The biased values are then set into the physics simulation. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + + # get default joint state + joint_pos = asset.data.default_joint_pos[env_ids].clone() + joint_vel = asset.data.default_joint_vel[env_ids].clone() + + # bias these values randomly + joint_pos += math_utils.sample_uniform(*position_range, joint_pos.shape, joint_pos.device) + joint_vel += math_utils.sample_uniform(*velocity_range, joint_vel.shape, joint_vel.device) + + # clamp joint pos to limits + joint_pos_limits = asset.data.soft_joint_pos_limits[env_ids] + joint_pos = joint_pos.clamp_(joint_pos_limits[..., 0], joint_pos_limits[..., 1]) + # clamp joint vel to limits + joint_vel_limits = asset.data.soft_joint_vel_limits[env_ids] + joint_vel = joint_vel.clamp_(-joint_vel_limits, joint_vel_limits) + + # set into the physics simulation + asset.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids)
+ + +
[文档]def reset_nodal_state_uniform( + env: ManagerBasedEnv, + env_ids: torch.Tensor, + position_range: dict[str, tuple[float, float]], + velocity_range: dict[str, tuple[float, float]], + asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), +): + """Reset the asset nodal state to a random position and velocity uniformly within the given ranges. + + This function randomizes the nodal position and velocity of the asset. + + * It samples the root position from the given ranges and adds them to the default nodal position, before setting + them into the physics simulation. + * It samples the root velocity from the given ranges and sets them into the physics simulation. + + The function takes a dictionary of position and velocity ranges for each axis. The keys of the + dictionary are ``x``, ``y``, ``z``. The values are tuples of the form ``(min, max)``. + If the dictionary does not contain a key, the position or velocity is set to zero for that axis. + """ + # extract the used quantities (to enable type-hinting) + asset: DeformableObject = env.scene[asset_cfg.name] + # get default root state + nodal_state = asset.data.default_nodal_state_w[env_ids].clone() + + # position + range_list = [position_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 1, 3), device=asset.device) + + nodal_state[..., :3] += rand_samples + + # velocities + range_list = [velocity_range.get(key, (0.0, 0.0)) for key in ["x", "y", "z"]] + ranges = torch.tensor(range_list, device=asset.device) + rand_samples = math_utils.sample_uniform(ranges[:, 0], ranges[:, 1], (len(env_ids), 1, 3), device=asset.device) + + nodal_state[..., 3:] += rand_samples + + # set into the physics simulation + asset.write_nodal_state_to_sim(nodal_state, env_ids=env_ids)
+ + +
[文档]def reset_scene_to_default(env: ManagerBasedEnv, env_ids: torch.Tensor): + """Reset the scene to the default state specified in the scene configuration.""" + # rigid bodies + for rigid_object in env.scene.rigid_objects.values(): + # obtain default and deal with the offset for env origins + default_root_state = rigid_object.data.default_root_state[env_ids].clone() + default_root_state[:, 0:3] += env.scene.env_origins[env_ids] + # set into the physics simulation + rigid_object.write_root_state_to_sim(default_root_state, env_ids=env_ids) + # articulations + for articulation_asset in env.scene.articulations.values(): + # obtain default and deal with the offset for env origins + default_root_state = articulation_asset.data.default_root_state[env_ids].clone() + default_root_state[:, 0:3] += env.scene.env_origins[env_ids] + # set into the physics simulation + articulation_asset.write_root_state_to_sim(default_root_state, env_ids=env_ids) + # obtain default joint positions + default_joint_pos = articulation_asset.data.default_joint_pos[env_ids].clone() + default_joint_vel = articulation_asset.data.default_joint_vel[env_ids].clone() + # set into the physics simulation + articulation_asset.write_joint_state_to_sim(default_joint_pos, default_joint_vel, env_ids=env_ids) + # deformable objects + for deformable_object in env.scene.deformable_objects.values(): + # obtain default and set into the physics simulation + nodal_state = deformable_object.data.default_nodal_state_w[env_ids].clone() + deformable_object.write_nodal_state_to_sim(nodal_state, env_ids=env_ids)
+ + +""" +Internal helper functions. +""" + + +def _randomize_prop_by_op( + data: torch.Tensor, + distribution_parameters: tuple[float | torch.Tensor, float | torch.Tensor], + dim_0_ids: torch.Tensor | None, + dim_1_ids: torch.Tensor | slice, + operation: Literal["add", "scale", "abs"], + distribution: Literal["uniform", "log_uniform", "gaussian"], +) -> torch.Tensor: + """Perform data randomization based on the given operation and distribution. + + Args: + data: The data tensor to be randomized. Shape is (dim_0, dim_1). + distribution_parameters: The parameters for the distribution to sample values from. + dim_0_ids: The indices of the first dimension to randomize. + dim_1_ids: The indices of the second dimension to randomize. + operation: The operation to perform on the data. Options: 'add', 'scale', 'abs'. + distribution: The distribution to sample the random values from. Options: 'uniform', 'log_uniform'. + + Returns: + The data tensor after randomization. Shape is (dim_0, dim_1). + + Raises: + NotImplementedError: If the operation or distribution is not supported. + """ + # resolve shape + # -- dim 0 + if dim_0_ids is None: + n_dim_0 = data.shape[0] + dim_0_ids = slice(None) + else: + n_dim_0 = len(dim_0_ids) + if not isinstance(dim_1_ids, slice): + dim_0_ids = dim_0_ids[:, None] + # -- dim 1 + if isinstance(dim_1_ids, slice): + n_dim_1 = data.shape[1] + else: + n_dim_1 = len(dim_1_ids) + + # resolve the distribution + if distribution == "uniform": + dist_fn = math_utils.sample_uniform + elif distribution == "log_uniform": + dist_fn = math_utils.sample_log_uniform + elif distribution == "gaussian": + dist_fn = math_utils.sample_gaussian + else: + raise NotImplementedError( + f"Unknown distribution: '{distribution}' for joint properties randomization." + " Please use 'uniform', 'log_uniform', 'gaussian'." + ) + # perform the operation + if operation == "add": + data[dim_0_ids, dim_1_ids] += dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) + elif operation == "scale": + data[dim_0_ids, dim_1_ids] *= dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) + elif operation == "abs": + data[dim_0_ids, dim_1_ids] = dist_fn(*distribution_parameters, (n_dim_0, n_dim_1), device=data.device) + else: + raise NotImplementedError( + f"Unknown operation: '{operation}' for property randomization. Please use 'add', 'scale', or 'abs'." + ) + return data +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/observations.html b/_modules/omni/isaac/lab/envs/mdp/observations.html new file mode 100644 index 0000000000..808b2e0507 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/observations.html @@ -0,0 +1,1090 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.observations — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.observations 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Common functions that can be used to create observation terms.
+
+The functions can be passed to the :class:`omni.isaac.lab.managers.ObservationTermCfg` object to enable
+the observation introduced by the function.
+"""
+
+from __future__ import annotations
+
+import torch
+from typing import TYPE_CHECKING
+
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.assets import Articulation, RigidObject
+from omni.isaac.lab.managers import SceneEntityCfg
+from omni.isaac.lab.managers.manager_base import ManagerTermBase
+from omni.isaac.lab.managers.manager_term_cfg import ObservationTermCfg
+from omni.isaac.lab.sensors import Camera, Imu, RayCaster, RayCasterCamera, TiledCamera
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedEnv, ManagerBasedRLEnv
+
+
+"""
+Root state.
+"""
+
+
+
[文档]def base_pos_z(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Root height in the simulation world frame.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return asset.data.root_pos_w[:, 2].unsqueeze(-1)
+ + +
[文档]def base_lin_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Root linear velocity in the asset's root frame.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.root_lin_vel_b
+ + +
[文档]def base_ang_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Root angular velocity in the asset's root frame.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.root_ang_vel_b
+ + +
[文档]def projected_gravity(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Gravity projection on the asset's root frame.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.projected_gravity_b
+ + +
[文档]def root_pos_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Asset root position in the environment frame.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.root_pos_w - env.scene.env_origins
+ + +
[文档]def root_quat_w( + env: ManagerBasedEnv, make_quat_unique: bool = False, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Asset root orientation (w, x, y, z) in the environment frame. + + If :attr:`make_quat_unique` is True, then returned quaternion is made unique by ensuring + the quaternion has non-negative real component. This is because both ``q`` and ``-q`` represent + the same orientation. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + + quat = asset.data.root_quat_w + # make the quaternion real-part positive if configured + return math_utils.quat_unique(quat) if make_quat_unique else quat
+ + +
[文档]def root_lin_vel_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Asset root linear velocity in the environment frame.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.root_lin_vel_w
+ + +
[文档]def root_ang_vel_w(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Asset root angular velocity in the environment frame.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.root_ang_vel_w
+ + +""" +Joint state. +""" + + +
[文档]def joint_pos(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """The joint positions of the asset. + + Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their positions returned. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return asset.data.joint_pos[:, asset_cfg.joint_ids]
+ + +
[文档]def joint_pos_rel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """The joint positions of the asset w.r.t. the default joint positions. + + Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their positions returned. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids]
+ + +
[文档]def joint_pos_limit_normalized( + env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """The joint positions of the asset normalized with the asset's joint limits. + + Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their normalized positions returned. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return math_utils.scale_transform( + asset.data.joint_pos[:, asset_cfg.joint_ids], + asset.data.soft_joint_pos_limits[:, asset_cfg.joint_ids, 0], + asset.data.soft_joint_pos_limits[:, asset_cfg.joint_ids, 1], + )
+ + +
[文档]def joint_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")): + """The joint velocities of the asset. + + Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their velocities returned. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return asset.data.joint_vel[:, asset_cfg.joint_ids]
+ + +
[文档]def joint_vel_rel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")): + """The joint velocities of the asset w.r.t. the default joint velocities. + + Note: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their velocities returned. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return asset.data.joint_vel[:, asset_cfg.joint_ids] - asset.data.default_joint_vel[:, asset_cfg.joint_ids]
+ + +""" +Sensors. +""" + + +
[文档]def height_scan(env: ManagerBasedEnv, sensor_cfg: SceneEntityCfg, offset: float = 0.5) -> torch.Tensor: + """Height scan from the given sensor w.r.t. the sensor's frame. + + The provided offset (Defaults to 0.5) is subtracted from the returned values. + """ + # extract the used quantities (to enable type-hinting) + sensor: RayCaster = env.scene.sensors[sensor_cfg.name] + # height scan: height = sensor_height - hit_point_z - offset + return sensor.data.pos_w[:, 2].unsqueeze(1) - sensor.data.ray_hits_w[..., 2] - offset
+ + +
[文档]def body_incoming_wrench(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Incoming spatial wrench on bodies of an articulation in the simulation world frame. + + This is the 6-D wrench (force and torque) applied to the body link by the incoming joint force. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # obtain the link incoming forces in world frame + link_incoming_forces = asset.root_physx_view.get_link_incoming_joint_force()[:, asset_cfg.body_ids] + return link_incoming_forces.view(env.num_envs, -1)
+ + +
[文档]def imu_orientation(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor: + """Imu sensor orientation in the simulation world frame. + + Args: + env: The environment. + asset_cfg: The SceneEntity associated with an IMU sensor. Defaults to SceneEntityCfg("imu"). + + Returns: + Orientation in the world frame in (w, x, y, z) quaternion form. Shape is (num_envs, 4). + """ + # extract the used quantities (to enable type-hinting) + asset: Imu = env.scene[asset_cfg.name] + # return the orientation quaternion + return asset.data.quat_w
+ + +
[文档]def imu_ang_vel(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor: + """Imu sensor angular velocity w.r.t. environment origin expressed in the sensor frame. + + Args: + env: The environment. + asset_cfg: The SceneEntity associated with an IMU sensor. Defaults to SceneEntityCfg("imu"). + + Returns: + The angular velocity (rad/s) in the sensor frame. Shape is (num_envs, 3). + """ + # extract the used quantities (to enable type-hinting) + asset: Imu = env.scene[asset_cfg.name] + # return the angular velocity + return asset.data.ang_vel_b
+ + +
[文档]def imu_lin_acc(env: ManagerBasedEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("imu")) -> torch.Tensor: + """Imu sensor linear acceleration w.r.t. the environment origin expressed in sensor frame. + + Args: + env: The environment. + asset_cfg: The SceneEntity associated with an IMU sensor. Defaults to SceneEntityCfg("imu"). + + Returns: + The linear acceleration (m/s^2) in the sensor frame. Shape is (num_envs, 3). + """ + asset: Imu = env.scene[asset_cfg.name] + return asset.data.lin_acc_b
+ + +
[文档]def image( + env: ManagerBasedEnv, + sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"), + data_type: str = "rgb", + convert_perspective_to_orthogonal: bool = False, + normalize: bool = True, +) -> torch.Tensor: + """Images of a specific datatype from the camera sensor. + + If the flag :attr:`normalize` is True, post-processing of the images are performed based on their + data-types: + + - "rgb": Scales the image to (0, 1) and subtracts with the mean of the current image batch. + - "depth" or "distance_to_camera" or "distance_to_plane": Replaces infinity values with zero. + + Args: + env: The environment the cameras are placed within. + sensor_cfg: The desired sensor to read from. Defaults to SceneEntityCfg("tiled_camera"). + data_type: The data type to pull from the desired camera. Defaults to "rgb". + convert_perspective_to_orthogonal: Whether to orthogonalize perspective depth images. + This is used only when the data type is "distance_to_camera". Defaults to False. + normalize: Whether to normalize the images. This depends on the selected data type. + Defaults to True. + + Returns: + The images produced at the last time-step + """ + # extract the used quantities (to enable type-hinting) + sensor: TiledCamera | Camera | RayCasterCamera = env.scene.sensors[sensor_cfg.name] + + # obtain the input image + images = sensor.data.output[data_type] + + # depth image conversion + if (data_type == "distance_to_camera") and convert_perspective_to_orthogonal: + images = math_utils.orthogonalize_perspective_depth(images, sensor.data.intrinsic_matrices) + + # rgb/depth image normalization + if normalize: + if data_type == "rgb": + images = images.float() / 255.0 + mean_tensor = torch.mean(images, dim=(1, 2), keepdim=True) + images -= mean_tensor + elif "distance_to" in data_type or "depth" in data_type: + images[images == float("inf")] = 0 + + return images.clone()
+ + +
[文档]class image_features(ManagerTermBase): + """Extracted image features from a pre-trained frozen encoder. + + This term uses models from the model zoo in PyTorch and extracts features from the images. + + It calls the :func:`image` function to get the images and then processes them using the model zoo. + + A user can provide their own model zoo configuration to use different models for feature extraction. + The model zoo configuration should be a dictionary that maps different model names to a dictionary + that defines the model, preprocess and inference functions. The dictionary should have the following + entries: + + - "model": A callable that returns the model when invoked without arguments. + - "reset": A callable that resets the model. This is useful when the model has a state that needs to be reset. + - "inference": A callable that, when given the model and the images, returns the extracted features. + + If the model zoo configuration is not provided, the default model zoo configurations are used. The default + model zoo configurations include the models from Theia :cite:`shang2024theia` and ResNet :cite:`he2016deep`. + These models are loaded from `Hugging-Face transformers <https://huggingface.co/docs/transformers/index>`_ and + `PyTorch torchvision <https://pytorch.org/vision/stable/models.html>`_ respectively. + + Args: + sensor_cfg: The sensor configuration to poll. Defaults to SceneEntityCfg("tiled_camera"). + data_type: The sensor data type. Defaults to "rgb". + convert_perspective_to_orthogonal: Whether to orthogonalize perspective depth images. + This is used only when the data type is "distance_to_camera". Defaults to False. + model_zoo_cfg: A user-defined dictionary that maps different model names to their respective configurations. + Defaults to None. If None, the default model zoo configurations are used. + model_name: The name of the model to use for inference. Defaults to "resnet18". + model_device: The device to store and infer the model on. This is useful when offloading the computation + from the environment simulation device. Defaults to the environment device. + inference_kwargs: Additional keyword arguments to pass to the inference function. Defaults to None, + which means no additional arguments are passed. + + Returns: + The extracted features tensor. Shape is (num_envs, feature_dim). + + Raises: + ValueError: When the model name is not found in the provided model zoo configuration. + ValueError: When the model name is not found in the default model zoo configuration. + """ + +
[文档] def __init__(self, cfg: ObservationTermCfg, env: ManagerBasedEnv): + # initialize the base class + super().__init__(cfg, env) + + # extract parameters from the configuration + self.model_zoo_cfg: dict = cfg.params.get("model_zoo_cfg") # type: ignore + self.model_name: str = cfg.params.get("model_name", "resnet18") # type: ignore + self.model_device: str = cfg.params.get("model_device", env.device) # type: ignore + + # List of Theia models - These are configured through `_prepare_theia_transformer_model` function + default_theia_models = [ + "theia-tiny-patch16-224-cddsv", + "theia-tiny-patch16-224-cdiv", + "theia-small-patch16-224-cdiv", + "theia-base-patch16-224-cdiv", + "theia-small-patch16-224-cddsv", + "theia-base-patch16-224-cddsv", + ] + # List of ResNet models - These are configured through `_prepare_resnet_model` function + default_resnet_models = ["resnet18", "resnet34", "resnet50", "resnet101"] + + # Check if model name is specified in the model zoo configuration + if self.model_zoo_cfg is not None and self.model_name not in self.model_zoo_cfg: + raise ValueError( + f"Model name '{self.model_name}' not found in the provided model zoo configuration." + " Please add the model to the model zoo configuration or use a different model name." + f" Available models in the provided list: {list(self.model_zoo_cfg.keys())}." + "\nHint: If you want to use a default model, consider using one of the following models:" + f" {default_theia_models + default_resnet_models}. In this case, you can remove the" + " 'model_zoo_cfg' parameter from the observation term configuration." + ) + if self.model_zoo_cfg is None: + if self.model_name in default_theia_models: + model_config = self._prepare_theia_transformer_model(self.model_name, self.model_device) + elif self.model_name in default_resnet_models: + model_config = self._prepare_resnet_model(self.model_name, self.model_device) + else: + raise ValueError( + f"Model name '{self.model_name}' not found in the default model zoo configuration." + f" Available models: {default_theia_models + default_resnet_models}." + ) + else: + model_config = self.model_zoo_cfg[self.model_name] + + # Retrieve the model, preprocess and inference functions + self._model = model_config["model"]() + self._reset_fn = model_config.get("reset") + self._inference_fn = model_config["inference"]
+ +
[文档] def reset(self, env_ids: torch.Tensor | None = None): + # reset the model if a reset function is provided + # this might be useful when the model has a state that needs to be reset + # for example: video transformers + if self._reset_fn is not None: + self._reset_fn(self._model, env_ids)
+ + def __call__( + self, + env: ManagerBasedEnv, + sensor_cfg: SceneEntityCfg = SceneEntityCfg("tiled_camera"), + data_type: str = "rgb", + convert_perspective_to_orthogonal: bool = False, + model_zoo_cfg: dict | None = None, + model_name: str = "resnet18", + model_device: str | None = None, + inference_kwargs: dict | None = None, + ) -> torch.Tensor: + # obtain the images from the sensor + image_data = image( + env=env, + sensor_cfg=sensor_cfg, + data_type=data_type, + convert_perspective_to_orthogonal=convert_perspective_to_orthogonal, + normalize=False, # we pre-process based on model + ) + # store the device of the image + image_device = image_data.device + # forward the images through the model + features = self._inference_fn(self._model, image_data, **(inference_kwargs or {})) + + # move the features back to the image device + return features.detach().to(image_device) + + """ + Helper functions. + """ + + def _prepare_theia_transformer_model(self, model_name: str, model_device: str) -> dict: + """Prepare the Theia transformer model for inference. + + Args: + model_name: The name of the Theia transformer model to prepare. + model_device: The device to store and infer the model on. + + Returns: + A dictionary containing the model and inference functions. + """ + from transformers import AutoModel + + def _load_model() -> torch.nn.Module: + """Load the Theia transformer model.""" + model = AutoModel.from_pretrained(f"theaiinstitute/{model_name}", trust_remote_code=True).eval() + return model.to(model_device) + + def _inference(model, images: torch.Tensor) -> torch.Tensor: + """Inference the Theia transformer model. + + Args: + model: The Theia transformer model. + images: The preprocessed image tensor. Shape is (num_envs, height, width, channel). + + Returns: + The extracted features tensor. Shape is (num_envs, feature_dim). + """ + # Move the image to the model device + image_proc = images.to(model_device) + # permute the image to (num_envs, channel, height, width) + image_proc = image_proc.permute(0, 3, 1, 2).float() / 255.0 + # Normalize the image + mean = torch.tensor([0.485, 0.456, 0.406], device=model_device).view(1, 3, 1, 1) + std = torch.tensor([0.229, 0.224, 0.225], device=model_device).view(1, 3, 1, 1) + image_proc = (image_proc - mean) / std + + # Taken from Transformers; inference converted to be GPU only + features = model.backbone.model(pixel_values=image_proc, interpolate_pos_encoding=True) + return features.last_hidden_state[:, 1:] + + # return the model, preprocess and inference functions + return {"model": _load_model, "inference": _inference} + + def _prepare_resnet_model(self, model_name: str, model_device: str) -> dict: + """Prepare the ResNet model for inference. + + Args: + model_name: The name of the ResNet model to prepare. + model_device: The device to store and infer the model on. + + Returns: + A dictionary containing the model and inference functions. + """ + from torchvision import models + + def _load_model() -> torch.nn.Module: + """Load the ResNet model.""" + # map the model name to the weights + resnet_weights = { + "resnet18": "ResNet18_Weights.IMAGENET1K_V1", + "resnet34": "ResNet34_Weights.IMAGENET1K_V1", + "resnet50": "ResNet50_Weights.IMAGENET1K_V1", + "resnet101": "ResNet101_Weights.IMAGENET1K_V1", + } + + # load the model + model = getattr(models, model_name)(weights=resnet_weights[model_name]).eval() + return model.to(model_device) + + def _inference(model, images: torch.Tensor) -> torch.Tensor: + """Inference the ResNet model. + + Args: + model: The ResNet model. + images: The preprocessed image tensor. Shape is (num_envs, channel, height, width). + + Returns: + The extracted features tensor. Shape is (num_envs, feature_dim). + """ + # move the image to the model device + image_proc = images.to(model_device) + # permute the image to (num_envs, channel, height, width) + image_proc = image_proc.permute(0, 3, 1, 2).float() / 255.0 + # normalize the image + mean = torch.tensor([0.485, 0.456, 0.406], device=model_device).view(1, 3, 1, 1) + std = torch.tensor([0.229, 0.224, 0.225], device=model_device).view(1, 3, 1, 1) + image_proc = (image_proc - mean) / std + + # forward the image through the model + return model(image_proc) + + # return the model, preprocess and inference functions + return {"model": _load_model, "inference": _inference}
+ + +""" +Actions. +""" + + +
[文档]def last_action(env: ManagerBasedEnv, action_name: str | None = None) -> torch.Tensor: + """The last input action to the environment. + + The name of the action term for which the action is required. If None, the + entire action tensor is returned. + """ + if action_name is None: + return env.action_manager.action + else: + return env.action_manager.get_term(action_name).raw_actions
+ + +""" +Commands. +""" + + +
[文档]def generated_commands(env: ManagerBasedRLEnv, command_name: str) -> torch.Tensor: + """The generated command from command term in the command manager with the given name.""" + return env.command_manager.get_command(command_name)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/rewards.html b/_modules/omni/isaac/lab/envs/mdp/rewards.html new file mode 100644 index 0000000000..9ca6913c7d --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/rewards.html @@ -0,0 +1,857 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.rewards — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.rewards 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Common functions that can be used to enable reward functions.
+
+The functions can be passed to the :class:`omni.isaac.lab.managers.RewardTermCfg` object to include
+the reward introduced by the function.
+"""
+
+from __future__ import annotations
+
+import torch
+from typing import TYPE_CHECKING
+
+from omni.isaac.lab.assets import Articulation, RigidObject
+from omni.isaac.lab.managers import SceneEntityCfg
+from omni.isaac.lab.managers.manager_base import ManagerTermBase
+from omni.isaac.lab.managers.manager_term_cfg import RewardTermCfg
+from omni.isaac.lab.sensors import ContactSensor
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+
+"""
+General.
+"""
+
+
+
[文档]def is_alive(env: ManagerBasedRLEnv) -> torch.Tensor: + """Reward for being alive.""" + return (~env.termination_manager.terminated).float()
+ + +
[文档]def is_terminated(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize terminated episodes that don't correspond to episodic timeouts.""" + return env.termination_manager.terminated.float()
+ + +
[文档]class is_terminated_term(ManagerTermBase): + """Penalize termination for specific terms that don't correspond to episodic timeouts. + + The parameters are as follows: + + * attr:`term_keys`: The termination terms to penalize. This can be a string, a list of strings + or regular expressions. Default is ".*" which penalizes all terminations. + + The reward is computed as the sum of the termination terms that are not episodic timeouts. + This means that the reward is 0 if the episode is terminated due to an episodic timeout. Otherwise, + if two termination terms are active, the reward is 2. + """ + +
[文档] def __init__(self, cfg: RewardTermCfg, env: ManagerBasedRLEnv): + # initialize the base class + super().__init__(cfg, env) + # find and store the termination terms + term_keys = cfg.params.get("term_keys", ".*") + self._term_names = env.termination_manager.find_terms(term_keys)
+ + def __call__(self, env: ManagerBasedRLEnv, term_keys: str | list[str] = ".*") -> torch.Tensor: + # Return the unweighted reward for the termination terms + reset_buf = torch.zeros(env.num_envs, device=env.device) + for term in self._term_names: + # Sums over terminations term values to account for multiple terminations in the same step + reset_buf += env.termination_manager.get_term(term) + + return (reset_buf * (~env.termination_manager.time_outs)).float()
+ + +""" +Root penalties. +""" + + +
[文档]def lin_vel_z_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize z-axis base linear velocity using L2 squared kernel.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return torch.square(asset.data.root_lin_vel_b[:, 2])
+ + +
[文档]def ang_vel_xy_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize xy-axis base angular velocity using L2 squared kernel.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return torch.sum(torch.square(asset.data.root_ang_vel_b[:, :2]), dim=1)
+ + +
[文档]def flat_orientation_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize non-flat base orientation using L2 squared kernel. + + This is computed by penalizing the xy-components of the projected gravity vector. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return torch.sum(torch.square(asset.data.projected_gravity_b[:, :2]), dim=1)
+ + +
[文档]def base_height_l2( + env: ManagerBasedRLEnv, target_height: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Penalize asset height from its target using L2 squared kernel. + + Note: + Currently, it assumes a flat terrain, i.e. the target height is in the world frame. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + # TODO: Fix this for rough-terrain. + return torch.square(asset.data.root_pos_w[:, 2] - target_height)
+ + +
[文档]def body_lin_acc_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize the linear acceleration of bodies using L2-kernel.""" + asset: Articulation = env.scene[asset_cfg.name] + return torch.sum(torch.norm(asset.data.body_lin_acc_w[:, asset_cfg.body_ids, :], dim=-1), dim=1)
+ + +""" +Joint penalties. +""" + + +
[文档]def joint_torques_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize joint torques applied on the articulation using L2 squared kernel. + + NOTE: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint torques contribute to the term. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return torch.sum(torch.square(asset.data.applied_torque[:, asset_cfg.joint_ids]), dim=1)
+ + +
[文档]def joint_vel_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg) -> torch.Tensor: + """Penalize joint velocities on the articulation using an L1-kernel.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return torch.sum(torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids]), dim=1)
+ + +
[文档]def joint_vel_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize joint velocities on the articulation using L2 squared kernel. + + NOTE: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint velocities contribute to the term. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return torch.sum(torch.square(asset.data.joint_vel[:, asset_cfg.joint_ids]), dim=1)
+ + +
[文档]def joint_acc_l2(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize joint accelerations on the articulation using L2 squared kernel. + + NOTE: Only the joints configured in :attr:`asset_cfg.joint_ids` will have their joint accelerations contribute to the term. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + return torch.sum(torch.square(asset.data.joint_acc[:, asset_cfg.joint_ids]), dim=1)
+ + +
[文档]def joint_deviation_l1(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize joint positions that deviate from the default one.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute out of limits constraints + angle = asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.default_joint_pos[:, asset_cfg.joint_ids] + return torch.sum(torch.abs(angle), dim=1)
+ + +
[文档]def joint_pos_limits(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize joint positions if they cross the soft limits. + + This is computed as a sum of the absolute value of the difference between the joint position and the soft limits. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute out of limits constraints + out_of_limits = -( + asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.soft_joint_pos_limits[:, asset_cfg.joint_ids, 0] + ).clip(max=0.0) + out_of_limits += ( + asset.data.joint_pos[:, asset_cfg.joint_ids] - asset.data.soft_joint_pos_limits[:, asset_cfg.joint_ids, 1] + ).clip(min=0.0) + return torch.sum(out_of_limits, dim=1)
+ + +
[文档]def joint_vel_limits( + env: ManagerBasedRLEnv, soft_ratio: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Penalize joint velocities if they cross the soft limits. + + This is computed as a sum of the absolute value of the difference between the joint velocity and the soft limits. + + Args: + soft_ratio: The ratio of the soft limits to be used. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute out of limits constraints + out_of_limits = ( + torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids]) + - asset.data.soft_joint_vel_limits[:, asset_cfg.joint_ids] * soft_ratio + ) + # clip to max error = 1 rad/s per joint to avoid huge penalties + out_of_limits = out_of_limits.clip_(min=0.0, max=1.0) + return torch.sum(out_of_limits, dim=1)
+ + +""" +Action penalties. +""" + + +
[文档]def applied_torque_limits(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Penalize applied torques if they cross the limits. + + This is computed as a sum of the absolute value of the difference between the applied torques and the limits. + + .. caution:: + Currently, this only works for explicit actuators since we manually compute the applied torques. + For implicit actuators, we currently cannot retrieve the applied torques from the physics engine. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute out of limits constraints + # TODO: We need to fix this to support implicit joints. + out_of_limits = torch.abs( + asset.data.applied_torque[:, asset_cfg.joint_ids] - asset.data.computed_torque[:, asset_cfg.joint_ids] + ) + return torch.sum(out_of_limits, dim=1)
+ + +
[文档]def action_rate_l2(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize the rate of change of the actions using L2 squared kernel.""" + return torch.sum(torch.square(env.action_manager.action - env.action_manager.prev_action), dim=1)
+ + +
[文档]def action_l2(env: ManagerBasedRLEnv) -> torch.Tensor: + """Penalize the actions using L2 squared kernel.""" + return torch.sum(torch.square(env.action_manager.action), dim=1)
+ + +""" +Contact sensor. +""" + + +
[文档]def undesired_contacts(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: + """Penalize undesired contacts as the number of violations that are above a threshold.""" + # extract the used quantities (to enable type-hinting) + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + # check if contact force is above threshold + net_contact_forces = contact_sensor.data.net_forces_w_history + is_contact = torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold + # sum over contacts for each environment + return torch.sum(is_contact, dim=1)
+ + +
[文档]def contact_forces(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: + """Penalize contact forces as the amount of violations of the net contact force.""" + # extract the used quantities (to enable type-hinting) + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + net_contact_forces = contact_sensor.data.net_forces_w_history + # compute the violation + violation = torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] - threshold + # compute the penalty + return torch.sum(violation.clip(min=0.0), dim=1)
+ + +""" +Velocity-tracking rewards. +""" + + +
[文档]def track_lin_vel_xy_exp( + env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Reward tracking of linear velocity commands (xy axes) using exponential kernel.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + # compute the error + lin_vel_error = torch.sum( + torch.square(env.command_manager.get_command(command_name)[:, :2] - asset.data.root_lin_vel_b[:, :2]), + dim=1, + ) + return torch.exp(-lin_vel_error / std**2)
+ + +
[文档]def track_ang_vel_z_exp( + env: ManagerBasedRLEnv, std: float, command_name: str, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Reward tracking of angular velocity commands (yaw) using exponential kernel.""" + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + # compute the error + ang_vel_error = torch.square(env.command_manager.get_command(command_name)[:, 2] - asset.data.root_ang_vel_b[:, 2]) + return torch.exp(-ang_vel_error / std**2)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/mdp/terminations.html b/_modules/omni/isaac/lab/envs/mdp/terminations.html new file mode 100644 index 0000000000..7587cf82d6 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/mdp/terminations.html @@ -0,0 +1,717 @@ + + + + + + + + + + + omni.isaac.lab.envs.mdp.terminations — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.mdp.terminations 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Common functions that can be used to activate certain terminations.
+
+The functions can be passed to the :class:`omni.isaac.lab.managers.TerminationTermCfg` object to enable
+the termination introduced by the function.
+"""
+
+from __future__ import annotations
+
+import torch
+from typing import TYPE_CHECKING
+
+from omni.isaac.lab.assets import Articulation, RigidObject
+from omni.isaac.lab.managers import SceneEntityCfg
+from omni.isaac.lab.sensors import ContactSensor
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+    from omni.isaac.lab.managers.command_manager import CommandTerm
+
+"""
+MDP terminations.
+"""
+
+
+
[文档]def time_out(env: ManagerBasedRLEnv) -> torch.Tensor: + """Terminate the episode when the episode length exceeds the maximum episode length.""" + return env.episode_length_buf >= env.max_episode_length
+ + +
[文档]def command_resample(env: ManagerBasedRLEnv, command_name: str, num_resamples: int = 1) -> torch.Tensor: + """Terminate the episode based on the total number of times commands have been re-sampled. + + This makes the maximum episode length fluid in nature as it depends on how the commands are + sampled. It is useful in situations where delayed rewards are used :cite:`rudin2022advanced`. + """ + command: CommandTerm = env.command_manager.get_term(command_name) + return torch.logical_and((command.time_left <= env.step_dt), (command.command_counter == num_resamples))
+ + +""" +Root terminations. +""" + + +
[文档]def bad_orientation( + env: ManagerBasedRLEnv, limit_angle: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Terminate when the asset's orientation is too far from the desired orientation limits. + + This is computed by checking the angle between the projected gravity vector and the z-axis. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return torch.acos(-asset.data.projected_gravity_b[:, 2]).abs() > limit_angle
+ + +
[文档]def root_height_below_minimum( + env: ManagerBasedRLEnv, minimum_height: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Terminate when the asset's root height is below the minimum height. + + Note: + This is currently only supported for flat terrains, i.e. the minimum height is in the world frame. + """ + # extract the used quantities (to enable type-hinting) + asset: RigidObject = env.scene[asset_cfg.name] + return asset.data.root_pos_w[:, 2] < minimum_height
+ + +""" +Joint terminations. +""" + + +
[文档]def joint_pos_out_of_limit(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Terminate when the asset's joint positions are outside of the soft joint limits.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute any violations + out_of_upper_limits = torch.any(asset.data.joint_pos > asset.data.soft_joint_pos_limits[..., 1], dim=1) + out_of_lower_limits = torch.any(asset.data.joint_pos < asset.data.soft_joint_pos_limits[..., 0], dim=1) + return torch.logical_or(out_of_upper_limits[:, asset_cfg.joint_ids], out_of_lower_limits[:, asset_cfg.joint_ids])
+ + +
[文档]def joint_pos_out_of_manual_limit( + env: ManagerBasedRLEnv, bounds: tuple[float, float], asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Terminate when the asset's joint positions are outside of the configured bounds. + + Note: + This function is similar to :func:`joint_pos_out_of_limit` but allows the user to specify the bounds manually. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + if asset_cfg.joint_ids is None: + asset_cfg.joint_ids = slice(None) + # compute any violations + out_of_upper_limits = torch.any(asset.data.joint_pos[:, asset_cfg.joint_ids] > bounds[1], dim=1) + out_of_lower_limits = torch.any(asset.data.joint_pos[:, asset_cfg.joint_ids] < bounds[0], dim=1) + return torch.logical_or(out_of_upper_limits, out_of_lower_limits)
+ + +
[文档]def joint_vel_out_of_limit(env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot")) -> torch.Tensor: + """Terminate when the asset's joint velocities are outside of the soft joint limits.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute any violations + limits = asset.data.soft_joint_vel_limits + return torch.any(torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids]) > limits[:, asset_cfg.joint_ids], dim=1)
+ + +
[文档]def joint_vel_out_of_manual_limit( + env: ManagerBasedRLEnv, max_velocity: float, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Terminate when the asset's joint velocities are outside the provided limits.""" + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # compute any violations + return torch.any(torch.abs(asset.data.joint_vel[:, asset_cfg.joint_ids]) > max_velocity, dim=1)
+ + +
[文档]def joint_effort_out_of_limit( + env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot") +) -> torch.Tensor: + """Terminate when effort applied on the asset's joints are outside of the soft joint limits. + + In the actuators, the applied torque are the efforts applied on the joints. These are computed by clipping + the computed torques to the joint limits. Hence, we check if the computed torques are equal to the applied + torques. + """ + # extract the used quantities (to enable type-hinting) + asset: Articulation = env.scene[asset_cfg.name] + # check if any joint effort is out of limit + out_of_limits = torch.isclose( + asset.data.computed_torque[:, asset_cfg.joint_ids], asset.data.applied_torque[:, asset_cfg.joint_ids] + ) + return torch.any(out_of_limits, dim=1)
+ + +""" +Contact sensor. +""" + + +
[文档]def illegal_contact(env: ManagerBasedRLEnv, threshold: float, sensor_cfg: SceneEntityCfg) -> torch.Tensor: + """Terminate when the contact force on the sensor exceeds the force threshold.""" + # extract the used quantities (to enable type-hinting) + contact_sensor: ContactSensor = env.scene.sensors[sensor_cfg.name] + net_contact_forces = contact_sensor.data.net_forces_w_history + # check if any contact force exceeds the threshold + return torch.any( + torch.max(torch.norm(net_contact_forces[:, :, sensor_cfg.body_ids], dim=-1), dim=1)[0] > threshold, dim=1 + )
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/ui/base_env_window.html b/_modules/omni/isaac/lab/envs/ui/base_env_window.html new file mode 100644 index 0000000000..0c6744c0e9 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/ui/base_env_window.html @@ -0,0 +1,962 @@ + + + + + + + + + + + omni.isaac.lab.envs.ui.base_env_window — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.ui.base_env_window 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import asyncio
+import os
+import weakref
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+import omni.kit.app
+import omni.kit.commands
+import omni.usd
+from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics
+
+if TYPE_CHECKING:
+    import omni.ui
+
+    from ..manager_based_env import ManagerBasedEnv
+
+
+
[文档]class BaseEnvWindow: + """Window manager for the basic environment. + + This class creates a window that is used to control the environment. The window + contains controls for rendering, debug visualization, and other environment-specific + UI elements. + + Users can add their own UI elements to the window by using the `with` context manager. + This can be done either be inheriting the class or by using the `env.window` object + directly from the standalone execution script. + + Example for adding a UI element from the standalone execution script: + >>> with env.window.ui_window_elements["main_vstack"]: + >>> ui.Label("My UI element") + + """ + +
[文档] def __init__(self, env: ManagerBasedEnv, window_name: str = "IsaacLab"): + """Initialize the window. + + Args: + env: The environment object. + window_name: The name of the window. Defaults to "IsaacLab". + """ + # store inputs + self.env = env + # prepare the list of assets that can be followed by the viewport camera + # note that the first two options are "World" and "Env" which are special cases + self._viewer_assets_options = [ + "World", + "Env", + *self.env.scene.rigid_objects.keys(), + *self.env.scene.articulations.keys(), + ] + + print("Creating window for environment.") + # create window for UI + self.ui_window = omni.ui.Window( + window_name, width=400, height=500, visible=True, dock_preference=omni.ui.DockPreference.RIGHT_TOP + ) + # dock next to properties window + asyncio.ensure_future(self._dock_window(window_title=self.ui_window.title)) + + # keep a dictionary of stacks so that child environments can add their own UI elements + # this can be done by using the `with` context manager + self.ui_window_elements = dict() + # create main frame + self.ui_window_elements["main_frame"] = self.ui_window.frame + with self.ui_window_elements["main_frame"]: + # create main stack + self.ui_window_elements["main_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["main_vstack"]: + # create collapsable frame for simulation + self._build_sim_frame() + # create collapsable frame for viewer + self._build_viewer_frame() + # create collapsable frame for debug visualization + self._build_debug_vis_frame()
+ + def __del__(self): + """Destructor for the window.""" + # destroy the window + if self.ui_window is not None: + self.ui_window.visible = False + self.ui_window.destroy() + self.ui_window = None + + """ + Build sub-sections of the UI. + """ + + def _build_sim_frame(self): + """Builds the sim-related controls frame for the UI.""" + # create collapsable frame for controls + self.ui_window_elements["sim_frame"] = omni.ui.CollapsableFrame( + title="Simulation Settings", + width=omni.ui.Fraction(1), + height=0, + collapsed=False, + style=omni.isaac.ui.ui_utils.get_style(), + horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, + vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.ui_window_elements["sim_frame"]: + # create stack for controls + self.ui_window_elements["sim_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["sim_vstack"]: + # create rendering mode dropdown + render_mode_cfg = { + "label": "Rendering Mode", + "type": "dropdown", + "default_val": self.env.sim.render_mode.value, + "items": [member.name for member in self.env.sim.RenderMode if member.value >= 0], + "tooltip": "Select a rendering mode\n" + self.env.sim.RenderMode.__doc__, + "on_clicked_fn": lambda value: self.env.sim.set_render_mode(self.env.sim.RenderMode[value]), + } + self.ui_window_elements["render_dropdown"] = omni.isaac.ui.ui_utils.dropdown_builder(**render_mode_cfg) + + # create animation recording box + record_animate_cfg = { + "label": "Record Animation", + "type": "state_button", + "a_text": "START", + "b_text": "STOP", + "tooltip": "Record the animation of the scene. Only effective if fabric is disabled.", + "on_clicked_fn": lambda value: self._toggle_recording_animation_fn(value), + } + self.ui_window_elements["record_animation"] = omni.isaac.ui.ui_utils.state_btn_builder( + **record_animate_cfg + ) + # disable the button if fabric is not enabled + self.ui_window_elements["record_animation"].enabled = not self.env.sim.is_fabric_enabled() + + def _build_viewer_frame(self): + """Build the viewer-related control frame for the UI.""" + # create collapsable frame for viewer + self.ui_window_elements["viewer_frame"] = omni.ui.CollapsableFrame( + title="Viewer Settings", + width=omni.ui.Fraction(1), + height=0, + collapsed=False, + style=omni.isaac.ui.ui_utils.get_style(), + horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, + vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.ui_window_elements["viewer_frame"]: + # create stack for controls + self.ui_window_elements["viewer_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["viewer_vstack"]: + # create a number slider to move to environment origin + # NOTE: slider is 1-indexed, whereas the env index is 0-indexed + viewport_origin_cfg = { + "label": "Environment Index", + "type": "button", + "default_val": self.env.cfg.viewer.env_index + 1, + "min": 1, + "max": self.env.num_envs, + "tooltip": "The environment index to follow. Only effective if follow mode is not 'World'.", + } + self.ui_window_elements["viewer_env_index"] = omni.isaac.ui.ui_utils.int_builder(**viewport_origin_cfg) + # create a number slider to move to environment origin + self.ui_window_elements["viewer_env_index"].add_value_changed_fn(self._set_viewer_env_index_fn) + + # create a tracker for the camera location + viewer_follow_cfg = { + "label": "Follow Mode", + "type": "dropdown", + "default_val": 0, + "items": [name.replace("_", " ").title() for name in self._viewer_assets_options], + "tooltip": "Select the viewport camera following mode.", + "on_clicked_fn": self._set_viewer_origin_type_fn, + } + self.ui_window_elements["viewer_follow"] = omni.isaac.ui.ui_utils.dropdown_builder(**viewer_follow_cfg) + + # add viewer default eye and lookat locations + self.ui_window_elements["viewer_eye"] = omni.isaac.ui.ui_utils.xyz_builder( + label="Camera Eye", + tooltip="Modify the XYZ location of the viewer eye.", + default_val=self.env.cfg.viewer.eye, + step=0.1, + on_value_changed_fn=[self._set_viewer_location_fn] * 3, + ) + self.ui_window_elements["viewer_lookat"] = omni.isaac.ui.ui_utils.xyz_builder( + label="Camera Target", + tooltip="Modify the XYZ location of the viewer target.", + default_val=self.env.cfg.viewer.lookat, + step=0.1, + on_value_changed_fn=[self._set_viewer_location_fn] * 3, + ) + + def _build_debug_vis_frame(self): + """Builds the debug visualization frame for various scene elements. + + This function inquires the scene for all elements that have a debug visualization + implemented and creates a checkbox to toggle the debug visualization for each element + that has it implemented. If the element does not have a debug visualization implemented, + a label is created instead. + """ + # import omni.isaac.ui.ui_utils as ui_utils + # import omni.ui + + # create collapsable frame for debug visualization + self.ui_window_elements["debug_frame"] = omni.ui.CollapsableFrame( + title="Scene Debug Visualization", + width=omni.ui.Fraction(1), + height=0, + collapsed=False, + style=omni.isaac.ui.ui_utils.get_style(), + horizontal_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_AS_NEEDED, + vertical_scrollbar_policy=omni.ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON, + ) + with self.ui_window_elements["debug_frame"]: + # create stack for debug visualization + self.ui_window_elements["debug_vstack"] = omni.ui.VStack(spacing=5, height=0) + with self.ui_window_elements["debug_vstack"]: + elements = [ + self.env.scene.terrain, + *self.env.scene.rigid_objects.values(), + *self.env.scene.articulations.values(), + *self.env.scene.sensors.values(), + ] + names = [ + "terrain", + *self.env.scene.rigid_objects.keys(), + *self.env.scene.articulations.keys(), + *self.env.scene.sensors.keys(), + ] + # create one for the terrain + for elem, name in zip(elements, names): + if elem is not None: + self._create_debug_vis_ui_element(name, elem) + + """ + Custom callbacks for UI elements. + """ + + def _toggle_recording_animation_fn(self, value: bool): + """Toggles the animation recording.""" + if value: + # log directory to save the recording + if not hasattr(self, "animation_log_dir"): + # create a new log directory + log_dir = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + self.animation_log_dir = os.path.join(os.getcwd(), "recordings", log_dir) + # start the recording + _ = omni.kit.commands.execute( + "StartRecording", + target_paths=[("/World", True)], + live_mode=True, + use_frame_range=False, + start_frame=0, + end_frame=0, + use_preroll=False, + preroll_frame=0, + record_to="FILE", + fps=0, + apply_root_anim=False, + increment_name=True, + record_folder=self.animation_log_dir, + take_name="TimeSample", + ) + else: + # stop the recording + _ = omni.kit.commands.execute("StopRecording") + # save the current stage + stage = omni.usd.get_context().get_stage() + source_layer = stage.GetRootLayer() + # output the stage to a file + stage_usd_path = os.path.join(self.animation_log_dir, "Stage.usd") + source_prim_path = "/" + # creates empty anon layer + temp_layer = Sdf.Find(stage_usd_path) + if temp_layer is None: + temp_layer = Sdf.Layer.CreateNew(stage_usd_path) + temp_stage = Usd.Stage.Open(temp_layer) + # update stage data + UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.GetStageUpAxis(stage)) + UsdGeom.SetStageMetersPerUnit(temp_stage, UsdGeom.GetStageMetersPerUnit(stage)) + # copy the prim + Sdf.CreatePrimInLayer(temp_layer, source_prim_path) + Sdf.CopySpec(source_layer, source_prim_path, temp_layer, source_prim_path) + # set the default prim + temp_layer.defaultPrim = Sdf.Path(source_prim_path).name + # remove all physics from the stage + for prim in temp_stage.TraverseAll(): + # skip if the prim is an instance + if prim.IsInstanceable(): + continue + # if prim has articulation then disable it + if prim.HasAPI(UsdPhysics.ArticulationRootAPI): + prim.RemoveAPI(UsdPhysics.ArticulationRootAPI) + prim.RemoveAPI(PhysxSchema.PhysxArticulationAPI) + # if prim has rigid body then disable it + if prim.HasAPI(UsdPhysics.RigidBodyAPI): + prim.RemoveAPI(UsdPhysics.RigidBodyAPI) + prim.RemoveAPI(PhysxSchema.PhysxRigidBodyAPI) + # if prim is a joint type then disable it + if prim.IsA(UsdPhysics.Joint): + prim.GetAttribute("physics:jointEnabled").Set(False) + # resolve all paths relative to layer path + omni.usd.resolve_paths(source_layer.identifier, temp_layer.identifier) + # save the stage + temp_layer.Save() + # print the path to the saved stage + print("Recording completed.") + print(f"\tSaved recorded stage to : {stage_usd_path}") + print(f"\tSaved recorded animation to: {os.path.join(self.animation_log_dir, 'TimeSample_tk001.usd')}") + print("\nTo play the animation, check the instructions in the following link:") + print( + "\thttps://docs.omniverse.nvidia.com/extensions/latest/ext_animation_stage-recorder.html#using-the-captured-timesamples" + ) + print("\n") + # reset the log directory + self.animation_log_dir = None + + def _set_viewer_origin_type_fn(self, value: str): + """Sets the origin of the viewport's camera. This is based on the drop-down menu in the UI.""" + # Extract the viewport camera controller from environment + vcc = self.env.viewport_camera_controller + if vcc is None: + raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") + + # Based on origin type, update the camera view + if value == "World": + vcc.update_view_to_world() + elif value == "Env": + vcc.update_view_to_env() + else: + # find which index the asset is + fancy_names = [name.replace("_", " ").title() for name in self._viewer_assets_options] + # store the desired env index + viewer_asset_name = self._viewer_assets_options[fancy_names.index(value)] + # update the camera view + vcc.update_view_to_asset_root(viewer_asset_name) + + def _set_viewer_location_fn(self, model: omni.ui.SimpleFloatModel): + """Sets the viewport camera location based on the UI.""" + # access the viewport camera controller (for brevity) + vcc = self.env.viewport_camera_controller + if vcc is None: + raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") + # obtain the camera locations and set them in the viewpoint camera controller + eye = [self.ui_window_elements["viewer_eye"][i].get_value_as_float() for i in range(3)] + lookat = [self.ui_window_elements["viewer_lookat"][i].get_value_as_float() for i in range(3)] + # update the camera view + vcc.update_view_location(eye, lookat) + + def _set_viewer_env_index_fn(self, model: omni.ui.SimpleIntModel): + """Sets the environment index and updates the camera if in 'env' origin mode.""" + # access the viewport camera controller (for brevity) + vcc = self.env.viewport_camera_controller + if vcc is None: + raise ValueError("Viewport camera controller is not initialized! Please check the rendering mode.") + # store the desired env index, UI is 1-indexed + vcc.set_view_env_index(model.as_int - 1) + + """ + Helper functions - UI building. + """ + + def _create_debug_vis_ui_element(self, name: str, elem: object): + """Create a checkbox for toggling debug visualization for the given element.""" + from omni.kit.window.extensions import SimpleCheckBox + + with omni.ui.HStack(): + # create the UI element + text = ( + "Toggle debug visualization." + if elem.has_debug_vis_implementation + else "Debug visualization not implemented." + ) + omni.ui.Label( + name.replace("_", " ").title(), + width=omni.isaac.ui.ui_utils.LABEL_WIDTH - 12, + alignment=omni.ui.Alignment.LEFT_CENTER, + tooltip=text, + ) + self.ui_window_elements[f"{name}_cb"] = SimpleCheckBox( + model=omni.ui.SimpleBoolModel(), + enabled=elem.has_debug_vis_implementation, + checked=elem.cfg.debug_vis if elem.cfg else False, + on_checked_fn=lambda value, e=weakref.proxy(elem): e.set_debug_vis(value), + ) + omni.isaac.ui.ui_utils.add_line_rect_flourish() + + async def _dock_window(self, window_title: str): + """Docks the custom UI window to the property window.""" + # wait for the window to be created + for _ in range(5): + if omni.ui.Workspace.get_window(window_title): + break + await self.env.sim.app.next_update_async() + + # dock next to properties window + custom_window = omni.ui.Workspace.get_window(window_title) + property_window = omni.ui.Workspace.get_window("Property") + if custom_window and property_window: + custom_window.dock_in(property_window, omni.ui.DockPosition.SAME, 1.0) + custom_window.focus()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/ui/manager_based_rl_env_window.html b/_modules/omni/isaac/lab/envs/ui/manager_based_rl_env_window.html new file mode 100644 index 0000000000..b871bb13cd --- /dev/null +++ b/_modules/omni/isaac/lab/envs/ui/manager_based_rl_env_window.html @@ -0,0 +1,597 @@ + + + + + + + + + + + omni.isaac.lab.envs.ui.manager_based_rl_env_window — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.ui.manager_based_rl_env_window 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from .base_env_window import BaseEnvWindow
+
+if TYPE_CHECKING:
+    from ..manager_based_rl_env import ManagerBasedRLEnv
+
+
+
[文档]class ManagerBasedRLEnvWindow(BaseEnvWindow): + """Window manager for the RL environment. + + On top of the basic environment window, this class adds controls for the RL environment. + This includes visualization of the command manager. + """ + +
[文档] def __init__(self, env: ManagerBasedRLEnv, window_name: str = "IsaacLab"): + """Initialize the window. + + Args: + env: The environment object. + window_name: The name of the window. Defaults to "IsaacLab". + """ + # initialize base window + super().__init__(env, window_name) + + # add custom UI elements + with self.ui_window_elements["main_vstack"]: + with self.ui_window_elements["debug_frame"]: + with self.ui_window_elements["debug_vstack"]: + self._create_debug_vis_ui_element("commands", self.env.command_manager) + self._create_debug_vis_ui_element("actions", self.env.action_manager)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/envs/ui/viewport_camera_controller.html b/_modules/omni/isaac/lab/envs/ui/viewport_camera_controller.html new file mode 100644 index 0000000000..4ea51a2425 --- /dev/null +++ b/_modules/omni/isaac/lab/envs/ui/viewport_camera_controller.html @@ -0,0 +1,751 @@ + + + + + + + + + + + omni.isaac.lab.envs.ui.viewport_camera_controller — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.envs.ui.viewport_camera_controller 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import copy
+import numpy as np
+import torch
+import weakref
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.kit.app
+import omni.timeline
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import DirectRLEnv, ManagerBasedEnv, ViewerCfg
+
+
+
[文档]class ViewportCameraController: + """This class handles controlling the camera associated with a viewport in the simulator. + + It can be used to set the viewpoint camera to track different origin types: + + - **world**: the center of the world (static) + - **env**: the center of an environment (static) + - **asset_root**: the root of an asset in the scene (e.g. tracking a robot moving in the scene) + + On creation, the camera is set to track the origin type specified in the configuration. + + For the :attr:`asset_root` origin type, the camera is updated at each rendering step to track the asset's + root position. For this, it registers a callback to the post update event stream from the simulation app. + """ + +
[文档] def __init__(self, env: ManagerBasedEnv | DirectRLEnv, cfg: ViewerCfg): + """Initialize the ViewportCameraController. + + Args: + env: The environment. + cfg: The configuration for the viewport camera controller. + + Raises: + ValueError: If origin type is configured to be "env" but :attr:`cfg.env_index` is out of bounds. + ValueError: If origin type is configured to be "asset_root" but :attr:`cfg.asset_name` is unset. + + """ + # store inputs + self._env = env + self._cfg = copy.deepcopy(cfg) + # cast viewer eye and look-at to numpy arrays + self.default_cam_eye = np.array(self._cfg.eye) + self.default_cam_lookat = np.array(self._cfg.lookat) + + # set the camera origins + if self.cfg.origin_type == "env": + # check that the env_index is within bounds + self.set_view_env_index(self.cfg.env_index) + # set the camera origin to the center of the environment + self.update_view_to_env() + elif self.cfg.origin_type == "asset_root": + # note: we do not yet update camera for tracking an asset origin, as the asset may not yet be + # in the scene when this is called. Instead, we subscribe to the post update event to update the camera + # at each rendering step. + if self.cfg.asset_name is None: + raise ValueError(f"No asset name provided for viewer with origin type: '{self.cfg.origin_type}'.") + else: + # set the camera origin to the center of the world + self.update_view_to_world() + + # subscribe to post update event so that camera view can be updated at each rendering step + app_interface = omni.kit.app.get_app_interface() + app_event_stream = app_interface.get_post_update_event_stream() + self._viewport_camera_update_handle = app_event_stream.create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._update_tracking_callback(event) + )
+ + def __del__(self): + """Unsubscribe from the callback.""" + # use hasattr to handle case where __init__ has not completed before __del__ is called + if hasattr(self, "_viewport_camera_update_handle") and self._viewport_camera_update_handle is not None: + self._viewport_camera_update_handle.unsubscribe() + self._viewport_camera_update_handle = None + + """ + Properties + """ + + @property + def cfg(self) -> ViewerCfg: + """The configuration for the viewer.""" + return self._cfg + + """ + Public Functions + """ + +
[文档] def set_view_env_index(self, env_index: int): + """Sets the environment index for the camera view. + + Args: + env_index: The index of the environment to set the camera view to. + + Raises: + ValueError: If the environment index is out of bounds. It should be between 0 and num_envs - 1. + """ + # check that the env_index is within bounds + if env_index < 0 or env_index >= self._env.num_envs: + raise ValueError( + f"Out of range value for attribute 'env_index': {env_index}." + f" Expected a value between 0 and {self._env.num_envs - 1} for the current environment." + ) + # update the environment index + self.cfg.env_index = env_index + # update the camera view if the origin is set to env type (since, the camera view is static) + # note: for assets, the camera view is updated at each rendering step + if self.cfg.origin_type == "env": + self.update_view_to_env()
+ +
[文档] def update_view_to_world(self): + """Updates the viewer's origin to the origin of the world which is (0, 0, 0).""" + # set origin type to world + self.cfg.origin_type = "world" + # update the camera origins + self.viewer_origin = torch.zeros(3) + # update the camera view + self.update_view_location()
+ +
[文档] def update_view_to_env(self): + """Updates the viewer's origin to the origin of the selected environment.""" + # set origin type to world + self.cfg.origin_type = "env" + # update the camera origins + self.viewer_origin = self._env.scene.env_origins[self.cfg.env_index] + # update the camera view + self.update_view_location()
+ +
[文档] def update_view_to_asset_root(self, asset_name: str): + """Updates the viewer's origin based upon the root of an asset in the scene. + + Args: + asset_name: The name of the asset in the scene. The name should match the name of the + asset in the scene. + + Raises: + ValueError: If the asset is not in the scene. + """ + # check if the asset is in the scene + if self.cfg.asset_name != asset_name: + asset_entities = [*self._env.scene.rigid_objects.keys(), *self._env.scene.articulations.keys()] + if asset_name not in asset_entities: + raise ValueError(f"Asset '{asset_name}' is not in the scene. Available entities: {asset_entities}.") + # update the asset name + self.cfg.asset_name = asset_name + # set origin type to asset_root + self.cfg.origin_type = "asset_root" + # update the camera origins + self.viewer_origin = self._env.scene[self.cfg.asset_name].data.root_pos_w[self.cfg.env_index] + # update the camera view + self.update_view_location()
+ +
[文档] def update_view_location(self, eye: Sequence[float] | None = None, lookat: Sequence[float] | None = None): + """Updates the camera view pose based on the current viewer origin and the eye and lookat positions. + + Args: + eye: The eye position of the camera. If None, the current eye position is used. + lookat: The lookat position of the camera. If None, the current lookat position is used. + """ + # store the camera view pose for later use + if eye is not None: + self.default_cam_eye = np.asarray(eye) + if lookat is not None: + self.default_cam_lookat = np.asarray(lookat) + # set the camera locations + viewer_origin = self.viewer_origin.detach().cpu().numpy() + cam_eye = viewer_origin + self.default_cam_eye + cam_target = viewer_origin + self.default_cam_lookat + + # set the camera view + self._env.sim.set_camera_view(eye=cam_eye, target=cam_target)
+ + """ + Private Functions + """ + + def _update_tracking_callback(self, event): + """Updates the camera view at each rendering step.""" + # update the camera view if the origin is set to asset_root + # in other cases, the camera view is static and does not need to be updated continuously + if self.cfg.origin_type == "asset_root" and self.cfg.asset_name is not None: + self.update_view_to_asset_root(self.cfg.asset_name)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/action_manager.html b/_modules/omni/isaac/lab/managers/action_manager.html new file mode 100644 index 0000000000..be1d95a2ab --- /dev/null +++ b/_modules/omni/isaac/lab/managers/action_manager.html @@ -0,0 +1,932 @@ + + + + + + + + + + + omni.isaac.lab.managers.action_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.action_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Action manager for processing actions sent to the environment."""
+
+from __future__ import annotations
+
+import inspect
+import torch
+import weakref
+from abc import abstractmethod
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+import omni.kit.app
+
+from omni.isaac.lab.assets import AssetBase
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import ActionTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedEnv
+
+
+
[文档]class ActionTerm(ManagerTermBase): + """Base class for action terms. + + The action term is responsible for processing the raw actions sent to the environment + and applying them to the asset managed by the term. The action term is comprised of two + operations: + + * Processing of actions: This operation is performed once per **environment step** and + is responsible for pre-processing the raw actions sent to the environment. + * Applying actions: This operation is performed once per **simulation step** and is + responsible for applying the processed actions to the asset managed by the term. + """ + +
[文档] def __init__(self, cfg: ActionTermCfg, env: ManagerBasedEnv): + """Initialize the action term. + + Args: + cfg: The configuration object. + env: The environment instance. + """ + # call the base class constructor + super().__init__(cfg, env) + # parse config to obtain asset to which the term is applied + self._asset: AssetBase = self._env.scene[self.cfg.asset_name] + + # add handle for debug visualization (this is set to a valid handle inside set_debug_vis) + self._debug_vis_handle = None + # set initial state of debug visualization + self.set_debug_vis(self.cfg.debug_vis)
+ + def __del__(self): + """Unsubscribe from the callbacks.""" + if self._debug_vis_handle: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + + """ + Properties. + """ + + @property + @abstractmethod + def action_dim(self) -> int: + """Dimension of the action term.""" + raise NotImplementedError + + @property + @abstractmethod + def raw_actions(self) -> torch.Tensor: + """The input/raw actions sent to the term.""" + raise NotImplementedError + + @property + @abstractmethod + def processed_actions(self) -> torch.Tensor: + """The actions computed by the term after applying any processing.""" + raise NotImplementedError + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the action term has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_debug_vis_impl) + return "NotImplementedError" not in source_code + + """ + Operations. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Sets whether to visualize the action term data. + Args: + debug_vis: Whether to visualize the action term data. + Returns: + Whether the debug visualization was successfully set. False if the action term does + not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_debug_vis_implementation: + return False + # toggle debug visualization objects + self._set_debug_vis_impl(debug_vis) + # toggle debug visualization handles + if debug_vis: + # create a subscriber for the post update event if it doesn't exist + if self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + # return success + return True
+ +
[文档] @abstractmethod + def process_actions(self, actions: torch.Tensor): + """Processes the actions sent to the environment. + + Note: + This function is called once per environment step by the manager. + + Args: + actions: The actions to process. + """ + raise NotImplementedError
+ +
[文档] @abstractmethod + def apply_actions(self): + """Applies the actions to the asset managed by the term. + + Note: + This is called at every simulation step by the manager. + """ + raise NotImplementedError
+ + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects. + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + def _debug_vis_callback(self, event): + """Callback for debug visualization. + This function calls the visualization objects and sets the data to visualize into them. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.")
+ + +
[文档]class ActionManager(ManagerBase): + """Manager for processing and applying actions for a given world. + + The action manager handles the interpretation and application of user-defined + actions on a given world. It is comprised of different action terms that decide + the dimension of the expected actions. + + The action manager performs operations at two stages: + + * processing of actions: It splits the input actions to each term and performs any + pre-processing needed. This should be called once at every environment step. + * apply actions: This operation typically sets the processed actions into the assets in the + scene (such as robots). It should be called before every simulation step. + """ + +
[文档] def __init__(self, cfg: object, env: ManagerBasedEnv): + """Initialize the action manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, ActionTermCfg]``). + env: The environment instance. + + Raises: + ValueError: If the configuration is None. + """ + # check if config is None + if cfg is None: + raise ValueError("Action manager configuration is None. Please provide a valid configuration.") + + # call the base class constructor (this prepares the terms) + super().__init__(cfg, env) + # create buffers to store actions + self._action = torch.zeros((self.num_envs, self.total_action_dim), device=self.device) + self._prev_action = torch.zeros_like(self._action) + + # check if any term has debug visualization implemented + self.cfg.debug_vis = False + for term in self._terms.values(): + self.cfg.debug_vis |= term.cfg.debug_vis
+ + def __str__(self) -> str: + """Returns: A string representation for action manager.""" + msg = f"<ActionManager> contains {len(self._term_names)} active terms.\n" + + # create table for term information + table = PrettyTable() + table.title = f"Active Action Terms (shape: {self.total_action_dim})" + table.field_names = ["Index", "Name", "Dimension"] + # set alignment of table columns + table.align["Name"] = "l" + table.align["Dimension"] = "r" + # add info on each term + for index, (name, term) in enumerate(self._terms.items()): + table.add_row([index, name, term.action_dim]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def total_action_dim(self) -> int: + """Total dimension of actions.""" + return sum(self.action_term_dim) + + @property + def active_terms(self) -> list[str]: + """Name of active action terms.""" + return self._term_names + + @property + def action_term_dim(self) -> list[int]: + """Shape of each action term.""" + return [term.action_dim for term in self._terms.values()] + + @property + def action(self) -> torch.Tensor: + """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).""" + return self._prev_action + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the command terms have debug visualization implemented.""" + # check if function raises NotImplementedError + has_debug_vis = False + for term in self._terms.values(): + has_debug_vis |= term.has_debug_vis_implementation + return has_debug_vis + + """ + Operations. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Sets whether to visualize the action data. + Args: + debug_vis: Whether to visualize the action data. + Returns: + Whether the debug visualization was successfully set. False if the action + does not support debug visualization. + """ + for term in self._terms.values(): + term.set_debug_vis(debug_vis)
+ +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """Resets the action history. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + + Returns: + An empty dictionary. + """ + # resolve environment ids + if env_ids is None: + env_ids = slice(None) + # reset the action history + self._prev_action[env_ids] = 0.0 + self._action[env_ids] = 0.0 + # reset all action terms + for term in self._terms.values(): + term.reset(env_ids=env_ids) + # nothing to log here + return {}
+ +
[文档] def process_action(self, action: torch.Tensor): + """Processes the actions sent to the environment. + + Note: + This function should be called once per environment step. + + Args: + action: The actions to process. + """ + # check if action dimension is valid + if self.total_action_dim != action.shape[1]: + raise ValueError(f"Invalid action shape, expected: {self.total_action_dim}, received: {action.shape[1]}.") + # store the input actions + self._prev_action[:] = self._action + self._action[:] = action.to(self.device) + + # split the actions and apply to each tensor + idx = 0 + for term in self._terms.values(): + term_actions = action[:, idx : idx + term.action_dim] + term.process_actions(term_actions) + idx += term.action_dim
+ +
[文档] def apply_action(self) -> None: + """Applies the actions to the environment/simulation. + + Note: + This should be called at every simulation step. + """ + for term in self._terms.values(): + term.apply_actions()
+ +
[文档] def get_term(self, name: str) -> ActionTerm: + """Returns the action term with the specified name. + + Args: + name: The name of the action term. + + Returns: + The action term with the specified name. + """ + return self._terms[name]
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._terms: dict[str, ActionTerm] = dict() + + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + # parse action terms from the config + for term_name, term_cfg in cfg_items: + # check if term config is None + if term_cfg is None: + continue + # check valid type + if not isinstance(term_cfg, ActionTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type ActionTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # create the action term + term = term_cfg.class_type(term_cfg, self._env) + # sanity check if term is valid type + if not isinstance(term, ActionTerm): + raise TypeError(f"Returned object for the term '{term_name}' is not of type ActionType.") + # add term name and parameters + self._term_names.append(term_name) + self._terms[term_name] = term
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/command_manager.html b/_modules/omni/isaac/lab/managers/command_manager.html new file mode 100644 index 0000000000..7f84f5b988 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/command_manager.html @@ -0,0 +1,960 @@ + + + + + + + + + + + omni.isaac.lab.managers.command_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.command_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Command manager for generating and updating commands."""
+
+from __future__ import annotations
+
+import inspect
+import torch
+import weakref
+from abc import abstractmethod
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+import omni.kit.app
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import CommandTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+
+
+
[文档]class CommandTerm(ManagerTermBase): + """The base class for implementing a command term. + + A command term is used to generate commands for goal-conditioned tasks. For example, + in the case of a goal-conditioned navigation task, the command term can be used to + generate a target position for the robot to navigate to. + + It implements a resampling mechanism that allows the command to be resampled at a fixed + frequency. The resampling frequency can be specified in the configuration object. + Additionally, it is possible to assign a visualization function to the command term + that can be used to visualize the command in the simulator. + """ + + def __init__(self, cfg: CommandTermCfg, env: ManagerBasedRLEnv): + """Initialize the command generator class. + + Args: + cfg: The configuration parameters for the command generator. + env: The environment object. + """ + super().__init__(cfg, env) + + # create buffers to store the command + # -- metrics that can be used for logging + self.metrics = dict() + # -- time left before resampling + self.time_left = torch.zeros(self.num_envs, device=self.device) + # -- counter for the number of times the command has been resampled within the current episode + self.command_counter = torch.zeros(self.num_envs, device=self.device, dtype=torch.long) + + # add handle for debug visualization (this is set to a valid handle inside set_debug_vis) + self._debug_vis_handle = None + # set initial state of debug visualization + self.set_debug_vis(self.cfg.debug_vis) + + def __del__(self): + """Unsubscribe from the callbacks.""" + if self._debug_vis_handle: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + + """ + Properties + """ + + @property + @abstractmethod + def command(self) -> torch.Tensor: + """The command tensor. Shape is (num_envs, command_dim).""" + raise NotImplementedError + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the command generator has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_debug_vis_impl) + return "NotImplementedError" not in source_code + + """ + Operations. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Sets whether to visualize the command data. + + Args: + debug_vis: Whether to visualize the command data. + + Returns: + Whether the debug visualization was successfully set. False if the command + generator does not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_debug_vis_implementation: + return False + # toggle debug visualization objects + self._set_debug_vis_impl(debug_vis) + # toggle debug visualization handles + if debug_vis: + # create a subscriber for the post update event if it doesn't exist + if self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + # return success + return True
+ +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + """Reset the command generator and log metrics. + + This function resets the command counter and resamples the command. It should be called + at the beginning of each episode. + + Args: + env_ids: The list of environment IDs to reset. Defaults to None. + + Returns: + A dictionary containing the information to log under the "{name}" key. + """ + # resolve the environment IDs + if env_ids is None: + env_ids = slice(None) + # set the command counter to zero + self.command_counter[env_ids] = 0 + # resample the command + self._resample(env_ids) + # add logging metrics + extras = {} + for metric_name, metric_value in self.metrics.items(): + # compute the mean metric value + extras[metric_name] = torch.mean(metric_value[env_ids]).item() + # reset the metric value + metric_value[env_ids] = 0.0 + return extras
+ +
[文档] def compute(self, dt: float): + """Compute the command. + + Args: + dt: The time step passed since the last call to compute. + """ + # update the metrics based on current state + self._update_metrics() + # reduce the time left before resampling + self.time_left -= dt + # resample the command if necessary + resample_env_ids = (self.time_left <= 0.0).nonzero().flatten() + if len(resample_env_ids) > 0: + self._resample(resample_env_ids) + # update the command + self._update_command()
+ + """ + Helper functions. + """ + + def _resample(self, env_ids: Sequence[int]): + """Resample the command. + + This function resamples the command and time for which the command is applied for the + specified environment indices. + + Args: + env_ids: The list of environment IDs to resample. + """ + # resample the time left before resampling + if len(env_ids) != 0: + self.time_left[env_ids] = self.time_left[env_ids].uniform_(*self.cfg.resampling_time_range) + # increment the command counter + self.command_counter[env_ids] += 1 + # resample the command + self._resample_command(env_ids) + + """ + Implementation specific functions. + """ + + @abstractmethod + def _update_metrics(self): + """Update the metrics based on the current state.""" + raise NotImplementedError + + @abstractmethod + def _resample_command(self, env_ids: Sequence[int]): + """Resample the command for the specified environments.""" + raise NotImplementedError + + @abstractmethod + def _update_command(self): + """Update the command based on the current state.""" + raise NotImplementedError + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects. + + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + def _debug_vis_callback(self, event): + """Callback for debug visualization. + + This function calls the visualization objects and sets the data to visualize into them. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.")
+ + +
[文档]class CommandManager(ManagerBase): + """Manager for generating commands. + + The command manager is used to generate commands for an agent to execute. It makes it convenient to switch + between different command generation strategies within the same environment. For instance, in an environment + consisting of a quadrupedal robot, the command to it could be a velocity command or position command. + By keeping the command generation logic separate from the environment, it is easy to switch between different + command generation strategies. + + The command terms are implemented as classes that inherit from the :class:`CommandTerm` class. + Each command generator term should also have a corresponding configuration class that inherits from the + :class:`CommandTermCfg` class. + """ + + _env: ManagerBasedRLEnv + """The environment instance.""" + +
[文档] def __init__(self, cfg: object, env: ManagerBasedRLEnv): + """Initialize the command manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, CommandTermCfg]``). + env: The environment instance. + """ + # create buffers to parse and store terms + self._terms: dict[str, CommandTerm] = dict() + + # call the base class constructor (this prepares the terms) + super().__init__(cfg, env) + # store the commands + self._commands = dict() + if self.cfg: + self.cfg.debug_vis = False + for term in self._terms.values(): + self.cfg.debug_vis |= term.cfg.debug_vis
+ + def __str__(self) -> str: + """Returns: A string representation for the command manager.""" + msg = f"<CommandManager> contains {len(self._terms.values())} active terms.\n" + + # create table for term information + table = PrettyTable() + table.title = "Active Command Terms" + table.field_names = ["Index", "Name", "Type"] + # set alignment of table columns + table.align["Name"] = "l" + # add info on each term + for index, (name, term) in enumerate(self._terms.items()): + table.add_row([index, name, term.__class__.__name__]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def active_terms(self) -> list[str]: + """Name of active command terms.""" + return list(self._terms.keys()) + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the command terms have debug visualization implemented.""" + # check if function raises NotImplementedError + has_debug_vis = False + for term in self._terms.values(): + has_debug_vis |= term.has_debug_vis_implementation + return has_debug_vis + + """ + Operations. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Sets whether to visualize the command data. + + Args: + debug_vis: Whether to visualize the command data. + + Returns: + Whether the debug visualization was successfully set. False if the command + generator does not support debug visualization. + """ + for term in self._terms.values(): + term.set_debug_vis(debug_vis)
+ +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """Reset the command terms and log their metrics. + + This function resets the command counter and resamples the command for each term. It should be called + at the beginning of each episode. + + Args: + env_ids: The list of environment IDs to reset. Defaults to None. + + Returns: + A dictionary containing the information to log under the "Metrics/{term_name}/{metric_name}" key. + """ + # resolve environment ids + if env_ids is None: + env_ids = slice(None) + # store information + extras = {} + for name, term in self._terms.items(): + # reset the command term + metrics = term.reset(env_ids=env_ids) + # compute the mean metric value + for metric_name, metric_value in metrics.items(): + extras[f"Metrics/{name}/{metric_name}"] = metric_value + # return logged information + return extras
+ +
[文档] def compute(self, dt: float): + """Updates the commands. + + This function calls each command term managed by the class. + + Args: + dt: The time-step interval of the environment. + + """ + # iterate over all the command terms + for term in self._terms.values(): + # compute term's value + term.compute(dt)
+ +
[文档] def get_command(self, name: str) -> torch.Tensor: + """Returns the command for the specified command term. + + Args: + name: The name of the command term. + + Returns: + The command tensor of the specified command term. + """ + return self._terms[name].command
+ +
[文档] def get_term(self, name: str) -> CommandTerm: + """Returns the command term with the specified name. + + Args: + name: The name of the command term. + + Returns: + The command term with the specified name. + """ + return self._terms[name]
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + # iterate over all the terms + for term_name, term_cfg in cfg_items: + # check for non config + if term_cfg is None: + continue + # check for valid config type + if not isinstance(term_cfg, CommandTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type CommandTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # create the action term + term = term_cfg.class_type(term_cfg, self._env) + # sanity check if term is valid type + if not isinstance(term, CommandTerm): + raise TypeError(f"Returned object for the term '{term_name}' is not of type CommandType.") + # add class to dict + self._terms[term_name] = term
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/curriculum_manager.html b/_modules/omni/isaac/lab/managers/curriculum_manager.html new file mode 100644 index 0000000000..352dec1366 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/curriculum_manager.html @@ -0,0 +1,728 @@ + + + + + + + + + + + omni.isaac.lab.managers.curriculum_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.curriculum_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Curriculum manager for updating environment quantities subject to a training curriculum."""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import CurriculumTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+
+
+
[文档]class CurriculumManager(ManagerBase): + """Manager to implement and execute specific curricula. + + The curriculum manager updates various quantities of the environment subject to a training curriculum by + calling a list of terms. These help stabilize learning by progressively making the learning tasks harder + as the agent improves. + + The curriculum terms are parsed from a config class containing the manager's settings and each term's + parameters. Each curriculum term should instantiate the :class:`CurriculumTermCfg` class. + """ + + _env: ManagerBasedRLEnv + """The environment instance.""" + +
[文档] def __init__(self, cfg: object, env: ManagerBasedRLEnv): + """Initialize the manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, CurriculumTermCfg]``) + env: An environment object. + + Raises: + TypeError: If curriculum term is not of type :class:`CurriculumTermCfg`. + ValueError: If curriculum term configuration does not satisfy its function signature. + """ + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._term_cfgs: list[CurriculumTermCfg] = list() + self._class_term_cfgs: list[CurriculumTermCfg] = list() + + # call the base class constructor (this will parse the terms config) + super().__init__(cfg, env) + + # prepare logging + self._curriculum_state = dict() + for term_name in self._term_names: + self._curriculum_state[term_name] = None
+ + def __str__(self) -> str: + """Returns: A string representation for curriculum manager.""" + msg = f"<CurriculumManager> contains {len(self._term_names)} active terms.\n" + + # create table for term information + table = PrettyTable() + table.title = "Active Curriculum Terms" + table.field_names = ["Index", "Name"] + # set alignment of table columns + table.align["Name"] = "l" + # add info on each term + for index, name in enumerate(self._term_names): + table.add_row([index, name]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def active_terms(self) -> list[str]: + """Name of active curriculum terms.""" + return self._term_names + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + """Returns the current state of individual curriculum terms. + + Note: + This function does not use the environment indices :attr:`env_ids` + and logs the state of all the terms. The argument is only present + to maintain consistency with other classes. + + Returns: + Dictionary of curriculum terms and their states. + """ + extras = {} + for term_name, term_state in self._curriculum_state.items(): + if term_state is not None: + # deal with dict + if isinstance(term_state, dict): + # each key is a separate state to log + for key, value in term_state.items(): + if isinstance(value, torch.Tensor): + value = value.item() + extras[f"Curriculum/{term_name}/{key}"] = value + else: + # log directly if not a dict + if isinstance(term_state, torch.Tensor): + term_state = term_state.item() + extras[f"Curriculum/{term_name}"] = term_state + # reset all the curriculum terms + for term_cfg in self._class_term_cfgs: + term_cfg.func.reset(env_ids=env_ids) + # return logged information + return extras
+ +
[文档] def compute(self, env_ids: Sequence[int] | None = None): + """Update the curriculum terms. + + This function calls each curriculum term managed by the class. + + Args: + env_ids: The list of environment IDs to update. + If None, all the environments are updated. Defaults to None. + """ + # resolve environment indices + if env_ids is None: + env_ids = slice(None) + # iterate over all the curriculum terms + for name, term_cfg in zip(self._term_names, self._term_cfgs): + state = term_cfg.func(self._env, env_ids, **term_cfg.params) + self._curriculum_state[name] = state
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + # iterate over all the terms + for term_name, term_cfg in cfg_items: + # check for non config + if term_cfg is None: + continue + # check if the term is a valid term config + if not isinstance(term_cfg, CurriculumTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type CurriculumTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # resolve common parameters + self._resolve_common_term_cfg(term_name, term_cfg, min_argc=2) + # add name and config to list + self._term_names.append(term_name) + self._term_cfgs.append(term_cfg) + # check if the term is a class + if isinstance(term_cfg.func, ManagerTermBase): + self._class_term_cfgs.append(term_cfg)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/event_manager.html b/_modules/omni/isaac/lab/managers/event_manager.html new file mode 100644 index 0000000000..b1de4a8573 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/event_manager.html @@ -0,0 +1,939 @@ + + + + + + + + + + + omni.isaac.lab.managers.event_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.event_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Event manager for orchestrating operations based on different simulation events."""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+import omni.log
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import EventTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedEnv
+
+
+
[文档]class EventManager(ManagerBase): + """Manager for orchestrating operations based on different simulation events. + + The event manager applies operations to the environment based on different simulation events. For example, + changing the masses of objects or their friction coefficients during initialization/ reset, or applying random + pushes to the robot at a fixed interval of steps. The user can specify several modes of events to fine-tune the + behavior based on when to apply the event. + + The event terms are parsed from a config class containing the manager's settings and each term's + parameters. Each event term should instantiate the :class:`EventTermCfg` class. + + Event terms can be grouped by their mode. The mode is a user-defined string that specifies when + the event term should be applied. This provides the user complete control over when event + terms should be applied. + + For a typical training process, you may want to apply events in the following modes: + + - "startup": Event is applied once at the beginning of the training. + - "reset": Event is applied at every reset. + - "interval": Event is applied at pre-specified intervals of time. + + However, you can also define your own modes and use them in the training process as you see fit. + For this you will need to add the triggering of that mode in the environment implementation as well. + + .. note:: + + The triggering of operations corresponding to the mode ``"interval"`` are the only mode that are + directly handled by the manager itself. The other modes are handled by the environment implementation. + + """ + + _env: ManagerBasedEnv + """The environment instance.""" + +
[文档] def __init__(self, cfg: object, env: ManagerBasedEnv): + """Initialize the event manager. + + Args: + cfg: A configuration object or dictionary (``dict[str, EventTermCfg]``). + env: An environment object. + """ + # create buffers to parse and store terms + self._mode_term_names: dict[str, list[str]] = dict() + self._mode_term_cfgs: dict[str, list[EventTermCfg]] = dict() + self._mode_class_term_cfgs: dict[str, list[EventTermCfg]] = dict() + + # call the base class (this will parse the terms config) + super().__init__(cfg, env)
+ + def __str__(self) -> str: + """Returns: A string representation for event manager.""" + msg = f"<EventManager> contains {len(self._mode_term_names)} active terms.\n" + + # add info on each mode + for mode in self._mode_term_names: + # create table for term information + table = PrettyTable() + table.title = f"Active Event Terms in Mode: '{mode}'" + # add table headers based on mode + if mode == "interval": + table.field_names = ["Index", "Name", "Interval time range (s)"] + table.align["Name"] = "l" + for index, (name, cfg) in enumerate(zip(self._mode_term_names[mode], self._mode_term_cfgs[mode])): + table.add_row([index, name, cfg.interval_range_s]) + else: + table.field_names = ["Index", "Name"] + table.align["Name"] = "l" + for index, name in enumerate(self._mode_term_names[mode]): + table.add_row([index, name]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def active_terms(self) -> dict[str, list[str]]: + """Name of active event terms. + + The keys are the modes of event and the values are the names of the event terms. + """ + return self._mode_term_names + + @property + def available_modes(self) -> list[str]: + """Modes of events.""" + return list(self._mode_term_names.keys()) + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + # call all terms that are classes + for mode_cfg in self._mode_class_term_cfgs.values(): + for term_cfg in mode_cfg: + term_cfg.func.reset(env_ids=env_ids) + # nothing to log here + return {}
+ +
[文档] def apply( + self, + mode: str, + env_ids: Sequence[int] | None = None, + dt: float | None = None, + global_env_step_count: int | None = None, + ): + """Calls each event term in the specified mode. + + This function iterates over all the event terms in the specified mode and calls the function + corresponding to the term. The function is called with the environment instance and the environment + indices to apply the event to. + + For the "interval" mode, the function is called when the time interval has passed. This requires + specifying the time step of the environment. + + For the "reset" mode, the function is called when the mode is "reset" and the total number of environment + steps that have happened since the last trigger of the function is equal to its configured parameter for + the number of environment steps between resets. + + Args: + mode: The mode of event. + env_ids: The indices of the environments to apply the event to. + Defaults to None, in which case the event is applied to all environments when applicable. + dt: The time step of the environment. This is only used for the "interval" mode. + Defaults to None to simplify the call for other modes. + global_env_step_count: The total number of environment steps that have happened. This is only used + for the "reset" mode. Defaults to None to simplify the call for other modes. + + Raises: + ValueError: If the mode is ``"interval"`` and the time step is not provided. + ValueError: If the mode is ``"interval"`` and the environment indices are provided. This is an undefined + behavior as the environment indices are computed based on the time left for each environment. + ValueError: If the mode is ``"reset"`` and the total number of environment steps that have happened + is not provided. + """ + # check if mode is valid + if mode not in self._mode_term_names: + omni.log.warn(f"Event mode '{mode}' is not defined. Skipping event.") + return + # check if mode is interval and dt is not provided + if mode == "interval" and dt is None: + raise ValueError(f"Event mode '{mode}' requires the time-step of the environment.") + if mode == "interval" and env_ids is not None: + raise ValueError( + f"Event mode '{mode}' does not require environment indices. This is an undefined behavior" + " as the environment indices are computed based on the time left for each environment." + ) + # check if mode is reset and env step count is not provided + if mode == "reset" and global_env_step_count is None: + raise ValueError(f"Event mode '{mode}' requires the total number of environment steps to be provided.") + + # iterate over all the event terms + for index, term_cfg in enumerate(self._mode_term_cfgs[mode]): + if mode == "interval": + # extract time left for this term + time_left = self._interval_term_time_left[index] + # update the time left for each environment + time_left -= dt + + # check if the interval has passed and sample a new interval + # note: we compare with a small value to handle floating point errors + if term_cfg.is_global_time: + if time_left < 1e-6: + lower, upper = term_cfg.interval_range_s + sampled_interval = torch.rand(1) * (upper - lower) + lower + self._interval_term_time_left[index][:] = sampled_interval + + # call the event term (with None for env_ids) + term_cfg.func(self._env, None, **term_cfg.params) + else: + valid_env_ids = (time_left < 1e-6).nonzero().flatten() + if len(valid_env_ids) > 0: + lower, upper = term_cfg.interval_range_s + sampled_time = torch.rand(len(valid_env_ids), device=self.device) * (upper - lower) + lower + self._interval_term_time_left[index][valid_env_ids] = sampled_time + + # call the event term + term_cfg.func(self._env, valid_env_ids, **term_cfg.params) + elif mode == "reset": + # obtain the minimum step count between resets + min_step_count = term_cfg.min_step_count_between_reset + # resolve the environment indices + if env_ids is None: + env_ids = slice(None) + + # We bypass the trigger mechanism if min_step_count is zero, i.e. apply term on every reset call. + # This should avoid the overhead of checking the trigger condition. + if min_step_count == 0: + self._reset_term_last_triggered_step_id[index][env_ids] = global_env_step_count + self._reset_term_last_triggered_once[index][env_ids] = True + + # call the event term with the environment indices + term_cfg.func(self._env, env_ids, **term_cfg.params) + else: + # extract last reset step for this term + last_triggered_step = self._reset_term_last_triggered_step_id[index][env_ids] + triggered_at_least_once = self._reset_term_last_triggered_once[index][env_ids] + # compute the steps since last reset + steps_since_triggered = global_env_step_count - last_triggered_step + + # check if the term can be applied after the minimum step count between triggers has passed + valid_trigger = steps_since_triggered >= min_step_count + # check if the term has not been triggered yet (in that case, we trigger it at least once) + # this is usually only needed at the start of the environment + valid_trigger |= (last_triggered_step == 0) & ~triggered_at_least_once + + # select the valid environment indices based on the trigger + if env_ids == slice(None): + valid_env_ids = valid_trigger.nonzero().flatten() + else: + valid_env_ids = env_ids[valid_trigger] + + # reset the last reset step for each environment to the current env step count + if len(valid_env_ids) > 0: + self._reset_term_last_triggered_once[index][valid_env_ids] = True + self._reset_term_last_triggered_step_id[index][valid_env_ids] = global_env_step_count + + # call the event term + term_cfg.func(self._env, valid_env_ids, **term_cfg.params) + else: + # call the event term + term_cfg.func(self._env, env_ids, **term_cfg.params)
+ + """ + Operations - Term settings. + """ + +
[文档] def set_term_cfg(self, term_name: str, cfg: EventTermCfg): + """Sets the configuration of the specified term into the manager. + + The method finds the term by name by searching through all the modes. + It then updates the configuration of the term with the first matching name. + + Args: + term_name: The name of the event term. + cfg: The configuration for the event term. + + Raises: + ValueError: If the term name is not found. + """ + term_found = False + for mode, terms in self._mode_term_names.items(): + if term_name in terms: + self._mode_term_cfgs[mode][terms.index(term_name)] = cfg + term_found = True + break + if not term_found: + raise ValueError(f"Event term '{term_name}' not found.")
+ +
[文档] def get_term_cfg(self, term_name: str) -> EventTermCfg: + """Gets the configuration for the specified term. + + The method finds the term by name by searching through all the modes. + It then returns the configuration of the term with the first matching name. + + Args: + term_name: The name of the event term. + + Returns: + The configuration of the event term. + + Raises: + ValueError: If the term name is not found. + """ + for mode, terms in self._mode_term_names.items(): + if term_name in terms: + return self._mode_term_cfgs[mode][terms.index(term_name)] + raise ValueError(f"Event term '{term_name}' not found.")
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + # buffer to store the time left for "interval" mode + # if interval is global, then it is a single value, otherwise it is per environment + self._interval_term_time_left: list[torch.Tensor] = list() + # buffer to store the step count when the term was last triggered for each environment for "reset" mode + self._reset_term_last_triggered_step_id: list[torch.Tensor] = list() + self._reset_term_last_triggered_once: list[torch.Tensor] = list() + + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + # iterate over all the terms + for term_name, term_cfg in cfg_items: + # check for non config + if term_cfg is None: + continue + # check for valid config type + if not isinstance(term_cfg, EventTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type EventTermCfg." + f" Received: '{type(term_cfg)}'." + ) + + if term_cfg.mode != "reset" and term_cfg.min_step_count_between_reset != 0: + omni.log.warn( + f"Event term '{term_name}' has 'min_step_count_between_reset' set to a non-zero value" + " but the mode is not 'reset'. Ignoring the 'min_step_count_between_reset' value." + ) + + # resolve common parameters + self._resolve_common_term_cfg(term_name, term_cfg, min_argc=2) + # check if mode is a new mode + if term_cfg.mode not in self._mode_term_names: + # add new mode + self._mode_term_names[term_cfg.mode] = list() + self._mode_term_cfgs[term_cfg.mode] = list() + self._mode_class_term_cfgs[term_cfg.mode] = list() + # add term name and parameters + self._mode_term_names[term_cfg.mode].append(term_name) + self._mode_term_cfgs[term_cfg.mode].append(term_cfg) + + # check if the term is a class + if isinstance(term_cfg.func, ManagerTermBase): + self._mode_class_term_cfgs[term_cfg.mode].append(term_cfg) + + # resolve the mode of the events + # -- interval mode + if term_cfg.mode == "interval": + if term_cfg.interval_range_s is None: + raise ValueError( + f"Event term '{term_name}' has mode 'interval' but 'interval_range_s' is not specified." + ) + + # sample the time left for global + if term_cfg.is_global_time: + lower, upper = term_cfg.interval_range_s + time_left = torch.rand(1) * (upper - lower) + lower + self._interval_term_time_left.append(time_left) + else: + # sample the time left for each environment + lower, upper = term_cfg.interval_range_s + time_left = torch.rand(self.num_envs, device=self.device) * (upper - lower) + lower + self._interval_term_time_left.append(time_left) + # -- reset mode + elif term_cfg.mode == "reset": + if term_cfg.min_step_count_between_reset < 0: + raise ValueError( + f"Event term '{term_name}' has mode 'reset' but 'min_step_count_between_reset' is" + f" negative: {term_cfg.min_step_count_between_reset}. Please provide a non-negative value." + ) + + # initialize the current step count for each environment to zero + step_count = torch.zeros(self.num_envs, device=self.device, dtype=torch.int32) + self._reset_term_last_triggered_step_id.append(step_count) + # initialize the trigger flag for each environment to zero + no_trigger = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) + self._reset_term_last_triggered_once.append(no_trigger)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/manager_base.html b/_modules/omni/isaac/lab/managers/manager_base.html new file mode 100644 index 0000000000..7ccd8274cd --- /dev/null +++ b/_modules/omni/isaac/lab/managers/manager_base.html @@ -0,0 +1,847 @@ + + + + + + + + + + + omni.isaac.lab.managers.manager_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.manager_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import copy
+import inspect
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any
+
+import omni.log
+
+import omni.isaac.lab.utils.string as string_utils
+from omni.isaac.lab.utils import string_to_callable
+
+from .manager_term_cfg import ManagerTermBaseCfg
+from .scene_entity_cfg import SceneEntityCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedEnv
+
+
+
[文档]class ManagerTermBase(ABC): + """Base class for manager terms. + + Manager term implementations can be functions or classes. If the term is a class, it should + inherit from this base class and implement the required methods. + + Each manager is implemented as a class that inherits from the :class:`ManagerBase` class. Each manager + class should also have a corresponding configuration class that defines the configuration terms for the + manager. Each term should the :class:`ManagerTermBaseCfg` class or its subclass. + + Example pseudo-code for creating a manager: + + .. code-block:: python + + from omni.isaac.lab.utils import configclass + from omni.isaac.lab.utils.mdp import ManagerBase, ManagerTermBaseCfg + + @configclass + class MyManagerCfg: + + my_term_1: ManagerTermBaseCfg = ManagerTermBaseCfg(...) + my_term_2: ManagerTermBaseCfg = ManagerTermBaseCfg(...) + my_term_3: ManagerTermBaseCfg = ManagerTermBaseCfg(...) + + # define manager instance + my_manager = ManagerBase(cfg=ManagerCfg(), env=env) + + """ + +
[文档] def __init__(self, cfg: ManagerTermBaseCfg, env: ManagerBasedEnv): + """Initialize the manager term. + + Args: + cfg: The configuration object. + env: The environment instance. + """ + # store the inputs + self.cfg = cfg + self._env = env
+ + """ + Properties. + """ + + @property + def num_envs(self) -> int: + """Number of environments.""" + return self._env.num_envs + + @property + def device(self) -> str: + """Device on which to perform computations.""" + return self._env.device + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> None: + """Resets the manager term. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + """ + pass
+ + def __call__(self, *args) -> Any: + """Returns the value of the term required by the manager. + + In case of a class implementation, this function is called by the manager + to get the value of the term. The arguments passed to this function are + the ones specified in the term configuration (see :attr:`ManagerTermBaseCfg.params`). + + .. attention:: + To be consistent with memory-less implementation of terms with functions, it is + recommended to ensure that the returned mutable quantities are cloned before + returning them. For instance, if the term returns a tensor, it is recommended + to ensure that the returned tensor is a clone of the original tensor. This prevents + the manager from storing references to the tensors and altering the original tensors. + + Args: + *args: Variable length argument list. + + Returns: + The value of the term. + """ + raise NotImplementedError
+ + +
[文档]class ManagerBase(ABC): + """Base class for all managers.""" + +
[文档] def __init__(self, cfg: object, env: ManagerBasedEnv): + """Initialize the manager. + + Args: + cfg: The configuration object. If None, the manager is initialized without any terms. + env: The environment instance. + """ + # store the inputs + self.cfg = copy.deepcopy(cfg) + self._env = env + # parse config to create terms information + if self.cfg: + self._prepare_terms()
+ + """ + Properties. + """ + + @property + def num_envs(self) -> int: + """Number of environments.""" + return self._env.num_envs + + @property + def device(self) -> str: + """Device on which to perform computations.""" + return self._env.device + + @property + @abstractmethod + def active_terms(self) -> list[str] | dict[str, list[str]]: + """Name of active terms.""" + raise NotImplementedError + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + """Resets the manager and returns logging information for the current time-step. + + Args: + env_ids: The environment ids for which to log data. + Defaults None, which logs data for all environments. + + Returns: + Dictionary containing the logging information. + """ + return {}
+ +
[文档] def find_terms(self, name_keys: str | Sequence[str]) -> list[str]: + """Find terms in the manager based on the names. + + This function searches the manager for terms based on the names. The names can be + specified as regular expressions or a list of regular expressions. The search is + performed on the active terms in the manager. + + Please check the :meth:`omni.isaac.lab.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 term names. + + Returns: + A list of term names that match the input keys. + """ + # resolve search keys + if isinstance(self.active_terms, dict): + list_of_strings = [] + for names in self.active_terms.values(): + list_of_strings.extend(names) + else: + list_of_strings = self.active_terms + + # return the matching names + return string_utils.resolve_matching_names(name_keys, list_of_strings)[1]
+ + """ + Implementation specific. + """ + + @abstractmethod + def _prepare_terms(self): + """Prepare terms information from the configuration object.""" + raise NotImplementedError + + """ + Helper functions. + """ + + def _resolve_common_term_cfg(self, term_name: str, term_cfg: ManagerTermBaseCfg, min_argc: int = 1): + """Resolve common term configuration. + + Usually, called by the :meth:`_prepare_terms` method to resolve common term configuration. + + Note: + By default, all term functions are expected to have at least one argument, which is the + environment object. Some other managers may expect functions to take more arguments, for + instance, the environment indices as the second argument. In such cases, the + ``min_argc`` argument can be used to specify the minimum number of arguments + required by the term function to be called correctly by the manager. + + Args: + term_name: The name of the term. + term_cfg: The term configuration. + min_argc: The minimum number of arguments required by the term function to be called correctly + by the manager. + + Raises: + TypeError: If the term configuration is not of type :class:`ManagerTermBaseCfg`. + ValueError: If the scene entity defined in the term configuration does not exist. + AttributeError: If the term function is not callable. + ValueError: If the term function's arguments are not matched by the parameters. + """ + # check if the term is a valid term config + if not isinstance(term_cfg, ManagerTermBaseCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type ManagerTermBaseCfg." + f" Received: '{type(term_cfg)}'." + ) + # iterate over all the entities and parse the joint and body names + for key, value in term_cfg.params.items(): + # deal with string + if isinstance(value, SceneEntityCfg): + # load the entity + try: + value.resolve(self._env.scene) + except ValueError as e: + raise ValueError(f"Error while parsing '{term_name}:{key}'. {e}") + # log the entity for checking later + msg = f"[{term_cfg.__class__.__name__}:{term_name}] Found entity '{value.name}'." + if value.joint_ids is not None: + msg += f"\n\tJoint names: {value.joint_names} [{value.joint_ids}]" + if value.body_ids is not None: + msg += f"\n\tBody names: {value.body_names} [{value.body_ids}]" + # print the information + omni.log.info(msg) + # store the entity + term_cfg.params[key] = value + + # get the corresponding function or functional class + if isinstance(term_cfg.func, str): + term_cfg.func = string_to_callable(term_cfg.func) + + # initialize the term if it is a class + if inspect.isclass(term_cfg.func): + if not issubclass(term_cfg.func, ManagerTermBase): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type ManagerTermBase." + f" Received: '{type(term_cfg.func)}'." + ) + term_cfg.func = term_cfg.func(cfg=term_cfg, env=self._env) + # check if function is callable + if not callable(term_cfg.func): + raise AttributeError(f"The term '{term_name}' is not callable. Received: {term_cfg.func}") + + # check if term's arguments are matched by params + term_params = list(term_cfg.params.keys()) + args = inspect.signature(term_cfg.func).parameters + args_with_defaults = [arg for arg in args if args[arg].default is not inspect.Parameter.empty] + args_without_defaults = [arg for arg in args if args[arg].default is inspect.Parameter.empty] + args = args_without_defaults + args_with_defaults + # ignore first two arguments for env and env_ids + # Think: Check for cases when kwargs are set inside the function? + if len(args) > min_argc: + if set(args[min_argc:]) != set(term_params + args_with_defaults): + raise ValueError( + f"The term '{term_name}' expects mandatory parameters: {args_without_defaults[min_argc:]}" + f" and optional parameters: {args_with_defaults}, but received: {term_params}." + )
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/manager_term_cfg.html b/_modules/omni/isaac/lab/managers/manager_term_cfg.html new file mode 100644 index 0000000000..68b5dfcb8e --- /dev/null +++ b/_modules/omni/isaac/lab/managers/manager_term_cfg.html @@ -0,0 +1,854 @@ + + + + + + + + + + + omni.isaac.lab.managers.manager_term_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.manager_term_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Configuration terms for different managers."""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import TYPE_CHECKING, Any
+
+from omni.isaac.lab.utils import configclass
+from omni.isaac.lab.utils.modifiers import ModifierCfg
+from omni.isaac.lab.utils.noise import NoiseCfg
+
+from .scene_entity_cfg import SceneEntityCfg
+
+if TYPE_CHECKING:
+    from .action_manager import ActionTerm
+    from .command_manager import CommandTerm
+    from .manager_base import ManagerTermBase
+
+
+
[文档]@configclass +class ManagerTermBaseCfg: + """Configuration for a manager term.""" + + func: Callable | ManagerTermBase = MISSING + """The function or class to be called for the term. + + The function must take the environment object as the first argument. + The remaining arguments are specified in the :attr:`params` attribute. + + It also supports `callable classes`_, i.e. classes that implement the :meth:`__call__` + method. In this case, the class should inherit from the :class:`ManagerTermBase` class + and implement the required methods. + + .. _`callable classes`: https://docs.python.org/3/reference/datamodel.html#object.__call__ + """ + + params: dict[str, Any | SceneEntityCfg] = dict() + """The parameters to be passed to the function as keyword arguments. Defaults to an empty dict. + + .. note:: + If the value is a :class:`SceneEntityCfg` object, the manager will query the scene entity + from the :class:`InteractiveScene` and process the entity's joints and bodies as specified + in the :class:`SceneEntityCfg` object. + """
+ + +## +# Action manager. +## + + +
[文档]@configclass +class ActionTermCfg: + """Configuration for an action term.""" + + class_type: type[ActionTerm] = MISSING + """The associated action term class. + + The class should inherit from :class:`omni.isaac.lab.managers.action_manager.ActionTerm`. + """ + + asset_name: str = MISSING + """The name of the scene entity. + + This is the name defined in the scene configuration file. See the :class:`InteractiveSceneCfg` + class for more details. + """ + + debug_vis: bool = False + """Whether to visualize debug information. Defaults to False."""
+ + +## +# Command manager. +## + + +
[文档]@configclass +class CommandTermCfg: + """Configuration for a command generator term.""" + + class_type: type[CommandTerm] = MISSING + """The associated command term class to use. + + The class should inherit from :class:`omni.isaac.lab.managers.command_manager.CommandTerm`. + """ + + resampling_time_range: tuple[float, float] = MISSING + """Time before commands are changed [s].""" + debug_vis: bool = False + """Whether to visualize debug information. Defaults to False."""
+ + +## +# Curriculum manager. +## + + +
[文档]@configclass +class CurriculumTermCfg(ManagerTermBaseCfg): + """Configuration for a curriculum term.""" + + func: Callable[..., float | dict[str, float] | None] = MISSING + """The name of the function to be called. + + This function should take the environment object, environment indices + and any other parameters as input and return the curriculum state for + logging purposes. If the function returns None, the curriculum state + is not logged. + """
+ + +## +# Observation manager. +## + + +
[文档]@configclass +class ObservationTermCfg(ManagerTermBaseCfg): + """Configuration for an observation term.""" + + func: Callable[..., torch.Tensor] = MISSING + """The name of the function to be called. + + 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). + """ + + modifiers: list[ModifierCfg] | None = None + """The list of data modifiers to apply to the observation in order. Defaults to None, + in which case no modifications will be applied. + + Modifiers are applied in the order they are specified in the list. They can be stateless + or stateful, and can be used to apply transformations to the observation data. For example, + a modifier can be used to normalize the observation data or to apply a rolling average. + + For more information on modifiers, see the :class:`~omni.isaac.lab.utils.modifiers.ModifierCfg` class. + """ + + noise: NoiseCfg | None = None + """The noise to add to the observation. Defaults to None, in which case no noise is added.""" + + clip: tuple[float, float] | None = None + """The clipping range for the observation after adding noise. Defaults to None, + in which case no clipping is applied.""" + + scale: tuple[float, ...] | float | None = None + """The scale to apply to the observation after clipping. Defaults to None, + in which case no scaling is applied (same as setting scale to :obj:`1`). + + We leverage PyTorch broadcasting to scale the observation tensor with the provided value. If a tuple is provided, + please make sure the length of the tuple matches the dimensions of the tensor outputted from the term. + """
+ + +
[文档]@configclass +class ObservationGroupCfg: + """Configuration for an observation group.""" + + concatenate_terms: bool = True + """Whether to concatenate the observation terms in the group. Defaults to True. + + If true, the observation terms in the group are concatenated along the last dimension. + Otherwise, they are kept separate and returned as a dictionary. + + If the observation group contains terms of different dimensions, it must be set to False. + """ + + enable_corruption: bool = False + """Whether to enable corruption for the observation group. Defaults to False. + + If true, the observation terms in the group are corrupted by adding noise (if specified). + Otherwise, no corruption is applied. + """
+ + +## +# Event manager +## + + +
[文档]@configclass +class EventTermCfg(ManagerTermBaseCfg): + """Configuration for a event term.""" + + func: Callable[..., None] = MISSING + """The name of the function to be called. + + This function should take the environment object, environment indices + and any other parameters as input. + """ + + mode: str = MISSING + """The mode in which the event term is applied. + + Note: + The mode name ``"interval"`` is a special mode that is handled by the + manager Hence, its name is reserved and cannot be used for other modes. + """ + + interval_range_s: tuple[float, float] | None = None + """The range of time in seconds at which the term is applied. Defaults to None. + + Based on this, the interval is sampled uniformly between the specified + range for each environment instance. The term is applied on the environment + instances where the current time hits the interval time. + + Note: + This is only used if the mode is ``"interval"``. + """ + + is_global_time: bool = False + """Whether randomization should be tracked on a per-environment basis. Defaults to False. + + If True, the same interval time is used for all the environment instances. + If False, the interval time is sampled independently for each environment instance + and the term is applied when the current time hits the interval time for that instance. + + Note: + This is only used if the mode is ``"interval"``. + """ + + min_step_count_between_reset: int = 0 + """The number of environment steps after which the term is applied since its last application. Defaults to 0. + + When the mode is "reset", the term is only applied if the number of environment steps since + its last application exceeds this quantity. This helps to avoid calling the term too often, + thereby improving performance. + + If the value is zero, the term is applied on every call to the manager with the mode "reset". + + Note: + This is only used if the mode is ``"reset"``. + """
+ + +## +# Reward manager. +## + + +
[文档]@configclass +class RewardTermCfg(ManagerTermBaseCfg): + """Configuration for a reward term.""" + + func: Callable[..., torch.Tensor] = MISSING + """The name of the function to be called. + + 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,). + """ + + weight: float = MISSING + """The weight of the reward term. + + This is multiplied with the reward term's value to compute the final + reward. + + Note: + If the weight is zero, the reward term is ignored. + """
+ + +## +# Termination manager. +## + + +
[文档]@configclass +class TerminationTermCfg(ManagerTermBaseCfg): + """Configuration for a termination term.""" + + func: Callable[..., torch.Tensor] = MISSING + """The name of the function to be called. + + 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,). + """ + + time_out: bool = False + """Whether the termination term contributes towards episodic timeouts. Defaults to False. + + Note: + These usually correspond to tasks that have a fixed time limit. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/observation_manager.html b/_modules/omni/isaac/lab/managers/observation_manager.html new file mode 100644 index 0000000000..2c80cb2852 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/observation_manager.html @@ -0,0 +1,972 @@ + + + + + + + + + + + omni.isaac.lab.managers.observation_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.observation_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Observation manager for computing observation signals for a given world."""
+
+from __future__ import annotations
+
+import inspect
+import torch
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+from omni.isaac.lab.utils import modifiers
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import ObservationGroupCfg, ObservationTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedEnv
+
+
+
[文档]class ObservationManager(ManagerBase): + """Manager for computing observation signals for a given world. + + Observations are organized into groups based on their intended usage. This allows having different observation + groups for different types of learning such as asymmetric actor-critic and student-teacher training. Each + group contains observation terms which contain information about the observation function to call, the noise + corruption model to use, and the sensor to retrieve data from. + + Each observation group should inherit from the :class:`ObservationGroupCfg` class. Within each group, each + observation term should instantiate the :class:`ObservationTermCfg` class. Based on the configuration, the + observations in a group can be concatenated into a single tensor or returned as a dictionary with keys + corresponding to the term's name. + + If the observations in a group are concatenated, the shape of the concatenated tensor is computed based on the + shapes of the individual observation terms. This information is stored in the :attr:`group_obs_dim` dictionary + with keys as the group names and values as the shape of the observation tensor. When the terms in a group are not + concatenated, the attribute stores a list of shapes for each term in the group. + + .. note:: + When the observation terms in a group do not have the same shape, the observation terms cannot be + concatenated. In this case, please set the :attr:`ObservationGroupCfg.concatenate_terms` attribute in the + group configuration to False. + + The observation manager can be used to compute observations for all the groups or for a specific group. The + observations 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, ...). + + If a noise model or custom modifier 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 shape as the observation. + The observations are clipped and scaled as per the configuration settings. + """ + +
[文档] def __init__(self, cfg: object, env: ManagerBasedEnv): + """Initialize observation manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, ObservationGroupCfg]``). + env: The environment instance. + + Raises: + ValueError: If the configuration is None. + RuntimeError: If the shapes of the observation terms in a group are not compatible for concatenation + and the :attr:`~ObservationGroupCfg.concatenate_terms` attribute is set to True. + """ + # check that cfg is not None + if cfg is None: + raise ValueError("Observation manager configuration is None. Please provide a valid configuration.") + + # call the base class constructor (this will parse the terms config) + super().__init__(cfg, env) + + # compute combined vector for obs group + self._group_obs_dim: dict[str, tuple[int, ...] | list[tuple[int, ...]]] = dict() + for group_name, group_term_dims in self._group_obs_term_dim.items(): + # if terms are concatenated, compute the combined shape into a single tuple + # otherwise, keep the list of shapes as is + if self._group_obs_concatenate[group_name]: + try: + term_dims = [torch.tensor(dims, device="cpu") for dims in group_term_dims] + self._group_obs_dim[group_name] = tuple(torch.sum(torch.stack(term_dims, dim=0), dim=0).tolist()) + except RuntimeError: + raise RuntimeError( + f"Unable to concatenate observation terms in group '{group_name}'." + f" The shapes of the terms are: {group_term_dims}." + " Please ensure that the shapes are compatible for concatenation." + " Otherwise, set 'concatenate_terms' to False in the group configuration." + ) + else: + self._group_obs_dim[group_name] = group_term_dims
+ + def __str__(self) -> str: + """Returns: A string representation for the observation manager.""" + msg = f"<ObservationManager> contains {len(self._group_obs_term_names)} groups.\n" + + # add info for each group + for group_name, group_dim in self._group_obs_dim.items(): + # create table for term information + table = PrettyTable() + table.title = f"Active Observation Terms in Group: '{group_name}'" + if self._group_obs_concatenate[group_name]: + table.title += f" (shape: {group_dim})" + table.field_names = ["Index", "Name", "Shape"] + # set alignment of table columns + table.align["Name"] = "l" + # add info for each term + obs_terms = zip( + self._group_obs_term_names[group_name], + self._group_obs_term_dim[group_name], + ) + for index, (name, dims) in enumerate(obs_terms): + # resolve inputs to simplify prints + tab_dims = tuple(dims) + # add row + table.add_row([index, name, tab_dims]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def active_terms(self) -> dict[str, list[str]]: + """Name of active observation terms in each group. + + The keys are the group names and the values are the list of observation term names in the group. + """ + return self._group_obs_term_names + + @property + def group_obs_dim(self) -> dict[str, tuple[int, ...] | list[tuple[int, ...]]]: + """Shape of computed observations in each group. + + The key is the group name and the value is the shape of the observation tensor. + If the terms in the group are concatenated, the value is a single tuple representing the + shape of the concatenated observation tensor. Otherwise, the value is a list of tuples, + where each tuple represents the shape of the observation tensor for a term in the group. + """ + return self._group_obs_dim + + @property + def group_obs_term_dim(self) -> dict[str, list[tuple[int, ...]]]: + """Shape of individual observation terms in each group. + + The key is the group name and the value is a list of tuples representing the shape of the observation terms + in the group. The order of the tuples corresponds to the order of the terms in the group. + This matches the order of the terms in the :attr:`active_terms`. + """ + return self._group_obs_term_dim + + @property + def group_obs_concatenate(self) -> dict[str, bool]: + """Whether the observation terms are concatenated in each group or not. + + The key is the group name and the value is a boolean specifying whether the observation terms in the group + are concatenated into a single tensor. If True, the observations are concatenated along the last dimension. + + The values are set based on the :attr:`~ObservationGroupCfg.concatenate_terms` attribute in the group + configuration. + """ + return self._group_obs_concatenate + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, float]: + # call all terms that are classes + for group_cfg in self._group_obs_class_term_cfgs.values(): + for term_cfg in group_cfg: + term_cfg.func.reset(env_ids=env_ids) + # call all modifiers that are classes + for mod in self._group_obs_class_modifiers: + mod.reset(env_ids=env_ids) + # nothing to log here + return {}
+ +
[文档] def compute(self) -> dict[str, torch.Tensor | dict[str, torch.Tensor]]: + """Compute the observations per group for all groups. + + The method computes the observations for all the groups handled by the observation manager. + Please check the :meth:`compute_group` on the processing of observations per group. + + Returns: + A dictionary with keys as the group names and values as the computed observations. + The observations are either concatenated into a single tensor or returned as a dictionary + with keys corresponding to the term's name. + """ + # create a buffer for storing obs from all the groups + obs_buffer = dict() + # iterate over all the terms in each group + for group_name in self._group_obs_term_names: + obs_buffer[group_name] = self.compute_group(group_name) + # otherwise return a dict with observations of all groups + return obs_buffer
+ +
[文档] def compute_group(self, group_name: str) -> torch.Tensor | dict[str, torch.Tensor]: + """Computes the observations for a given group. + + 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, ...). + + The following steps are performed for each observation term: + + 1. Compute observation term by calling the function + 2. Apply custom modifiers in the order specified in :attr:`ObservationTermCfg.modifiers` + 3. Apply corruption/noise model based on :attr:`ObservationTermCfg.noise` + 4. Apply clipping based on :attr:`ObservationTermCfg.clip` + 5. Apply scaling based on :attr:`ObservationTermCfg.scale` + + We apply noise to the computed term first to maintain the integrity of how noise affects the data + as it truly exists in the real world. If the noise is applied after clipping or scaling, the noise + could be artificially constrained or amplified, which might misrepresent how noise naturally occurs + in the data. + + Args: + 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: + Depending on the group's configuration, the tensors for individual observation terms are + concatenated along the last dimension into a single tensor. Otherwise, they are returned as + a dictionary with keys corresponding to the term's name. + + Raises: + ValueError: If input ``group_name`` is not a valid group handled by the manager. + """ + # check ig group name is valid + if group_name not in self._group_obs_term_names: + raise ValueError( + f"Unable to find the group '{group_name}' in the observation manager." + f" Available groups are: {list(self._group_obs_term_names.keys())}" + ) + # iterate over all the terms in each group + group_term_names = self._group_obs_term_names[group_name] + # buffer to store obs per group + group_obs = dict.fromkeys(group_term_names, None) + # read attributes for each term + obs_terms = zip(group_term_names, self._group_obs_term_cfgs[group_name]) + + # evaluate terms: compute, add noise, clip, scale, custom modifiers + for name, term_cfg in obs_terms: + # compute term's value + obs: torch.Tensor = term_cfg.func(self._env, **term_cfg.params).clone() + # apply post-processing + if term_cfg.modifiers is not None: + for modifier in term_cfg.modifiers: + obs = modifier.func(obs, **modifier.params) + if term_cfg.noise: + obs = term_cfg.noise.func(obs, term_cfg.noise) + if term_cfg.clip: + obs = obs.clip_(min=term_cfg.clip[0], max=term_cfg.clip[1]) + if term_cfg.scale is not None: + obs = obs.mul_(term_cfg.scale) + # add value to list + group_obs[name] = obs + + # concatenate all observations in the group together + if self._group_obs_concatenate[group_name]: + return torch.cat(list(group_obs.values()), dim=-1) + else: + return group_obs
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + """Prepares a list of observation terms functions.""" + # create buffers to store information for each observation group + # TODO: Make this more convenient by using data structures. + self._group_obs_term_names: dict[str, list[str]] = dict() + self._group_obs_term_dim: dict[str, list[tuple[int, ...]]] = dict() + self._group_obs_term_cfgs: dict[str, list[ObservationTermCfg]] = dict() + self._group_obs_class_term_cfgs: dict[str, list[ObservationTermCfg]] = dict() + self._group_obs_concatenate: dict[str, bool] = dict() + + # create a list to store modifiers that are classes + # we store it as a separate list to only call reset on them and prevent unnecessary calls + self._group_obs_class_modifiers: list[modifiers.ModifierBase] = list() + + # check if config is dict already + if isinstance(self.cfg, dict): + group_cfg_items = self.cfg.items() + else: + group_cfg_items = self.cfg.__dict__.items() + # iterate over all the groups + for group_name, group_cfg in group_cfg_items: + # check for non config + if group_cfg is None: + continue + # check if the term is a curriculum term + if not isinstance(group_cfg, ObservationGroupCfg): + raise TypeError( + f"Observation group '{group_name}' is not of type 'ObservationGroupCfg'." + f" Received: '{type(group_cfg)}'." + ) + # initialize list for the group settings + self._group_obs_term_names[group_name] = list() + self._group_obs_term_dim[group_name] = list() + self._group_obs_term_cfgs[group_name] = list() + self._group_obs_class_term_cfgs[group_name] = list() + # read common config for the group + self._group_obs_concatenate[group_name] = group_cfg.concatenate_terms + # check if config is dict already + if isinstance(group_cfg, dict): + group_cfg_items = group_cfg.items() + else: + group_cfg_items = group_cfg.__dict__.items() + # iterate over all the terms in each group + for term_name, term_cfg in group_cfg_items: + # skip non-obs settings + if term_name in ["enable_corruption", "concatenate_terms"]: + continue + # check for non config + if term_cfg is None: + continue + if not isinstance(term_cfg, ObservationTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type ObservationTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # resolve common terms in the config + self._resolve_common_term_cfg(f"{group_name}/{term_name}", term_cfg, min_argc=1) + + # check noise settings + if not group_cfg.enable_corruption: + term_cfg.noise = None + # add term config to list to list + self._group_obs_term_names[group_name].append(term_name) + self._group_obs_term_cfgs[group_name].append(term_cfg) + + # call function the first time to fill up dimensions + obs_dims = tuple(term_cfg.func(self._env, **term_cfg.params).shape) + self._group_obs_term_dim[group_name].append(obs_dims[1:]) + + # if scale is set, check if single float or tuple + if term_cfg.scale is not None: + if not isinstance(term_cfg.scale, (float, int, tuple)): + raise TypeError( + f"Scale for observation term '{term_name}' in group '{group_name}'" + f" is not of type float, int or tuple. Received: '{type(term_cfg.scale)}'." + ) + if isinstance(term_cfg.scale, tuple) and len(term_cfg.scale) != obs_dims[1]: + raise ValueError( + f"Scale for observation term '{term_name}' in group '{group_name}'" + f" does not match the dimensions of the observation. Expected: {obs_dims[1]}" + f" but received: {len(term_cfg.scale)}." + ) + + # cast the scale into torch tensor + term_cfg.scale = torch.tensor(term_cfg.scale, dtype=torch.float, device=self._env.device) + + # prepare modifiers for each observation + if term_cfg.modifiers is not None: + # initialize list of modifiers for term + for mod_cfg in term_cfg.modifiers: + # check if class modifier and initialize with observation size when adding + if isinstance(mod_cfg, modifiers.ModifierCfg): + # to list of modifiers + if inspect.isclass(mod_cfg.func): + if not issubclass(mod_cfg.func, modifiers.ModifierBase): + raise TypeError( + f"Modifier function '{mod_cfg.func}' for observation term '{term_name}'" + f" is not a subclass of 'ModifierBase'. Received: '{type(mod_cfg.func)}'." + ) + mod_cfg.func = mod_cfg.func(cfg=mod_cfg, data_dim=obs_dims, device=self._env.device) + + # add to list of class modifiers + self._group_obs_class_modifiers.append(mod_cfg.func) + else: + raise TypeError( + f"Modifier configuration '{mod_cfg}' of observation term '{term_name}' is not of" + f" required type ModifierCfg, Received: '{type(mod_cfg)}'" + ) + + # check if function is callable + if not callable(mod_cfg.func): + raise AttributeError( + f"Modifier '{mod_cfg}' of observation term '{term_name}' is not callable." + f" Received: {mod_cfg.func}" + ) + + # check if term's arguments are matched by params + term_params = list(mod_cfg.params.keys()) + args = inspect.signature(mod_cfg.func).parameters + args_with_defaults = [arg for arg in args if args[arg].default is not inspect.Parameter.empty] + args_without_defaults = [arg for arg in args if args[arg].default is inspect.Parameter.empty] + args = args_without_defaults + args_with_defaults + # ignore first two arguments for env and env_ids + # Think: Check for cases when kwargs are set inside the function? + if len(args) > 1: + if set(args[1:]) != set(term_params + args_with_defaults): + raise ValueError( + f"Modifier '{mod_cfg}' of observation term '{term_name}' expects" + f" mandatory parameters: {args_without_defaults[1:]}" + f" and optional parameters: {args_with_defaults}, but received: {term_params}." + ) + + # add term in a separate list if term is a class + if isinstance(term_cfg.func, ManagerTermBase): + self._group_obs_class_term_cfgs[group_name].append(term_cfg) + # call reset (in-case above call to get obs dims changed the state) + term_cfg.func.reset()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/reward_manager.html b/_modules/omni/isaac/lab/managers/reward_manager.html new file mode 100644 index 0000000000..a5bb966b15 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/reward_manager.html @@ -0,0 +1,782 @@ + + + + + + + + + + + omni.isaac.lab.managers.reward_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.reward_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Reward manager for computing reward signals for a given world."""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import RewardTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+
+
+
[文档]class RewardManager(ManagerBase): + """Manager for computing reward signals for a given world. + + The reward manager computes the total reward as a sum of the weighted reward terms. The reward + terms are parsed from a nested config class containing the reward manger's settings and reward + terms configuration. + + The reward terms are parsed from a config class containing the manager's settings and each term's + parameters. Each reward term should instantiate the :class:`RewardTermCfg` class. + + .. note:: + + The reward manager multiplies the reward term's ``weight`` with the time-step interval ``dt`` + of the environment. This is done to ensure that the computed reward terms are balanced with + respect to the chosen time-step interval in the environment. + + """ + + _env: ManagerBasedRLEnv + """The environment instance.""" + +
[文档] def __init__(self, cfg: object, env: ManagerBasedRLEnv): + """Initialize the reward manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, RewardTermCfg]``). + env: The environment instance. + """ + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._term_cfgs: list[RewardTermCfg] = list() + self._class_term_cfgs: list[RewardTermCfg] = list() + + # call the base class constructor (this will parse the terms config) + super().__init__(cfg, env) + # prepare extra info to store individual reward term information + self._episode_sums = dict() + for term_name in self._term_names: + self._episode_sums[term_name] = torch.zeros(self.num_envs, dtype=torch.float, device=self.device) + # create buffer for managing reward per environment + self._reward_buf = torch.zeros(self.num_envs, dtype=torch.float, device=self.device)
+ + def __str__(self) -> str: + """Returns: A string representation for reward manager.""" + msg = f"<RewardManager> contains {len(self._term_names)} active terms.\n" + + # create table for term information + table = PrettyTable() + table.title = "Active Reward Terms" + table.field_names = ["Index", "Name", "Weight"] + # set alignment of table columns + table.align["Name"] = "l" + table.align["Weight"] = "r" + # add info on each term + for index, (name, term_cfg) in enumerate(zip(self._term_names, self._term_cfgs)): + table.add_row([index, name, term_cfg.weight]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def active_terms(self) -> list[str]: + """Name of active reward terms.""" + return self._term_names + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """Returns the episodic sum of individual reward terms. + + Args: + env_ids: The environment ids for which the episodic sum of + individual reward terms is to be returned. Defaults to all the environment ids. + + Returns: + Dictionary of episodic sum of individual reward terms. + """ + # resolve environment ids + if env_ids is None: + env_ids = slice(None) + # store information + extras = {} + for key in self._episode_sums.keys(): + # store information + # r_1 + r_2 + ... + r_n + episodic_sum_avg = torch.mean(self._episode_sums[key][env_ids]) + extras["Episode_Reward/" + key] = episodic_sum_avg / self._env.max_episode_length_s + # reset episodic sum + self._episode_sums[key][env_ids] = 0.0 + # reset all the reward terms + for term_cfg in self._class_term_cfgs: + term_cfg.func.reset(env_ids=env_ids) + # return logged information + return extras
+ +
[文档] def compute(self, dt: float) -> torch.Tensor: + """Computes the reward signal as a weighted sum of individual terms. + + This function calls each reward term managed by the class and adds them to compute the net + reward signal. It also updates the episodic sums corresponding to individual reward terms. + + Args: + dt: The time-step interval of the environment. + + Returns: + The net reward signal of shape (num_envs,). + """ + # reset computation + self._reward_buf[:] = 0.0 + # iterate over all the reward terms + for name, term_cfg in zip(self._term_names, self._term_cfgs): + # skip if weight is zero (kind of a micro-optimization) + if term_cfg.weight == 0.0: + continue + # compute term's value + value = term_cfg.func(self._env, **term_cfg.params) * term_cfg.weight * dt + # update total reward + self._reward_buf += value + # update episodic sum + self._episode_sums[name] += value + + return self._reward_buf
+ + """ + Operations - Term settings. + """ + +
[文档] def set_term_cfg(self, term_name: str, cfg: RewardTermCfg): + """Sets the configuration of the specified term into the manager. + + Args: + term_name: The name of the reward term. + cfg: The configuration for the reward term. + + Raises: + ValueError: If the term name is not found. + """ + if term_name not in self._term_names: + raise ValueError(f"Reward term '{term_name}' not found.") + # set the configuration + self._term_cfgs[self._term_names.index(term_name)] = cfg
+ +
[文档] def get_term_cfg(self, term_name: str) -> RewardTermCfg: + """Gets the configuration for the specified term. + + Args: + term_name: The name of the reward term. + + Returns: + The configuration of the reward term. + + Raises: + ValueError: If the term name is not found. + """ + if term_name not in self._term_names: + raise ValueError(f"Reward term '{term_name}' not found.") + # return the configuration + return self._term_cfgs[self._term_names.index(term_name)]
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + # iterate over all the terms + for term_name, term_cfg in cfg_items: + # check for non config + if term_cfg is None: + continue + # check for valid config type + if not isinstance(term_cfg, RewardTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type RewardTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # check for valid weight type + if not isinstance(term_cfg.weight, (float, int)): + raise TypeError( + f"Weight for the term '{term_name}' is not of type float or int." + f" Received: '{type(term_cfg.weight)}'." + ) + # resolve common parameters + self._resolve_common_term_cfg(term_name, term_cfg, min_argc=1) + # add function to list + self._term_names.append(term_name) + self._term_cfgs.append(term_cfg) + # check if the term is a class + if isinstance(term_cfg.func, ManagerTermBase): + self._class_term_cfgs.append(term_cfg)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/scene_entity_cfg.html b/_modules/omni/isaac/lab/managers/scene_entity_cfg.html new file mode 100644 index 0000000000..5d9f7263b6 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/scene_entity_cfg.html @@ -0,0 +1,844 @@ + + + + + + + + + + + omni.isaac.lab.managers.scene_entity_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.scene_entity_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Configuration terms for different managers."""
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.assets import Articulation, RigidObject, RigidObjectCollection
+from omni.isaac.lab.scene import InteractiveScene
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class SceneEntityCfg: + """Configuration for a scene entity that is used by the manager's term. + + This class is used to specify the name of the scene entity that is queried from the + :class:`InteractiveScene` and passed to the manager's term function. + """ + + name: str = MISSING + """The name of the scene entity. + + This is the name defined in the scene configuration file. See the :class:`InteractiveSceneCfg` + class for more details. + """ + + joint_names: str | list[str] | None = None + """The names of the joints from the scene entity. Defaults to None. + + The names can be either joint names or a regular expression matching the joint names. + + These are converted to joint indices on initialization of the manager and passed to the term + function as a list of joint indices under :attr:`joint_ids`. + """ + + joint_ids: list[int] | slice = slice(None) + """The indices of the joints from the asset required by the term. Defaults to slice(None), which means + all the joints in the asset (if present). + + If :attr:`joint_names` is specified, this is filled in automatically on initialization of the + manager. + """ + + fixed_tendon_names: str | list[str] | None = None + """The names of the fixed tendons from the scene entity. Defaults to None. + + The names can be either joint names or a regular expression matching the joint names. + + These are converted to fixed tendon indices on initialization of the manager and passed to the term + function as a list of fixed tendon indices under :attr:`fixed_tendon_ids`. + """ + + fixed_tendon_ids: list[int] | slice = slice(None) + """The indices of the fixed tendons from the asset required by the term. Defaults to slice(None), which means + all the fixed tendons in the asset (if present). + + If :attr:`fixed_tendon_names` is specified, this is filled in automatically on initialization of the + manager. + """ + + body_names: str | list[str] | None = None + """The names of the bodies from the asset required by the term. Defaults to None. + + The names can be either body names or a regular expression matching the body names. + + These are converted to body indices on initialization of the manager and passed to the term + function as a list of body indices under :attr:`body_ids`. + """ + + body_ids: list[int] | slice = slice(None) + """The indices of the bodies from the asset required by the term. Defaults to slice(None), which means + all the bodies in the asset. + + If :attr:`body_names` is specified, this is filled in automatically on initialization of the + manager. + """ + + object_collection_names: str | list[str] | None = None + """The names of the objects in the rigid object collection required by the term. Defaults to None. + + The names can be either names or a regular expression matching the object names in the collection. + + These are converted to object indices on initialization of the manager and passed to the term + function as a list of object indices under :attr:`object_collection_ids`. + """ + + object_collection_ids: list[int] | slice = slice(None) + """The indices of the objects from the rigid object collection required by the term. Defaults to slice(None), + which means all the objects in the collection. + + If :attr:`object_collection_names` is specified, this is filled in automatically on initialization of the manager. + """ + + preserve_order: bool = False + """Whether to preserve indices ordering to match with that in the specified joint, body, or object collection names. + Defaults to False. + + If False, the ordering of the indices are sorted in ascending order (i.e. the ordering in the entity's joints, + bodies, or object in the object collection). Otherwise, the indices are preserved in the order of the specified + joint, body, or object collection names. + + For more details, see the :meth:`omni.isaac.lab.utils.string.resolve_matching_names` function. + + .. note:: + This attribute is only used when :attr:`joint_names`, :attr:`body_names`, or :attr:`object_collection_names` are specified. + + """ + +
[文档] def resolve(self, scene: InteractiveScene): + """Resolves the scene entity and converts the joint and body names to indices. + + This function examines the scene entity from the :class:`InteractiveScene` and resolves the indices + and names of the joints and bodies. It is an expensive operation as it resolves regular expressions + and should be called only once. + + Args: + scene: The interactive scene instance. + + Raises: + ValueError: If the scene entity is not found. + ValueError: If both ``joint_names`` and ``joint_ids`` are specified and are not consistent. + ValueError: If both ``fixed_tendon_names`` and ``fixed_tendon_ids`` are specified and are not consistent. + ValueError: If both ``body_names`` and ``body_ids`` are specified and are not consistent. + ValueError: If both ``object_collection_names`` and ``object_collection_ids`` are specified and are not consistent. + """ + # check if the entity is valid + if self.name not in scene.keys(): + raise ValueError(f"The scene entity '{self.name}' does not exist. Available entities: {scene.keys()}.") + + # convert joint names to indices based on regex + self._resolve_joint_names(scene) + + # convert fixed tendon names to indices based on regex + self._resolve_fixed_tendon_names(scene) + + # convert body names to indices based on regex + self._resolve_body_names(scene) + + # convert object collection names to indices based on regex + self._resolve_object_collection_names(scene)
+ + def _resolve_joint_names(self, scene: InteractiveScene): + # convert joint names to indices based on regex + if self.joint_names is not None or self.joint_ids != slice(None): + entity: Articulation = scene[self.name] + # -- if both are not their default values, check if they are valid + if self.joint_names is not None and self.joint_ids != slice(None): + if isinstance(self.joint_names, str): + self.joint_names = [self.joint_names] + if isinstance(self.joint_ids, int): + self.joint_ids = [self.joint_ids] + joint_ids, _ = entity.find_joints(self.joint_names, preserve_order=self.preserve_order) + joint_names = [entity.joint_names[i] for i in self.joint_ids] + if joint_ids != self.joint_ids or joint_names != self.joint_names: + raise ValueError( + "Both 'joint_names' and 'joint_ids' are specified, and are not consistent." + f"\n\tfrom joint names: {self.joint_names} [{joint_ids}]" + f"\n\tfrom joint ids: {joint_names} [{self.joint_ids}]" + "\nHint: Use either 'joint_names' or 'joint_ids' to avoid confusion." + ) + # -- from joint names to joint indices + elif self.joint_names is not None: + if isinstance(self.joint_names, str): + self.joint_names = [self.joint_names] + self.joint_ids, _ = entity.find_joints(self.joint_names, preserve_order=self.preserve_order) + # performance optimization (slice offers faster indexing than list of indices) + # only all joint in the entity order are selected + if len(self.joint_ids) == entity.num_joints and self.joint_names == entity.joint_names: + self.joint_ids = slice(None) + # -- from joint indices to joint names + elif self.joint_ids != slice(None): + if isinstance(self.joint_ids, int): + self.joint_ids = [self.joint_ids] + self.joint_names = [entity.joint_names[i] for i in self.joint_ids] + + def _resolve_fixed_tendon_names(self, scene: InteractiveScene): + # convert tendon names to indices based on regex + if self.fixed_tendon_names is not None or self.fixed_tendon_ids != slice(None): + entity: Articulation = scene[self.name] + # -- if both are not their default values, check if they are valid + if self.fixed_tendon_names is not None and self.fixed_tendon_ids != slice(None): + if isinstance(self.fixed_tendon_names, str): + self.fixed_tendon_names = [self.fixed_tendon_names] + if isinstance(self.fixed_tendon_ids, int): + self.fixed_tendon_ids = [self.fixed_tendon_ids] + fixed_tendon_ids, _ = entity.find_fixed_tendons( + self.fixed_tendon_names, preserve_order=self.preserve_order + ) + fixed_tendon_names = [entity.fixed_tendon_names[i] for i in self.fixed_tendon_ids] + if fixed_tendon_ids != self.fixed_tendon_ids or fixed_tendon_names != self.fixed_tendon_names: + raise ValueError( + "Both 'fixed_tendon_names' and 'fixed_tendon_ids' are specified, and are not consistent." + f"\n\tfrom joint names: {self.fixed_tendon_names} [{fixed_tendon_ids}]" + f"\n\tfrom joint ids: {fixed_tendon_names} [{self.fixed_tendon_ids}]" + "\nHint: Use either 'fixed_tendon_names' or 'fixed_tendon_ids' to avoid confusion." + ) + # -- from fixed tendon names to fixed tendon indices + elif self.fixed_tendon_names is not None: + if isinstance(self.fixed_tendon_names, str): + self.fixed_tendon_names = [self.fixed_tendon_names] + self.fixed_tendon_ids, _ = entity.find_fixed_tendons( + self.fixed_tendon_names, preserve_order=self.preserve_order + ) + # performance optimization (slice offers faster indexing than list of indices) + # only all fixed tendon in the entity order are selected + if ( + len(self.fixed_tendon_ids) == entity.num_fixed_tendons + and self.fixed_tendon_names == entity.fixed_tendon_names + ): + self.fixed_tendon_ids = slice(None) + # -- from fixed tendon indices to fixed tendon names + elif self.fixed_tendon_ids != slice(None): + if isinstance(self.fixed_tendon_ids, int): + self.fixed_tendon_ids = [self.fixed_tendon_ids] + self.fixed_tendon_names = [entity.fixed_tendon_names[i] for i in self.fixed_tendon_ids] + + def _resolve_body_names(self, scene: InteractiveScene): + # convert body names to indices based on regex + if self.body_names is not None or self.body_ids != slice(None): + entity: RigidObject = scene[self.name] + # -- if both are not their default values, check if they are valid + if self.body_names is not None and self.body_ids != slice(None): + if isinstance(self.body_names, str): + self.body_names = [self.body_names] + if isinstance(self.body_ids, int): + self.body_ids = [self.body_ids] + body_ids, _ = entity.find_bodies(self.body_names, preserve_order=self.preserve_order) + body_names = [entity.body_names[i] for i in self.body_ids] + if body_ids != self.body_ids or body_names != self.body_names: + raise ValueError( + "Both 'body_names' and 'body_ids' are specified, and are not consistent." + f"\n\tfrom body names: {self.body_names} [{body_ids}]" + f"\n\tfrom body ids: {body_names} [{self.body_ids}]" + "\nHint: Use either 'body_names' or 'body_ids' to avoid confusion." + ) + # -- from body names to body indices + elif self.body_names is not None: + if isinstance(self.body_names, str): + self.body_names = [self.body_names] + self.body_ids, _ = entity.find_bodies(self.body_names, preserve_order=self.preserve_order) + # performance optimization (slice offers faster indexing than list of indices) + # only all bodies in the entity order are selected + if len(self.body_ids) == entity.num_bodies and self.body_names == entity.body_names: + self.body_ids = slice(None) + # -- from body indices to body names + elif self.body_ids != slice(None): + if isinstance(self.body_ids, int): + self.body_ids = [self.body_ids] + self.body_names = [entity.body_names[i] for i in self.body_ids] + + def _resolve_object_collection_names(self, scene: InteractiveScene): + # convert object names to indices based on regex + if self.object_collection_names is not None or self.object_collection_ids != slice(None): + entity: RigidObjectCollection = scene[self.name] + # -- if both are not their default values, check if they are valid + if self.object_collection_names is not None and self.object_collection_ids != slice(None): + if isinstance(self.object_collection_names, str): + self.object_collection_names = [self.object_collection_names] + if isinstance(self.object_collection_ids, int): + self.object_collection_ids = [self.object_collection_ids] + object_ids, _ = entity.find_objects(self.object_collection_names, preserve_order=self.preserve_order) + object_names = [entity.object_names[i] for i in self.object_collection_ids] + if object_ids != self.object_collection_ids or object_names != self.object_collection_names: + raise ValueError( + "Both 'object_collection_names' and 'object_collection_ids' are specified, and are not" + " consistent.\n\tfrom object collection names:" + f" {self.object_collection_names} [{object_ids}]\n\tfrom object collection ids:" + f" {object_names} [{self.object_collection_ids}]\nHint: Use either 'object_collection_names' or" + " 'object_collection_ids' to avoid confusion." + ) + # -- from object names to object indices + elif self.object_collection_names is not None: + if isinstance(self.object_collection_names, str): + self.object_collection_names = [self.object_collection_names] + self.object_collection_ids, _ = entity.find_objects( + self.object_collection_names, preserve_order=self.preserve_order + ) + # -- from object indices to object names + elif self.object_collection_ids != slice(None): + if isinstance(self.object_collection_ids, int): + self.object_collection_ids = [self.object_collection_ids] + self.object_collection_names = [entity.object_names[i] for i in self.object_collection_ids]
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/managers/termination_manager.html b/_modules/omni/isaac/lab/managers/termination_manager.html new file mode 100644 index 0000000000..0692231529 --- /dev/null +++ b/_modules/omni/isaac/lab/managers/termination_manager.html @@ -0,0 +1,810 @@ + + + + + + + + + + + omni.isaac.lab.managers.termination_manager — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.managers.termination_manager 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Termination manager for computing done signals for a given world."""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from prettytable import PrettyTable
+from typing import TYPE_CHECKING
+
+from .manager_base import ManagerBase, ManagerTermBase
+from .manager_term_cfg import TerminationTermCfg
+
+if TYPE_CHECKING:
+    from omni.isaac.lab.envs import ManagerBasedRLEnv
+
+
+
[文档]class TerminationManager(ManagerBase): + """Manager for computing done signals for a given world. + + 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 + computes the termination signal as the union (logical or) of all the termination terms. + + Following the `Gymnasium API <https://gymnasium.farama.org/tutorials/gymnasium_basics/handling_time_limits/>`_, + the termination signal is computed as the logical OR of the following signals: + + * **Time-out**: 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 + timed out (i.e. reached max episode length). + * **Terminated**: 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. + + These signals can be individually accessed using the :attr:`time_outs` and :attr:`terminated` properties. + + The termination terms are parsed from a config class containing the manager's settings and each term's + parameters. Each termination term should instantiate the :class:`TerminationTermCfg` class. The term's + configuration :attr:`TerminationTermCfg.time_out` decides whether the term is a timeout or a termination term. + """ + + _env: ManagerBasedRLEnv + """The environment instance.""" + +
[文档] def __init__(self, cfg: object, env: ManagerBasedRLEnv): + """Initializes the termination manager. + + Args: + cfg: The configuration object or dictionary (``dict[str, TerminationTermCfg]``). + env: An environment object. + """ + # create buffers to parse and store terms + self._term_names: list[str] = list() + self._term_cfgs: list[TerminationTermCfg] = list() + self._class_term_cfgs: list[TerminationTermCfg] = list() + + # call the base class constructor (this will parse the terms config) + super().__init__(cfg, env) + # prepare extra info to store individual termination term information + self._term_dones = dict() + for term_name in self._term_names: + self._term_dones[term_name] = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) + # create buffer for managing termination per environment + self._truncated_buf = torch.zeros(self.num_envs, device=self.device, dtype=torch.bool) + self._terminated_buf = torch.zeros_like(self._truncated_buf)
+ + def __str__(self) -> str: + """Returns: A string representation for termination manager.""" + msg = f"<TerminationManager> contains {len(self._term_names)} active terms.\n" + + # create table for term information + table = PrettyTable() + table.title = "Active Termination Terms" + table.field_names = ["Index", "Name", "Time Out"] + # set alignment of table columns + table.align["Name"] = "l" + # add info on each term + for index, (name, term_cfg) in enumerate(zip(self._term_names, self._term_cfgs)): + table.add_row([index, name, term_cfg.time_out]) + # convert table to string + msg += table.get_string() + msg += "\n" + + return msg + + """ + Properties. + """ + + @property + def active_terms(self) -> list[str]: + """Name of active termination terms.""" + return self._term_names + + @property + def dones(self) -> torch.Tensor: + """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,). + + 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 + timed out (i.e. reached max episode length). + """ + return self._truncated_buf + + @property + def terminated(self) -> torch.Tensor: + """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. + """ + return self._terminated_buf + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None) -> dict[str, torch.Tensor]: + """Returns the episodic counts of individual termination terms. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + + Returns: + Dictionary of episodic sum of individual reward terms. + """ + # resolve environment ids + if env_ids is None: + env_ids = slice(None) + # add to episode dict + extras = {} + for key in self._term_dones.keys(): + # store information + extras["Episode_Termination/" + key] = torch.count_nonzero(self._term_dones[key][env_ids]).item() + # reset all the reward terms + for term_cfg in self._class_term_cfgs: + term_cfg.func.reset(env_ids=env_ids) + # return logged information + return extras
+ +
[文档] def compute(self) -> torch.Tensor: + """Computes the termination signal as union of individual terms. + + This function calls each termination term managed by the class and performs a logical OR operation + to compute the net termination signal. + + Returns: + The combined termination signal of shape (num_envs,). + """ + # reset computation + self._truncated_buf[:] = False + self._terminated_buf[:] = False + # iterate over all the termination terms + for name, term_cfg in zip(self._term_names, self._term_cfgs): + value = term_cfg.func(self._env, **term_cfg.params) + # store timeout signal separately + if term_cfg.time_out: + self._truncated_buf |= value + else: + self._terminated_buf |= value + # add to episode dones + self._term_dones[name][:] = value + # return combined termination signal + return self._truncated_buf | self._terminated_buf
+ +
[文档] def get_term(self, name: str) -> torch.Tensor: + """Returns the termination term with the specified name. + + Args: + name: The name of the termination term. + + Returns: + The corresponding termination term value. Shape is (num_envs,). + """ + return self._term_dones[name]
+ + """ + Operations - Term settings. + """ + +
[文档] def set_term_cfg(self, term_name: str, cfg: TerminationTermCfg): + """Sets the configuration of the specified term into the manager. + + Args: + term_name: The name of the termination term. + cfg: The configuration for the termination term. + + Raises: + ValueError: If the term name is not found. + """ + if term_name not in self._term_names: + raise ValueError(f"Termination term '{term_name}' not found.") + # set the configuration + self._term_cfgs[self._term_names.index(term_name)] = cfg
+ +
[文档] def get_term_cfg(self, term_name: str) -> TerminationTermCfg: + """Gets the configuration for the specified term. + + Args: + term_name: The name of the termination term. + + Returns: + The configuration of the termination term. + + Raises: + ValueError: If the term name is not found. + """ + if term_name not in self._term_names: + raise ValueError(f"Termination term '{term_name}' not found.") + # return the configuration + return self._term_cfgs[self._term_names.index(term_name)]
+ + """ + Helper functions. + """ + + def _prepare_terms(self): + # check if config is dict already + if isinstance(self.cfg, dict): + cfg_items = self.cfg.items() + else: + cfg_items = self.cfg.__dict__.items() + # iterate over all the terms + for term_name, term_cfg in cfg_items: + # check for non config + if term_cfg is None: + continue + # check for valid config type + if not isinstance(term_cfg, TerminationTermCfg): + raise TypeError( + f"Configuration for the term '{term_name}' is not of type TerminationTermCfg." + f" Received: '{type(term_cfg)}'." + ) + # resolve common parameters + self._resolve_common_term_cfg(term_name, term_cfg, min_argc=1) + # add function to list + self._term_names.append(term_name) + self._term_cfgs.append(term_cfg) + # check if the term is a class + if isinstance(term_cfg.func, ManagerTermBase): + self._class_term_cfgs.append(term_cfg)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/markers/visualization_markers.html b/_modules/omni/isaac/lab/markers/visualization_markers.html new file mode 100644 index 0000000000..cd9b564001 --- /dev/null +++ b/_modules/omni/isaac/lab/markers/visualization_markers.html @@ -0,0 +1,969 @@ + + + + + + + + + + + omni.isaac.lab.markers.visualization_markers — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.markers.visualization_markers 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""A class to coordinate groups of visual markers (such as spheres, frames or arrows)
+using `UsdGeom.PointInstancer`_ class.
+
+The class :class:`VisualizationMarkers` is used to create a group of visual markers and
+visualize them in the viewport. The markers are represented as :class:`UsdGeom.PointInstancer` prims
+in the USD stage. The markers are created as prototypes in the :class:`UsdGeom.PointInstancer` prim
+and are instanced in the :class:`UsdGeom.PointInstancer` prim. The markers can be visualized by
+passing the indices of the marker prototypes and their translations, orientations and scales.
+The marker prototypes can be configured with the :class:`VisualizationMarkersCfg` class.
+
+.. _UsdGeom.PointInstancer: https://graphics.pixar.com/usd/dev/api/class_usd_geom_point_instancer.html
+"""
+
+# needed to import for allowing type-hinting: np.ndarray | torch.Tensor | None
+from __future__ import annotations
+
+import numpy as np
+import torch
+from dataclasses import MISSING
+
+import omni.isaac.core.utils.stage as stage_utils
+import omni.kit.commands
+import omni.physx.scripts.utils as physx_utils
+from pxr import Gf, PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, Vt
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.sim.spawners import SpawnerCfg
+from omni.isaac.lab.utils.configclass import configclass
+from omni.isaac.lab.utils.math import convert_quat
+
+
+
[文档]@configclass +class VisualizationMarkersCfg: + """A class to configure a :class:`VisualizationMarkers`.""" + + prim_path: str = MISSING + """The prim path where the :class:`UsdGeom.PointInstancer` will be created.""" + + markers: dict[str, SpawnerCfg] = MISSING + """The dictionary of marker configurations. + + The key is the name of the marker, and the value is the configuration of the marker. + The key is used to identify the marker in the class. + """
+ + +
[文档]class VisualizationMarkers: + """A class to coordinate groups of visual markers (loaded from USD). + + This class allows visualization of different UI markers in the scene, such as points and frames. + The class wraps around the `UsdGeom.PointInstancer`_ for efficient handling of objects + in the stage via instancing the created marker prototype prims. + + A marker prototype prim is a reusable template prim used for defining variations of objects + in the scene. For example, a sphere prim can be used as a marker prototype prim to create + multiple sphere prims in the scene at different locations. Thus, prototype prims are useful + for creating multiple instances of the same prim in the scene. + + The class parses the configuration to create different the marker prototypes into the stage. Each marker + prototype prim is created as a child of the :class:`UsdGeom.PointInstancer` prim. The prim path for the + the marker prim is resolved using the key of the marker in the :attr:`VisualizationMarkersCfg.markers` + dictionary. The marker prototypes are created using the :meth:`omni.isaac.core.utils.create_prim` + function, and then then instanced using :class:`UsdGeom.PointInstancer` prim to allow creating multiple + instances of the marker prims. + + Switching between different marker prototypes is possible by calling the :meth:`visualize` method with + the prototype indices corresponding to the marker prototype. The prototype indices are based on the order + in the :attr:`VisualizationMarkersCfg.markers` dictionary. For example, if the dictionary has two markers, + "marker1" and "marker2", then their prototype indices are 0 and 1 respectively. The prototype indices + can be passed as a list or array of integers. + + Usage: + The following snippet shows how to create 24 sphere markers with a radius of 1.0 at random translations + within the range [-1.0, 1.0]. The first 12 markers will be colored red and the rest will be colored green. + + .. code-block:: python + + import omni.isaac.lab.sim as sim_utils + from omni.isaac.lab.markers import VisualizationMarkersCfg, VisualizationMarkers + + # Create the markers configuration + # This creates two marker prototypes, "marker1" and "marker2" which are spheres with a radius of 1.0. + # The color of "marker1" is red and the color of "marker2" is green. + cfg = VisualizationMarkersCfg( + prim_path="/World/Visuals/testMarkers", + markers={ + "marker1": sim_utils.SphereCfg( + radius=1.0, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(1.0, 0.0, 0.0)), + ), + "marker2": VisualizationMarkersCfg.SphereCfg( + radius=1.0, + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + ), + } + ) + # Create the markers instance + # This will create a UsdGeom.PointInstancer prim at the given path along with the marker prototypes. + marker = VisualizationMarkers(cfg) + + # Set position of the marker + # -- randomly sample translations between -1.0 and 1.0 + marker_translations = np.random.uniform(-1.0, 1.0, (24, 3)) + # -- this will create 24 markers at the given translations + # note: the markers will all be `marker1` since the marker indices are not given + marker.visualize(translations=marker_translations) + + # alter the markers based on their prototypes indices + # first 12 markers will be marker1 and the rest will be marker2 + # 0 -> marker1, 1 -> marker2 + marker_indices = [0] * 12 + [1] * 12 + # this will change the marker prototypes at the given indices + # note: the translations of the markers will not be changed from the previous call + # since the translations are not given. + marker.visualize(marker_indices=marker_indices) + + # alter the markers based on their prototypes indices and translations + marker.visualize(marker_indices=marker_indices, translations=marker_translations) + + .. _UsdGeom.PointInstancer: https://graphics.pixar.com/usd/dev/api/class_usd_geom_point_instancer.html + + """ + +
[文档] def __init__(self, cfg: VisualizationMarkersCfg): + """Initialize the class. + + When the class is initialized, the :class:`UsdGeom.PointInstancer` is created into the stage + and the marker prims are registered into it. + + .. note:: + If a prim already exists at the given path, the function will find the next free path + and create the :class:`UsdGeom.PointInstancer` prim there. + + Args: + cfg: The configuration for the markers. + + Raises: + ValueError: When no markers are provided in the :obj:`cfg`. + """ + # get next free path for the prim + prim_path = stage_utils.get_next_free_path(cfg.prim_path) + # create a new prim + stage = stage_utils.get_current_stage() + self._instancer_manager = UsdGeom.PointInstancer.Define(stage, prim_path) + # store inputs + self.prim_path = prim_path + self.cfg = cfg + # check if any markers is provided + if len(self.cfg.markers) == 0: + raise ValueError(f"The `cfg.markers` cannot be empty. Received: {self.cfg.markers}") + + # create a child prim for the marker + self._add_markers_prototypes(self.cfg.markers) + # Note: We need to do this the first time to initialize the instancer. + # Otherwise, the instancer will not be "created" and the function `GetInstanceIndices()` will fail. + self._instancer_manager.GetProtoIndicesAttr().Set(list(range(self.num_prototypes))) + self._instancer_manager.GetPositionsAttr().Set([Gf.Vec3f(0.0)] * self.num_prototypes) + self._count = self.num_prototypes
+ + def __str__(self) -> str: + """Return: A string representation of the class.""" + msg = f"VisualizationMarkers(prim_path={self.prim_path})" + msg += f"\n\tCount: {self.count}" + msg += f"\n\tNumber of prototypes: {self.num_prototypes}" + msg += "\n\tMarkers Prototypes:" + for index, (name, marker) in enumerate(self.cfg.markers.items()): + msg += f"\n\t\t[Index: {index}]: {name}: {marker.to_dict()}" + return msg + + """ + Properties. + """ + + @property + def num_prototypes(self) -> int: + """The number of marker prototypes available.""" + return len(self.cfg.markers) + + @property + def count(self) -> int: + """The total number of marker instances.""" + # TODO: Update this when the USD API is available (Isaac Sim 2023.1) + # return self._instancer_manager.GetInstanceCount() + return self._count + + """ + Operations. + """ + +
[文档] def set_visibility(self, visible: bool): + """Sets the visibility of the markers. + + The method does this through the USD API. + + Args: + visible: flag to set the visibility. + """ + imageable = UsdGeom.Imageable(self._instancer_manager) + if visible: + imageable.MakeVisible() + else: + imageable.MakeInvisible()
+ +
[文档] def is_visible(self) -> bool: + """Checks the visibility of the markers. + + Returns: + True if the markers are visible, False otherwise. + """ + return self._instancer_manager.GetVisibilityAttr().Get() != UsdGeom.Tokens.invisible
+ +
[文档] def visualize( + self, + translations: np.ndarray | torch.Tensor | None = None, + orientations: np.ndarray | torch.Tensor | None = None, + scales: np.ndarray | torch.Tensor | None = None, + marker_indices: list[int] | np.ndarray | torch.Tensor | None = None, + ): + """Update markers in the viewport. + + .. note:: + If the prim `PointInstancer` is hidden in the stage, the function will simply return + without updating the markers. This helps in unnecessary computation when the markers + are not visible. + + Whenever updating the markers, the input arrays must have the same number of elements + in the first dimension. If the number of elements is different, the `UsdGeom.PointInstancer` + will raise an error complaining about the mismatch. + + Additionally, the function supports dynamic update of the markers. This means that the + number of markers can change between calls. For example, if you have 24 points that you + want to visualize, you can pass 24 translations, orientations, and scales. If you want to + visualize only 12 points, you can pass 12 translations, orientations, and scales. The + function will automatically update the number of markers in the scene. + + The function will also update the marker prototypes based on their prototype indices. For instance, + if you have two marker prototypes, and you pass the following marker indices: [0, 1, 0, 1], the function + will update the first and third markers with the first prototype, and the second and fourth markers + with the second prototype. This is useful when you want to visualize different markers in the same + scene. The list of marker indices must have the same number of elements as the translations, orientations, + or scales. If the number of elements is different, the function will raise an error. + + .. caution:: + This function will update all the markers instanced from the prototypes. That means + if you have 24 markers, you will need to pass 24 translations, orientations, and scales. + + If you want to update only a subset of the markers, you will need to handle the indices + yourself and pass the complete arrays to this function. + + Args: + translations: Translations w.r.t. parent prim frame. Shape is (M, 3). + 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 None, which means left unchanged. + scales: Scale applied before any rotation is applied. Shape is (M, 3). + Defaults to None, which means left unchanged. + marker_indices: Decides which marker prototype to visualize. Shape is (M). + 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 None arguments. + """ + # check if it is visible (if not then let's not waste time) + if not self.is_visible(): + return + # check if we have any markers to visualize + num_markers = 0 + # resolve inputs + # -- position + if translations is not None: + if isinstance(translations, torch.Tensor): + translations = translations.detach().cpu().numpy() + # check that shape is correct + if translations.shape[1] != 3 or len(translations.shape) != 2: + raise ValueError(f"Expected `translations` to have shape (M, 3). Received: {translations.shape}.") + # apply translations + self._instancer_manager.GetPositionsAttr().Set(Vt.Vec3fArray.FromNumpy(translations)) + # update number of markers + num_markers = translations.shape[0] + # -- orientation + if orientations is not None: + if isinstance(orientations, torch.Tensor): + orientations = orientations.detach().cpu().numpy() + # check that shape is correct + if orientations.shape[1] != 4 or len(orientations.shape) != 2: + raise ValueError(f"Expected `orientations` to have shape (M, 4). Received: {orientations.shape}.") + # roll orientations from (w, x, y, z) to (x, y, z, w) + # internally USD expects (x, y, z, w) + orientations = convert_quat(orientations, to="xyzw") + # apply orientations + self._instancer_manager.GetOrientationsAttr().Set(Vt.QuathArray.FromNumpy(orientations)) + # update number of markers + num_markers = orientations.shape[0] + # -- scales + if scales is not None: + if isinstance(scales, torch.Tensor): + scales = scales.detach().cpu().numpy() + # check that shape is correct + if scales.shape[1] != 3 or len(scales.shape) != 2: + raise ValueError(f"Expected `scales` to have shape (M, 3). Received: {scales.shape}.") + # apply scales + self._instancer_manager.GetScalesAttr().Set(Vt.Vec3fArray.FromNumpy(scales)) + # update number of markers + num_markers = scales.shape[0] + # -- status + if marker_indices is not None or num_markers != self._count: + # apply marker indices + if marker_indices is not None: + if isinstance(marker_indices, torch.Tensor): + marker_indices = marker_indices.detach().cpu().numpy() + elif isinstance(marker_indices, list): + marker_indices = np.array(marker_indices) + # check that shape is correct + if len(marker_indices.shape) != 1: + raise ValueError(f"Expected `marker_indices` to have shape (M,). Received: {marker_indices.shape}.") + # apply proto indices + self._instancer_manager.GetProtoIndicesAttr().Set(Vt.IntArray.FromNumpy(marker_indices)) + # update number of markers + num_markers = marker_indices.shape[0] + else: + # check that number of markers is not zero + if num_markers == 0: + raise ValueError("Number of markers cannot be zero! Hint: The function was called with no inputs?") + # set all markers to be the first prototype + self._instancer_manager.GetProtoIndicesAttr().Set([0] * num_markers) + # set number of markers + self._count = num_markers
+ + """ + Helper functions. + """ + + def _add_markers_prototypes(self, markers_cfg: dict[str, sim_utils.SpawnerCfg]): + """Adds markers prototypes to the scene and sets the markers instancer to use them.""" + # add markers based on config + for name, cfg in markers_cfg.items(): + # resolve prim path + marker_prim_path = f"{self.prim_path}/{name}" + # create a child prim for the marker + marker_prim = cfg.func(prim_path=marker_prim_path, cfg=cfg) + # make the asset uninstanceable (in case it is) + # point instancer defines its own prototypes so if an asset is already instanced, this doesn't work. + self._process_prototype_prim(marker_prim) + # add child reference to point instancer + self._instancer_manager.GetPrototypesRel().AddTarget(marker_prim_path) + # check that we loaded all the prototypes + prototypes = self._instancer_manager.GetPrototypesRel().GetTargets() + if len(prototypes) != len(markers_cfg): + raise RuntimeError( + f"Failed to load all the prototypes. Expected: {len(markers_cfg)}. Received: {len(prototypes)}." + ) + + def _process_prototype_prim(self, prim: Usd.Prim): + """Process a prim and its descendants to make them suitable for defining prototypes. + + Point instancer defines its own prototypes so if an asset is already instanced, this doesn't work. + This function checks if the prim at the specified prim path and its descendants are instanced. + If so, it makes the respective prim uninstanceable by disabling instancing on the prim. + + Additionally, it makes the prim invisible to secondary rays. This is useful when we do not want + to see the marker prims on camera images. + + Args: + prim_path: The prim path to check. + stage: The stage where the prim exists. + Defaults to None, in which case the current stage is used. + """ + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim.GetPrimAtPath()}' is not valid.") + # iterate over all prims under prim-path + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + # check if it is physics body -> if so, remove it + if child_prim.HasAPI(UsdPhysics.ArticulationRootAPI): + child_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI) + child_prim.RemoveAPI(PhysxSchema.PhysxArticulationAPI) + if child_prim.HasAPI(UsdPhysics.RigidBodyAPI): + child_prim.RemoveAPI(UsdPhysics.RigidBodyAPI) + child_prim.RemoveAPI(PhysxSchema.PhysxRigidBodyAPI) + if child_prim.IsA(UsdPhysics.Joint): + child_prim.GetAttribute("physics:jointEnabled").Set(False) + # check if prim is instanced -> if so, make it uninstanceable + if child_prim.IsInstance(): + child_prim.SetInstanceable(False) + # check if prim is a mesh -> if so, make it invisible to secondary rays + if child_prim.IsA(UsdGeom.Gprim): + # invisible to secondary rays such as depth images + omni.kit.commands.execute( + "ChangePropertyCommand", + prop_path=Sdf.Path(f"{child_prim.GetPrimPath().pathString}.primvars:invisibleToSecondaryRays"), + value=True, + prev=None, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, + ) + # add children to list + all_prims += child_prim.GetChildren() + + # remove any physics on the markers because they are only for visualization! + physx_utils.removeRigidBodySubtree(prim)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/scene/interactive_scene.html b/_modules/omni/isaac/lab/scene/interactive_scene.html new file mode 100644 index 0000000000..60a1db75e0 --- /dev/null +++ b/_modules/omni/isaac/lab/scene/interactive_scene.html @@ -0,0 +1,1086 @@ + + + + + + + + + + + omni.isaac.lab.scene.interactive_scene — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.scene.interactive_scene 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from collections.abc import Sequence
+from typing import Any
+
+import carb
+import omni.usd
+from omni.isaac.cloner import GridCloner
+from omni.isaac.core.prims import XFormPrimView
+from pxr import PhysxSchema
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.assets import (
+    Articulation,
+    ArticulationCfg,
+    AssetBaseCfg,
+    DeformableObject,
+    DeformableObjectCfg,
+    RigidObject,
+    RigidObjectCfg,
+    RigidObjectCollection,
+    RigidObjectCollectionCfg,
+)
+from omni.isaac.lab.sensors import ContactSensorCfg, FrameTransformerCfg, SensorBase, SensorBaseCfg
+from omni.isaac.lab.terrains import TerrainImporter, TerrainImporterCfg
+
+from .interactive_scene_cfg import InteractiveSceneCfg
+
+
+
[文档]class InteractiveScene: + """A scene that contains entities added to the simulation. + + The interactive scene parses the :class:`InteractiveSceneCfg` class to create the scene. + Based on the specified number of environments, it clones the entities and groups them into different + categories (e.g., articulations, sensors, etc.). + + Cloning can be performed in two ways: + + * For tasks where all environments contain the same assets, a more performant cloning paradigm + can be used to allow for faster environment creation. This is specified by the ``replicate_physics`` flag. + + .. code-block:: python + + scene = InteractiveScene(cfg=InteractiveSceneCfg(replicate_physics=True)) + + * For tasks that require having separate assets in the environments, ``replicate_physics`` would have to + be set to False, which will add some costs to the overall startup time. + + .. code-block:: python + + scene = InteractiveScene(cfg=InteractiveSceneCfg(replicate_physics=False)) + + Each entity is registered to scene based on its name in the configuration class. For example, if the user + specifies a robot in the configuration class as follows: + + .. code-block:: python + + from omni.isaac.lab.scene import InteractiveSceneCfg + from omni.isaac.lab.utils import configclass + + from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG + + @configclass + class MySceneCfg(InteractiveSceneCfg): + + robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot") + + Then the robot can be accessed from the scene as follows: + + .. code-block:: python + + from omni.isaac.lab.scene import InteractiveScene + + # create 128 environments + scene = InteractiveScene(cfg=MySceneCfg(num_envs=128)) + + # access the robot from the scene + robot = scene["robot"] + # access the robot based on its type + robot = scene.articulations["robot"] + + If the :class:`InteractiveSceneCfg` class does not include asset entities, the cloning process + can still be triggered if assets were added to the stage outside of the :class:`InteractiveScene` class: + + .. code-block:: python + + scene = InteractiveScene(cfg=InteractiveSceneCfg(num_envs=128, replicate_physics=True)) + scene.clone_environments() + + .. note:: + It is important to note that the scene only performs common operations on the entities. For example, + resetting the internal buffers, writing the buffers to the simulation and updating the buffers from the + simulation. The scene does not perform any task specific to the entity. For example, it does not apply + actions to the robot or compute observations from the robot. These tasks are handled by different + modules called "managers" in the framework. Please refer to the :mod:`omni.isaac.lab.managers` sub-package + for more details. + """ + +
[文档] def __init__(self, cfg: InteractiveSceneCfg): + """Initializes the scene. + + Args: + cfg: The configuration class for the scene. + """ + # check that the config is valid + cfg.validate() + # store inputs + self.cfg = cfg + # initialize scene elements + self._terrain = None + self._articulations = dict() + self._deformable_objects = dict() + self._rigid_objects = dict() + self._rigid_object_collections = dict() + self._sensors = dict() + self._extras = dict() + # obtain the current stage + self.stage = omni.usd.get_context().get_stage() + # physics scene path + self._physics_scene_path = None + # prepare cloner for environment replication + self.cloner = GridCloner(spacing=self.cfg.env_spacing) + self.cloner.define_base_env(self.env_ns) + self.env_prim_paths = self.cloner.generate_paths(f"{self.env_ns}/env", self.cfg.num_envs) + # create source prim + self.stage.DefinePrim(self.env_prim_paths[0], "Xform") + + # when replicate_physics=False, we assume heterogeneous environments and clone the xforms first. + # this triggers per-object level cloning in the spawner. + if not self.cfg.replicate_physics: + # clone the env xform + env_origins = self.cloner.clone( + source_prim_path=self.env_prim_paths[0], + prim_paths=self.env_prim_paths, + replicate_physics=False, + copy_from_source=True, + ) + self._default_env_origins = torch.tensor(env_origins, device=self.device, dtype=torch.float32) + else: + # otherwise, environment origins will be initialized during cloning at the end of environment creation + self._default_env_origins = None + + self._global_prim_paths = list() + if self._is_scene_setup_from_cfg(): + # add entities from config + self._add_entities_from_cfg() + # clone environments on a global scope if environment is homogeneous + if self.cfg.replicate_physics: + self.clone_environments(copy_from_source=False) + # replicate physics if we have more than one environment + # this is done to make scene initialization faster at play time + if self.cfg.replicate_physics and self.cfg.num_envs > 1: + self.cloner.replicate_physics( + source_prim_path=self.env_prim_paths[0], + prim_paths=self.env_prim_paths, + base_env_path=self.env_ns, + root_path=self.env_regex_ns.replace(".*", ""), + ) + + self.filter_collisions(self._global_prim_paths)
+ +
[文档] def clone_environments(self, copy_from_source: bool = False): + """Creates clones of the environment ``/World/envs/env_0``. + + Args: + copy_from_source: (bool): If set to False, clones inherit from /World/envs/env_0 and mirror its changes. + If True, clones are independent copies of the source prim and won't reflect its changes (start-up time + may increase). Defaults to False. + """ + # check if user spawned different assets in individual environments + # this flag will be None if no multi asset is spawned + carb_settings_iface = carb.settings.get_settings() + has_multi_assets = carb_settings_iface.get("/isaaclab/spawn/multi_assets") + if has_multi_assets and self.cfg.replicate_physics: + omni.log.warn( + "Varying assets might have been spawned under different environments." + " However, the replicate physics flag is enabled in the 'InteractiveScene' configuration." + " This may adversely affect PhysX parsing. We recommend disabling this property." + ) + + # clone the environment + env_origins = self.cloner.clone( + source_prim_path=self.env_prim_paths[0], + prim_paths=self.env_prim_paths, + replicate_physics=self.cfg.replicate_physics, + copy_from_source=copy_from_source, + ) + + # in case of heterogeneous cloning, the env origins is specified at init + if self._default_env_origins is None: + self._default_env_origins = torch.tensor(env_origins, device=self.device, dtype=torch.float32)
+ +
[文档] def filter_collisions(self, global_prim_paths: list[str] | None = None): + """Filter environments collisions. + + Disables collisions between the environments in ``/World/envs/env_.*`` and enables collisions with the prims + in global prim paths (e.g. ground plane). + + Args: + global_prim_paths: A list of global prim paths to enable collisions with. + Defaults to None, in which case no global prim paths are considered. + """ + # validate paths in global prim paths + if global_prim_paths is None: + global_prim_paths = [] + else: + # remove duplicates in paths + global_prim_paths = list(set(global_prim_paths)) + + # set global prim paths list if not previously defined + if len(self._global_prim_paths) < 1: + self._global_prim_paths += global_prim_paths + + # filter collisions within each environment instance + self.cloner.filter_collisions( + self.physics_scene_path, + "/World/collisions", + self.env_prim_paths, + global_paths=self._global_prim_paths, + )
+ + def __str__(self) -> str: + """Returns a string representation of the scene.""" + msg = f"<class {self.__class__.__name__}>\n" + msg += f"\tNumber of environments: {self.cfg.num_envs}\n" + msg += f"\tEnvironment spacing : {self.cfg.env_spacing}\n" + msg += f"\tSource prim name : {self.env_prim_paths[0]}\n" + msg += f"\tGlobal prim paths : {self._global_prim_paths}\n" + msg += f"\tReplicate physics : {self.cfg.replicate_physics}" + return msg + + """ + Properties. + """ + + @property + def physics_scene_path(self) -> str: + """The path to the USD Physics Scene.""" + if self._physics_scene_path is None: + for prim in self.stage.Traverse(): + if prim.HasAPI(PhysxSchema.PhysxSceneAPI): + self._physics_scene_path = prim.GetPrimPath().pathString + omni.log.info(f"Physics scene prim path: {self._physics_scene_path}") + break + if self._physics_scene_path is None: + raise RuntimeError("No physics scene found! Please make sure one exists.") + return self._physics_scene_path + + @property + def physics_dt(self) -> float: + """The physics timestep of the scene.""" + return sim_utils.SimulationContext.instance().get_physics_dt() # pyright: ignore [reportOptionalMemberAccess] + + @property + def device(self) -> str: + """The device on which the scene is created.""" + return sim_utils.SimulationContext.instance().device # pyright: ignore [reportOptionalMemberAccess] + + @property + def env_ns(self) -> str: + """The namespace ``/World/envs`` in which all environments created. + + The environments are present w.r.t. this namespace under "env_{N}" prim, + where N is a natural number. + """ + return "/World/envs" + + @property + def env_regex_ns(self) -> str: + """The namespace ``/World/envs/env_.*`` in which all environments created.""" + return f"{self.env_ns}/env_.*" + + @property + def num_envs(self) -> int: + """The number of environments handled by the scene.""" + return self.cfg.num_envs + + @property + def env_origins(self) -> torch.Tensor: + """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: + return self._default_env_origins + + @property + def terrain(self) -> TerrainImporter | None: + """The terrain in the scene. If None, then the scene has no terrain. + + Note: + We treat terrain separate from :attr:`extras` since terrains define environment origins and are + handled differently from other miscellaneous entities. + """ + return self._terrain + + @property + def articulations(self) -> dict[str, Articulation]: + """A dictionary of articulations in the scene.""" + return self._articulations + + @property + def deformable_objects(self) -> dict[str, DeformableObject]: + """A dictionary of deformable objects in the scene.""" + return self._deformable_objects + + @property + def rigid_objects(self) -> dict[str, RigidObject]: + """A dictionary of rigid objects in the scene.""" + return self._rigid_objects + + @property + def rigid_object_collections(self) -> dict[str, RigidObjectCollection]: + """A dictionary of rigid object collections in the scene.""" + return self._rigid_object_collections + + @property + def sensors(self) -> dict[str, SensorBase]: + """A dictionary of the sensors in the scene, such as cameras and contact reporters.""" + return self._sensors + + @property + def extras(self) -> dict[str, XFormPrimView]: + """A dictionary of miscellaneous simulation objects that neither inherit from assets nor sensors. + + The keys are the names of the miscellaneous objects, and the values are the `XFormPrimView`_ + of the corresponding prims. + + As an example, lights or other props in the scene that do not have any attributes or properties that you + want to alter at runtime can be added to this dictionary. + + Note: + These are not reset or updated by the scene. They are mainly other prims that are not necessarily + handled by the interactive scene, but are useful to be accessed by the user. + + .. _XFormPrimView: https://docs.omniverse.nvidia.com/py/isaacsim/source/extensions/omni.isaac.core/docs/index.html#omni.isaac.core.prims.XFormPrimView + + """ + return self._extras + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + """Resets the scene entities. + + Args: + env_ids: The indices of the environments to reset. + Defaults to None (all instances). + """ + # -- assets + for articulation in self._articulations.values(): + articulation.reset(env_ids) + for deformable_object in self._deformable_objects.values(): + deformable_object.reset(env_ids) + for rigid_object in self._rigid_objects.values(): + rigid_object.reset(env_ids) + for rigid_object_collection in self._rigid_object_collections.values(): + rigid_object_collection.reset(env_ids) + # -- sensors + for sensor in self._sensors.values(): + sensor.reset(env_ids)
+ +
[文档] def write_data_to_sim(self): + """Writes the data of the scene entities to the simulation.""" + # -- assets + for articulation in self._articulations.values(): + articulation.write_data_to_sim() + for deformable_object in self._deformable_objects.values(): + deformable_object.write_data_to_sim() + for rigid_object in self._rigid_objects.values(): + rigid_object.write_data_to_sim() + for rigid_object_collection in self._rigid_object_collections.values(): + rigid_object_collection.write_data_to_sim()
+ +
[文档] def update(self, dt: float) -> None: + """Update the scene entities. + + Args: + dt: The amount of time passed from last :meth:`update` call. + """ + # -- assets + for articulation in self._articulations.values(): + articulation.update(dt) + for deformable_object in self._deformable_objects.values(): + deformable_object.update(dt) + for rigid_object in self._rigid_objects.values(): + rigid_object.update(dt) + for rigid_object_collection in self._rigid_object_collections.values(): + rigid_object_collection.update(dt) + # -- sensors + for sensor in self._sensors.values(): + sensor.update(dt, force_recompute=not self.cfg.lazy_sensor_update)
+ + """ + Operations: Iteration. + """ + +
[文档] def keys(self) -> list[str]: + """Returns the keys of the scene entities. + + Returns: + The keys of the scene entities. + """ + all_keys = ["terrain"] + for asset_family in [ + self._articulations, + self._deformable_objects, + self._rigid_objects, + self._rigid_object_collections, + self._sensors, + self._extras, + ]: + all_keys += list(asset_family.keys()) + return all_keys
+ + def __getitem__(self, key: str) -> Any: + """Returns the scene entity with the given key. + + Args: + key: The key of the scene entity. + + Returns: + The scene entity. + """ + # check if it is a terrain + if key == "terrain": + return self._terrain + + all_keys = ["terrain"] + # check if it is in other dictionaries + for asset_family in [ + self._articulations, + self._deformable_objects, + self._rigid_objects, + self._rigid_object_collections, + self._sensors, + self._extras, + ]: + out = asset_family.get(key) + # if found, return + if out is not None: + return out + all_keys += list(asset_family.keys()) + # if not found, raise error + raise KeyError(f"Scene entity with key '{key}' not found. Available Entities: '{all_keys}'") + + """ + Internal methods. + """ + + def _is_scene_setup_from_cfg(self): + return any( + not (asset_name in InteractiveSceneCfg.__dataclass_fields__ or asset_cfg is None) + for asset_name, asset_cfg in self.cfg.__dict__.items() + ) + + def _add_entities_from_cfg(self): + """Add scene entities from the config.""" + # store paths that are in global collision filter + self._global_prim_paths = list() + # parse the entire scene config and resolve regex + for asset_name, asset_cfg in self.cfg.__dict__.items(): + # skip keywords + # note: easier than writing a list of keywords: [num_envs, env_spacing, lazy_sensor_update] + if asset_name in InteractiveSceneCfg.__dataclass_fields__ or asset_cfg is None: + continue + # resolve regex + if hasattr(asset_cfg, "prim_path"): + asset_cfg.prim_path = asset_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) + # create asset + if isinstance(asset_cfg, TerrainImporterCfg): + # terrains are special entities since they define environment origins + asset_cfg.num_envs = self.cfg.num_envs + asset_cfg.env_spacing = self.cfg.env_spacing + self._terrain = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, ArticulationCfg): + self._articulations[asset_name] = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, DeformableObjectCfg): + self._deformable_objects[asset_name] = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, RigidObjectCfg): + self._rigid_objects[asset_name] = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, RigidObjectCollectionCfg): + for rigid_object_cfg in asset_cfg.rigid_objects.values(): + rigid_object_cfg.prim_path = rigid_object_cfg.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) + self._rigid_object_collections[asset_name] = asset_cfg.class_type(asset_cfg) + for rigid_object_cfg in asset_cfg.rigid_objects.values(): + if hasattr(rigid_object_cfg, "collision_group") and rigid_object_cfg.collision_group == -1: + asset_paths = sim_utils.find_matching_prim_paths(rigid_object_cfg.prim_path) + self._global_prim_paths += asset_paths + elif isinstance(asset_cfg, SensorBaseCfg): + # Update target frame path(s)' regex name space for FrameTransformer + if isinstance(asset_cfg, FrameTransformerCfg): + updated_target_frames = [] + for target_frame in asset_cfg.target_frames: + target_frame.prim_path = target_frame.prim_path.format(ENV_REGEX_NS=self.env_regex_ns) + updated_target_frames.append(target_frame) + asset_cfg.target_frames = updated_target_frames + elif isinstance(asset_cfg, ContactSensorCfg): + updated_filter_prim_paths_expr = [] + for filter_prim_path in asset_cfg.filter_prim_paths_expr: + updated_filter_prim_paths_expr.append(filter_prim_path.format(ENV_REGEX_NS=self.env_regex_ns)) + asset_cfg.filter_prim_paths_expr = updated_filter_prim_paths_expr + + self._sensors[asset_name] = asset_cfg.class_type(asset_cfg) + elif isinstance(asset_cfg, AssetBaseCfg): + # manually spawn asset + if asset_cfg.spawn is not None: + asset_cfg.spawn.func( + asset_cfg.prim_path, + asset_cfg.spawn, + translation=asset_cfg.init_state.pos, + orientation=asset_cfg.init_state.rot, + ) + # store xform prim view corresponding to this asset + # all prims in the scene are Xform prims (i.e. have a transform component) + self._extras[asset_name] = XFormPrimView(asset_cfg.prim_path, reset_xform_properties=False) + else: + raise ValueError(f"Unknown asset config type for {asset_name}: {asset_cfg}") + # store global collision paths + if hasattr(asset_cfg, "collision_group") and asset_cfg.collision_group == -1: + asset_paths = sim_utils.find_matching_prim_paths(asset_cfg.prim_path) + self._global_prim_paths += asset_paths
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/scene/interactive_scene_cfg.html b/_modules/omni/isaac/lab/scene/interactive_scene_cfg.html new file mode 100644 index 0000000000..a02666f387 --- /dev/null +++ b/_modules/omni/isaac/lab/scene/interactive_scene_cfg.html @@ -0,0 +1,658 @@ + + + + + + + + + + + omni.isaac.lab.scene.interactive_scene_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.scene.interactive_scene_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.utils.configclass import configclass
+
+
+
[文档]@configclass +class InteractiveSceneCfg: + """Configuration for the interactive scene. + + The users can inherit from this class to add entities to their scene. This is then parsed by the + :class:`InteractiveScene` class to create the scene. + + .. note:: + The adding of entities to the scene is sensitive to the order of the attributes in the configuration. + Please make sure to add the entities in the order you want them to be added to the scene. + The recommended order of specification is terrain, physics-related assets (articulations and rigid bodies), + sensors and non-physics-related assets (lights). + + For example, to add a robot to the scene, the user can create a configuration class as follows: + + .. code-block:: python + + import omni.isaac.lab.sim as sim_utils + from omni.isaac.lab.assets import AssetBaseCfg + from omni.isaac.lab.scene import InteractiveSceneCfg + from omni.isaac.lab.sensors.ray_caster import GridPatternCfg, RayCasterCfg + from omni.isaac.lab.utils import configclass + + from omni.isaac.lab_assets.anymal import ANYMAL_C_CFG + + @configclass + class MySceneCfg(InteractiveSceneCfg): + + # terrain - flat terrain plane + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + ) + + # articulation - robot 1 + robot_1 = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot_1") + # articulation - robot 2 + robot_2 = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot_2") + robot_2.init_state.pos = (0.0, 1.0, 0.6) + + # sensor - ray caster attached to the base of robot 1 that scans the ground + height_scanner = RayCasterCfg( + prim_path="{ENV_REGEX_NS}/Robot_1/base", + offset=RayCasterCfg.OffsetCfg(pos=(0.0, 0.0, 20.0)), + attach_yaw_only=True, + pattern_cfg=GridPatternCfg(resolution=0.1, size=[1.6, 1.0]), + debug_vis=True, + mesh_prim_paths=["/World/ground"], + ) + + # extras - light + light = AssetBaseCfg( + prim_path="/World/light", + spawn=sim_utils.DistantLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)), + init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, 500.0)), + ) + + """ + + 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 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. + + If true, the sensor data is only updated when their attribute ``data`` is accessed. Otherwise, the sensor + data is updated every time sensors are updated. + """ + + replicate_physics: bool = True + """Enable/disable replication of physics schemas when using the Cloner APIs. Default is True. + + If True, the simulation will have the same asset instances (USD prims) in all the cloned environments. + Internally, this ensures optimization in setting up the scene and parsing it via the physics stage parser. + + If False, the simulation allows having separate asset instances (USD prims) in each environment. + This flexibility comes at a cost of slowdowns in setting up and parsing the scene. + + .. note:: + Optimized parsing of certain prim types (such as deformable objects) is not currently supported + by the physics engine. In these cases, this flag needs to be set to False. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/camera/camera.html b/_modules/omni/isaac/lab/sensors/camera/camera.html new file mode 100644 index 0000000000..324b55eae8 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/camera/camera.html @@ -0,0 +1,1251 @@ + + + + + + + + + + + omni.isaac.lab.sensors.camera.camera — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.camera.camera 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import numpy as np
+import re
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, Literal
+
+import carb
+import omni.isaac.core.utils.stage as stage_utils
+import omni.kit.commands
+import omni.usd
+from omni.isaac.core.prims import XFormPrimView
+from pxr import UsdGeom
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.utils import to_camel_case
+from omni.isaac.lab.utils.array import convert_to_torch
+from omni.isaac.lab.utils.math import (
+    convert_camera_frame_orientation_convention,
+    create_rotation_matrix_from_view,
+    quat_from_matrix,
+)
+
+from ..sensor_base import SensorBase
+from .camera_data import CameraData
+
+if TYPE_CHECKING:
+    from .camera_cfg import CameraCfg
+
+
+
[文档]class Camera(SensorBase): + r"""The camera sensor for acquiring visual data. + + This class wraps over the `UsdGeom Camera`_ for providing a consistent API for acquiring visual data. + It ensures that the camera follows the ROS convention for the coordinate system. + + Summarizing from the `replicator extension`_, the following sensor types are supported: + + - ``"rgb"``: A 3-channel rendered color image. + - ``"rgba"``: A 4-channel rendered color image with alpha channel. + - ``"distance_to_camera"``: An image containing the distance to camera optical center. + - ``"distance_to_image_plane"``: An image containing distances of 3D points from camera plane along camera's z-axis. + - ``"depth"``: The same as ``"distance_to_image_plane"``. + - ``"normals"``: An image containing the local surface normal vectors at each pixel. + - ``"motion_vectors"``: An image containing the motion vector data at each pixel. + - ``"semantic_segmentation"``: The semantic segmentation data. + - ``"instance_segmentation_fast"``: The instance segmentation data. + - ``"instance_id_segmentation_fast"``: The instance id segmentation data. + + .. note:: + Currently the following sensor types are not supported in a "view" format: + + - ``"instance_segmentation"``: The instance segmentation data. Please use the fast counterparts instead. + - ``"instance_id_segmentation"``: The instance id segmentation data. Please use the fast counterparts instead. + - ``"bounding_box_2d_tight"``: The tight 2D bounding box data (only contains non-occluded regions). + - ``"bounding_box_2d_tight_fast"``: The tight 2D bounding box data (only contains non-occluded regions). + - ``"bounding_box_2d_loose"``: The loose 2D bounding box data (contains occluded regions). + - ``"bounding_box_2d_loose_fast"``: The loose 2D bounding box data (contains occluded regions). + - ``"bounding_box_3d"``: The 3D view space bounding box data. + - ``"bounding_box_3d_fast"``: The 3D view space bounding box data. + + .. _replicator extension: https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/annotators_details.html#annotator-output + .. _USDGeom Camera: https://graphics.pixar.com/usd/docs/api/class_usd_geom_camera.html + + """ + + cfg: CameraCfg + """The configuration parameters.""" + + UNSUPPORTED_TYPES: set[str] = { + "instance_id_segmentation", + "instance_segmentation", + "bounding_box_2d_tight", + "bounding_box_2d_loose", + "bounding_box_3d", + "bounding_box_2d_tight_fast", + "bounding_box_2d_loose_fast", + "bounding_box_3d_fast", + } + """The set of sensor types that are not supported by the camera class.""" + +
[文档] def __init__(self, cfg: CameraCfg): + """Initializes the camera sensor. + + Args: + cfg: The configuration parameters. + + Raises: + RuntimeError: If no camera prim is found at the given path. + ValueError: If the provided data types are not supported by the camera. + """ + # check if sensor path is valid + # note: currently we do not handle environment indices if there is a regex pattern in the leaf + # For example, if the prim path is "/World/Sensor_[1,2]". + sensor_path = cfg.prim_path.split("/")[-1] + sensor_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", sensor_path) is None + if sensor_path_is_regex: + raise RuntimeError( + f"Invalid prim path for the camera sensor: {self.cfg.prim_path}." + "\n\tHint: Please ensure that the prim path does not contain any regex patterns in the leaf." + ) + # perform check on supported data types + self._check_supported_data_types(cfg) + # initialize base class + super().__init__(cfg) + + # toggle rendering of rtx sensors as True + # this flag is read by SimulationContext to determine if rtx sensors should be rendered + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/isaaclab/render/rtx_sensors", True) + + # spawn the asset + if self.cfg.spawn is not None: + # compute the rotation offset + rot = torch.tensor(self.cfg.offset.rot, dtype=torch.float32).unsqueeze(0) + rot_offset = convert_camera_frame_orientation_convention( + rot, origin=self.cfg.offset.convention, target="opengl" + ) + rot_offset = rot_offset.squeeze(0).numpy() + # ensure vertical aperture is set, otherwise replace with default for squared pixels + if self.cfg.spawn.vertical_aperture is None: + self.cfg.spawn.vertical_aperture = self.cfg.spawn.horizontal_aperture * self.cfg.height / self.cfg.width + # spawn the asset + self.cfg.spawn.func( + self.cfg.prim_path, self.cfg.spawn, translation=self.cfg.offset.pos, orientation=rot_offset + ) + # check that spawn was successful + matching_prims = sim_utils.find_matching_prims(self.cfg.prim_path) + if len(matching_prims) == 0: + raise RuntimeError(f"Could not find prim with path {self.cfg.prim_path}.") + + # UsdGeom Camera prim for the sensor + self._sensor_prims: list[UsdGeom.Camera] = list() + # Create empty variables for storing output data + self._data = CameraData()
+ + def __del__(self): + """Unsubscribes from callbacks and detach from the replicator registry.""" + # unsubscribe callbacks + super().__del__() + # delete from replicator registry + for _, annotators in self._rep_registry.items(): + for annotator, render_product_path in zip(annotators, self._render_product_paths): + annotator.detach([render_product_path]) + annotator = None + + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + # message for class + return ( + f"Camera @ '{self.cfg.prim_path}': \n" + f"\tdata types : {list(self.data.output.keys())} \n" + f"\tsemantic filter : {self.cfg.semantic_filter}\n" + f"\tcolorize semantic segm. : {self.cfg.colorize_semantic_segmentation}\n" + f"\tcolorize instance segm. : {self.cfg.colorize_instance_segmentation}\n" + f"\tcolorize instance id segm.: {self.cfg.colorize_instance_id_segmentation}\n" + f"\tupdate period (s): {self.cfg.update_period}\n" + f"\tshape : {self.image_shape}\n" + f"\tnumber of sensors : {self._view.count}" + ) + + """ + Properties + """ + + @property + def num_instances(self) -> int: + return self._view.count + + @property + def data(self) -> CameraData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + @property + def frame(self) -> torch.tensor: + """Frame number when the measurement took place.""" + return self._frame + + @property + def render_product_paths(self) -> list[str]: + """The path of the render products for the cameras. + + This can be used via replicator interfaces to attach to writes or external annotator registry. + """ + return self._render_product_paths + + @property + def image_shape(self) -> tuple[int, int]: + """A tuple containing (height, width) of the camera sensor.""" + return (self.cfg.height, self.cfg.width) + + """ + Configuration + """ + +
[文档] def set_intrinsic_matrices( + self, matrices: torch.Tensor, focal_length: float = 1.0, env_ids: Sequence[int] | None = None + ): + """Set parameters of the USD camera from its intrinsic matrix. + + The intrinsic matrix and focal length are used to set the following parameters to the USD camera: + + - ``focal_length``: The focal length of the camera. + - ``horizontal_aperture``: The horizontal aperture of the camera. + - ``vertical_aperture``: The vertical aperture of the camera. + - ``horizontal_aperture_offset``: The horizontal offset of the camera. + - ``vertical_aperture_offset``: The vertical offset of the camera. + + .. warning:: + + Due to limitations of Omniverse camera, we need to assume that the camera is a spherical lens, + i.e. has square pixels, and the optical center is centered at the camera eye. If this assumption + is not true in the input intrinsic matrix, then the camera will not set up correctly. + + Args: + matrices: The intrinsic matrices for the camera. Shape is (N, 3, 3). + focal_length: Focal length to use when computing aperture values (in cm). Defaults to 1.0. + env_ids: A sensor ids to manipulate. Defaults to None, which means all sensor indices. + """ + # resolve env_ids + if env_ids is None: + env_ids = self._ALL_INDICES + # convert matrices to numpy tensors + if isinstance(matrices, torch.Tensor): + matrices = matrices.cpu().numpy() + else: + matrices = np.asarray(matrices, dtype=float) + # iterate over env_ids + for i, intrinsic_matrix in zip(env_ids, matrices): + # extract parameters from matrix + f_x = intrinsic_matrix[0, 0] + c_x = intrinsic_matrix[0, 2] + f_y = intrinsic_matrix[1, 1] + c_y = intrinsic_matrix[1, 2] + # get viewport parameters + height, width = self.image_shape + height, width = float(height), float(width) + # resolve parameters for usd camera + params = { + "focal_length": focal_length, + "horizontal_aperture": width * focal_length / f_x, + "vertical_aperture": height * focal_length / f_y, + "horizontal_aperture_offset": (c_x - width / 2) / f_x, + "vertical_aperture_offset": (c_y - height / 2) / f_y, + } + + # TODO: Adjust to handle aperture offsets once supported by omniverse + # Internal ticket from rendering team: OM-42611 + if params["horizontal_aperture_offset"] > 1e-4 or params["vertical_aperture_offset"] > 1e-4: + omni.log.warn("Camera aperture offsets are not supported by Omniverse. These parameters are ignored.") + + # change data for corresponding camera index + sensor_prim = self._sensor_prims[i] + # set parameters for camera + for param_name, param_value in params.items(): + # convert to camel case (CC) + param_name = to_camel_case(param_name, to="CC") + # get attribute from the class + param_attr = getattr(sensor_prim, f"Get{param_name}Attr") + # set value + # note: We have to do it this way because the camera might be on a different + # layer (default cameras are on session layer), and this is the simplest + # way to set the property on the right layer. + omni.usd.set_prop_val(param_attr(), param_value) + # update the internal buffers + self._update_intrinsic_matrices(env_ids)
+ + """ + Operations - Set pose. + """ + +
[文档] def set_world_poses( + self, + positions: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + env_ids: Sequence[int] | None = None, + convention: Literal["opengl", "ros", "world"] = "ros", + ): + r"""Set the pose of the camera w.r.t. the world frame using specified convention. + + Since different fields use different conventions for camera orientations, the method allows users to + set the camera poses in the specified convention. Possible conventions are: + + - :obj:`"opengl"` - forward axis: -Z - up axis +Y - Offset is applied in the OpenGL (Usd.Camera) convention + - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention + - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention + + See :meth:`omni.isaac.lab.sensors.camera.utils.convert_camera_frame_orientation_convention` for more details + on the conventions. + + Args: + positions: The cartesian coordinates (in meters). Shape is (N, 3). + Defaults to None, in which case the camera position in not changed. + orientations: The quaternion orientation in (w, x, y, z). Shape is (N, 4). + Defaults to None, in which case the camera orientation in not changed. + env_ids: A sensor ids to manipulate. Defaults to None, which means all sensor indices. + convention: The convention in which the poses are fed. Defaults to "ros". + + Raises: + RuntimeError: If the camera prim is not set. Need to call :meth:`initialize` method first. + """ + # resolve env_ids + if env_ids is None: + env_ids = self._ALL_INDICES + # convert to backend tensor + if positions is not None: + if isinstance(positions, np.ndarray): + positions = torch.from_numpy(positions).to(device=self._device) + elif not isinstance(positions, torch.Tensor): + positions = torch.tensor(positions, device=self._device) + # convert rotation matrix from input convention to OpenGL + if orientations is not None: + if isinstance(orientations, np.ndarray): + orientations = torch.from_numpy(orientations).to(device=self._device) + elif not isinstance(orientations, torch.Tensor): + orientations = torch.tensor(orientations, device=self._device) + orientations = convert_camera_frame_orientation_convention(orientations, origin=convention, target="opengl") + # set the pose + self._view.set_world_poses(positions, orientations, env_ids)
+ +
[文档] def set_world_poses_from_view( + self, eyes: torch.Tensor, targets: torch.Tensor, env_ids: Sequence[int] | None = None + ): + """Set the poses of the camera from the eye position and look-at target position. + + Args: + eyes: The positions of the camera's eye. Shape is (N, 3). + targets: The target locations to look at. Shape is (N, 3). + env_ids: A sensor ids to manipulate. Defaults to None, which means all sensor indices. + + Raises: + RuntimeError: If the camera prim is not set. Need to call :meth:`initialize` method first. + NotImplementedError: If the stage up-axis is not "Y" or "Z". + """ + # resolve env_ids + if env_ids is None: + env_ids = self._ALL_INDICES + # get up axis of current stage + up_axis = stage_utils.get_stage_up_axis() + # set camera poses using the view + orientations = quat_from_matrix(create_rotation_matrix_from_view(eyes, targets, up_axis, device=self._device)) + self._view.set_world_poses(eyes, orientations, env_ids)
+ + """ + Operations + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + if not self._is_initialized: + raise RuntimeError( + "Camera could not be initialized. Please ensure --enable_cameras is used to enable rendering." + ) + # reset the timestamps + super().reset(env_ids) + # resolve None + # note: cannot do smart indexing here since we do a for loop over data. + if env_ids is None: + env_ids = self._ALL_INDICES + # reset the data + # note: this recomputation is useful if one performs events such as randomizations on the camera poses. + self._update_poses(env_ids) + # Reset the frame count + self._frame[env_ids] = 0
+ + """ + Implementation. + """ + + def _initialize_impl(self): + """Initializes the sensor handles and internal buffers. + + This function creates handles and registers the provided data types with the replicator registry to + be able to access the data from the sensor. It also initializes the internal buffers to store the data. + + Raises: + RuntimeError: If the number of camera prims in the view does not match the number of environments. + RuntimeError: If replicator was not found. + """ + carb_settings_iface = carb.settings.get_settings() + if not carb_settings_iface.get("/isaaclab/cameras_enabled"): + raise RuntimeError( + "A camera was spawned without the --enable_cameras flag. Please use --enable_cameras to enable" + " rendering." + ) + + import omni.replicator.core as rep + from omni.syntheticdata.scripts.SyntheticData import SyntheticData + + # Initialize parent class + super()._initialize_impl() + # Create a view for the sensor + self._view = XFormPrimView(self.cfg.prim_path, reset_xform_properties=False) + self._view.initialize() + # Check that sizes are correct + if self._view.count != self._num_envs: + raise RuntimeError( + f"Number of camera prims in the view ({self._view.count}) does not match" + f" the number of environments ({self._num_envs})." + ) + + # WAR: use DLAA antialiasing to avoid frame offset issue at small resolutions + if self.cfg.width < 265 or self.cfg.height < 265: + rep.settings.set_render_rtx_realtime(antialiasing="DLAA") + + # Create all env_ids buffer + self._ALL_INDICES = torch.arange(self._view.count, device=self._device, dtype=torch.long) + # Create frame count buffer + self._frame = torch.zeros(self._view.count, device=self._device, dtype=torch.long) + + # Attach the sensor data types to render node + self._render_product_paths: list[str] = list() + self._rep_registry: dict[str, list[rep.annotators.Annotator]] = {name: list() for name in self.cfg.data_types} + + # Obtain current stage + stage = omni.usd.get_context().get_stage() + # Convert all encapsulated prims to Camera + for cam_prim_path in self._view.prim_paths: + # Get camera prim + cam_prim = stage.GetPrimAtPath(cam_prim_path) + # Check if prim is a camera + if not cam_prim.IsA(UsdGeom.Camera): + raise RuntimeError(f"Prim at path '{cam_prim_path}' is not a Camera.") + # Add to list + sensor_prim = UsdGeom.Camera(cam_prim) + self._sensor_prims.append(sensor_prim) + + # Get render product + # From Isaac Sim 2023.1 onwards, render product is a HydraTexture so we need to extract the path + render_prod_path = rep.create.render_product(cam_prim_path, resolution=(self.cfg.width, self.cfg.height)) + if not isinstance(render_prod_path, str): + render_prod_path = render_prod_path.path + self._render_product_paths.append(render_prod_path) + + # Check if semantic types or semantic filter predicate is provided + if isinstance(self.cfg.semantic_filter, list): + semantic_filter_predicate = ":*; ".join(self.cfg.semantic_filter) + ":*" + elif isinstance(self.cfg.semantic_filter, str): + semantic_filter_predicate = self.cfg.semantic_filter + else: + raise ValueError(f"Semantic types must be a list or a string. Received: {self.cfg.semantic_filter}.") + # set the semantic filter predicate + # copied from rep.scripts.writes_default.basic_writer.py + SyntheticData.Get().set_instance_mapping_semantic_filter(semantic_filter_predicate) + + # Iterate over each data type and create annotator + # TODO: This will move out of the loop once Replicator supports multiple render products within a single + # annotator, i.e.: rep_annotator.attach(self._render_product_paths) + for name in self.cfg.data_types: + # note: we are verbose here to make it easier to understand the code. + # if colorize is true, the data is mapped to colors and a uint8 4 channel image is returned. + # if colorize is false, the data is returned as a uint32 image with ids as values. + if name == "semantic_segmentation": + init_params = {"colorize": self.cfg.colorize_semantic_segmentation} + elif name == "instance_segmentation_fast": + init_params = {"colorize": self.cfg.colorize_instance_segmentation} + elif name == "instance_id_segmentation_fast": + init_params = {"colorize": self.cfg.colorize_instance_id_segmentation} + else: + init_params = None + + # Resolve device name + if "cuda" in self._device: + device_name = self._device.split(":")[0] + else: + device_name = "cpu" + + # Map special cases to their corresponding annotator names + special_cases = {"rgba": "rgb", "depth": "distance_to_image_plane"} + # Get the annotator name, falling back to the original name if not a special case + annotator_name = special_cases.get(name, name) + # Create the annotator node + rep_annotator = rep.AnnotatorRegistry.get_annotator(annotator_name, init_params, device=device_name) + + # attach annotator to render product + rep_annotator.attach(render_prod_path) + # add to registry + self._rep_registry[name].append(rep_annotator) + + # Create internal buffers + self._create_buffers() + self._update_intrinsic_matrices(self._ALL_INDICES) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + # Increment frame count + self._frame[env_ids] += 1 + # -- pose + self._update_poses(env_ids) + # -- read the data from annotator registry + # check if buffer is called for the first time. If so then, allocate the memory + if len(self._data.output) == 0: + # this is the first time buffer is called + # it allocates memory for all the sensors + self._create_annotator_data() + else: + # iterate over all the data types + for name, annotators in self._rep_registry.items(): + # iterate over all the annotators + for index in env_ids: + # get the output + output = annotators[index].get_data() + # process the output + data, info = self._process_annotator_output(name, output) + # add data to output + self._data.output[name][index] = data + # add info to output + self._data.info[index][name] = info + + """ + Private Helpers + """ + + def _check_supported_data_types(self, cfg: CameraCfg): + """Checks if the data types are supported by the ray-caster camera.""" + # check if there is any intersection in unsupported types + # reason: these use np structured data types which we can't yet convert to torch tensor + common_elements = set(cfg.data_types) & Camera.UNSUPPORTED_TYPES + if common_elements: + # provide alternative fast counterparts + fast_common_elements = [] + for item in common_elements: + if "instance_segmentation" in item or "instance_id_segmentation" in item: + fast_common_elements.append(item + "_fast") + # raise error + raise ValueError( + f"Camera class does not support the following sensor types: {common_elements}." + "\n\tThis is because these sensor types output numpy structured data types which" + "can't be converted to torch tensors easily." + "\n\tHint: If you need to work with these sensor types, we recommend using their fast counterparts." + f"\n\t\tFast counterparts: {fast_common_elements}" + ) + + def _create_buffers(self): + """Create buffers for storing data.""" + # create the data object + # -- pose of the cameras + self._data.pos_w = torch.zeros((self._view.count, 3), device=self._device) + self._data.quat_w_world = torch.zeros((self._view.count, 4), device=self._device) + # -- intrinsic matrix + self._data.intrinsic_matrices = torch.zeros((self._view.count, 3, 3), device=self._device) + self._data.image_shape = self.image_shape + # -- output data + # lazy allocation of data dictionary + # since the size of the output data is not known in advance, we leave it as None + # the memory will be allocated when the buffer() function is called for the first time. + self._data.output = {} + self._data.info = [{name: None for name in self.cfg.data_types} for _ in range(self._view.count)] + + def _update_intrinsic_matrices(self, env_ids: Sequence[int]): + """Compute camera's matrix of intrinsic parameters. + + Also called calibration matrix. This matrix works for linear depth images. We assume square pixels. + + Note: + The calibration matrix projects points in the 3D scene onto an imaginary screen of the camera. + The coordinates of points on the image plane are in the homogeneous representation. + """ + # iterate over all cameras + for i in env_ids: + # Get corresponding sensor prim + sensor_prim = self._sensor_prims[i] + # get camera parameters + focal_length = sensor_prim.GetFocalLengthAttr().Get() + horiz_aperture = sensor_prim.GetHorizontalApertureAttr().Get() + vert_aperture = sensor_prim.GetVerticalApertureAttr().Get() + horiz_aperture_offset = sensor_prim.GetHorizontalApertureOffsetAttr().Get() + vert_aperture_offset = sensor_prim.GetVerticalApertureOffsetAttr().Get() + # get viewport parameters + height, width = self.image_shape + # extract intrinsic parameters + f_x = (width * focal_length) / horiz_aperture + f_y = (height * focal_length) / vert_aperture + c_x = width * 0.5 + horiz_aperture_offset * f_x + c_y = height * 0.5 + vert_aperture_offset * f_y + # create intrinsic matrix for depth linear + self._data.intrinsic_matrices[i, 0, 0] = f_x + self._data.intrinsic_matrices[i, 0, 2] = c_x + self._data.intrinsic_matrices[i, 1, 1] = f_y + self._data.intrinsic_matrices[i, 1, 2] = c_y + self._data.intrinsic_matrices[i, 2, 2] = 1 + + def _update_poses(self, env_ids: Sequence[int]): + """Computes the pose of the camera in the world frame with ROS convention. + + This methods uses the ROS convention to resolve the input pose. In this convention, + we assume that the camera front-axis is +Z-axis and up-axis is -Y-axis. + + Returns: + A tuple of the position (in meters) and quaternion (w, x, y, z). + """ + # check camera prim exists + if len(self._sensor_prims) == 0: + raise RuntimeError("Camera prim is None. Please call 'sim.play()' first.") + + # get the poses from the view + poses, quat = self._view.get_world_poses(env_ids) + self._data.pos_w[env_ids] = poses + self._data.quat_w_world[env_ids] = convert_camera_frame_orientation_convention( + quat, origin="opengl", target="world" + ) + + def _create_annotator_data(self): + """Create the buffers to store the annotator data. + + We create a buffer for each annotator and store the data in a dictionary. Since the data + shape is not known beforehand, we create a list of buffers and concatenate them later. + + This is an expensive operation and should be called only once. + """ + # add data from the annotators + for name, annotators in self._rep_registry.items(): + # create a list to store the data for each annotator + data_all_cameras = list() + # iterate over all the annotators + for index in self._ALL_INDICES: + # get the output + output = annotators[index].get_data() + # process the output + data, info = self._process_annotator_output(name, output) + # append the data + data_all_cameras.append(data) + # store the info + self._data.info[index][name] = info + # concatenate the data along the batch dimension + self._data.output[name] = torch.stack(data_all_cameras, dim=0) + + def _process_annotator_output(self, name: str, output: Any) -> tuple[torch.tensor, dict | None]: + """Process the annotator output. + + This function is called after the data has been collected from all the cameras. + """ + # extract info and data from the output + if isinstance(output, dict): + data = output["data"] + info = output["info"] + else: + data = output + info = None + # convert data into torch tensor + data = convert_to_torch(data, device=self.device) + + # process data for different segmentation types + # Note: Replicator returns raw buffers of dtype int32 for segmentation types + # so we need to convert them to uint8 4 channel images for colorized types + height, width = self.image_shape + if name == "semantic_segmentation": + if self.cfg.colorize_semantic_segmentation: + data = data.view(torch.uint8).reshape(height, width, -1) + else: + data = data.view(height, width, 1) + elif name == "instance_segmentation_fast": + if self.cfg.colorize_instance_segmentation: + data = data.view(torch.uint8).reshape(height, width, -1) + else: + data = data.view(height, width, 1) + elif name == "instance_id_segmentation_fast": + if self.cfg.colorize_instance_id_segmentation: + data = data.view(torch.uint8).reshape(height, width, -1) + else: + data = data.view(height, width, 1) + # make sure buffer dimensions are consistent as (H, W, C) + elif name == "distance_to_camera" or name == "distance_to_image_plane" or name == "depth": + data = data.view(height, width, 1) + # we only return the RGB channels from the RGBA output if rgb is required + # normals return (x, y, z) in first 3 channels, 4th channel is unused + elif name == "rgb" or name == "normals": + data = data[..., :3] + # motion vectors return (x, y) in first 2 channels, 3rd and 4th channels are unused + elif name == "motion_vectors": + data = data[..., :2] + + # return the data and info + return data, info + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/camera/camera_cfg.html b/_modules/omni/isaac/lab/sensors/camera/camera_cfg.html new file mode 100644 index 0000000000..3701b43de2 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/camera/camera_cfg.html @@ -0,0 +1,667 @@ + + + + + + + + + + + omni.isaac.lab.sensors.camera.camera_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.camera.camera_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.sim import FisheyeCameraCfg, PinholeCameraCfg
+from omni.isaac.lab.utils import configclass
+
+from ..sensor_base_cfg import SensorBaseCfg
+from .camera import Camera
+
+
+
[文档]@configclass +class CameraCfg(SensorBaseCfg): + """Configuration for a camera sensor.""" + +
[文档] @configclass + class OffsetCfg: + """The offset pose of the sensor's frame from the sensor's parent frame.""" + + 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).""" + + convention: Literal["opengl", "ros", "world"] = "ros" + """The convention in which the frame offset is applied. Defaults to "ros". + + - ``"opengl"`` - forward axis: ``-Z`` - up axis: ``+Y`` - Offset is applied in the OpenGL (Usd.Camera) convention. + - ``"ros"`` - forward axis: ``+Z`` - up axis: ``-Y`` - Offset is applied in the ROS convention. + - ``"world"`` - forward axis: ``+X`` - up axis: ``+Z`` - Offset is applied in the World Frame convention. + + """
+ + class_type: type = Camera + + offset: OffsetCfg = OffsetCfg() + """The offset pose of the sensor's frame from the sensor's parent frame. Defaults to identity. + + Note: + The parent frame is the frame the sensor attaches to. For example, the parent frame of a + camera at path ``/World/envs/env_0/Robot/Camera`` is ``/World/envs/env_0/Robot``. + """ + + spawn: PinholeCameraCfg | FisheyeCameraCfg | None = MISSING + """Spawn configuration for the asset. + + If None, then the prim is not spawned by the asset. Instead, it is assumed that the + asset is already present in the scene. + """ + + data_types: list[str] = ["rgb"] + """List of sensor names/types to enable for the camera. Defaults to ["rgb"]. + + Please refer to the :class:`Camera` class for a list of available data types. + """ + + width: int = MISSING + """Width of the image in pixels.""" + + height: int = MISSING + """Height of the image in pixels.""" + + semantic_filter: str | list[str] = "*:*" + """A string or a list specifying a semantic filter predicate. Defaults to ``"*:*"``. + + If a string, it should be a disjunctive normal form of (semantic type, labels). For examples: + + * ``"typeA : labelA & !labelB | labelC , typeB: labelA ; typeC: labelE"``: + All prims with semantic type "typeA" and label "labelA" but not "labelB" or with label "labelC". + Also, all prims with semantic type "typeB" and label "labelA", or with semantic type "typeC" and label "labelE". + * ``"typeA : * ; * : labelA"``: All prims with semantic type "typeA" or with label "labelA" + + If a list of strings, each string should be a semantic type. The segmentation for prims with + semantics of the specified types will be retrieved. For example, if the list is ["class"], only + the segmentation for prims with semantics of type "class" will be retrieved. + + .. seealso:: + + For more information on the semantics filter, see the documentation on `Replicator Semantics Schema Editor`_. + + .. _Replicator Semantics Schema Editor: https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/semantics_schema_editor.html#semantics-filtering + """ + + colorize_semantic_segmentation: bool = True + """Whether to colorize the semantic segmentation images. Defaults to True. + + If True, semantic segmentation is converted to an image where semantic IDs are mapped to colors + and returned as a ``uint8`` 4-channel array. If False, the output is returned as a ``int32`` array. + """ + + colorize_instance_id_segmentation: bool = True + """Whether to colorize the instance ID segmentation images. Defaults to True. + + If True, instance id segmentation is converted to an image where instance IDs are mapped to colors. + and returned as a ``uint8`` 4-channel array. If False, the output is returned as a ``int32`` array. + """ + + colorize_instance_segmentation: bool = True + """Whether to colorize the instance ID segmentation images. Defaults to True. + + If True, instance segmentation is converted to an image where instance IDs are mapped to colors. + and returned as a ``uint8`` 4-channel array. If False, the output is returned as a ``int32`` array. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/camera/camera_data.html b/_modules/omni/isaac/lab/sensors/camera/camera_data.html new file mode 100644 index 0000000000..7ab0bc22ae --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/camera/camera_data.html @@ -0,0 +1,650 @@ + + + + + + + + + + + omni.isaac.lab.sensors.camera.camera_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.camera.camera_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from dataclasses import dataclass
+from typing import Any
+
+from omni.isaac.lab.utils.math import convert_camera_frame_orientation_convention
+
+
+
[文档]@dataclass +class CameraData: + """Data container for the camera sensor.""" + + ## + # Frame state. + ## + + 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. + """ + + quat_w_world: torch.Tensor = None + """Quaternion orientation `(w, x, y, z)` of the sensor origin in world frame, following the world coordinate frame + + .. 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. + """ + + ## + # Camera data + ## + + image_shape: tuple[int, int] = None + """A tuple containing (height, width) of the camera sensor.""" + + intrinsic_matrices: torch.Tensor = None + """The intrinsic matrices for the camera. + + Shape is (N, 3, 3) where N is the number of sensors. + """ + + output: dict[str, torch.Tensor] = None + """The retrieved sensor data with sensor types as key. + + The format of the data is available in the `Replicator Documentation`_. For semantic-based data, + this corresponds to the ``"data"`` key in the output of the sensor. + + .. _Replicator Documentation: https://docs.omniverse.nvidia.com/prod_extensions/prod_extensions/ext_replicator/annotators_details.html#annotator-output + """ + + info: list[dict[str, Any]] = None + """The retrieved sensor info with sensor types as key. + + This contains extra information provided by the sensor such as semantic segmentation label mapping, prim paths. + For semantic-based data, this corresponds to the ``"info"`` key in the output of the sensor. For other sensor + types, the info is empty. + """ + + ## + # Additional Frame orientation conventions + ## + + @property + def quat_w_ros(self) -> torch.Tensor: + """Quaternion orientation `(w, x, y, z)` of the sensor origin in the world frame, following ROS convention. + + .. 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. + """ + return convert_camera_frame_orientation_convention(self.quat_w_world, origin="world", target="ros") + + @property + def quat_w_opengl(self) -> torch.Tensor: + """Quaternion orientation `(w, x, y, z)` of the sensor origin in the world frame, following + Opengl / USD Camera convention. + + .. 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. + """ + return convert_camera_frame_orientation_convention(self.quat_w_world, origin="world", target="opengl")
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/camera/tiled_camera.html b/_modules/omni/isaac/lab/sensors/camera/tiled_camera.html new file mode 100644 index 0000000000..02aff4996a --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/camera/tiled_camera.html @@ -0,0 +1,964 @@ + + + + + + + + + + + omni.isaac.lab.sensors.camera.tiled_camera — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.camera.tiled_camera 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import math
+import numpy as np
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any
+
+import carb
+import omni.usd
+import warp as wp
+from omni.isaac.core.prims import XFormPrimView
+from omni.isaac.version import get_version
+from pxr import UsdGeom
+
+from omni.isaac.lab.utils.warp.kernels import reshape_tiled_image
+
+from ..sensor_base import SensorBase
+from .camera import Camera
+
+if TYPE_CHECKING:
+    from .tiled_camera_cfg import TiledCameraCfg
+
+
+
[文档]class TiledCamera(Camera): + r"""The tiled rendering based camera sensor for acquiring the same data as the Camera class. + + This class inherits from the :class:`Camera` class but uses the tiled-rendering API to acquire + the visual data. Tiled-rendering concatenates the rendered images from multiple cameras into a single image. + This allows for rendering multiple cameras in parallel and is useful for rendering large scenes with multiple + cameras efficiently. + + The following sensor types are supported: + + - ``"rgb"``: A 3-channel rendered color image. + - ``"rgba"``: A 4-channel rendered color image with alpha channel. + - ``"distance_to_camera"``: An image containing the distance to camera optical center. + - ``"distance_to_image_plane"``: An image containing distances of 3D points from camera plane along camera's z-axis. + - ``"depth"``: Alias for ``"distance_to_image_plane"``. + - ``"normals"``: An image containing the local surface normal vectors at each pixel. + - ``"motion_vectors"``: An image containing the motion vector data at each pixel. + - ``"semantic_segmentation"``: The semantic segmentation data. + - ``"instance_segmentation_fast"``: The instance segmentation data. + - ``"instance_id_segmentation_fast"``: The instance id segmentation data. + + .. note:: + Currently the following sensor types are not supported in a "view" format: + + - ``"instance_segmentation"``: The instance segmentation data. Please use the fast counterparts instead. + - ``"instance_id_segmentation"``: The instance id segmentation data. Please use the fast counterparts instead. + - ``"bounding_box_2d_tight"``: The tight 2D bounding box data (only contains non-occluded regions). + - ``"bounding_box_2d_tight_fast"``: The tight 2D bounding box data (only contains non-occluded regions). + - ``"bounding_box_2d_loose"``: The loose 2D bounding box data (contains occluded regions). + - ``"bounding_box_2d_loose_fast"``: The loose 2D bounding box data (contains occluded regions). + - ``"bounding_box_3d"``: The 3D view space bounding box data. + - ``"bounding_box_3d_fast"``: The 3D view space bounding box data. + + .. _replicator extension: https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/annotators_details.html#annotator-output + .. _USDGeom Camera: https://graphics.pixar.com/usd/docs/api/class_usd_geom_camera.html + + .. versionadded:: v1.0.0 + + This feature is available starting from Isaac Sim 4.2. Before this version, the tiled rendering APIs + were not available. + + """ + + cfg: TiledCameraCfg + """The configuration parameters.""" + +
[文档] def __init__(self, cfg: TiledCameraCfg): + """Initializes the tiled camera sensor. + + Args: + cfg: The configuration parameters. + + Raises: + RuntimeError: If no camera prim is found at the given path. + RuntimeError: If Isaac Sim version < 4.2 + ValueError: If the provided data types are not supported by the camera. + """ + isaac_sim_version = float(".".join(get_version()[2:4])) + if isaac_sim_version < 4.2: + raise RuntimeError( + f"TiledCamera is only available from Isaac Sim 4.2.0. Current version is {isaac_sim_version}. Please" + " update to Isaac Sim 4.2.0" + ) + super().__init__(cfg)
+ + def __del__(self): + """Unsubscribes from callbacks and detach from the replicator registry.""" + # unsubscribe from callbacks + SensorBase.__del__(self) + # detach from the replicator registry + for annotator in self._annotators.values(): + annotator.detach(self.render_product_paths) + + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + # message for class + return ( + f"Tiled Camera @ '{self.cfg.prim_path}': \n" + f"\tdata types : {list(self.data.output.keys())} \n" + f"\tsemantic filter : {self.cfg.semantic_filter}\n" + f"\tcolorize semantic segm. : {self.cfg.colorize_semantic_segmentation}\n" + f"\tcolorize instance segm. : {self.cfg.colorize_instance_segmentation}\n" + f"\tcolorize instance id segm.: {self.cfg.colorize_instance_id_segmentation}\n" + f"\tupdate period (s): {self.cfg.update_period}\n" + f"\tshape : {self.image_shape}\n" + f"\tnumber of sensors : {self._view.count}" + ) + + """ + Operations + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + if not self._is_initialized: + raise RuntimeError( + "TiledCamera could not be initialized. Please ensure --enable_cameras is used to enable rendering." + ) + # reset the timestamps + SensorBase.reset(self, env_ids) + # resolve None + if env_ids is None: + env_ids = slice(None) + # reset the frame count + self._frame[env_ids] = 0
+ + """ + Implementation. + """ + + def _initialize_impl(self): + """Initializes the sensor handles and internal buffers. + + This function creates handles and registers the provided data types with the replicator registry to + be able to access the data from the sensor. It also initializes the internal buffers to store the data. + + Raises: + RuntimeError: If the number of camera prims in the view does not match the number of environments. + RuntimeError: If replicator was not found. + """ + carb_settings_iface = carb.settings.get_settings() + if not carb_settings_iface.get("/isaaclab/cameras_enabled"): + raise RuntimeError( + "A camera was spawned without the --enable_cameras flag. Please use --enable_cameras to enable" + " rendering." + ) + + import omni.replicator.core as rep + + # Initialize parent class + SensorBase._initialize_impl(self) + # Create a view for the sensor + self._view = XFormPrimView(self.cfg.prim_path, reset_xform_properties=False) + self._view.initialize() + # Check that sizes are correct + if self._view.count != self._num_envs: + raise RuntimeError( + f"Number of camera prims in the view ({self._view.count}) does not match" + f" the number of environments ({self._num_envs})." + ) + + # Create all env_ids buffer + self._ALL_INDICES = torch.arange(self._view.count, device=self._device, dtype=torch.long) + # Create frame count buffer + self._frame = torch.zeros(self._view.count, device=self._device, dtype=torch.long) + + # Obtain current stage + stage = omni.usd.get_context().get_stage() + # Convert all encapsulated prims to Camera + for cam_prim_path in self._view.prim_paths: + # Get camera prim + cam_prim = stage.GetPrimAtPath(cam_prim_path) + # Check if prim is a camera + if not cam_prim.IsA(UsdGeom.Camera): + raise RuntimeError(f"Prim at path '{cam_prim_path}' is not a Camera.") + # Add to list + sensor_prim = UsdGeom.Camera(cam_prim) + self._sensor_prims.append(sensor_prim) + + # Create replicator tiled render product + rp = rep.create.render_product_tiled( + cameras=self._view.prim_paths, tile_resolution=(self.cfg.width, self.cfg.height) + ) + self._render_product_paths = [rp.path] + + # WAR: use DLAA antialiasing to avoid frame offset issue at small resolutions + if self._tiling_grid_shape()[0] * self.cfg.width < 265 or self._tiling_grid_shape()[1] * self.cfg.height < 265: + rep.settings.set_render_rtx_realtime(antialiasing="DLAA") + + # Define the annotators based on requested data types + self._annotators = dict() + for annotator_type in self.cfg.data_types: + if annotator_type == "rgba" or annotator_type == "rgb": + annotator = rep.AnnotatorRegistry.get_annotator("rgb", device=self.device, do_array_copy=False) + self._annotators["rgba"] = annotator + elif annotator_type == "depth" or annotator_type == "distance_to_image_plane": + # keep depth for backwards compatibility + annotator = rep.AnnotatorRegistry.get_annotator( + "distance_to_image_plane", device=self.device, do_array_copy=False + ) + self._annotators[annotator_type] = annotator + # note: we are verbose here to make it easier to understand the code. + # if colorize is true, the data is mapped to colors and a uint8 4 channel image is returned. + # if colorize is false, the data is returned as a uint32 image with ids as values. + else: + init_params = None + if annotator_type == "semantic_segmentation": + init_params = {"colorize": self.cfg.colorize_semantic_segmentation} + elif annotator_type == "instance_segmentation_fast": + init_params = {"colorize": self.cfg.colorize_instance_segmentation} + elif annotator_type == "instance_id_segmentation_fast": + init_params = {"colorize": self.cfg.colorize_instance_id_segmentation} + + annotator = rep.AnnotatorRegistry.get_annotator( + annotator_type, init_params, device=self.device, do_array_copy=False + ) + self._annotators[annotator_type] = annotator + + # Attach the annotator to the render product + for annotator in self._annotators.values(): + annotator.attach(self._render_product_paths) + + # Create internal buffers + self._create_buffers() + + def _update_buffers_impl(self, env_ids: Sequence[int]): + # Increment frame count + self._frame[env_ids] += 1 + + # Extract the flattened image buffer + for data_type, annotator in self._annotators.items(): + # check whether returned data is a dict (used for segmentation) + output = annotator.get_data() + if isinstance(output, dict): + tiled_data_buffer = output["data"] + self._data.info[data_type] = output["info"] + else: + tiled_data_buffer = output + + # convert data buffer to warp array + if isinstance(tiled_data_buffer, np.ndarray): + tiled_data_buffer = wp.array(tiled_data_buffer, device=self.device, dtype=wp.uint8) + else: + tiled_data_buffer = tiled_data_buffer.to(device=self.device) + + # process data for different segmentation types + # Note: Replicator returns raw buffers of dtype uint32 for segmentation types + # so we need to convert them to uint8 4 channel images for colorized types + if ( + (data_type == "semantic_segmentation" and self.cfg.colorize_semantic_segmentation) + or (data_type == "instance_segmentation_fast" and self.cfg.colorize_instance_segmentation) + or (data_type == "instance_id_segmentation_fast" and self.cfg.colorize_instance_id_segmentation) + ): + tiled_data_buffer = wp.array( + ptr=tiled_data_buffer.ptr, shape=(*tiled_data_buffer.shape, 4), dtype=wp.uint8, device=self.device + ) + + wp.launch( + kernel=reshape_tiled_image, + dim=(self._view.count, self.cfg.height, self.cfg.width), + inputs=[ + tiled_data_buffer.flatten(), + wp.from_torch(self._data.output[data_type]), # zero-copy alias + *list(self._data.output[data_type].shape[1:]), # height, width, num_channels + self._tiling_grid_shape()[0], # num_tiles_x + ], + device=self.device, + ) + + # alias rgb as first 3 channels of rgba + if data_type == "rgba" and "rgb" in self.cfg.data_types: + self._data.output["rgb"] = self._data.output["rgba"][..., :3] + + """ + Private Helpers + """ + + def _check_supported_data_types(self, cfg: TiledCameraCfg): + """Checks if the data types are supported by the ray-caster camera.""" + # check if there is any intersection in unsupported types + # reason: these use np structured data types which we can't yet convert to torch tensor + common_elements = set(cfg.data_types) & Camera.UNSUPPORTED_TYPES + if common_elements: + # provide alternative fast counterparts + fast_common_elements = [] + for item in common_elements: + if "instance_segmentation" in item or "instance_id_segmentation" in item: + fast_common_elements.append(item + "_fast") + # raise error + raise ValueError( + f"TiledCamera class does not support the following sensor types: {common_elements}." + "\n\tThis is because these sensor types output numpy structured data types which" + "can't be converted to torch tensors easily." + "\n\tHint: If you need to work with these sensor types, we recommend using their fast counterparts." + f"\n\t\tFast counterparts: {fast_common_elements}" + ) + + def _create_buffers(self): + """Create buffers for storing data.""" + # create the data object + # -- pose of the cameras + self._data.pos_w = torch.zeros((self._view.count, 3), device=self._device) + self._data.quat_w_world = torch.zeros((self._view.count, 4), device=self._device) + self._update_poses(self._ALL_INDICES) + # -- intrinsic matrix + self._data.intrinsic_matrices = torch.zeros((self._view.count, 3, 3), device=self._device) + self._update_intrinsic_matrices(self._ALL_INDICES) + self._data.image_shape = self.image_shape + # -- output data + data_dict = dict() + if "rgba" in self.cfg.data_types or "rgb" in self.cfg.data_types: + data_dict["rgba"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 4), device=self.device, dtype=torch.uint8 + ).contiguous() + if "rgb" in self.cfg.data_types: + # RGB is the first 3 channels of RGBA + data_dict["rgb"] = data_dict["rgba"][..., :3] + if "distance_to_image_plane" in self.cfg.data_types: + data_dict["distance_to_image_plane"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.float32 + ).contiguous() + if "depth" in self.cfg.data_types: + data_dict["depth"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.float32 + ).contiguous() + if "distance_to_camera" in self.cfg.data_types: + data_dict["distance_to_camera"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.float32 + ).contiguous() + if "normals" in self.cfg.data_types: + data_dict["normals"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 3), device=self.device, dtype=torch.float32 + ).contiguous() + if "motion_vectors" in self.cfg.data_types: + data_dict["motion_vectors"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 2), device=self.device, dtype=torch.float32 + ).contiguous() + if "semantic_segmentation" in self.cfg.data_types: + if self.cfg.colorize_semantic_segmentation: + data_dict["semantic_segmentation"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 4), device=self.device, dtype=torch.uint8 + ).contiguous() + else: + data_dict["semantic_segmentation"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.int32 + ).contiguous() + if "instance_segmentation_fast" in self.cfg.data_types: + if self.cfg.colorize_instance_segmentation: + data_dict["instance_segmentation_fast"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 4), device=self.device, dtype=torch.uint8 + ).contiguous() + else: + data_dict["instance_segmentation_fast"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.int32 + ).contiguous() + if "instance_id_segmentation_fast" in self.cfg.data_types: + if self.cfg.colorize_instance_id_segmentation: + data_dict["instance_id_segmentation_fast"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 4), device=self.device, dtype=torch.uint8 + ).contiguous() + else: + data_dict["instance_id_segmentation_fast"] = torch.zeros( + (self._view.count, self.cfg.height, self.cfg.width, 1), device=self.device, dtype=torch.int32 + ).contiguous() + + self._data.output = data_dict + self._data.info = dict() + + def _tiled_image_shape(self) -> tuple[int, int]: + """Returns a tuple containing the dimension of the tiled image.""" + cols, rows = self._tiling_grid_shape() + return (self.cfg.width * cols, self.cfg.height * rows) + + def _tiling_grid_shape(self) -> tuple[int, int]: + """Returns a tuple containing the tiling grid dimension.""" + cols = math.ceil(math.sqrt(self._view.count)) + rows = math.ceil(self._view.count / cols) + return (cols, rows) + + def _create_annotator_data(self): + # we do not need to create annotator data for the tiled camera sensor + raise RuntimeError("This function should not be called for the tiled camera sensor.") + + def _process_annotator_output(self, name: str, output: Any) -> tuple[torch.tensor, dict | None]: + # we do not need to process annotator output for the tiled camera sensor + raise RuntimeError("This function should not be called for the tiled camera sensor.") + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/camera/tiled_camera_cfg.html b/_modules/omni/isaac/lab/sensors/camera/tiled_camera_cfg.html new file mode 100644 index 0000000000..7996c05ffe --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/camera/tiled_camera_cfg.html @@ -0,0 +1,583 @@ + + + + + + + + + + + omni.isaac.lab.sensors.camera.tiled_camera_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.camera.tiled_camera_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from omni.isaac.lab.utils import configclass
+
+from .camera_cfg import CameraCfg
+from .tiled_camera import TiledCamera
+
+
+
[文档]@configclass +class TiledCameraCfg(CameraCfg): + """Configuration for a tiled rendering-based camera sensor.""" + + class_type: type = TiledCamera + + return_latest_camera_pose: bool = False + """Whether to return the latest camera pose when fetching the camera's data. Defaults to False. + + If True, the latest camera pose is returned in the camera's data which will slow down performance + due to the use of :class:`XformPrimView`. + If False, the pose of the camera during initialization is returned. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor.html b/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor.html new file mode 100644 index 0000000000..9e1174c883 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor.html @@ -0,0 +1,976 @@ + + + + + + + + + + + omni.isaac.lab.sensors.contact_sensor.contact_sensor — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.contact_sensor.contact_sensor 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# Ignore optional memory usage warning globally
+# pyright: reportOptionalSubscript=false
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.physics.tensors.impl.api as physx
+from pxr import PhysxSchema
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.string as string_utils
+from omni.isaac.lab.markers import VisualizationMarkers
+from omni.isaac.lab.utils.math import convert_quat
+
+from ..sensor_base import SensorBase
+from .contact_sensor_data import ContactSensorData
+
+if TYPE_CHECKING:
+    from .contact_sensor_cfg import ContactSensorCfg
+
+
+
[文档]class ContactSensor(SensorBase): + """A contact reporting sensor. + + The contact sensor reports the normal contact forces on a rigid body in the world frame. + It relies on the `PhysX ContactReporter`_ API to be activated on the rigid bodies. + + To enable the contact reporter on a rigid body, please make sure to enable the + :attr:`omni.isaac.lab.sim.spawner.RigidObjectSpawnerCfg.activate_contact_sensors` on your + asset spawner configuration. This will enable the contact reporter on all the rigid bodies + in the asset. + + The sensor can be configured to report the contact forces on a set of bodies with a given + filter pattern using the :attr:`ContactSensorCfg.filter_prim_paths_expr`. This is useful + when you want to report the contact forces between the sensor bodies and a specific set of + bodies in the scene. The data can be accessed using the :attr:`ContactSensorData.force_matrix_w`. + Please check the documentation on `RigidContactView`_ for more details. + + The reporting of the filtered contact forces is only possible as one-to-many. This means that only one + sensor body in an environment can be filtered against multiple bodies in that environment. If you need to + filter multiple sensor bodies against multiple bodies, you need to create separate sensors for each sensor + body. + + As an example, suppose you want to report the contact forces for all the feet of a robot against an object + exclusively. In that case, setting the :attr:`ContactSensorCfg.prim_path` and + :attr:`ContactSensorCfg.filter_prim_paths_expr` with ``{ENV_REGEX_NS}/Robot/.*_FOOT`` and ``{ENV_REGEX_NS}/Object`` + respectively will not work. Instead, you need to create a separate sensor for each foot and filter + it against the object. + + .. _PhysX ContactReporter: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_contact_report_a_p_i.html + .. _RigidContactView: https://docs.omniverse.nvidia.com/py/isaacsim/source/extensions/omni.isaac.core/docs/index.html#omni.isaac.core.prims.RigidContactView + """ + + cfg: ContactSensorCfg + """The configuration parameters.""" + +
[文档] def __init__(self, cfg: ContactSensorCfg): + """Initializes the contact sensor object. + + Args: + cfg: The configuration parameters. + """ + # initialize base class + super().__init__(cfg) + # Create empty variables for storing output data + self._data: ContactSensorData = ContactSensorData() + # initialize self._body_physx_view for running in extension mode + self._body_physx_view = None
+ + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Contact sensor @ '{self.cfg.prim_path}': \n" + f"\tview type : {self.body_physx_view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of bodies : {self.num_bodies}\n" + f"\tbody names : {self.body_names}\n" + ) + + """ + Properties + """ + + @property + def num_instances(self) -> int: + return self.body_physx_view.count + + @property + def data(self) -> ContactSensorData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + @property + def num_bodies(self) -> int: + """Number of bodies with contact sensors attached.""" + return self._num_bodies + + @property + def body_names(self) -> list[str]: + """Ordered names of bodies with contact sensors attached.""" + prim_paths = self.body_physx_view.prim_paths[: self.num_bodies] + return [path.split("/")[-1] for path in prim_paths] + + @property + def body_physx_view(self) -> physx.RigidBodyView: + """View for the rigid bodies captured (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._body_physx_view + + @property + def contact_physx_view(self) -> physx.RigidContactView: + """Contact reporter view for the bodies (PhysX). + + Note: + Use this view with caution. It requires handling of tensors in a specific way. + """ + return self._contact_physx_view + + """ + Operations + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # reset the timers and counters + super().reset(env_ids) + # resolve None + if env_ids is None: + env_ids = slice(None) + # reset accumulative data buffers + self._data.net_forces_w[env_ids] = 0.0 + self._data.net_forces_w_history[env_ids] = 0.0 + if self.cfg.history_length > 0: + self._data.net_forces_w_history[env_ids] = 0.0 + # reset force matrix + if len(self.cfg.filter_prim_paths_expr) != 0: + self._data.force_matrix_w[env_ids] = 0.0 + # reset the current air time + if self.cfg.track_air_time: + self._data.current_air_time[env_ids] = 0.0 + self._data.last_air_time[env_ids] = 0.0 + self._data.current_contact_time[env_ids] = 0.0 + self._data.last_contact_time[env_ids] = 0.0
+ +
[文档] def find_bodies(self, name_keys: str | Sequence[str], preserve_order: bool = False) -> tuple[list[int], list[str]]: + """Find bodies in the articulation based on the name keys. + + Args: + name_keys: A regular expression or a list of regular expressions to match the body names. + preserve_order: Whether to preserve the order of the name keys in the output. Defaults to False. + + Returns: + A tuple of lists containing the body indices and names. + """ + return string_utils.resolve_matching_names(name_keys, self.body_names, preserve_order)
+ +
[文档] def compute_first_contact(self, dt: float, abs_tol: float = 1.0e-8) -> torch.Tensor: + """Checks if bodies that have established contact within the last :attr:`dt` seconds. + + This function checks if the bodies have established contact within the last :attr:`dt` seconds + by comparing the current contact time with the given time period. If the contact time is less + than the given time period, then the bodies are considered to be in contact. + + Note: + The function assumes that :attr:`dt` is a factor of the sensor update time-step. In other + words :math:`dt / dt_sensor = n`, where :math:`n` is a natural number. This is always true + if the sensor is updated by the physics or the environment stepping time-step and the sensor + is read by the environment stepping time-step. + + Args: + dt: The time period since the contact was established. + abs_tol: The absolute tolerance for the comparison. + + Returns: + A boolean tensor indicating the bodies that have established contact within the last + :attr:`dt` seconds. Shape is (N, B), where N is the number of sensors and B is the + number of bodies in each sensor. + + Raises: + RuntimeError: If the sensor is not configured to track contact time. + """ + # check if the sensor is configured to track contact time + if not self.cfg.track_air_time: + raise RuntimeError( + "The contact sensor is not configured to track contact time." + "Please enable the 'track_air_time' in the sensor configuration." + ) + # check if the bodies are in contact + currently_in_contact = self.data.current_contact_time > 0.0 + less_than_dt_in_contact = self.data.current_contact_time < (dt + abs_tol) + return currently_in_contact * less_than_dt_in_contact
+ +
[文档] def compute_first_air(self, dt: float, abs_tol: float = 1.0e-8) -> torch.Tensor: + """Checks if bodies that have broken contact within the last :attr:`dt` seconds. + + This function checks if the bodies have broken contact within the last :attr:`dt` seconds + by comparing the current air time with the given time period. If the air time is less + than the given time period, then the bodies are considered to not be in contact. + + Note: + It assumes that :attr:`dt` is a factor of the sensor update time-step. In other words, + :math:`dt / dt_sensor = n`, where :math:`n` is a natural number. This is always true if + the sensor is updated by the physics or the environment stepping time-step and the sensor + is read by the environment stepping time-step. + + Args: + dt: The time period since the contract is broken. + abs_tol: The absolute tolerance for the comparison. + + Returns: + A boolean tensor indicating the bodies that have broken contact within the last :attr:`dt` seconds. + Shape is (N, B), where N is the number of sensors and B is the number of bodies in each sensor. + + Raises: + RuntimeError: If the sensor is not configured to track contact time. + """ + # check if the sensor is configured to track contact time + if not self.cfg.track_air_time: + raise RuntimeError( + "The contact sensor is not configured to track contact time." + "Please enable the 'track_air_time' in the sensor configuration." + ) + # check if the sensor is configured to track contact time + currently_detached = self.data.current_air_time > 0.0 + less_than_dt_detached = self.data.current_air_time < (dt + abs_tol) + return currently_detached * less_than_dt_detached
+ + """ + Implementation. + """ + + def _initialize_impl(self): + super()._initialize_impl() + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # check that only rigid bodies are selected + leaf_pattern = self.cfg.prim_path.rsplit("/", 1)[-1] + template_prim_path = self._parent_prims[0].GetPath().pathString + body_names = list() + for prim in sim_utils.find_matching_prims(template_prim_path + "/" + leaf_pattern): + # check if prim has contact reporter API + if prim.HasAPI(PhysxSchema.PhysxContactReportAPI): + prim_path = prim.GetPath().pathString + body_names.append(prim_path.rsplit("/", 1)[-1]) + # check that there is at least one body with contact reporter API + if not body_names: + raise RuntimeError( + f"Sensor at path '{self.cfg.prim_path}' could not find any bodies with contact reporter API." + "\nHINT: Make sure to enable 'activate_contact_sensors' in the corresponding asset spawn configuration." + ) + + # construct regex expression for the body names + body_names_regex = r"(" + "|".join(body_names) + r")" + body_names_regex = f"{self.cfg.prim_path.rsplit('/', 1)[0]}/{body_names_regex}" + # convert regex expressions to glob expressions for PhysX + body_names_glob = body_names_regex.replace(".*", "*") + filter_prim_paths_glob = [expr.replace(".*", "*") for expr in self.cfg.filter_prim_paths_expr] + + # create a rigid prim view for the sensor + self._body_physx_view = self._physics_sim_view.create_rigid_body_view(body_names_glob) + self._contact_physx_view = self._physics_sim_view.create_rigid_contact_view( + body_names_glob, filter_patterns=filter_prim_paths_glob + ) + # resolve the true count of bodies + self._num_bodies = self.body_physx_view.count // self._num_envs + # check that contact reporter succeeded + if self._num_bodies != len(body_names): + raise RuntimeError( + "Failed to initialize contact reporter for specified bodies." + f"\n\tInput prim path : {self.cfg.prim_path}" + f"\n\tResolved prim paths: {body_names_regex}" + ) + + # prepare data buffers + self._data.net_forces_w = torch.zeros(self._num_envs, self._num_bodies, 3, device=self._device) + # optional buffers + # -- history of net forces + if self.cfg.history_length > 0: + self._data.net_forces_w_history = torch.zeros( + self._num_envs, self.cfg.history_length, self._num_bodies, 3, device=self._device + ) + else: + self._data.net_forces_w_history = self._data.net_forces_w.unsqueeze(1) + # -- pose of sensor origins + if self.cfg.track_pose: + self._data.pos_w = torch.zeros(self._num_envs, self._num_bodies, 3, device=self._device) + self._data.quat_w = torch.zeros(self._num_envs, self._num_bodies, 4, device=self._device) + # -- air/contact time between contacts + if self.cfg.track_air_time: + self._data.last_air_time = torch.zeros(self._num_envs, self._num_bodies, device=self._device) + self._data.current_air_time = torch.zeros(self._num_envs, self._num_bodies, device=self._device) + self._data.last_contact_time = torch.zeros(self._num_envs, self._num_bodies, device=self._device) + self._data.current_contact_time = torch.zeros(self._num_envs, self._num_bodies, device=self._device) + # force matrix: (num_envs, num_bodies, num_filter_shapes, 3) + if len(self.cfg.filter_prim_paths_expr) != 0: + num_filters = self.contact_physx_view.filter_count + self._data.force_matrix_w = torch.zeros( + self._num_envs, self._num_bodies, num_filters, 3, device=self._device + ) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + # default to all sensors + if len(env_ids) == self._num_envs: + env_ids = slice(None) + + # obtain the contact forces + # TODO: We are handling the indexing ourself because of the shape; (N, B) vs expected (N * B). + # This isn't the most efficient way to do this, but it's the easiest to implement. + net_forces_w = self.contact_physx_view.get_net_contact_forces(dt=self._sim_physics_dt) + self._data.net_forces_w[env_ids, :, :] = net_forces_w.view(-1, self._num_bodies, 3)[env_ids] + # update contact force history + if self.cfg.history_length > 0: + self._data.net_forces_w_history[env_ids, 1:] = self._data.net_forces_w_history[env_ids, :-1].clone() + self._data.net_forces_w_history[env_ids, 0] = self._data.net_forces_w[env_ids] + + # obtain the contact force matrix + if len(self.cfg.filter_prim_paths_expr) != 0: + # shape of the filtering matrix: (num_envs, num_bodies, num_filter_shapes, 3) + num_filters = self.contact_physx_view.filter_count + # acquire and shape the force matrix + force_matrix_w = self.contact_physx_view.get_contact_force_matrix(dt=self._sim_physics_dt) + force_matrix_w = force_matrix_w.view(-1, self._num_bodies, num_filters, 3) + self._data.force_matrix_w[env_ids] = force_matrix_w[env_ids] + + # obtain the pose of the sensor origin + if self.cfg.track_pose: + pose = self.body_physx_view.get_transforms().view(-1, self._num_bodies, 7)[env_ids] + pose[..., 3:] = convert_quat(pose[..., 3:], to="wxyz") + self._data.pos_w[env_ids], self._data.quat_w[env_ids] = pose.split([3, 4], dim=-1) + + # obtain the air time + if self.cfg.track_air_time: + # -- time elapsed since last update + # since this function is called every frame, we can use the difference to get the elapsed time + elapsed_time = self._timestamp[env_ids] - self._timestamp_last_update[env_ids] + # -- check contact state of bodies + is_contact = torch.norm(self._data.net_forces_w[env_ids, :, :], dim=-1) > self.cfg.force_threshold + is_first_contact = (self._data.current_air_time[env_ids] > 0) * is_contact + is_first_detached = (self._data.current_contact_time[env_ids] > 0) * ~is_contact + # -- update the last contact time if body has just become in contact + self._data.last_air_time[env_ids] = torch.where( + is_first_contact, + self._data.current_air_time[env_ids] + elapsed_time.unsqueeze(-1), + self._data.last_air_time[env_ids], + ) + # -- increment time for bodies that are not in contact + self._data.current_air_time[env_ids] = torch.where( + ~is_contact, self._data.current_air_time[env_ids] + elapsed_time.unsqueeze(-1), 0.0 + ) + # -- update the last contact time if body has just detached + self._data.last_contact_time[env_ids] = torch.where( + is_first_detached, + self._data.current_contact_time[env_ids] + elapsed_time.unsqueeze(-1), + self._data.last_contact_time[env_ids], + ) + # -- increment time for bodies that are in contact + self._data.current_contact_time[env_ids] = torch.where( + is_contact, self._data.current_contact_time[env_ids] + elapsed_time.unsqueeze(-1), 0.0 + ) + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + # create markers if necessary for the first tome + if not hasattr(self, "contact_visualizer"): + self.contact_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.contact_visualizer.set_visibility(True) + else: + if hasattr(self, "contact_visualizer"): + self.contact_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # safely return if view becomes invalid + # note: this invalidity happens because of isaac sim view callbacks + if self.body_physx_view is None: + return + # marker indices + # 0: contact, 1: no contact + net_contact_force_w = torch.norm(self._data.net_forces_w, dim=-1) + marker_indices = torch.where(net_contact_force_w > self.cfg.force_threshold, 0, 1) + # check if prim is visualized + if self.cfg.track_pose: + frame_origins: torch.Tensor = self._data.pos_w + else: + pose = self.body_physx_view.get_transforms() + frame_origins = pose.view(-1, self._num_bodies, 7)[:, :, :3] + # visualize + self.contact_visualizer.visualize(frame_origins.view(-1, 3), marker_indices=marker_indices.view(-1)) + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._body_physx_view = None + self._contact_physx_view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor_cfg.html b/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor_cfg.html new file mode 100644 index 0000000000..cba40d4c50 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor_cfg.html @@ -0,0 +1,618 @@ + + + + + + + + + + + omni.isaac.lab.sensors.contact_sensor.contact_sensor_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.contact_sensor.contact_sensor_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from omni.isaac.lab.markers import VisualizationMarkersCfg
+from omni.isaac.lab.markers.config import CONTACT_SENSOR_MARKER_CFG
+from omni.isaac.lab.utils import configclass
+
+from ..sensor_base_cfg import SensorBaseCfg
+from .contact_sensor import ContactSensor
+
+
+
[文档]@configclass +class ContactSensorCfg(SensorBaseCfg): + """Configuration for the contact sensor.""" + + class_type: type = ContactSensor + + track_pose: bool = False + """Whether to track the pose of the sensor's origin. Defaults to False.""" + + track_air_time: bool = False + """Whether to track the air/contact time of the bodies (time between contacts). Defaults to False.""" + + force_threshold: float = 1.0 + """The threshold on the norm of the contact force that determines whether two bodies are in collision or not. + + This value is only used for tracking the mode duration (the time in contact or in air), + if :attr:`track_air_time` is True. + """ + + filter_prim_paths_expr: list[str] = list() + """The list of primitive paths (or expressions) to filter contacts with. Defaults to an empty list, in which case + no filtering is applied. + + The contact sensor allows reporting contacts between the primitive specified with :attr:`prim_path` and + other primitives in the scene. For instance, in a scene containing a robot, a ground plane and an object, + you can obtain individual contact reports of the base of the robot with the ground plane and the object. + + .. note:: + The expression in the list can contain the environment namespace regex ``{ENV_REGEX_NS}`` which + will be replaced with the environment namespace. + + Example: ``{ENV_REGEX_NS}/Object`` will be replaced with ``/World/envs/env_.*/Object``. + + .. attention:: + The reporting of filtered contacts only works when the sensor primitive :attr:`prim_path` corresponds to a + single primitive in that environment. If the sensor primitive corresponds to multiple primitives, the + filtering will not work as expected. Please check :class:`~omni.isaac.lab.sensors.contact_sensor.ContactSensor` + for more details. + """ + + visualizer_cfg: VisualizationMarkersCfg = CONTACT_SENSOR_MARKER_CFG.replace(prim_path="/Visuals/ContactSensor") + """The configuration object for the visualization markers. Defaults to CONTACT_SENSOR_MARKER_CFG. + + .. note:: + This attribute is only used when debug visualization is enabled. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor_data.html b/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor_data.html new file mode 100644 index 0000000000..d9abb8417c --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/contact_sensor/contact_sensor_data.html @@ -0,0 +1,661 @@ + + + + + + + + + + + omni.isaac.lab.sensors.contact_sensor.contact_sensor_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.contact_sensor.contact_sensor_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# needed to import for allowing type-hinting: torch.Tensor | None
+from __future__ import annotations
+
+import torch
+from dataclasses import dataclass
+
+
+
[文档]@dataclass +class ContactSensorData: + """Data container for the contact reporting sensor.""" + + 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. + + Note: + If the :attr:`ContactSensorCfg.track_pose` is False, then this quantity is None. + """ + + quat_w: torch.Tensor | None = None + """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. + + Note: + If the :attr:`ContactSensorCfg.track_pose` is False, then this quantity is None. + """ + + net_forces_w: torch.Tensor | None = None + """The net normal 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. + + Note: + This quantity is the sum of the normal contact forces acting on the sensor bodies. It must not be confused + with the total contact forces acting on the sensor bodies (which also includes the tangential forces). + """ + + net_forces_w_history: torch.Tensor | None = None + """The net normal 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. + + In the history dimension, the first index is the most recent and the last index is the oldest. + + Note: + This quantity is the sum of the normal contact forces acting on the sensor bodies. It must not be confused + with the total contact forces acting on the sensor bodies (which also includes the tangential forces). + """ + + force_matrix_w: torch.Tensor | None = None + """The normal contact forces filtered between the sensor bodies and filtered bodies in world frame. + + Shape is (N, B, M, 3), where N is the number of sensors, B is number of bodies in each sensor + and ``M`` is the number of filtered bodies. + + Note: + If the :attr:`ContactSensorCfg.filter_prim_paths_expr` is empty, then this quantity is None. + """ + + last_air_time: torch.Tensor | None = None + """Time spent (in s) in the air before the last contact. + + Shape is (N, B), where N is the number of sensors and B is the number of bodies in each sensor. + + Note: + If the :attr:`ContactSensorCfg.track_air_time` is False, then this quantity is None. + """ + + current_air_time: torch.Tensor | None = None + """Time spent (in s) in the air since the last detach. + + Shape is (N, B), where N is the number of sensors and B is the number of bodies in each sensor. + + Note: + If the :attr:`ContactSensorCfg.track_air_time` is False, then this quantity is None. + """ + + last_contact_time: torch.Tensor | None = None + """Time spent (in s) in contact before the last detach. + + Shape is (N, B), where N is the number of sensors and B is the number of bodies in each sensor. + + Note: + If the :attr:`ContactSensorCfg.track_air_time` is False, then this quantity is None. + """ + + current_contact_time: torch.Tensor | None = None + """Time spent (in s) in contact since the last contact. + + Shape is (N, B), where N is the number of sensors and B is the number of bodies in each sensor. + + Note: + If the :attr:`ContactSensorCfg.track_air_time` is False, then this quantity is None. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer.html b/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer.html new file mode 100644 index 0000000000..1ae42763fe --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer.html @@ -0,0 +1,978 @@ + + + + + + + + + + + omni.isaac.lab.sensors.frame_transformer.frame_transformer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.frame_transformer.frame_transformer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import re
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.log
+import omni.physics.tensors.impl.api as physx
+from pxr import UsdPhysics
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.markers import VisualizationMarkers
+from omni.isaac.lab.utils.math import (
+    combine_frame_transforms,
+    convert_quat,
+    is_identity_pose,
+    subtract_frame_transforms,
+)
+
+from ..sensor_base import SensorBase
+from .frame_transformer_data import FrameTransformerData
+
+if TYPE_CHECKING:
+    from .frame_transformer_cfg import FrameTransformerCfg
+
+
+
[文档]class FrameTransformer(SensorBase): + """A sensor for reporting frame transforms. + + This class provides an interface for reporting the transform of one or more frames (target frames) + with respect to another frame (source frame). The source frame is specified by the user as a prim path + (:attr:`FrameTransformerCfg.prim_path`) and the target frames are specified by the user as a list of + prim paths (:attr:`FrameTransformerCfg.target_frames`). + + The source frame and target frames are assumed to be rigid bodies. The transform of the target frames + with respect to the source frame is computed by first extracting the transform of the source frame + and target frames from the physics engine and then computing the relative transform between the two. + + Additionally, the user can specify an offset for the source frame and each target frame. This is useful + for specifying the transform of the desired frame with respect to the body's center of mass, for instance. + + A common example of using this sensor is to track the position and orientation of the end effector of a + robotic manipulator. In this case, the source frame would be the body corresponding to the base frame of the + manipulator, and the target frame would be the body corresponding to the end effector. Since the end-effector is + typically a fictitious body, the user may need to specify an offset from the end-effector to the body of the + manipulator. + + """ + + cfg: FrameTransformerCfg + """The configuration parameters.""" + +
[文档] def __init__(self, cfg: FrameTransformerCfg): + """Initializes the frame transformer object. + + Args: + cfg: The configuration parameters. + """ + # initialize base class + super().__init__(cfg) + # Create empty variables for storing output data + self._data: FrameTransformerData = FrameTransformerData()
+ + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"FrameTransformer @ '{self.cfg.prim_path}': \n" + f"\ttracked body frames: {[self._source_frame_body_name] + self._target_frame_body_names} \n" + f"\tnumber of envs: {self._num_envs}\n" + f"\tsource body frame: {self._source_frame_body_name}\n" + f"\ttarget frames (count: {self._target_frame_names}): {len(self._target_frame_names)}\n" + ) + + """ + Properties + """ + + @property + def data(self) -> FrameTransformerData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + """ + Operations + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # reset the timers and counters + super().reset(env_ids) + # resolve None + if env_ids is None: + env_ids = ...
+ + """ + Implementation. + """ + + def _initialize_impl(self): + super()._initialize_impl() + + # resolve source frame offset + source_frame_offset_pos = torch.tensor(self.cfg.source_frame_offset.pos, device=self.device) + source_frame_offset_quat = torch.tensor(self.cfg.source_frame_offset.rot, device=self.device) + # Only need to perform offsetting of source frame if the position offsets is non-zero and rotation offset is + # not the identity quaternion for efficiency in _update_buffer_impl + self._apply_source_frame_offset = True + # Handle source frame offsets + if is_identity_pose(source_frame_offset_pos, source_frame_offset_quat): + omni.log.verbose(f"No offset application needed for source frame as it is identity: {self.cfg.prim_path}") + self._apply_source_frame_offset = False + else: + omni.log.verbose(f"Applying offset to source frame as it is not identity: {self.cfg.prim_path}") + # Store offsets as tensors (duplicating each env's offsets for ease of multiplication later) + self._source_frame_offset_pos = source_frame_offset_pos.unsqueeze(0).repeat(self._num_envs, 1) + self._source_frame_offset_quat = source_frame_offset_quat.unsqueeze(0).repeat(self._num_envs, 1) + + # Keep track of mapping from the rigid body name to the desired frames and prim path, as there may be multiple frames + # based upon the same body name and we don't want to create unnecessary views + body_names_to_frames: dict[str, dict[str, set[str] | str]] = {} + # The offsets associated with each target frame + target_offsets: dict[str, dict[str, torch.Tensor]] = {} + # The frames whose offsets are not identity + non_identity_offset_frames: list[str] = [] + + # Only need to perform offsetting of target frame if any of the position offsets are non-zero or any of the + # rotation offsets are not the identity quaternion for efficiency in _update_buffer_impl + self._apply_target_frame_offset = False + + # Need to keep track of whether the source frame is also a target frame + self._source_is_also_target_frame = False + + # Collect all target frames, their associated body prim paths and their offsets so that we can extract + # the prim, check that it has the appropriate rigid body API in a single loop. + # First element is None because user can't specify source frame name + frames = [None] + [target_frame.name for target_frame in self.cfg.target_frames] + frame_prim_paths = [self.cfg.prim_path] + [target_frame.prim_path for target_frame in self.cfg.target_frames] + # First element is None because source frame offset is handled separately + frame_offsets = [None] + [target_frame.offset for target_frame in self.cfg.target_frames] + frame_types = ["source"] + ["target"] * len(self.cfg.target_frames) + for frame, prim_path, offset, frame_type in zip(frames, frame_prim_paths, frame_offsets, frame_types): + # Find correct prim + matching_prims = sim_utils.find_matching_prims(prim_path) + if len(matching_prims) == 0: + raise ValueError( + f"Failed to create frame transformer for frame '{frame}' with path '{prim_path}'." + " No matching prims were found." + ) + for prim in matching_prims: + # Get the prim path of the matching prim + matching_prim_path = prim.GetPath().pathString + # Check if it is a rigid prim + if not prim.HasAPI(UsdPhysics.RigidBodyAPI): + raise ValueError( + f"While resolving expression '{prim_path}' found a prim '{matching_prim_path}' which is not a" + " rigid body. The class only supports transformations between rigid bodies." + ) + + # Get the name of the body + body_name = matching_prim_path.rsplit("/", 1)[-1] + # Use body name if frame isn't specified by user + frame_name = frame if frame is not None else body_name + + # Keep track of which frames are associated with which bodies + if body_name in body_names_to_frames: + body_names_to_frames[body_name]["frames"].add(frame_name) + + # This is a corner case where the source frame is also a target frame + if body_names_to_frames[body_name]["type"] == "source" and frame_type == "target": + self._source_is_also_target_frame = True + + else: + # Store the first matching prim path and the type of frame + body_names_to_frames[body_name] = { + "frames": {frame_name}, + "prim_path": matching_prim_path, + "type": frame_type, + } + + if offset is not None: + offset_pos = torch.tensor(offset.pos, device=self.device) + offset_quat = torch.tensor(offset.rot, device=self.device) + # Check if we need to apply offsets (optimized code path in _update_buffer_impl) + if not is_identity_pose(offset_pos, offset_quat): + non_identity_offset_frames.append(frame_name) + self._apply_target_frame_offset = True + target_offsets[frame_name] = {"pos": offset_pos, "quat": offset_quat} + + if not self._apply_target_frame_offset: + omni.log.info( + f"No offsets application needed from '{self.cfg.prim_path}' to target frames as all" + f" are identity: {frames[1:]}" + ) + else: + omni.log.info( + f"Offsets application needed from '{self.cfg.prim_path}' to the following target frames:" + f" {non_identity_offset_frames}" + ) + + # The names of bodies that RigidPrimView will be tracking to later extract transforms from + tracked_prim_paths = [body_names_to_frames[body_name]["prim_path"] for body_name in body_names_to_frames.keys()] + tracked_body_names = [body_name for body_name in body_names_to_frames.keys()] + + body_names_regex = [tracked_prim_path.replace("env_0", "env_*") for tracked_prim_path in tracked_prim_paths] + + # Create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # Create a prim view for all frames and initialize it + # order of transforms coming out of view will be source frame followed by target frame(s) + self._frame_physx_view = self._physics_sim_view.create_rigid_body_view(body_names_regex) + + # Determine the order in which regex evaluated body names so we can later index into frame transforms + # by frame name correctly + all_prim_paths = self._frame_physx_view.prim_paths + + if "env_" in all_prim_paths[0]: + + def extract_env_num_and_prim_path(item: str) -> tuple[int, str]: + """Separates the environment number and prim_path from the item. + + Args: + item: The item to extract the environment number from. Assumes item is of the form + `/World/envs/env_1/blah` or `/World/envs/env_11/blah`. + Returns: + The environment number and the prim_path. + """ + match = re.search(r"env_(\d+)(.*)", item) + return (int(match.group(1)), match.group(2)) + + # Find the indices that would reorganize output to be per environment. We want `env_1/blah` to come before `env_11/blah` + # and env_1/Robot/base to come before env_1/Robot/foot so we need to use custom key function + self._per_env_indices = [ + index + for index, _ in sorted( + list(enumerate(all_prim_paths)), key=lambda x: extract_env_num_and_prim_path(x[1]) + ) + ] + + # Only need 0th env as the names and their ordering are the same across environments + sorted_prim_paths = [ + all_prim_paths[index] for index in self._per_env_indices if "env_0" in all_prim_paths[index] + ] + + else: + # If no environment is present, then the order of the body names is the same as the order of the prim paths sorted alphabetically + self._per_env_indices = [index for index, _ in sorted(enumerate(all_prim_paths), key=lambda x: x[1])] + sorted_prim_paths = [all_prim_paths[index] for index in self._per_env_indices] + + # -- target frames + self._target_frame_body_names = [prim_path.split("/")[-1] for prim_path in sorted_prim_paths] + + # -- source frame + self._source_frame_body_name = self.cfg.prim_path.split("/")[-1] + source_frame_index = self._target_frame_body_names.index(self._source_frame_body_name) + + # Only remove source frame from tracked bodies if it is not also a target frame + if not self._source_is_also_target_frame: + self._target_frame_body_names.remove(self._source_frame_body_name) + + # Determine indices into all tracked body frames for both source and target frames + all_ids = torch.arange(self._num_envs * len(tracked_body_names)) + self._source_frame_body_ids = torch.arange(self._num_envs) * len(tracked_body_names) + source_frame_index + + # If source frame is also a target frame, then the target frame body ids are the same as the source frame body ids + if self._source_is_also_target_frame: + self._target_frame_body_ids = all_ids + else: + self._target_frame_body_ids = all_ids[~torch.isin(all_ids, self._source_frame_body_ids)] + + # The name of each of the target frame(s) - either user specified or defaulted to the body name + self._target_frame_names: list[str] = [] + # The position and rotation components of target frame offsets + target_frame_offset_pos = [] + target_frame_offset_quat = [] + # Stores the indices of bodies that need to be duplicated. For instance, if body "LF_SHANK" is needed + # for 2 frames, this list enables us to duplicate the body to both frames when doing the calculations + # when updating sensor in _update_buffers_impl + duplicate_frame_indices = [] + + # Go through each body name and determine the number of duplicates we need for that frame + # and extract the offsets. This is all done to handle the case where multiple frames + # reference the same body, but have different names and/or offsets + for i, body_name in enumerate(self._target_frame_body_names): + for frame in body_names_to_frames[body_name]["frames"]: + # Only need to handle target frames here as source frame is handled separately + if frame in target_offsets: + target_frame_offset_pos.append(target_offsets[frame]["pos"]) + target_frame_offset_quat.append(target_offsets[frame]["quat"]) + self._target_frame_names.append(frame) + duplicate_frame_indices.append(i) + + # To handle multiple environments, need to expand so [0, 1, 1, 2] with 2 environments becomes + # [0, 1, 1, 2, 3, 4, 4, 5]. Again, this is a optimization to make _update_buffer_impl more efficient + duplicate_frame_indices = torch.tensor(duplicate_frame_indices, device=self.device) + if self._source_is_also_target_frame: + num_target_body_frames = len(tracked_body_names) + else: + num_target_body_frames = len(tracked_body_names) - 1 + + self._duplicate_frame_indices = torch.cat( + [duplicate_frame_indices + num_target_body_frames * env_num for env_num in range(self._num_envs)] + ) + + # Target frame offsets are only applied if at least one of the offsets are non-identity + if self._apply_target_frame_offset: + # Stack up all the frame offsets for shape (num_envs, num_frames, 3) and (num_envs, num_frames, 4) + self._target_frame_offset_pos = torch.stack(target_frame_offset_pos).repeat(self._num_envs, 1) + self._target_frame_offset_quat = torch.stack(target_frame_offset_quat).repeat(self._num_envs, 1) + + # fill the data buffer + self._data.target_frame_names = self._target_frame_names + self._data.source_pos_w = torch.zeros(self._num_envs, 3, device=self._device) + self._data.source_quat_w = torch.zeros(self._num_envs, 4, device=self._device) + self._data.target_pos_w = torch.zeros(self._num_envs, len(duplicate_frame_indices), 3, device=self._device) + self._data.target_quat_w = torch.zeros(self._num_envs, len(duplicate_frame_indices), 4, device=self._device) + self._data.target_pos_source = torch.zeros_like(self._data.target_pos_w) + self._data.target_quat_source = torch.zeros_like(self._data.target_quat_w) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + # default to all sensors + if len(env_ids) == self._num_envs: + env_ids = ... + + # Extract transforms from view - shape is: + # (the total number of source and target body frames being tracked * self._num_envs, 7) + transforms = self._frame_physx_view.get_transforms() + + # Reorder the transforms to be per environment as is expected of SensorData + transforms = transforms[self._per_env_indices] + + # Convert quaternions as PhysX uses xyzw form + transforms[:, 3:] = convert_quat(transforms[:, 3:], to="wxyz") + + # Process source frame transform + source_frames = transforms[self._source_frame_body_ids] + # Only apply offset if the offsets will result in a coordinate frame transform + if self._apply_source_frame_offset: + source_pos_w, source_quat_w = combine_frame_transforms( + source_frames[:, :3], + source_frames[:, 3:], + self._source_frame_offset_pos, + self._source_frame_offset_quat, + ) + else: + source_pos_w = source_frames[:, :3] + source_quat_w = source_frames[:, 3:] + + # Process target frame transforms + target_frames = transforms[self._target_frame_body_ids] + duplicated_target_frame_pos_w = target_frames[self._duplicate_frame_indices, :3] + duplicated_target_frame_quat_w = target_frames[self._duplicate_frame_indices, 3:] + + # Only apply offset if the offsets will result in a coordinate frame transform + if self._apply_target_frame_offset: + target_pos_w, target_quat_w = combine_frame_transforms( + duplicated_target_frame_pos_w, + duplicated_target_frame_quat_w, + self._target_frame_offset_pos, + self._target_frame_offset_quat, + ) + else: + target_pos_w = duplicated_target_frame_pos_w + target_quat_w = duplicated_target_frame_quat_w + + # Compute the transform of the target frame with respect to the source frame + total_num_frames = len(self._target_frame_names) + target_pos_source, target_quat_source = subtract_frame_transforms( + source_pos_w.unsqueeze(1).expand(-1, total_num_frames, -1).reshape(-1, 3), + source_quat_w.unsqueeze(1).expand(-1, total_num_frames, -1).reshape(-1, 4), + target_pos_w, + target_quat_w, + ) + + # Update buffers + # note: The frame names / ordering don't change so no need to update them after initialization + self._data.source_pos_w[:] = source_pos_w.view(-1, 3) + self._data.source_quat_w[:] = source_quat_w.view(-1, 4) + self._data.target_pos_w[:] = target_pos_w.view(-1, total_num_frames, 3) + self._data.target_quat_w[:] = target_quat_w.view(-1, total_num_frames, 4) + self._data.target_pos_source[:] = target_pos_source.view(-1, total_num_frames, 3) + self._data.target_quat_source[:] = target_quat_source.view(-1, total_num_frames, 4) + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + if not hasattr(self, "frame_visualizer"): + self.frame_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.frame_visualizer.set_visibility(True) + else: + if hasattr(self, "frame_visualizer"): + self.frame_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # Update the visualized markers + if self.frame_visualizer is not None: + self.frame_visualizer.visualize(self._data.target_pos_w.view(-1, 3), self._data.target_quat_w.view(-1, 4)) + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._frame_physx_view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer_cfg.html b/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer_cfg.html new file mode 100644 index 0000000000..3703ae8a35 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer_cfg.html @@ -0,0 +1,632 @@ + + + + + + + + + + + omni.isaac.lab.sensors.frame_transformer.frame_transformer_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.frame_transformer.frame_transformer_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.markers.config import FRAME_MARKER_CFG, VisualizationMarkersCfg
+from omni.isaac.lab.utils import configclass
+
+from ..sensor_base_cfg import SensorBaseCfg
+from .frame_transformer import FrameTransformer
+
+
+
[文档]@configclass +class OffsetCfg: + """The offset pose of one frame relative to another frame.""" + + 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)."""
+ + +
[文档]@configclass +class FrameTransformerCfg(SensorBaseCfg): + """Configuration for the frame transformer sensor.""" + +
[文档] @configclass + class FrameCfg: + """Information specific to a coordinate frame.""" + + prim_path: str = MISSING + """The prim path corresponding to a rigid body. + + This can be a regex pattern to match multiple prims. For example, "/Robot/.*" will match all prims under "/Robot". + + This means that if the source :attr:`FrameTransformerCfg.prim_path` is "/Robot/base", and the target :attr:`FrameTransformerCfg.FrameCfg.prim_path` is "/Robot/.*", + then the frame transformer will track the poses of all the prims under "/Robot", + including "/Robot/base" (even though this will result in an identity pose w.r.t. the source frame). + """ + + name: str | None = None + """User-defined name for the new coordinate frame. Defaults to None. + + If None, then the name is extracted from the leaf of the prim path. + """ + + offset: OffsetCfg = OffsetCfg() + """The pose offset from the parent prim frame."""
+ + class_type: type = FrameTransformer + + prim_path: str = MISSING + """The prim path of the body to transform from (source frame).""" + + source_frame_offset: OffsetCfg = OffsetCfg() + """The pose offset from the source prim frame.""" + + target_frames: list[FrameCfg] = MISSING + """A list of the target frames. + + This allows a single FrameTransformer to handle multiple target prims. For example, in a quadruped, + we can use a single FrameTransformer to track each foot's position and orientation in the body + frame using four frame offsets. + """ + + visualizer_cfg: VisualizationMarkersCfg = FRAME_MARKER_CFG.replace(prim_path="/Visuals/FrameTransformer") + """The configuration object for the visualization markers. Defaults to FRAME_MARKER_CFG. + + Note: + This attribute is only used when debug visualization is enabled. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer_data.html b/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer_data.html new file mode 100644 index 0000000000..b0eab06c59 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/frame_transformer/frame_transformer_data.html @@ -0,0 +1,615 @@ + + + + + + + + + + + omni.isaac.lab.sensors.frame_transformer.frame_transformer_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.frame_transformer.frame_transformer_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from dataclasses import dataclass
+
+
+
[文档]@dataclass +class FrameTransformerData: + """Data container for the frame transformer sensor.""" + + target_frame_names: list[str] = None + """Target frame names (this denotes the order in which that frame data is ordered). + + The frame names are resolved from the :attr:`FrameTransformerCfg.FrameCfg.name` field. + This does not necessarily follow the order in which the frames are defined in the config due to + the regex matching. + """ + + target_pos_source: torch.Tensor = None + """Position of the target frame(s) relative to source frame. + + Shape is (N, M, 3), where N is the number of environments, and M is the number of target frames. + """ + + target_quat_source: torch.Tensor = None + """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. + """ + + target_pos_w: torch.Tensor = None + """Position of the target frame(s) after offset (in world frame). + + Shape is (N, M, 3), where N is the number of environments, and M is the number of target frames. + """ + + target_quat_w: torch.Tensor = None + """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. + """ + + source_pos_w: torch.Tensor = None + """Position of the source frame after offset (in world frame). + + Shape is (N, 3), where N is the number of environments. + """ + + source_quat_w: torch.Tensor = None + """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. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/imu/imu.html b/_modules/omni/isaac/lab/sensors/imu/imu.html new file mode 100644 index 0000000000..1dd143be22 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/imu/imu.html @@ -0,0 +1,802 @@ + + + + + + + + + + + omni.isaac.lab.sensors.imu.imu — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.imu.imu 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.stage as stage_utils
+import omni.physics.tensors.impl.api as physx
+from pxr import UsdPhysics
+
+import omni.isaac.lab.sim as sim_utils
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.markers import VisualizationMarkers
+
+from ..sensor_base import SensorBase
+from .imu_data import ImuData
+
+if TYPE_CHECKING:
+    from .imu_cfg import ImuCfg
+
+
+
[文档]class Imu(SensorBase): + """The Inertia Measurement Unit (IMU) sensor. + + The sensor can be attached to any :class:`RigidObject` or :class:`Articulation` in the scene. The sensor provides complete state information. + The sensor is primarily used to provide the linear acceleration and angular velocity of the object in the body frame. The sensor also provides + the position and orientation of the object in the world frame and the angular acceleration and linear velocity in the body frame. The extra + data outputs are useful for simulating with or comparing against "perfect" state estimation. + + .. note:: + + We are computing the accelerations using numerical differentiation from the velocities. Consequently, the + IMU sensor accuracy depends on the chosen phsyx timestep. For a sufficient accuracy, we recommend to keep the + timestep at least as 200Hz. + + .. note:: + + It is suggested to use the OffsetCfg to define an IMU frame relative to a rigid body prim defined at the root of + a :class:`RigidObject` or a prim that is defined by a non-fixed joint in an :class:`Articulation` (except for the + root of a fixed based articulation). The use frames with fixed joints and small mass/inertia to emulate a transform + relative to a body frame can result in lower performance and accuracy. + + """ + + cfg: ImuCfg + """The configuration parameters.""" + +
[文档] def __init__(self, cfg: ImuCfg): + """Initializes the Imu sensor. + + Args: + cfg: The configuration parameters. + """ + # initialize base class + super().__init__(cfg) + # Create empty variables for storing output data + self._data = ImuData()
+ + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Imu sensor @ '{self.cfg.prim_path}': \n" + f"\tview type : {self._view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of sensors : {self._view.count}\n" + ) + + """ + Properties + """ + + @property + def data(self) -> ImuData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + @property + def num_instances(self) -> int: + return self._view.count + + """ + Operations + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # reset the timestamps + super().reset(env_ids) + # resolve None + if env_ids is None: + env_ids = slice(None) + # reset accumulative data buffers + self._data.quat_w[env_ids] = 0.0 + self._data.lin_vel_b[env_ids] = 0.0 + self._data.ang_vel_b[env_ids] = 0.0 + self._data.lin_acc_b[env_ids] = 0.0 + self._data.ang_acc_b[env_ids] = 0.0
+ + def update(self, dt: float, force_recompute: bool = False): + # save timestamp + self._dt = dt + # execute updating + super().update(dt, force_recompute) + + """ + Implementation. + """ + + def _initialize_impl(self): + """Initializes the sensor handles and internal buffers. + + This function creates handles and registers the provided data types with the replicator registry to + be able to access the data from the sensor. It also initializes the internal buffers to store the data. + + Raises: + RuntimeError: If the imu prim is not a RigidBodyPrim + """ + # Initialize parent class + super()._initialize_impl() + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # check if the prim at path is a rigid prim + prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if prim is None: + raise RuntimeError(f"Failed to find a prim at path expression: {self.cfg.prim_path}") + # check if it is a RigidBody Prim + if prim.HasAPI(UsdPhysics.RigidBodyAPI): + self._view = self._physics_sim_view.create_rigid_body_view(self.cfg.prim_path.replace(".*", "*")) + else: + raise RuntimeError(f"Failed to find a RigidBodyAPI for the prim paths: {self.cfg.prim_path}") + + # Create internal buffers + self._initialize_buffers_impl() + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + # check if self._dt is set (this is set in the update function) + if not hasattr(self, "_dt"): + raise RuntimeError( + "The update function must be called before the data buffers are accessed the first time." + ) + # default to all sensors + if len(env_ids) == self._num_envs: + env_ids = slice(None) + # obtain the poses of the sensors + pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = math_utils.convert_quat(quat_w, to="wxyz") + + # store the poses + self._data.pos_w[env_ids] = pos_w + math_utils.quat_rotate(quat_w, self._offset_pos_b[env_ids]) + self._data.quat_w[env_ids] = math_utils.quat_mul(quat_w, self._offset_quat_b[env_ids]) + + # get the offset from COM to link origin + com_pos_b = self._view.get_coms().to(self.device).split([3, 4], dim=-1)[0] + + # obtain the velocities of the link COM + lin_vel_w, ang_vel_w = self._view.get_velocities()[env_ids].split([3, 3], dim=-1) + # if an offset is present or the COM does not agree with the link origin, the linear velocity has to be + # transformed taking the angular velocity into account + lin_vel_w += torch.linalg.cross( + ang_vel_w, math_utils.quat_rotate(quat_w, self._offset_pos_b[env_ids] - com_pos_b[env_ids]), dim=-1 + ) + + # numerical derivative + lin_acc_w = (lin_vel_w - self._prev_lin_vel_w[env_ids]) / self._dt + self._gravity_bias_w[env_ids] + ang_acc_w = (ang_vel_w - self._prev_ang_vel_w[env_ids]) / self._dt + # store the velocities + self._data.lin_vel_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], lin_vel_w) + self._data.ang_vel_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], ang_vel_w) + # store the accelerations + self._data.lin_acc_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], lin_acc_w) + self._data.ang_acc_b[env_ids] = math_utils.quat_rotate_inverse(self._data.quat_w[env_ids], ang_acc_w) + + self._prev_lin_vel_w[env_ids] = lin_vel_w + self._prev_ang_vel_w[env_ids] = ang_vel_w + + def _initialize_buffers_impl(self): + """Create buffers for storing data.""" + # data buffers + self._data.pos_w = torch.zeros(self._view.count, 3, device=self._device) + self._data.quat_w = torch.zeros(self._view.count, 4, device=self._device) + self._data.quat_w[:, 0] = 1.0 + self._data.lin_vel_b = torch.zeros_like(self._data.pos_w) + self._data.ang_vel_b = torch.zeros_like(self._data.pos_w) + self._data.lin_acc_b = torch.zeros_like(self._data.pos_w) + self._data.ang_acc_b = torch.zeros_like(self._data.pos_w) + self._prev_lin_vel_w = torch.zeros_like(self._data.pos_w) + self._prev_ang_vel_w = torch.zeros_like(self._data.pos_w) + + # store sensor offset transformation + self._offset_pos_b = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._view.count, 1) + self._offset_quat_b = torch.tensor(list(self.cfg.offset.rot), device=self._device).repeat(self._view.count, 1) + # set gravity bias + self._gravity_bias_w = torch.tensor(list(self.cfg.gravity_bias), device=self._device).repeat( + self._view.count, 1 + ) + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + # create markers if necessary for the first tome + if not hasattr(self, "acceleration_visualizer"): + self.acceleration_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.acceleration_visualizer.set_visibility(True) + else: + if hasattr(self, "acceleration_visualizer"): + self.acceleration_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # safely return if view becomes invalid + # note: this invalidity happens because of isaac sim view callbacks + if self._view is None: + return + # get marker location + # -- base state + base_pos_w = self._data.pos_w.clone() + base_pos_w[:, 2] += 0.5 + # -- resolve the scales + default_scale = self.acceleration_visualizer.cfg.markers["arrow"].scale + arrow_scale = torch.tensor(default_scale, device=self.device).repeat(self._data.lin_acc_b.shape[0], 1) + # get up axis of current stage + up_axis = stage_utils.get_stage_up_axis() + # arrow-direction + quat_opengl = math_utils.quat_from_matrix( + math_utils.create_rotation_matrix_from_view( + self._data.pos_w, + self._data.pos_w + math_utils.quat_rotate(self._data.quat_w, self._data.lin_acc_b), + up_axis=up_axis, + device=self._device, + ) + ) + quat_w = math_utils.convert_camera_frame_orientation_convention(quat_opengl, "opengl", "world") + # display markers + self.acceleration_visualizer.visualize(base_pos_w, quat_w, arrow_scale)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/imu/imu_cfg.html b/_modules/omni/isaac/lab/sensors/imu/imu_cfg.html new file mode 100644 index 0000000000..5ec6eda1a8 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/imu/imu_cfg.html @@ -0,0 +1,605 @@ + + + + + + + + + + + omni.isaac.lab.sensors.imu.imu_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.imu.imu_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from omni.isaac.lab.markers import VisualizationMarkersCfg
+from omni.isaac.lab.markers.config import RED_ARROW_X_MARKER_CFG
+from omni.isaac.lab.utils import configclass
+
+from ..sensor_base_cfg import SensorBaseCfg
+from .imu import Imu
+
+
+
[文档]@configclass +class ImuCfg(SensorBaseCfg): + """Configuration for an Inertial Measurement Unit (IMU) sensor.""" + + class_type: type = Imu + +
[文档] @configclass + class OffsetCfg: + """The offset pose of the sensor's frame from the sensor's parent frame.""" + + 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)."""
+ + offset: OffsetCfg = OffsetCfg() + """The offset pose of the sensor's frame from the sensor's parent frame. Defaults to identity.""" + + visualizer_cfg: VisualizationMarkersCfg = RED_ARROW_X_MARKER_CFG.replace(prim_path="/Visuals/Command/velocity_goal") + """The configuration object for the visualization markers. Defaults to RED_ARROW_X_MARKER_CFG. + + This attribute is only used when debug visualization is enabled. + """ + gravity_bias: tuple[float, float, float] = (0.0, 0.0, 9.81) + """The linear acceleration bias applied to the linear acceleration in the world frame (x,y,z). + + Imu sensors typically output a positive gravity acceleration in opposition to the direction of gravity. This + config parameter allows users to subtract that bias if set to (0.,0.,0.). By default this is set to (0.0,0.0,9.81) + which results in a positive acceleration reading in the world Z. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster.html b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster.html new file mode 100644 index 0000000000..51ab3f81eb --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster.html @@ -0,0 +1,844 @@ + + + + + + + + + + + omni.isaac.lab.sensors.ray_caster.ray_caster — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.ray_caster.ray_caster 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import numpy as np
+import re
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+import omni.log
+import omni.physics.tensors.impl.api as physx
+import warp as wp
+from omni.isaac.core.prims import XFormPrimView
+from pxr import UsdGeom, UsdPhysics
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.markers import VisualizationMarkers
+from omni.isaac.lab.terrains.trimesh.utils import make_plane
+from omni.isaac.lab.utils.math import convert_quat, quat_apply, quat_apply_yaw
+from omni.isaac.lab.utils.warp import convert_to_warp_mesh, raycast_mesh
+
+from ..sensor_base import SensorBase
+from .ray_caster_data import RayCasterData
+
+if TYPE_CHECKING:
+    from .ray_caster_cfg import RayCasterCfg
+
+
+
[文档]class RayCaster(SensorBase): + """A ray-casting sensor. + + The ray-caster uses a set of rays to detect collisions with meshes in the scene. The rays are + defined in the sensor's local coordinate frame. The sensor can be configured to ray-cast against + a set of meshes with a given ray pattern. + + The meshes are parsed from the list of primitive paths provided in the configuration. These are then + converted to warp meshes and stored in the `warp_meshes` list. The ray-caster then ray-casts against + these warp meshes using the ray pattern provided in the configuration. + + .. note:: + Currently, only static meshes are supported. Extending the warp mesh to support dynamic meshes + is a work in progress. + """ + + cfg: RayCasterCfg + """The configuration parameters.""" + +
[文档] def __init__(self, cfg: RayCasterCfg): + """Initializes the ray-caster object. + + Args: + cfg: The configuration parameters. + """ + # check if sensor path is valid + # note: currently we do not handle environment indices if there is a regex pattern in the leaf + # For example, if the prim path is "/World/Sensor_[1,2]". + sensor_path = cfg.prim_path.split("/")[-1] + sensor_path_is_regex = re.match(r"^[a-zA-Z0-9/_]+$", sensor_path) is None + if sensor_path_is_regex: + raise RuntimeError( + f"Invalid prim path for the ray-caster sensor: {self.cfg.prim_path}." + "\n\tHint: Please ensure that the prim path does not contain any regex patterns in the leaf." + ) + # Initialize base class + super().__init__(cfg) + # Create empty variables for storing output data + self._data = RayCasterData() + # the warp meshes used for raycasting. + self.meshes: dict[str, wp.Mesh] = {}
+ + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Ray-caster @ '{self.cfg.prim_path}': \n" + f"\tview type : {self._view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of meshes : {len(self.meshes)}\n" + f"\tnumber of sensors : {self._view.count}\n" + f"\tnumber of rays/sensor: {self.num_rays}\n" + f"\ttotal number of rays : {self.num_rays * self._view.count}" + ) + + """ + Properties + """ + + @property + def num_instances(self) -> int: + return self._view.count + + @property + def data(self) -> RayCasterData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + """ + Operations. + """ + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # reset the timers and counters + super().reset(env_ids) + # resolve None + if env_ids is None: + env_ids = slice(None) + # resample the drift + self.drift[env_ids].uniform_(*self.cfg.drift_range)
+ + """ + Implementation. + """ + + def _initialize_impl(self): + super()._initialize_impl() + # create simulation view + self._physics_sim_view = physx.create_simulation_view(self._backend) + self._physics_sim_view.set_subspace_roots("/") + # check if the prim at path is an articulated or rigid prim + # we do this since for physics-based view classes we can access their data directly + # otherwise we need to use the xform view class which is slower + found_supported_prim_class = False + prim = sim_utils.find_first_matching_prim(self.cfg.prim_path) + if prim is None: + raise RuntimeError(f"Failed to find a prim at path expression: {self.cfg.prim_path}") + # create view based on the type of prim + if prim.HasAPI(UsdPhysics.ArticulationRootAPI): + self._view = self._physics_sim_view.create_articulation_view(self.cfg.prim_path.replace(".*", "*")) + found_supported_prim_class = True + elif prim.HasAPI(UsdPhysics.RigidBodyAPI): + self._view = self._physics_sim_view.create_rigid_body_view(self.cfg.prim_path.replace(".*", "*")) + found_supported_prim_class = True + else: + self._view = XFormPrimView(self.cfg.prim_path, reset_xform_properties=False) + found_supported_prim_class = True + omni.log.warn(f"The prim at path {prim.GetPath().pathString} is not a physics prim! Using XFormPrimView.") + # check if prim view class is found + if not found_supported_prim_class: + raise RuntimeError(f"Failed to find a valid prim view class for the prim paths: {self.cfg.prim_path}") + + # load the meshes by parsing the stage + self._initialize_warp_meshes() + # initialize the ray start and directions + self._initialize_rays_impl() + + def _initialize_warp_meshes(self): + # check number of mesh prims provided + if len(self.cfg.mesh_prim_paths) != 1: + raise NotImplementedError( + f"RayCaster currently only supports one mesh prim. Received: {len(self.cfg.mesh_prim_paths)}" + ) + + # read prims to ray-cast + for mesh_prim_path in self.cfg.mesh_prim_paths: + # check if the prim is a plane - handle PhysX plane as a special case + # if a plane exists then we need to create an infinite mesh that is a plane + mesh_prim = sim_utils.get_first_matching_child_prim( + mesh_prim_path, lambda prim: prim.GetTypeName() == "Plane" + ) + # if we did not find a plane then we need to read the mesh + if mesh_prim is None: + # obtain the mesh prim + mesh_prim = sim_utils.get_first_matching_child_prim( + mesh_prim_path, lambda prim: prim.GetTypeName() == "Mesh" + ) + # check if valid + if mesh_prim is None or not mesh_prim.IsValid(): + raise RuntimeError(f"Invalid mesh prim path: {mesh_prim_path}") + # cast into UsdGeomMesh + mesh_prim = UsdGeom.Mesh(mesh_prim) + # read the vertices and faces + points = np.asarray(mesh_prim.GetPointsAttr().Get()) + indices = np.asarray(mesh_prim.GetFaceVertexIndicesAttr().Get()) + wp_mesh = convert_to_warp_mesh(points, indices, device=self.device) + # print info + omni.log.info( + f"Read mesh prim: {mesh_prim.GetPath()} with {len(points)} vertices and {len(indices)} faces." + ) + else: + mesh = make_plane(size=(2e6, 2e6), height=0.0, center_zero=True) + wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) + # print info + omni.log.info(f"Created infinite plane mesh prim: {mesh_prim.GetPath()}.") + # add the warp mesh to the list + self.meshes[mesh_prim_path] = wp_mesh + + # throw an error if no meshes are found + if all([mesh_prim_path not in self.meshes for mesh_prim_path in self.cfg.mesh_prim_paths]): + raise RuntimeError( + f"No meshes found for ray-casting! Please check the mesh prim paths: {self.cfg.mesh_prim_paths}" + ) + + def _initialize_rays_impl(self): + # compute ray stars and directions + self.ray_starts, self.ray_directions = self.cfg.pattern_cfg.func(self.cfg.pattern_cfg, self._device) + self.num_rays = len(self.ray_directions) + # apply offset transformation to the rays + offset_pos = torch.tensor(list(self.cfg.offset.pos), device=self._device) + offset_quat = torch.tensor(list(self.cfg.offset.rot), device=self._device) + self.ray_directions = quat_apply(offset_quat.repeat(len(self.ray_directions), 1), self.ray_directions) + self.ray_starts += offset_pos + # repeat the rays for each sensor + self.ray_starts = self.ray_starts.repeat(self._view.count, 1, 1) + self.ray_directions = self.ray_directions.repeat(self._view.count, 1, 1) + # prepare drift + self.drift = torch.zeros(self._view.count, 3, device=self.device) + # fill the data buffer + self._data.pos_w = torch.zeros(self._view.count, 3, device=self._device) + self._data.quat_w = torch.zeros(self._view.count, 4, device=self._device) + self._data.ray_hits_w = torch.zeros(self._view.count, self.num_rays, 3, device=self._device) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + # obtain the poses of the sensors + if isinstance(self._view, XFormPrimView): + pos_w, quat_w = self._view.get_world_poses(env_ids) + elif isinstance(self._view, physx.ArticulationView): + pos_w, quat_w = self._view.get_root_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = convert_quat(quat_w, to="wxyz") + elif isinstance(self._view, physx.RigidBodyView): + pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = convert_quat(quat_w, to="wxyz") + else: + raise RuntimeError(f"Unsupported view type: {type(self._view)}") + # note: we clone here because we are read-only operations + pos_w = pos_w.clone() + quat_w = quat_w.clone() + # apply drift + pos_w += self.drift[env_ids] + # store the poses + self._data.pos_w[env_ids] = pos_w + self._data.quat_w[env_ids] = quat_w + + # ray cast based on the sensor poses + if self.cfg.attach_yaw_only: + # only yaw orientation is considered and directions are not rotated + ray_starts_w = quat_apply_yaw(quat_w.repeat(1, self.num_rays), self.ray_starts[env_ids]) + ray_starts_w += pos_w.unsqueeze(1) + ray_directions_w = self.ray_directions[env_ids] + else: + # full orientation is considered + ray_starts_w = quat_apply(quat_w.repeat(1, self.num_rays), self.ray_starts[env_ids]) + ray_starts_w += pos_w.unsqueeze(1) + ray_directions_w = quat_apply(quat_w.repeat(1, self.num_rays), self.ray_directions[env_ids]) + # ray cast and store the hits + # TODO: Make this work for multiple meshes? + self._data.ray_hits_w[env_ids] = raycast_mesh( + ray_starts_w, + ray_directions_w, + max_dist=self.cfg.max_distance, + mesh=self.meshes[self.cfg.mesh_prim_paths[0]], + )[0] + + def _set_debug_vis_impl(self, debug_vis: bool): + # set visibility of markers + # note: parent only deals with callbacks. not their visibility + if debug_vis: + if not hasattr(self, "ray_visualizer"): + self.ray_visualizer = VisualizationMarkers(self.cfg.visualizer_cfg) + # set their visibility to true + self.ray_visualizer.set_visibility(True) + else: + if hasattr(self, "ray_visualizer"): + self.ray_visualizer.set_visibility(False) + + def _debug_vis_callback(self, event): + # show ray hit positions + self.ray_visualizer.visualize(self._data.ray_hits_w.view(-1, 3)) + + """ + Internal simulation callbacks. + """ + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + # call parent + super()._invalidate_initialize_callback(event) + # set all existing views to None to invalidate them + self._physics_sim_view = None + self._view = None
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.html b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.html new file mode 100644 index 0000000000..a91607f20e --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_camera.html @@ -0,0 +1,980 @@ + + + + + + + + + + + omni.isaac.lab.sensors.ray_caster.ray_caster_camera — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.ray_caster.ray_caster_camera 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, ClassVar, Literal
+
+import omni.isaac.core.utils.stage as stage_utils
+import omni.physics.tensors.impl.api as physx
+from omni.isaac.core.prims import XFormPrimView
+
+import omni.isaac.lab.utils.math as math_utils
+from omni.isaac.lab.sensors.camera import CameraData
+from omni.isaac.lab.utils.warp import raycast_mesh
+
+from .ray_caster import RayCaster
+
+if TYPE_CHECKING:
+    from .ray_caster_camera_cfg import RayCasterCameraCfg
+
+
+
[文档]class RayCasterCamera(RayCaster): + """A ray-casting camera sensor. + + 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.lab.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. + + Currently, only the following annotators are supported: + + - ``"distance_to_camera"``: An image containing the distance to camera optical center. + - ``"distance_to_image_plane"``: An image containing distances of 3D points from camera plane along camera's z-axis. + - ``"normals"``: An image containing the local surface normal vectors at each pixel. + + .. note:: + Currently, only static meshes are supported. Extending the warp mesh to support dynamic meshes + is a work in progress. + """ + + cfg: RayCasterCameraCfg + """The configuration parameters.""" + UNSUPPORTED_TYPES: ClassVar[set[str]] = { + "rgb", + "instance_id_segmentation", + "instance_id_segmentation_fast", + "instance_segmentation", + "instance_segmentation_fast", + "semantic_segmentation", + "skeleton_data", + "motion_vectors", + "bounding_box_2d_tight", + "bounding_box_2d_tight_fast", + "bounding_box_2d_loose", + "bounding_box_2d_loose_fast", + "bounding_box_3d", + "bounding_box_3d_fast", + } + """A set of sensor types that are not supported by the ray-caster camera.""" + +
[文档] def __init__(self, cfg: RayCasterCameraCfg): + """Initializes the camera object. + + Args: + cfg: The configuration parameters. + + Raises: + ValueError: If the provided data types are not supported by the ray-caster camera. + """ + # perform check on supported data types + self._check_supported_data_types(cfg) + # initialize base class + super().__init__(cfg) + # create empty variables for storing output data + self._data = CameraData()
+ + def __str__(self) -> str: + """Returns: A string containing information about the instance.""" + return ( + f"Ray-Caster-Camera @ '{self.cfg.prim_path}': \n" + f"\tview type : {self._view.__class__}\n" + f"\tupdate period (s) : {self.cfg.update_period}\n" + f"\tnumber of meshes : {len(self.meshes)}\n" + f"\tnumber of sensors : {self._view.count}\n" + f"\tnumber of rays/sensor: {self.num_rays}\n" + f"\ttotal number of rays : {self.num_rays * self._view.count}\n" + f"\timage shape : {self.image_shape}" + ) + + """ + Properties + """ + + @property + def data(self) -> CameraData: + # update sensors if needed + self._update_outdated_buffers() + # return the data + return self._data + + @property + def image_shape(self) -> tuple[int, int]: + """A tuple containing (height, width) of the camera sensor.""" + return (self.cfg.pattern_cfg.height, self.cfg.pattern_cfg.width) + + @property + def frame(self) -> torch.tensor: + """Frame number when the measurement took place.""" + return self._frame + + """ + Operations. + """ + +
[文档] def set_intrinsic_matrices( + self, matrices: torch.Tensor, focal_length: float = 1.0, env_ids: Sequence[int] | None = None + ): + """Set the intrinsic matrix of the camera. + + Args: + matrices: The intrinsic matrices for the camera. Shape is (N, 3, 3). + focal_length: Focal length to use when computing aperture values (in cm). Defaults to 1.0. + env_ids: A sensor ids to manipulate. Defaults to None, which means all sensor indices. + """ + # resolve env_ids + if env_ids is None: + env_ids = slice(None) + # save new intrinsic matrices and focal length + self._data.intrinsic_matrices[env_ids] = matrices.to(self._device) + self._focal_length = focal_length + # recompute ray directions + self.ray_starts[env_ids], self.ray_directions[env_ids] = self.cfg.pattern_cfg.func( + self.cfg.pattern_cfg, self._data.intrinsic_matrices[env_ids], self._device + )
+ +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + # reset the timestamps + super().reset(env_ids) + # resolve None + if env_ids is None: + env_ids = slice(None) + # reset the data + # note: this recomputation is useful if one performs events such as randomizations on the camera poses. + pos_w, quat_w = self._compute_camera_world_poses(env_ids) + self._data.pos_w[env_ids] = pos_w + self._data.quat_w_world[env_ids] = quat_w + # Reset the frame count + self._frame[env_ids] = 0
+ +
[文档] def set_world_poses( + self, + positions: torch.Tensor | None = None, + orientations: torch.Tensor | None = None, + env_ids: Sequence[int] | None = None, + convention: Literal["opengl", "ros", "world"] = "ros", + ): + """Set the pose of the camera w.r.t. the world frame using specified convention. + + Since different fields use different conventions for camera orientations, the method allows users to + set the camera poses in the specified convention. Possible conventions are: + + - :obj:`"opengl"` - forward axis: -Z - up axis +Y - Offset is applied in the OpenGL (Usd.Camera) convention + - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention + - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention + + See :meth:`omni.isaac.lab.utils.maths.convert_camera_frame_orientation_convention` for more details + on the conventions. + + Args: + positions: The cartesian coordinates (in meters). Shape is (N, 3). + Defaults to None, in which case the camera position in not changed. + orientations: The quaternion orientation in (w, x, y, z). Shape is (N, 4). + Defaults to None, in which case the camera orientation in not changed. + env_ids: A sensor ids to manipulate. Defaults to None, which means all sensor indices. + convention: The convention in which the poses are fed. Defaults to "ros". + + Raises: + RuntimeError: If the camera prim is not set. Need to call :meth:`initialize` method first. + """ + # resolve env_ids + if env_ids is None: + env_ids = self._ALL_INDICES + + # get current positions + pos_w, quat_w = self._compute_view_world_poses(env_ids) + if positions is not None: + # transform to camera frame + pos_offset_world_frame = positions - pos_w + self._offset_pos[env_ids] = math_utils.quat_apply(math_utils.quat_inv(quat_w), pos_offset_world_frame) + if orientations is not None: + # convert rotation matrix from input convention to world + quat_w_set = math_utils.convert_camera_frame_orientation_convention( + orientations, origin=convention, target="world" + ) + self._offset_quat[env_ids] = math_utils.quat_mul(math_utils.quat_inv(quat_w), quat_w_set) + + # update the data + pos_w, quat_w = self._compute_camera_world_poses(env_ids) + self._data.pos_w[env_ids] = pos_w + self._data.quat_w_world[env_ids] = quat_w
+ +
[文档] def set_world_poses_from_view( + self, eyes: torch.Tensor, targets: torch.Tensor, env_ids: Sequence[int] | None = None + ): + """Set the poses of the camera from the eye position and look-at target position. + + Args: + eyes: The positions of the camera's eye. Shape is N, 3). + targets: The target locations to look at. Shape is (N, 3). + env_ids: A sensor ids to manipulate. Defaults to None, which means all sensor indices. + + Raises: + RuntimeError: If the camera prim is not set. Need to call :meth:`initialize` method first. + NotImplementedError: If the stage up-axis is not "Y" or "Z". + """ + # get up axis of current stage + up_axis = stage_utils.get_stage_up_axis() + # camera position and rotation in opengl convention + orientations = math_utils.quat_from_matrix( + math_utils.create_rotation_matrix_from_view(eyes, targets, up_axis=up_axis, device=self._device) + ) + self.set_world_poses(eyes, orientations, env_ids, convention="opengl")
+ + """ + Implementation. + """ + + def _initialize_rays_impl(self): + # Create all indices buffer + self._ALL_INDICES = torch.arange(self._view.count, device=self._device, dtype=torch.long) + # Create frame count buffer + self._frame = torch.zeros(self._view.count, device=self._device, dtype=torch.long) + # create buffers + self._create_buffers() + # compute intrinsic matrices + self._compute_intrinsic_matrices() + # compute ray stars and directions + self.ray_starts, self.ray_directions = self.cfg.pattern_cfg.func( + self.cfg.pattern_cfg, self._data.intrinsic_matrices, self._device + ) + self.num_rays = self.ray_directions.shape[1] + # create buffer to store ray hits + self.ray_hits_w = torch.zeros(self._view.count, self.num_rays, 3, device=self._device) + # set offsets + quat_w = math_utils.convert_camera_frame_orientation_convention( + torch.tensor([self.cfg.offset.rot], device=self._device), origin=self.cfg.offset.convention, target="world" + ) + self._offset_quat = quat_w.repeat(self._view.count, 1) + self._offset_pos = torch.tensor(list(self.cfg.offset.pos), device=self._device).repeat(self._view.count, 1) + + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the buffers of the sensor data.""" + # increment frame count + self._frame[env_ids] += 1 + + # compute poses from current view + pos_w, quat_w = self._compute_camera_world_poses(env_ids) + # update the data + self._data.pos_w[env_ids] = pos_w + self._data.quat_w_world[env_ids] = quat_w + + # note: full orientation is considered + ray_starts_w = math_utils.quat_apply(quat_w.repeat(1, self.num_rays), self.ray_starts[env_ids]) + ray_starts_w += pos_w.unsqueeze(1) + ray_directions_w = math_utils.quat_apply(quat_w.repeat(1, self.num_rays), self.ray_directions[env_ids]) + + # ray cast and store the hits + # note: we set max distance to 1e6 during the ray-casting. THis is because we clip the distance + # to the image plane and distance to the camera to the maximum distance afterwards in-order to + # match the USD camera behavior. + + # TODO: Make ray-casting work for multiple meshes? + # necessary for regular dictionaries. + self.ray_hits_w, ray_depth, ray_normal, _ = raycast_mesh( + ray_starts_w, + ray_directions_w, + mesh=self.meshes[self.cfg.mesh_prim_paths[0]], + max_dist=1e6, + return_distance=any( + [name in self.cfg.data_types for name in ["distance_to_image_plane", "distance_to_camera"]] + ), + return_normal="normals" in self.cfg.data_types, + ) + # update output buffers + if "distance_to_image_plane" in self.cfg.data_types: + # note: data is in camera frame so we only take the first component (z-axis of camera frame) + distance_to_image_plane = ( + math_utils.quat_apply( + math_utils.quat_inv(quat_w).repeat(1, self.num_rays), + (ray_depth[:, :, None] * ray_directions_w), + ) + )[:, :, 0] + # apply the maximum distance after the transformation + distance_to_image_plane = torch.clip(distance_to_image_plane, max=self.cfg.max_distance) + self._data.output["distance_to_image_plane"][env_ids] = distance_to_image_plane.view( + -1, *self.image_shape, 1 + ) + if "distance_to_camera" in self.cfg.data_types: + self._data.output["distance_to_camera"][env_ids] = torch.clip( + ray_depth.view(-1, *self.image_shape, 1), max=self.cfg.max_distance + ) + if "normals" in self.cfg.data_types: + self._data.output["normals"][env_ids] = ray_normal.view(-1, *self.image_shape, 3) + + def _debug_vis_callback(self, event): + # in case it crashes be safe + if not hasattr(self, "ray_hits_w"): + return + # show ray hit positions + self.ray_visualizer.visualize(self.ray_hits_w.view(-1, 3)) + + """ + Private Helpers + """ + + def _check_supported_data_types(self, cfg: RayCasterCameraCfg): + """Checks if the data types are supported by the ray-caster camera.""" + # check if there is any intersection in unsupported types + # reason: we cannot obtain this data from simplified warp-based ray caster + common_elements = set(cfg.data_types) & RayCasterCamera.UNSUPPORTED_TYPES + if common_elements: + raise ValueError( + f"RayCasterCamera class does not support the following sensor types: {common_elements}." + "\n\tThis is because these sensor types cannot be obtained in a fast way using ''warp''." + "\n\tHint: If you need to work with these sensor types, we recommend using the USD camera" + " interface from the omni.isaac.lab.sensors.camera module." + ) + + def _create_buffers(self): + """Create buffers for storing data.""" + # prepare drift + self.drift = torch.zeros(self._view.count, 3, device=self.device) + # create the data object + # -- pose of the cameras + self._data.pos_w = torch.zeros((self._view.count, 3), device=self._device) + self._data.quat_w_world = torch.zeros((self._view.count, 4), device=self._device) + # -- intrinsic matrix + self._data.intrinsic_matrices = torch.zeros((self._view.count, 3, 3), device=self._device) + self._data.intrinsic_matrices[:, 2, 2] = 1.0 + self._data.image_shape = self.image_shape + # -- output data + # create the buffers to store the annotator data. + self._data.output = {} + self._data.info = [{name: None for name in self.cfg.data_types}] * self._view.count + for name in self.cfg.data_types: + if name in ["distance_to_image_plane", "distance_to_camera"]: + shape = (self.cfg.pattern_cfg.height, self.cfg.pattern_cfg.width, 1) + elif name in ["normals"]: + shape = (self.cfg.pattern_cfg.height, self.cfg.pattern_cfg.width, 3) + else: + raise ValueError(f"Received unknown data type: {name}. Please check the configuration.") + # allocate tensor to store the data + self._data.output[name] = torch.zeros((self._view.count, *shape), device=self._device) + + def _compute_intrinsic_matrices(self): + """Computes the intrinsic matrices for the camera based on the config provided.""" + # get the sensor properties + pattern_cfg = self.cfg.pattern_cfg + + # check if vertical aperture is provided + # if not then it is auto-computed based on the aspect ratio to preserve squared pixels + if pattern_cfg.vertical_aperture is None: + pattern_cfg.vertical_aperture = pattern_cfg.horizontal_aperture * pattern_cfg.height / pattern_cfg.width + + # compute the intrinsic matrix + f_x = pattern_cfg.width * pattern_cfg.focal_length / pattern_cfg.horizontal_aperture + f_y = pattern_cfg.height * pattern_cfg.focal_length / pattern_cfg.vertical_aperture + c_x = pattern_cfg.horizontal_aperture_offset * f_x + pattern_cfg.width / 2 + c_y = pattern_cfg.vertical_aperture_offset * f_y + pattern_cfg.height / 2 + # allocate the intrinsic matrices + self._data.intrinsic_matrices[:, 0, 0] = f_x + self._data.intrinsic_matrices[:, 0, 2] = c_x + self._data.intrinsic_matrices[:, 1, 1] = f_y + self._data.intrinsic_matrices[:, 1, 2] = c_y + + # save focal length + self._focal_length = pattern_cfg.focal_length + + def _compute_view_world_poses(self, env_ids: Sequence[int]) -> tuple[torch.Tensor, torch.Tensor]: + """Obtains the pose of the view the camera is attached to in the world frame. + + Returns: + A tuple of the position (in meters) and quaternion (w, x, y, z). + """ + # obtain the poses of the sensors + # note: clone arg doesn't exist for xform prim view so we need to do this manually + if isinstance(self._view, XFormPrimView): + pos_w, quat_w = self._view.get_world_poses(env_ids) + elif isinstance(self._view, physx.ArticulationView): + pos_w, quat_w = self._view.get_root_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = math_utils.convert_quat(quat_w, to="wxyz") + elif isinstance(self._view, physx.RigidBodyView): + pos_w, quat_w = self._view.get_transforms()[env_ids].split([3, 4], dim=-1) + quat_w = math_utils.convert_quat(quat_w, to="wxyz") + else: + raise RuntimeError(f"Unsupported view type: {type(self._view)}") + # return the pose + return pos_w.clone(), quat_w.clone() + + def _compute_camera_world_poses(self, env_ids: Sequence[int]) -> tuple[torch.Tensor, torch.Tensor]: + """Computes the pose of the camera in the world frame. + + This function applies the offset pose to the pose of the view the camera is attached to. + + Returns: + A tuple of the position (in meters) and quaternion (w, x, y, z) in "world" convention. + """ + # get the pose of the view the camera is attached to + pos_w, quat_w = self._compute_view_world_poses(env_ids) + # apply offsets + # need to apply quat because offset relative to parent frame + pos_w += math_utils.quat_apply(quat_w, self._offset_pos[env_ids]) + quat_w = math_utils.quat_mul(quat_w, self._offset_quat[env_ids]) + + return pos_w, quat_w
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.html b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.html new file mode 100644 index 0000000000..089360248f --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_camera_cfg.html @@ -0,0 +1,613 @@ + + + + + + + + + + + omni.isaac.lab.sensors.ray_caster.ray_caster_camera_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.ray_caster.ray_caster_camera_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Configuration for the ray-cast camera sensor."""
+
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+from .patterns import PinholeCameraPatternCfg
+from .ray_caster_camera import RayCasterCamera
+from .ray_caster_cfg import RayCasterCfg
+
+
+
[文档]@configclass +class RayCasterCameraCfg(RayCasterCfg): + """Configuration for the ray-cast sensor.""" + +
[文档] @configclass + class OffsetCfg: + """The offset pose of the sensor's frame from the sensor's parent frame.""" + + 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).""" + + convention: Literal["opengl", "ros", "world"] = "ros" + """The convention in which the frame offset is applied. Defaults to "ros". + + - ``"opengl"`` - forward axis: ``-Z`` - up axis: ``+Y`` - Offset is applied in the OpenGL (Usd.Camera) convention. + - ``"ros"`` - forward axis: ``+Z`` - up axis: ``-Y`` - Offset is applied in the ROS convention. + - ``"world"`` - forward axis: ``+X`` - up axis: ``+Z`` - Offset is applied in the World Frame convention. + + """
+ + class_type: type = RayCasterCamera + + offset: OffsetCfg = OffsetCfg() + """The offset pose of the sensor's frame from the sensor's parent frame. Defaults to identity.""" + + data_types: list[str] = ["distance_to_image_plane"] + """List of sensor names/types to enable for the camera. Defaults to ["distance_to_image_plane"].""" + + pattern_cfg: PinholeCameraPatternCfg = MISSING + """The pattern that defines the local ray starting positions and directions in a pinhole camera pattern.""" + + def __post_init__(self): + # for cameras, this quantity should be False always. + self.attach_yaw_only = False
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_cfg.html b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_cfg.html new file mode 100644 index 0000000000..4a773425a2 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_cfg.html @@ -0,0 +1,628 @@ + + + + + + + + + + + omni.isaac.lab.sensors.ray_caster.ray_caster_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.ray_caster.ray_caster_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Configuration for the ray-cast sensor."""
+
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.markers import VisualizationMarkersCfg
+from omni.isaac.lab.markers.config import RAY_CASTER_MARKER_CFG
+from omni.isaac.lab.utils import configclass
+
+from ..sensor_base_cfg import SensorBaseCfg
+from .patterns.patterns_cfg import PatternBaseCfg
+from .ray_caster import RayCaster
+
+
+
[文档]@configclass +class RayCasterCfg(SensorBaseCfg): + """Configuration for the ray-cast sensor.""" + +
[文档] @configclass + class OffsetCfg: + """The offset pose of the sensor's frame from the sensor's parent frame.""" + + 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)."""
+ + class_type: type = RayCaster + + mesh_prim_paths: list[str] = MISSING + """The list of mesh primitive paths to ray cast against. + + Note: + Currently, only a single static mesh is supported. We are working on supporting multiple + static meshes and dynamic meshes. + """ + + offset: OffsetCfg = OffsetCfg() + """The offset pose of the sensor's frame from the sensor's parent frame. Defaults to identity.""" + + attach_yaw_only: bool = MISSING + """Whether the rays' starting positions and directions only track the yaw orientation. + + This is useful for ray-casting height maps, where only yaw rotation is needed. + """ + + pattern_cfg: PatternBaseCfg = MISSING + """The pattern that defines the local ray starting positions and directions.""" + + max_distance: float = 1e6 + """Maximum distance (in meters) from the sensor to ray cast to. Defaults to 1e6.""" + + drift_range: tuple[float, float] = (0.0, 0.0) + """The range of drift (in meters) to add to the ray starting positions (xyz). Defaults to (0.0, 0.0). + + For floating base robots, this is useful for simulating drift in the robot's pose estimation. + """ + + visualizer_cfg: VisualizationMarkersCfg = RAY_CASTER_MARKER_CFG.replace(prim_path="/Visuals/RayCaster") + """The configuration object for the visualization markers. Defaults to RAY_CASTER_MARKER_CFG. + + Note: + This attribute is only used when debug visualization is enabled. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_data.html b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_data.html new file mode 100644 index 0000000000..3843c511bb --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/ray_caster/ray_caster_data.html @@ -0,0 +1,588 @@ + + + + + + + + + + + omni.isaac.lab.sensors.ray_caster.ray_caster_data — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.ray_caster.ray_caster_data 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from dataclasses import dataclass
+
+
+
[文档]@dataclass +class RayCasterData: + """Data container for the ray-cast sensor.""" + + pos_w: torch.Tensor = None + """Position of the sensor origin in world frame. + + 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. + + 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 + in the scan pattern per sensor. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/sensor_base.html b/_modules/omni/isaac/lab/sensors/sensor_base.html new file mode 100644 index 0000000000..cb9987d137 --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/sensor_base.html @@ -0,0 +1,852 @@ + + + + + + + + + + + omni.isaac.lab.sensors.sensor_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.sensor_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Base class for sensors.
+
+This class defines an interface for sensors similar to how the :class:`omni.isaac.lab.assets.AssetBase` class works.
+Each sensor class should inherit from this class and implement the abstract methods.
+"""
+
+from __future__ import annotations
+
+import inspect
+import torch
+import weakref
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any
+
+import omni.kit.app
+import omni.timeline
+
+import omni.isaac.lab.sim as sim_utils
+
+if TYPE_CHECKING:
+    from .sensor_base_cfg import SensorBaseCfg
+
+
+
[文档]class SensorBase(ABC): + """The base class for implementing a sensor. + + The implementation is based on lazy evaluation. The sensor data is only updated when the user + tries accessing the data through the :attr:`data` property or sets ``force_compute=True`` in + the :meth:`update` method. This is done to avoid unnecessary computation when the sensor data + is not used. + + The sensor is updated at the specified update period. If the update period is zero, then the + sensor is updated at every simulation step. + """ + +
[文档] def __init__(self, cfg: SensorBaseCfg): + """Initialize the sensor class. + + Args: + cfg: The configuration parameters for the sensor. + """ + # check that config is valid + if cfg.history_length < 0: + raise ValueError(f"History length must be greater than 0! Received: {cfg.history_length}") + # check that the config is valid + cfg.validate() + # store inputs + self.cfg = cfg + # flag for whether the sensor is initialized + self._is_initialized = False + # flag for whether the sensor is in visualization mode + self._is_visualizing = False + + # note: Use weakref on callbacks to ensure that this object can be deleted when its destructor is called. + # add callbacks for stage play/stop + # The order is set to 10 which is arbitrary but should be lower priority than the default order of 0 + timeline_event_stream = omni.timeline.get_timeline_interface().get_timeline_event_stream() + self._initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.PLAY), + lambda event, obj=weakref.proxy(self): obj._initialize_callback(event), + order=10, + ) + self._invalidate_initialize_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.STOP), + lambda event, obj=weakref.proxy(self): obj._invalidate_initialize_callback(event), + order=10, + ) + # add handle for debug visualization (this is set to a valid handle inside set_debug_vis) + self._debug_vis_handle = None + # set initial state of debug visualization + self.set_debug_vis(self.cfg.debug_vis)
+ + def __del__(self): + """Unsubscribe from the callbacks.""" + # clear physics events handles + if self._initialize_handle: + self._initialize_handle.unsubscribe() + self._initialize_handle = None + if self._invalidate_initialize_handle: + self._invalidate_initialize_handle.unsubscribe() + self._invalidate_initialize_handle = None + # clear debug visualization + if self._debug_vis_handle: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + + """ + Properties + """ + + @property + def is_initialized(self) -> bool: + """Whether the sensor is initialized. + + Returns True if the sensor is initialized, False otherwise. + """ + return self._is_initialized + + @property + def num_instances(self) -> int: + """Number of instances of the sensor. + + This is equal to the number of sensors per environment multiplied by the number of environments. + """ + return self._num_envs + + @property + def device(self) -> str: + """Memory device for computation.""" + return self._device + + @property + @abstractmethod + def data(self) -> Any: + """Data from the sensor. + + This property is only updated when the user tries to access the data. This is done to avoid + unnecessary computation when the sensor data is not used. + + For updating the sensor when this property is accessed, you can use the following + code snippet in your sensor implementation: + + .. code-block:: python + + # update sensors if needed + self._update_outdated_buffers() + # return the data (where `_data` is the data for the sensor) + return self._data + """ + raise NotImplementedError + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the sensor has a debug visualization implemented.""" + # check if function raises NotImplementedError + source_code = inspect.getsource(self._set_debug_vis_impl) + return "NotImplementedError" not in source_code + + """ + Operations + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Sets whether to visualize the sensor data. + + Args: + debug_vis: Whether to visualize the sensor data. + + Returns: + Whether the debug visualization was successfully set. False if the sensor + does not support debug visualization. + """ + # check if debug visualization is supported + if not self.has_debug_vis_implementation: + return False + # toggle debug visualization objects + self._set_debug_vis_impl(debug_vis) + # toggle debug visualization flag + self._is_visualizing = debug_vis + # toggle debug visualization handles + if debug_vis: + # create a subscriber for the post update event if it doesn't exist + if self._debug_vis_handle is None: + app_interface = omni.kit.app.get_app_interface() + self._debug_vis_handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj._debug_vis_callback(event) + ) + else: + # remove the subscriber if it exists + if self._debug_vis_handle is not None: + self._debug_vis_handle.unsubscribe() + self._debug_vis_handle = None + # return success + return True
+ +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + """Resets the sensor internals. + + Args: + env_ids: The sensor ids to reset. Defaults to None. + """ + # Resolve sensor ids + if env_ids is None: + env_ids = slice(None) + # Reset the timestamp for the sensors + self._timestamp[env_ids] = 0.0 + self._timestamp_last_update[env_ids] = 0.0 + # Set all reset sensors to outdated so that they are updated when data is called the next time. + self._is_outdated[env_ids] = True
+ + def update(self, dt: float, force_recompute: bool = False): + # Update the timestamp for the sensors + self._timestamp += dt + self._is_outdated |= self._timestamp - self._timestamp_last_update + 1e-6 >= self.cfg.update_period + # Update the buffers + # TODO (from @mayank): Why is there a history length here when it doesn't mean anything in the sensor base?!? + # It is only for the contact sensor but there we should redefine the update function IMO. + if force_recompute or self._is_visualizing or (self.cfg.history_length > 0): + self._update_outdated_buffers() + + """ + Implementation specific. + """ + + @abstractmethod + def _initialize_impl(self): + """Initializes the sensor-related handles and internal buffers.""" + # Obtain Simulation Context + sim = sim_utils.SimulationContext.instance() + if sim is None: + raise RuntimeError("Simulation Context is not initialized!") + # Obtain device and backend + self._device = sim.device + self._backend = sim.backend + self._sim_physics_dt = sim.get_physics_dt() + # Count number of environments + env_prim_path_expr = self.cfg.prim_path.rsplit("/", 1)[0] + self._parent_prims = sim_utils.find_matching_prims(env_prim_path_expr) + self._num_envs = len(self._parent_prims) + # Boolean tensor indicating whether the sensor data has to be refreshed + self._is_outdated = torch.ones(self._num_envs, dtype=torch.bool, device=self._device) + # Current timestamp (in seconds) + self._timestamp = torch.zeros(self._num_envs, device=self._device) + # Timestamp from last update + self._timestamp_last_update = torch.zeros_like(self._timestamp) + + @abstractmethod + def _update_buffers_impl(self, env_ids: Sequence[int]): + """Fills the sensor data for provided environment ids. + + This function does not perform any time-based checks and directly fills the data into the + data container. + + Args: + env_ids: The indices of the sensors that are ready to capture. + """ + raise NotImplementedError + + def _set_debug_vis_impl(self, debug_vis: bool): + """Set debug visualization into visualization objects. + + This function is responsible for creating the visualization objects if they don't exist + and input ``debug_vis`` is True. If the visualization objects exist, the function should + set their visibility into the stage. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + def _debug_vis_callback(self, event): + """Callback for debug visualization. + + This function calls the visualization objects and sets the data to visualize into them. + """ + raise NotImplementedError(f"Debug visualization is not implemented for {self.__class__.__name__}.") + + """ + Internal simulation callbacks. + """ + + def _initialize_callback(self, event): + """Initializes the scene elements. + + Note: + PhysX handles are only enabled once the simulator starts playing. Hence, this function needs to be + called whenever the simulator "plays" from a "stop" state. + """ + if not self._is_initialized: + self._initialize_impl() + self._is_initialized = True + + def _invalidate_initialize_callback(self, event): + """Invalidates the scene elements.""" + self._is_initialized = False + + """ + Helper functions. + """ + + def _update_outdated_buffers(self): + """Fills the sensor data for the outdated sensors.""" + outdated_env_ids = self._is_outdated.nonzero().squeeze(-1) + if len(outdated_env_ids) > 0: + # obtain new data + self._update_buffers_impl(outdated_env_ids) + # update the timestamp from last update + self._timestamp_last_update[outdated_env_ids] = self._timestamp[outdated_env_ids] + # set outdated flag to false for the updated sensors + self._is_outdated[outdated_env_ids] = False
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sensors/sensor_base_cfg.html b/_modules/omni/isaac/lab/sensors/sensor_base_cfg.html new file mode 100644 index 0000000000..f8f91bfb0d --- /dev/null +++ b/_modules/omni/isaac/lab/sensors/sensor_base_cfg.html @@ -0,0 +1,601 @@ + + + + + + + + + + + omni.isaac.lab.sensors.sensor_base_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sensors.sensor_base_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.utils import configclass
+
+from .sensor_base import SensorBase
+
+
+
[文档]@configclass +class SensorBaseCfg: + """Configuration parameters for a sensor.""" + + class_type: type[SensorBase] = MISSING + """The associated sensor class. + + The class should inherit from :class:`omni.isaac.lab.sensors.sensor_base.SensorBase`. + """ + + prim_path: str = MISSING + """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``. + + """ + + update_period: float = 0.0 + """Update period of the sensor buffers (in seconds). Defaults to 0.0 (update every step).""" + + history_length: int = 0 + """Number of past frames to store in the sensor buffers. Defaults to 0, which means that only + the current data is stored (no history).""" + + debug_vis: bool = False + """Whether to visualize the sensor. Defaults to False."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/converters/asset_converter_base.html b/_modules/omni/isaac/lab/sim/converters/asset_converter_base.html new file mode 100644 index 0000000000..8173203942 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/converters/asset_converter_base.html @@ -0,0 +1,757 @@ + + + + + + + + + + + omni.isaac.lab.sim.converters.asset_converter_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.converters.asset_converter_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import abc
+import hashlib
+import json
+import os
+import pathlib
+import random
+from datetime import datetime
+
+from omni.isaac.lab.sim.converters.asset_converter_base_cfg import AssetConverterBaseCfg
+from omni.isaac.lab.utils.assets import check_file_path
+from omni.isaac.lab.utils.io import dump_yaml
+
+
+
[文档]class AssetConverterBase(abc.ABC): + """Base class for converting an asset file from different formats into USD format. + + This class provides a common interface for converting an asset file into USD. It does not + provide any implementation for the conversion. The derived classes must implement the + :meth:`_convert_asset` method to provide the actual conversion. + + The file conversion is lazy if the output directory (:obj:`AssetConverterBaseCfg.usd_dir`) is provided. + In the lazy conversion, the USD file is re-generated only if: + + * The asset file is modified. + * The configuration parameters are modified. + * The USD file does not exist. + + To override this behavior to force conversion, the flag :obj:`AssetConverterBaseCfg.force_usd_conversion` + can be set to True. + + When no output directory is defined, lazy conversion is deactivated and the generated USD file is + stored in folder ``/tmp/IsaacLab/usd_{date}_{time}_{random}``, where the parameters in braces are generated + at runtime. The random identifiers help avoid a race condition where two simultaneously triggered conversions + try to use the same directory for reading/writing the generated files. + + .. note:: + Changes to the parameters :obj:`AssetConverterBaseCfg.asset_path`, :obj:`AssetConverterBaseCfg.usd_dir`, and + :obj:`AssetConverterBaseCfg.usd_file_name` are not considered as modifications in the configuration instance that + trigger USD file re-generation. + + """ + +
[文档] def __init__(self, cfg: AssetConverterBaseCfg): + """Initializes the class. + + Args: + cfg: The configuration instance for converting an asset file to USD format. + + Raises: + ValueError: When provided asset file does not exist. + """ + # check that the config is valid + cfg.validate() + # check if the asset file exists + if not check_file_path(cfg.asset_path): + raise ValueError(f"The asset path does not exist: {cfg.asset_path}") + # save the inputs + self.cfg = cfg + + # resolve USD directory name + if cfg.usd_dir is None: + # a folder in "/tmp/IsaacLab" by the name: usd_{date}_{time}_{random} + time_tag = datetime.now().strftime("%Y%m%d_%H%M%S") + self._usd_dir = f"/tmp/IsaacLab/usd_{time_tag}_{random.randrange(10000)}" + else: + self._usd_dir = cfg.usd_dir + + # resolve the file name from asset file name if not provided + if cfg.usd_file_name is None: + usd_file_name = pathlib.PurePath(cfg.asset_path).stem + else: + usd_file_name = cfg.usd_file_name + # add USD extension if not provided + if not (usd_file_name.endswith(".usd") or usd_file_name.endswith(".usda")): + self._usd_file_name = usd_file_name + ".usd" + else: + self._usd_file_name = usd_file_name + + # create the USD directory + os.makedirs(self.usd_dir, exist_ok=True) + # check if usd files exist + self._usd_file_exists = os.path.isfile(self.usd_path) + # path to read/write asset hash file + self._dest_hash_path = os.path.join(self.usd_dir, ".asset_hash") + # create asset hash to check if the asset has changed + self._asset_hash = self._config_to_hash(cfg) + # read the saved hash + try: + with open(self._dest_hash_path) as f: + existing_asset_hash = f.readline() + self._is_same_asset = existing_asset_hash == self._asset_hash + except FileNotFoundError: + self._is_same_asset = False + + # convert the asset to USD if the hash is different or USD file does not exist + if cfg.force_usd_conversion or not self._usd_file_exists or not self._is_same_asset: + # write the updated hash + with open(self._dest_hash_path, "w") as f: + f.write(self._asset_hash) + # convert the asset to USD + self._convert_asset(cfg) + # dump the configuration to a file + dump_yaml(os.path.join(self.usd_dir, "config.yaml"), cfg.to_dict()) + # add comment to top of the saved config file with information about the converter + current_date = datetime.now().strftime("%Y-%m-%d") + current_time = datetime.now().strftime("%H:%M:%S") + generation_comment = ( + f"##\n# Generated by {self.__class__.__name__} on {current_date} at {current_time}.\n##\n" + ) + with open(os.path.join(self.usd_dir, "config.yaml"), "a") as f: + f.write(generation_comment)
+ + """ + Properties. + """ + + @property + def usd_dir(self) -> str: + """The absolute path to the directory where the generated USD files are stored.""" + return self._usd_dir + + @property + def usd_file_name(self) -> str: + """The file name of the generated USD file.""" + return self._usd_file_name + + @property + def usd_path(self) -> str: + """The absolute path to the generated USD file.""" + return os.path.join(self.usd_dir, self.usd_file_name) + + @property + def usd_instanceable_meshes_path(self) -> str: + """The relative path to the USD file with meshes. + + The path is with respect to the USD directory :attr:`usd_dir`. This is to ensure that the + mesh references in the generated USD file are resolved relatively. Otherwise, it becomes + difficult to move the USD asset to a different location. + """ + return os.path.join(".", "Props", "instanceable_meshes.usd") + + """ + Implementation specifics. + """ + + @abc.abstractmethod + def _convert_asset(self, cfg: AssetConverterBaseCfg): + """Converts the asset file to USD. + + Args: + cfg: The configuration instance for the input asset to USD conversion. + """ + raise NotImplementedError() + + """ + Private helpers. + """ + + @staticmethod + def _config_to_hash(cfg: AssetConverterBaseCfg) -> str: + """Converts the configuration object and asset file to an MD5 hash of a string. + + .. warning:: + It only checks the main asset file (:attr:`cfg.asset_path`). + + Args: + config : The asset converter configuration object. + + Returns: + An MD5 hash of a string. + """ + + # convert to dict and remove path related info + config_dic = cfg.to_dict() + _ = config_dic.pop("asset_path") + _ = config_dic.pop("usd_dir") + _ = config_dic.pop("usd_file_name") + # convert config dic to bytes + config_bytes = json.dumps(config_dic).encode() + # hash config + md5 = hashlib.md5() + md5.update(config_bytes) + + # read the asset file to observe changes + with open(cfg.asset_path, "rb") as f: + while True: + # read 64kb chunks to avoid memory issues for the large files! + data = f.read(65536) + if not data: + break + md5.update(data) + # return the hash + return md5.hexdigest()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/converters/asset_converter_base_cfg.html b/_modules/omni/isaac/lab/sim/converters/asset_converter_base_cfg.html new file mode 100644 index 0000000000..5e11d53a98 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/converters/asset_converter_base_cfg.html @@ -0,0 +1,607 @@ + + + + + + + + + + + omni.isaac.lab.sim.converters.asset_converter_base_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.converters.asset_converter_base_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class AssetConverterBaseCfg: + """The base configuration class for asset converters.""" + + asset_path: str = MISSING + """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 None. + + If None, it is resolved as ``/tmp/IsaacLab/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 None. + + 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 + ".usd" is appended to the file name. + """ + + force_usd_conversion: bool = False + """Force the conversion of the asset file to usd. Defaults to False. + + If True, then the USD file is always generated. It will overwrite the existing USD file if it exists. + """ + + make_instanceable: bool = True + """Make the generated USD file instanceable. Defaults to True. + + Note: + Instancing helps reduce the memory footprint of the asset when multiple copies of the asset are + used in the scene. For more information, please check the USD documentation on + `scene-graph instancing <https://openusd.org/dev/api/_usd__page__scenegraph_instancing.html>`_. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/converters/mesh_converter.html b/_modules/omni/isaac/lab/sim/converters/mesh_converter.html new file mode 100644 index 0000000000..357a32bc92 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/converters/mesh_converter.html @@ -0,0 +1,804 @@ + + + + + + + + + + + omni.isaac.lab.sim.converters.mesh_converter — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.converters.mesh_converter 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import asyncio
+import os
+
+import omni
+import omni.kit.commands
+import omni.usd
+from omni.isaac.core.utils.extensions import enable_extension
+from pxr import Gf, Tf, Usd, UsdGeom, UsdPhysics, UsdUtils
+
+from omni.isaac.lab.sim.converters.asset_converter_base import AssetConverterBase
+from omni.isaac.lab.sim.converters.mesh_converter_cfg import MeshConverterCfg
+from omni.isaac.lab.sim.schemas import schemas
+from omni.isaac.lab.sim.utils import export_prim_to_file
+
+
+
[文档]class MeshConverter(AssetConverterBase): + """Converter for a mesh file in OBJ / STL / FBX format to a USD file. + + This class wraps around the `omni.kit.asset_converter`_ extension to provide a lazy implementation + for mesh to USD conversion. It stores the output USD file in an instanceable format since that is + what is typically used in all learning related applications. + + To make the asset instanceable, we must follow a certain structure dictated by how USD scene-graph + instancing and physics work. The rigid body component must be added to each instance and not the + referenced asset (i.e. the prototype prim itself). This is because the rigid body component defines + properties that are specific to each instance and cannot be shared under the referenced asset. For + more information, please check the `documentation <https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/rigid-bodies.html#instancing-rigid-bodies>`_. + + Due to the above, we follow the following structure: + + * ``{prim_path}`` - The root prim that is an Xform with the rigid body and mass APIs if configured. + * ``{prim_path}/geometry`` - The prim that contains the mesh and optionally the materials if configured. + If instancing is enabled, this prim will be an instanceable reference to the prototype prim. + + .. _omni.kit.asset_converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_asset-converter.html + + .. caution:: + When converting STL files, Z-up convention is assumed, even though this is not the default for many CAD + export programs. Asset orientation convention can either be modified directly in the CAD program's export + process or an offset can be added within the config in Isaac Lab. + + """ + + cfg: MeshConverterCfg + """The configuration instance for mesh to USD conversion.""" + +
[文档] def __init__(self, cfg: MeshConverterCfg): + """Initializes the class. + + Args: + cfg: The configuration instance for mesh to USD conversion. + """ + super().__init__(cfg=cfg)
+ + """ + Implementation specific methods. + """ + + def _convert_asset(self, cfg: MeshConverterCfg): + """Generate USD from OBJ, STL or FBX. + + The USD file has Y-up axis and is scaled to meters. + The asset hierarchy is arranged as follows: + + .. code-block:: none + mesh_file_basename (default prim) + |- /geometry/Looks + |- /geometry/mesh + + Args: + cfg: The configuration for conversion of mesh to USD. + + Raises: + RuntimeError: If the conversion using the Omniverse asset converter fails. + """ + # resolve mesh name and format + mesh_file_basename, mesh_file_format = os.path.basename(cfg.asset_path).split(".") + mesh_file_format = mesh_file_format.lower() + + # Check if mesh_file_basename is a valid USD identifier + if not Tf.IsValidIdentifier(mesh_file_basename): + # Correct the name to a valid identifier and update the basename + mesh_file_basename_original = mesh_file_basename + mesh_file_basename = Tf.MakeValidIdentifier(mesh_file_basename) + omni.log.warn( + f"Input file name '{mesh_file_basename_original}' is an invalid identifier for the mesh prim path." + f" Renaming it to '{mesh_file_basename}' for the conversion." + ) + + # Convert USD + asyncio.get_event_loop().run_until_complete( + self._convert_mesh_to_usd(in_file=cfg.asset_path, out_file=self.usd_path) + ) + # Create a new stage, set Z up and meters per unit + temp_stage = Usd.Stage.CreateInMemory() + UsdGeom.SetStageUpAxis(temp_stage, UsdGeom.Tokens.z) + UsdGeom.SetStageMetersPerUnit(temp_stage, 1.0) + UsdPhysics.SetStageKilogramsPerUnit(temp_stage, 1.0) + # Add mesh to stage + base_prim = temp_stage.DefinePrim(f"/{mesh_file_basename}", "Xform") + prim = temp_stage.DefinePrim(f"/{mesh_file_basename}/geometry", "Xform") + prim.GetReferences().AddReference(self.usd_path) + temp_stage.SetDefaultPrim(base_prim) + temp_stage.Export(self.usd_path) + + # Open converted USD stage + stage = Usd.Stage.Open(self.usd_path) + # Need to reload the stage to get the new prim structure, otherwise it can be taken from the cache + stage.Reload() + # Add USD to stage cache + stage_id = UsdUtils.StageCache.Get().Insert(stage) + # Get the default prim (which is the root prim) -- "/{mesh_file_basename}" + xform_prim = stage.GetDefaultPrim() + geom_prim = stage.GetPrimAtPath(f"/{mesh_file_basename}/geometry") + # Move all meshes to underneath new Xform + for child_mesh_prim in geom_prim.GetChildren(): + if child_mesh_prim.GetTypeName() == "Mesh": + # Apply collider properties to mesh + if cfg.collision_props is not None: + # -- Collision approximation to mesh + # TODO: Move this to a new Schema: https://github.com/isaac-orbit/IsaacLab/issues/163 + mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(child_mesh_prim) + mesh_collision_api.GetApproximationAttr().Set(cfg.collision_approximation) + # -- Collider properties such as offset, scale, etc. + schemas.define_collision_properties( + prim_path=child_mesh_prim.GetPath(), cfg=cfg.collision_props, stage=stage + ) + # Delete the old Xform and make the new Xform the default prim + stage.SetDefaultPrim(xform_prim) + # Apply default Xform rotation to mesh -> enable to set rotation and scale + omni.kit.commands.execute( + "CreateDefaultXformOnPrimCommand", + prim_path=xform_prim.GetPath(), + **{"stage": stage}, + ) + + # Apply translation, rotation, and scale to the Xform + geom_xform = UsdGeom.Xform(geom_prim) + geom_xform.ClearXformOpOrder() + + # Remove any existing rotation attributes + rotate_attr = geom_prim.GetAttribute("xformOp:rotateXYZ") + if rotate_attr: + geom_prim.RemoveProperty(rotate_attr.GetName()) + + # translation + translate_op = geom_xform.AddTranslateOp(UsdGeom.XformOp.PrecisionDouble) + translate_op.Set(Gf.Vec3d(*cfg.translation)) + # rotation + orient_op = geom_xform.AddOrientOp(UsdGeom.XformOp.PrecisionDouble) + orient_op.Set(Gf.Quatd(*cfg.rotation)) + # scale + scale_op = geom_xform.AddScaleOp(UsdGeom.XformOp.PrecisionDouble) + scale_op.Set(Gf.Vec3d(*cfg.scale)) + + # Handle instanceable + # Create a new Xform prim that will be the prototype prim + if cfg.make_instanceable: + # Export Xform to a file so we can reference it from all instances + export_prim_to_file( + path=os.path.join(self.usd_dir, self.usd_instanceable_meshes_path), + source_prim_path=geom_prim.GetPath(), + stage=stage, + ) + # Delete the original prim that will now be a reference + geom_prim_path = geom_prim.GetPath().pathString + omni.kit.commands.execute("DeletePrims", paths=[geom_prim_path], stage=stage) + # Update references to exported Xform and make it instanceable + geom_undef_prim = stage.DefinePrim(geom_prim_path) + geom_undef_prim.GetReferences().AddReference(self.usd_instanceable_meshes_path, primPath=geom_prim_path) + geom_undef_prim.SetInstanceable(True) + + # Apply mass and rigid body properties after everything else + # Properties are applied to the top level prim to avoid the case where all instances of this + # asset unintentionally share the same rigid body properties + # apply mass properties + if cfg.mass_props is not None: + schemas.define_mass_properties(prim_path=xform_prim.GetPath(), cfg=cfg.mass_props, stage=stage) + # apply rigid body properties + if cfg.rigid_props is not None: + schemas.define_rigid_body_properties(prim_path=xform_prim.GetPath(), cfg=cfg.rigid_props, stage=stage) + + # Save changes to USD stage + stage.Save() + if stage_id is not None: + UsdUtils.StageCache.Get().Erase(stage_id) + + """ + Helper methods. + """ + + @staticmethod + async def _convert_mesh_to_usd(in_file: str, out_file: str, load_materials: bool = True) -> bool: + """Convert mesh from supported file types to USD. + + This function uses the Omniverse Asset Converter extension to convert a mesh file to USD. + It is an asynchronous function and should be called using `asyncio.get_event_loop().run_until_complete()`. + + The converted asset is stored in the USD format in the specified output file. + The USD file has Y-up axis and is scaled to cm. + + Args: + in_file: The file to convert. + out_file: The path to store the output file. + load_materials: Set to True to enable attaching materials defined in the input file + to the generated USD mesh. Defaults to True. + + Returns: + True if the conversion succeeds. + """ + enable_extension("omni.kit.asset_converter") + + import omni.kit.asset_converter + import omni.usd + + # Create converter context + converter_context = omni.kit.asset_converter.AssetConverterContext() + # Set up converter settings + # Don't import/export materials + converter_context.ignore_materials = not load_materials + converter_context.ignore_animations = True + converter_context.ignore_camera = True + converter_context.ignore_light = True + # Merge all meshes into one + converter_context.merge_all_meshes = True + # Sets world units to meters, this will also scale asset if it's centimeters model. + # This does not work right now :(, so we need to scale the mesh manually + converter_context.use_meter_as_world_unit = True + converter_context.baking_scales = True + # Uses double precision for all transform ops. + converter_context.use_double_precision_to_usd_transform_op = True + + # Create converter task + instance = omni.kit.asset_converter.get_instance() + task = instance.create_converter_task(in_file, out_file, None, converter_context) + # Start conversion task and wait for it to finish + success = await task.wait_until_finished() + if not success: + raise RuntimeError(f"Failed to convert {in_file} to USD. Error: {task.get_error_message()}") + return success
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/converters/mesh_converter_cfg.html b/_modules/omni/isaac/lab/sim/converters/mesh_converter_cfg.html new file mode 100644 index 0000000000..b4b8ce2d6d --- /dev/null +++ b/_modules/omni/isaac/lab/sim/converters/mesh_converter_cfg.html @@ -0,0 +1,612 @@ + + + + + + + + + + + omni.isaac.lab.sim.converters.mesh_converter_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.converters.mesh_converter_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from omni.isaac.lab.sim.converters.asset_converter_base_cfg import AssetConverterBaseCfg
+from omni.isaac.lab.sim.schemas import schemas_cfg
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class MeshConverterCfg(AssetConverterBaseCfg): + """The configuration class for MeshConverter.""" + + mass_props: schemas_cfg.MassPropertiesCfg | None = None + """Mass properties to apply to the USD. Defaults to None. + + Note: + If None, then no mass properties will be added. + """ + + rigid_props: schemas_cfg.RigidBodyPropertiesCfg | None = None + """Rigid body properties to apply to the USD. Defaults to None. + + Note: + If None, then no rigid body properties will be added. + """ + + collision_props: schemas_cfg.CollisionPropertiesCfg | None = None + """Collision properties to apply to the USD. Defaults to None. + + Note: + If None, then no collision properties will be added. + """ + + collision_approximation: str = "convexDecomposition" + """Collision approximation method to use. Defaults to "convexDecomposition". + + Valid options are: + "convexDecomposition", "convexHull", "boundingCube", + "boundingSphere", "meshSimplification", or "none" + + "none" causes no collision mesh to be added. + """ + + translation: tuple[float, float, float] = (0.0, 0.0, 0.0) + """The translation of the mesh to the origin. Defaults to (0.0, 0.0, 0.0).""" + + rotation: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0) + """The rotation of the mesh in quaternion format (w, x, y, z). Defaults to (1.0, 0.0, 0.0, 0.0).""" + + scale: tuple[float, float, float] = (1.0, 1.0, 1.0) + """The scale of the mesh. Defaults to (1.0, 1.0, 1.0)."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/converters/urdf_converter.html b/_modules/omni/isaac/lab/sim/converters/urdf_converter.html new file mode 100644 index 0000000000..d6a631fd62 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/converters/urdf_converter.html @@ -0,0 +1,722 @@ + + + + + + + + + + + omni.isaac.lab.sim.converters.urdf_converter — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.converters.urdf_converter 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import os
+
+import omni.kit.commands
+import omni.usd
+from omni.isaac.core.utils.extensions import enable_extension
+from omni.isaac.version import get_version
+from pxr import Usd
+
+from .asset_converter_base import AssetConverterBase
+from .urdf_converter_cfg import UrdfConverterCfg
+
+_DRIVE_TYPE = {
+    "none": 0,
+    "position": 1,
+    "velocity": 2,
+}
+"""Mapping from drive type name to URDF importer drive number."""
+
+_NORMALS_DIVISION = {
+    "catmullClark": 0,
+    "loop": 1,
+    "bilinear": 2,
+    "none": 3,
+}
+"""Mapping from normals division name to urdf importer normals division number."""
+
+
+
[文档]class UrdfConverter(AssetConverterBase): + """Converter for a URDF description file to a USD file. + + This class wraps around the `omni.isaac.urdf_importer`_ extension to provide a lazy implementation + for URDF to USD conversion. It stores the output USD file in an instanceable format since that is + what is typically used in all learning related applications. + + .. caution:: + The current lazy conversion implementation does not automatically trigger USD generation if + only the mesh files used by the URDF are modified. To force generation, either set + :obj:`AssetConverterBaseCfg.force_usd_conversion` to True or delete the output directory. + + .. note:: + From Isaac Sim 2023.1 onwards, the extension name changed from ``omni.isaac.urdf`` to + ``omni.importer.urdf``. This converter class automatically detects the version of Isaac Sim + and uses the appropriate extension. + + The new extension supports a custom XML tag``"dont_collapse"`` for joints. Setting this parameter + to true in the URDF joint tag prevents the child link from collapsing when the associated joint type + is "fixed". + + .. _omni.isaac.urdf_importer: https://docs.omniverse.nvidia.com/isaacsim/latest/ext_omni_isaac_urdf.html + """ + + cfg: UrdfConverterCfg + """The configuration instance for URDF to USD conversion.""" + +
[文档] def __init__(self, cfg: UrdfConverterCfg): + """Initializes the class. + + Args: + cfg: The configuration instance for URDF to USD conversion. + """ + super().__init__(cfg=cfg)
+ + """ + Implementation specific methods. + """ + + def _convert_asset(self, cfg: UrdfConverterCfg): + """Calls underlying Omniverse command to convert URDF to USD. + + Args: + cfg: The URDF conversion configuration. + """ + import_config = self._get_urdf_import_config(cfg) + omni.kit.commands.execute( + "URDFParseAndImportFile", + urdf_path=cfg.asset_path, + import_config=import_config, + dest_path=self.usd_path, + ) + # fix the issue that material paths are not relative + if self.cfg.make_instanceable: + instanced_usd_path = os.path.join(self.usd_dir, self.usd_instanceable_meshes_path) + stage = Usd.Stage.Open(instanced_usd_path) + # resolve all paths relative to layer path + source_layer = stage.GetRootLayer() + omni.usd.resolve_paths(source_layer.identifier, source_layer.identifier) + stage.Save() + + # fix the issue that material paths are not relative + # note: This issue seems to have popped up in Isaac Sim 2023.1.1 + stage = Usd.Stage.Open(self.usd_path) + # resolve all paths relative to layer path + source_layer = stage.GetRootLayer() + omni.usd.resolve_paths(source_layer.identifier, source_layer.identifier) + stage.Save() + + """ + Helper methods. + """ + + def _get_urdf_import_config(self, cfg: UrdfConverterCfg) -> omni.importer.urdf.ImportConfig: + """Create and fill URDF ImportConfig with desired settings + + Args: + cfg: The URDF conversion configuration. + + Returns: + The constructed ``ImportConfig`` object containing the desired settings. + """ + # Enable urdf extension + enable_extension("omni.importer.urdf") + + from omni.importer.urdf import _urdf as omni_urdf + + import_config = omni_urdf.ImportConfig() + + # set the unit scaling factor, 1.0 means meters, 100.0 means cm + import_config.set_distance_scale(1.0) + # set imported robot as default prim + import_config.set_make_default_prim(True) + # add a physics scene to the stage on import if none exists + import_config.set_create_physics_scene(False) + + # -- instancing settings + # meshes will be placed in a separate usd file + import_config.set_make_instanceable(cfg.make_instanceable) + import_config.set_instanceable_usd_path(self.usd_instanceable_meshes_path) + + # -- asset settings + # default density used for links, use 0 to auto-compute + import_config.set_density(cfg.link_density) + # import inertia tensor from urdf, if it is not specified in urdf it will import as identity + import_config.set_import_inertia_tensor(cfg.import_inertia_tensor) + # decompose a convex mesh into smaller pieces for a closer fit + import_config.set_convex_decomp(cfg.convex_decompose_mesh) + import_config.set_subdivision_scheme(_NORMALS_DIVISION["bilinear"]) + + # -- physics settings + # create fix joint for base link + import_config.set_fix_base(cfg.fix_base) + # consolidating links that are connected by fixed joints + import_config.set_merge_fixed_joints(cfg.merge_fixed_joints) + # self collisions between links in the articulation + import_config.set_self_collision(cfg.self_collision) + + # default drive type used for joints + import_config.set_default_drive_type(_DRIVE_TYPE[cfg.default_drive_type]) + # default proportional gains + import_config.set_default_drive_strength(cfg.default_drive_stiffness) + # default derivative gains + import_config.set_default_position_drive_damping(cfg.default_drive_damping) + if get_version()[2] == "4": + # override joint dynamics parsed from urdf + import_config.set_override_joint_dynamics(cfg.override_joint_dynamics) + + return import_config
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/converters/urdf_converter_cfg.html b/_modules/omni/isaac/lab/sim/converters/urdf_converter_cfg.html new file mode 100644 index 0000000000..795afd273d --- /dev/null +++ b/_modules/omni/isaac/lab/sim/converters/urdf_converter_cfg.html @@ -0,0 +1,622 @@ + + + + + + + + + + + omni.isaac.lab.sim.converters.urdf_converter_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.converters.urdf_converter_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.sim.converters.asset_converter_base_cfg import AssetConverterBaseCfg
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class UrdfConverterCfg(AssetConverterBaseCfg): + """The configuration class for UrdfConverter.""" + + link_density = 0.0 + """Default density used for links. Defaults to 0. + + This setting is only effective if ``"inertial"`` properties are missing in the URDF. + """ + + import_inertia_tensor: bool = True + """Import the inertia tensor from urdf. Defaults to True. + + If the ``"inertial"`` tag is missing, then it is imported as an identity. + """ + + convex_decompose_mesh = False + """Decompose a convex mesh into smaller pieces for a closer fit. Defaults to False.""" + + fix_base: bool = MISSING + """Create a fix joint to the root/base link.""" + + merge_fixed_joints: bool = False + """Consolidate links that are connected by fixed joints. Defaults to False.""" + + self_collision: bool = False + """Activate self-collisions between links of the articulation. Defaults to False.""" + + default_drive_type: Literal["none", "position", "velocity"] = "none" + """The drive type used for joints. Defaults to ``"none"``. + + The drive type dictates the loaded joint PD gains and USD attributes for joint control: + + * ``"none"``: The joint stiffness and damping are set to 0.0. + * ``"position"``: The joint stiff and damping are set based on the URDF file or provided configuration. + * ``"velocity"``: The joint stiff is set to zero and damping is based on the URDF file or provided configuration. + """ + + override_joint_dynamics: bool = False + """Override the joint dynamics parsed from the URDF file. Defaults to False.""" + + default_drive_stiffness: float = 0.0 + """The default stiffness of the joint drive. Defaults to 0.0.""" + + default_drive_damping: float = 0.0 + """The default damping of the joint drive. Defaults to 0.0. + + Note: + If ``override_joint_dynamics`` is True, the values parsed from the URDF joint tag ``"<dynamics><damping>"`` are used. + Otherwise, it is overridden by the configured value. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/schemas/schemas.html b/_modules/omni/isaac/lab/sim/schemas/schemas.html new file mode 100644 index 0000000000..6d477d05a5 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/schemas/schemas.html @@ -0,0 +1,1377 @@ + + + + + + + + + + + omni.isaac.lab.sim.schemas.schemas — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.schemas.schemas 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# needed to import for allowing type-hinting: Usd.Stage | None
+from __future__ import annotations
+
+import omni.isaac.core.utils.stage as stage_utils
+import omni.log
+import omni.physx.scripts.utils as physx_utils
+from omni.physx.scripts import deformableUtils as deformable_utils
+from pxr import PhysxSchema, Usd, UsdPhysics
+
+from ..utils import (
+    apply_nested,
+    find_global_fixed_joint_prim,
+    get_all_matching_child_prims,
+    safe_set_attribute_on_usd_schema,
+)
+from . import schemas_cfg
+
+"""
+Articulation root properties.
+"""
+
+
+
[文档]def define_articulation_root_properties( + prim_path: str, cfg: schemas_cfg.ArticulationRootPropertiesCfg, stage: Usd.Stage | None = None +): + """Apply the articulation root schema on the input prim and set its properties. + + 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. + cfg: The configuration for the articulation root. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: When the prim path is not valid. + TypeError: When the prim already has conflicting API schemas. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get articulation USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + # check if prim has articulation applied on it + if not UsdPhysics.ArticulationRootAPI(prim): + UsdPhysics.ArticulationRootAPI.Apply(prim) + # set articulation root properties + modify_articulation_root_properties(prim_path, cfg, stage)
+ + +
[文档]@apply_nested +def modify_articulation_root_properties( + prim_path: str, cfg: schemas_cfg.ArticulationRootPropertiesCfg, stage: Usd.Stage | None = None +) -> bool: + """Modify PhysX parameters for an articulation root prim. + + The `articulation root`_ marks the root of an articulation tree. For floating articulations, this should be on + the root body. For fixed articulations, this API can be on a direct or indirect parent of the root joint + which is fixed to the world. + + The schema comprises of attributes that belong to the `ArticulationRootAPI`_ and `PhysxArticulationAPI`_. + schemas. The latter contains the PhysX parameters for the articulation root. + + The properties are applied to the articulation root prim. The common properties (such as solver position + and velocity iteration counts, sleep threshold, stabilization threshold) take precedence over those specified + in the rigid body schemas for all the rigid bodies in the articulation. + + .. caution:: + When the attribute :attr:`schemas_cfg.ArticulationRootPropertiesCfg.fix_root_link` is set to True, + a fixed joint is created between the root link and the world frame (if it does not already exist). However, + to deal with physics parser limitations, the articulation root schema needs to be applied to the parent of + the root link. + + .. note:: + This function is decorated with :func:`apply_nested` that set the properties to all the prims + (that have the schema applied on them) under the input prim path. + + .. _articulation root: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/Articulations.html + .. _ArticulationRootAPI: https://openusd.org/dev/api/class_usd_physics_articulation_root_a_p_i.html + .. _PhysxArticulationAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_articulation_a_p_i.html + + Args: + prim_path: The prim path to the articulation root. + cfg: The configuration for the articulation root. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + + Raises: + NotImplementedError: When the root prim is not a rigid body and a fixed joint is to be created. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get articulation USD prim + articulation_prim = stage.GetPrimAtPath(prim_path) + # check if prim has articulation applied on it + if not UsdPhysics.ArticulationRootAPI(articulation_prim): + return False + # retrieve the articulation api + physx_articulation_api = PhysxSchema.PhysxArticulationAPI(articulation_prim) + if not physx_articulation_api: + physx_articulation_api = PhysxSchema.PhysxArticulationAPI.Apply(articulation_prim) + + # convert to dict + cfg = cfg.to_dict() + # extract non-USD properties + fix_root_link = cfg.pop("fix_root_link", None) + + # set into physx api + for attr_name, value in cfg.items(): + safe_set_attribute_on_usd_schema(physx_articulation_api, attr_name, value, camel_case=True) + + # fix root link based on input + # we do the fixed joint processing later to not interfere with setting other properties + if fix_root_link is not None: + # check if a global fixed joint exists under the root prim + existing_fixed_joint_prim = find_global_fixed_joint_prim(prim_path) + + # if we found a fixed joint, enable/disable it based on the input + # otherwise, create a fixed joint between the world and the root link + if existing_fixed_joint_prim is not None: + omni.log.info( + f"Found an existing fixed joint for the articulation: '{prim_path}'. Setting it to: {fix_root_link}." + ) + existing_fixed_joint_prim.GetJointEnabledAttr().Set(fix_root_link) + elif fix_root_link: + omni.log.info(f"Creating a fixed joint for the articulation: '{prim_path}'.") + + # note: we have to assume that the root prim is a rigid body, + # i.e. we don't handle the case where the root prim is not a rigid body but has articulation api on it + # Currently, there is no obvious way to get first rigid body link identified by the PhysX parser + if not articulation_prim.HasAPI(UsdPhysics.RigidBodyAPI): + raise NotImplementedError( + f"The articulation prim '{prim_path}' does not have the RigidBodyAPI applied." + " To create a fixed joint, we need to determine the first rigid body link in" + " the articulation tree. However, this is not implemented yet." + ) + + # create a fixed joint between the root link and the world frame + physx_utils.createJoint(stage=stage, joint_type="Fixed", from_prim=None, to_prim=articulation_prim) + + # Having a fixed joint on a rigid body is not treated as "fixed base articulation". + # instead, it is treated as a part of the maximal coordinate tree. + # Moving the articulation root to the parent solves this issue. This is a limitation of the PhysX parser. + # get parent prim + parent_prim = articulation_prim.GetParent() + # apply api to parent + UsdPhysics.ArticulationRootAPI.Apply(parent_prim) + PhysxSchema.PhysxArticulationAPI.Apply(parent_prim) + + # copy the attributes + # -- usd attributes + usd_articulation_api = UsdPhysics.ArticulationRootAPI(articulation_prim) + for attr_name in usd_articulation_api.GetSchemaAttributeNames(): + attr = articulation_prim.GetAttribute(attr_name) + parent_prim.GetAttribute(attr_name).Set(attr.Get()) + # -- physx attributes + physx_articulation_api = PhysxSchema.PhysxArticulationAPI(articulation_prim) + for attr_name in physx_articulation_api.GetSchemaAttributeNames(): + attr = articulation_prim.GetAttribute(attr_name) + parent_prim.GetAttribute(attr_name).Set(attr.Get()) + + # remove api from root + articulation_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI) + articulation_prim.RemoveAPI(PhysxSchema.PhysxArticulationAPI) + + # success + return True
+ + +""" +Rigid body properties. +""" + + +
[文档]def define_rigid_body_properties( + prim_path: str, cfg: schemas_cfg.RigidBodyPropertiesCfg, stage: Usd.Stage | None = None +): + """Apply the rigid body schema on the input prim and set its properties. + + 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. + cfg: The configuration for the rigid body. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: When the prim path is not valid. + TypeError: When the prim already has conflicting API schemas. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + # check if prim has rigid body applied on it + if not UsdPhysics.RigidBodyAPI(prim): + UsdPhysics.RigidBodyAPI.Apply(prim) + # set rigid body properties + modify_rigid_body_properties(prim_path, cfg, stage)
+ + +
[文档]@apply_nested +def modify_rigid_body_properties( + prim_path: str, cfg: schemas_cfg.RigidBodyPropertiesCfg, stage: Usd.Stage | None = None +) -> bool: + """Modify PhysX parameters for a rigid body prim. + + A `rigid body`_ is a single body that can be simulated by PhysX. It can be either dynamic or kinematic. + A dynamic body responds to forces and collisions. A `kinematic body`_ can be moved by the user, but does not + respond to forces. They are similar to having static bodies that can be moved around. + + The schema comprises of attributes that belong to the `RigidBodyAPI`_ and `PhysxRigidBodyAPI`_. + schemas. The latter contains the PhysX parameters for the rigid body. + + .. 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. + + .. _rigid body: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/RigidBodyOverview.html + .. _kinematic body: https://openusd.org/release/wp_rigid_body_physics.html#kinematic-bodies + .. _RigidBodyAPI: https://openusd.org/dev/api/class_usd_physics_rigid_body_a_p_i.html + .. _PhysxRigidBodyAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_rigid_body_a_p_i.html + + Args: + prim_path: The prim path to the rigid body. + cfg: The configuration for the rigid body. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get rigid-body USD prim + rigid_body_prim = stage.GetPrimAtPath(prim_path) + # check if prim has rigid-body applied on it + if not UsdPhysics.RigidBodyAPI(rigid_body_prim): + return False + # retrieve the USD rigid-body api + usd_rigid_body_api = UsdPhysics.RigidBodyAPI(rigid_body_prim) + # retrieve the physx rigid-body api + physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI(rigid_body_prim) + if not physx_rigid_body_api: + physx_rigid_body_api = PhysxSchema.PhysxRigidBodyAPI.Apply(rigid_body_prim) + + # convert to dict + cfg = cfg.to_dict() + # set into USD API + for attr_name in ["rigid_body_enabled", "kinematic_enabled"]: + value = cfg.pop(attr_name, None) + safe_set_attribute_on_usd_schema(usd_rigid_body_api, attr_name, value, camel_case=True) + # set into PhysX API + for attr_name, value in cfg.items(): + safe_set_attribute_on_usd_schema(physx_rigid_body_api, attr_name, value, camel_case=True) + # success + return True
+ + +""" +Collision properties. +""" + + +
[文档]def define_collision_properties( + prim_path: str, cfg: schemas_cfg.CollisionPropertiesCfg, stage: Usd.Stage | None = None +): + """Apply the collision schema on the input prim and set its properties. + + 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. + cfg: The configuration for the collider. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: When the prim path is not valid. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + # check if prim has collision applied on it + if not UsdPhysics.CollisionAPI(prim): + UsdPhysics.CollisionAPI.Apply(prim) + # set collision properties + modify_collision_properties(prim_path, cfg, stage)
+ + +
[文档]@apply_nested +def modify_collision_properties( + prim_path: str, cfg: schemas_cfg.CollisionPropertiesCfg, stage: Usd.Stage | None = None +) -> bool: + """Modify PhysX properties of collider prim. + + 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 <https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/AdvancedCollisionDetection.html>`__. + + .. 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 + + Args: + prim_path: The prim path of parent. + cfg: The configuration for the collider. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + collider_prim = stage.GetPrimAtPath(prim_path) + # check if prim has collision applied on it + if not UsdPhysics.CollisionAPI(collider_prim): + return False + # retrieve the USD collision api + usd_collision_api = UsdPhysics.CollisionAPI(collider_prim) + # retrieve the collision api + physx_collision_api = PhysxSchema.PhysxCollisionAPI(collider_prim) + if not physx_collision_api: + physx_collision_api = PhysxSchema.PhysxCollisionAPI.Apply(collider_prim) + + # convert to dict + cfg = cfg.to_dict() + # set into USD API + for attr_name in ["collision_enabled"]: + value = cfg.pop(attr_name, None) + safe_set_attribute_on_usd_schema(usd_collision_api, attr_name, value, camel_case=True) + # set into PhysX API + for attr_name, value in cfg.items(): + safe_set_attribute_on_usd_schema(physx_collision_api, attr_name, value, camel_case=True) + # success + return True
+ + +""" +Mass 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:`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. + cfg: The configuration for the mass properties. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: When the prim path is not valid. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + # check if prim has mass applied on it + if not UsdPhysics.MassAPI(prim): + UsdPhysics.MassAPI.Apply(prim) + # set mass properties + modify_mass_properties(prim_path, cfg, stage)
+ + +
[文档]@apply_nested +def modify_mass_properties(prim_path: str, cfg: schemas_cfg.MassPropertiesCfg, stage: Usd.Stage | None = None) -> bool: + """Set properties for the mass of a rigid body prim. + + 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 <https://openusd.org/release/wp_rigid_body_physics.html#body-mass-properties>`__. + + .. caution:: + + The mass of an object can be specified in multiple ways and have several conflicting settings + that are resolved based on precedence. Please make sure to understand the precedence rules + before using this property. + + .. 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.MassAPI: https://openusd.org/dev/api/class_usd_physics_mass_a_p_i.html + + Args: + prim_path: The prim path of the rigid body. + cfg: The configuration for the mass properties. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + rigid_prim = stage.GetPrimAtPath(prim_path) + # check if prim has mass API applied on it + if not UsdPhysics.MassAPI(rigid_prim): + return False + # retrieve the USD mass api + usd_physics_mass_api = UsdPhysics.MassAPI(rigid_prim) + + # convert to dict + cfg = cfg.to_dict() + # set into USD API + for attr_name in ["mass", "density"]: + value = cfg.pop(attr_name, None) + safe_set_attribute_on_usd_schema(usd_physics_mass_api, attr_name, value, camel_case=True) + # success + return True
+ + +""" +Contact sensor. +""" + + +
[文档]def activate_contact_sensors(prim_path: str, threshold: float = 0.0, stage: Usd.Stage = None): + """Activate the contact sensor on all rigid bodies under a specified prim path. + + This function adds the PhysX contact report API to all rigid bodies under the specified prim path. + It also sets the force threshold beyond which the contact sensor reports the contact. The contact + reporting API can only be added to rigid bodies. + + Args: + prim_path: The prim path under which to search and prepare contact sensors. + threshold: The threshold for the contact sensor. Defaults to 0.0. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: If the input prim path is not valid. + ValueError: If there are no rigid bodies under the prim path. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get prim + prim: Usd.Prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + # iterate over all children + num_contact_sensors = 0 + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + # check if prim is a rigid body + # nested rigid bodies are not allowed by SDK so we can safely assume that + # if a prim has a rigid body API, it is a rigid body and we don't need to + # check its children + if child_prim.HasAPI(UsdPhysics.RigidBodyAPI): + # set sleep threshold to zero + rb = PhysxSchema.PhysxRigidBodyAPI.Get(stage, prim.GetPrimPath()) + rb.CreateSleepThresholdAttr().Set(0.0) + # add contact report API with threshold of zero + if not child_prim.HasAPI(PhysxSchema.PhysxContactReportAPI): + omni.log.verbose(f"Adding contact report API to prim: '{child_prim.GetPrimPath()}'") + cr_api = PhysxSchema.PhysxContactReportAPI.Apply(child_prim) + else: + omni.log.verbose(f"Contact report API already exists on prim: '{child_prim.GetPrimPath()}'") + cr_api = PhysxSchema.PhysxContactReportAPI.Get(stage, child_prim.GetPrimPath()) + # set threshold to zero + cr_api.CreateThresholdAttr().Set(threshold) + # increment number of contact sensors + num_contact_sensors += 1 + else: + # add all children to tree + all_prims += child_prim.GetChildren() + # check if no contact sensors were found + if num_contact_sensors == 0: + raise ValueError( + f"No contact sensors added to the prim: '{prim_path}'. This means that no rigid bodies" + " are present under this prim. Please check the prim path." + ) + # success + return True
+ + +""" +Joint drive properties. +""" + + +
[文档]@apply_nested +def modify_joint_drive_properties( + prim_path: str, drive_props: schemas_cfg.JointDrivePropertiesCfg, stage: Usd.Stage | None = None +) -> bool: + """Modify PhysX parameters for a joint prim. + + This function checks if the input prim is a prismatic or revolute joint and applies the joint drive schema + on it. If the joint is a tendon (i.e., it has the `PhysxTendonAxisAPI`_ schema applied on it), then the joint + drive schema is not applied. + + Based on the configuration, this method modifies the properties of the joint drive. These properties are + based on the `UsdPhysics.DriveAPI`_ schema. For more information on the properties, please refer to the + official documentation. + + .. caution:: + + We highly recommend modifying joint properties of articulations through the functionalities in the + :mod:`omni.isaac.lab.actuators` module. The methods here are for setting simulation low-level + properties only. + + .. _UsdPhysics.DriveAPI: https://openusd.org/dev/api/class_usd_physics_drive_a_p_i.html + .. _PhysxTendonAxisAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_tendon_axis_a_p_i.html + + Args: + prim_path: The prim path where to apply the joint drive schema. + drive_props: The configuration for the joint drive. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + + Raises: + ValueError: If the input prim path is not valid. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + + # check if prim has joint drive applied on it + if prim.IsA(UsdPhysics.RevoluteJoint): + drive_api_name = "angular" + elif prim.IsA(UsdPhysics.PrismaticJoint): + drive_api_name = "linear" + else: + return False + # check that prim is not a tendon child prim + # note: root prim is what "controls" the tendon so we still want to apply the drive to it + if prim.HasAPI(PhysxSchema.PhysxTendonAxisAPI) and not prim.HasAPI(PhysxSchema.PhysxTendonAxisRootAPI): + return False + + # check if prim has joint drive applied on it + usd_drive_api = UsdPhysics.DriveAPI(prim, drive_api_name) + if not usd_drive_api: + usd_drive_api = UsdPhysics.DriveAPI.Apply(prim, drive_api_name) + + # change the drive type to input + if drive_props.drive_type is not None: + usd_drive_api.CreateTypeAttr().Set(drive_props.drive_type) + + return True
+ + +""" +Fixed tendon properties. +""" + + +
[文档]@apply_nested +def modify_fixed_tendon_properties( + prim_path: str, cfg: schemas_cfg.FixedTendonPropertiesCfg, stage: Usd.Stage | None = None +) -> bool: + """Modify PhysX parameters for a fixed tendon attachment prim. + + A `fixed tendon`_ can be used to link multiple degrees of freedom of articulation joints + through length and limit constraints. For instance, it can be used to set up an equality constraint + between a driven and passive revolute joints. + + The schema comprises of attributes that belong to the `PhysxTendonAxisRootAPI`_ schema. + + .. 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. + + .. _fixed tendon: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/classPxArticulationFixedTendon.html + .. _PhysxTendonAxisRootAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_tendon_axis_root_a_p_i.html + + Args: + prim_path: The prim path to the tendon attachment. + cfg: The configuration for the tendon attachment. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + + Raises: + ValueError: If the input prim path is not valid. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + tendon_prim = stage.GetPrimAtPath(prim_path) + # check if prim has fixed tendon applied on it + has_root_fixed_tendon = tendon_prim.HasAPI(PhysxSchema.PhysxTendonAxisRootAPI) + if not has_root_fixed_tendon: + return False + + # resolve all available instances of the schema since it is multi-instance + for schema_name in tendon_prim.GetAppliedSchemas(): + # only consider the fixed tendon schema + if "PhysxTendonAxisRootAPI" not in schema_name: + continue + # retrieve the USD tendon api + instance_name = schema_name.split(":")[-1] + physx_tendon_axis_api = PhysxSchema.PhysxTendonAxisRootAPI(tendon_prim, instance_name) + + # convert to dict + cfg = cfg.to_dict() + # set into PhysX API + for attr_name, value in cfg.items(): + safe_set_attribute_on_usd_schema(physx_tendon_axis_api, attr_name, value, camel_case=True) + # success + return True
+ + +""" +Deformable body properties. +""" + + +
[文档]def define_deformable_body_properties( + prim_path: str, cfg: schemas_cfg.DeformableBodyPropertiesCfg, stage: Usd.Stage | None = None +): + """Apply the deformable body schema on the input prim and set its properties. + + See :func:`modify_deformable_body_properties` for more details on how the properties are set. + + .. note:: + If the input prim is not a mesh, this function will traverse the prim and find the first mesh + under it. If no mesh or multiple meshes are found, an error is raised. This is because the deformable + body schema can only be applied to a single mesh. + + Args: + prim_path: The prim path where to apply the deformable body schema. + cfg: The configuration for the deformable body. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: When the prim path is not valid. + ValueError: When the prim has no mesh or multiple meshes. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim path is valid + if not prim.IsValid(): + raise ValueError(f"Prim path '{prim_path}' is not valid.") + + # traverse the prim and get the mesh + matching_prims = get_all_matching_child_prims(prim_path, lambda p: p.GetTypeName() == "Mesh") + # check if the mesh is valid + if len(matching_prims) == 0: + raise ValueError(f"Could not find any mesh in '{prim_path}'. Please check asset.") + if len(matching_prims) > 1: + # get list of all meshes found + mesh_paths = [p.GetPrimPath() for p in matching_prims] + raise ValueError( + f"Found multiple meshes in '{prim_path}': {mesh_paths}." + " Deformable body schema can only be applied to one mesh." + ) + + # get deformable-body USD prim + mesh_prim = matching_prims[0] + # check if prim has deformable-body applied on it + if not PhysxSchema.PhysxDeformableBodyAPI(mesh_prim): + PhysxSchema.PhysxDeformableBodyAPI.Apply(mesh_prim) + # set deformable body properties + modify_deformable_body_properties(mesh_prim.GetPrimPath(), cfg, stage)
+ + +
[文档]@apply_nested +def modify_deformable_body_properties( + prim_path: str, cfg: schemas_cfg.DeformableBodyPropertiesCfg, stage: Usd.Stage | None = None +): + """Modify PhysX parameters for a deformable body prim. + + A `deformable body`_ is a single body that can be simulated by PhysX. Unlike rigid bodies, deformable bodies + support relative motion of the nodes in the mesh. Consequently, they can be used to simulate deformations + under applied forces. + + PhysX soft body simulation employs Finite Element Analysis (FEA) to simulate the deformations of the mesh. + It uses two tetrahedral meshes to represent the deformable body: + + 1. **Simulation mesh**: This mesh is used for the simulation and is the one that is deformed by the solver. + 2. **Collision mesh**: This mesh only needs to match the surface of the simulation mesh and is used for + collision detection. + + For most applications, we assume that the above two meshes are computed from the "render mesh" of the deformable + body. The render mesh is the mesh that is visible in the scene and is used for rendering purposes. It is composed + of triangles and is the one that is used to compute the above meshes based on PhysX cookings. + + The schema comprises of attributes that belong to the `PhysxDeformableBodyAPI`_. schemas containing the PhysX + parameters for the deformable body. + + .. caution:: + The deformable body schema is still under development by the Omniverse team. The current implementation + works with the PhysX schemas shipped with Isaac Sim 4.0.0 onwards. It may change in future releases. + + .. 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. + + .. _deformable body: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html + .. _PhysxDeformableBodyAPI: https://docs.omniverse.nvidia.com/kit/docs/omni_usd_schema_physics/104.2/class_physx_schema_physx_deformable_a_p_i.html + + Args: + prim_path: The prim path to the deformable body. + cfg: The configuration for the deformable body. + stage: The stage where to find the prim. Defaults to None, in which case the + current stage is used. + + Returns: + True if the properties were successfully set, False otherwise. + """ + # obtain stage + if stage is None: + stage = stage_utils.get_current_stage() + + # get deformable-body USD prim + deformable_body_prim = stage.GetPrimAtPath(prim_path) + + # check if the prim is valid and has the deformable-body API + if not deformable_body_prim.IsValid() or not PhysxSchema.PhysxDeformableBodyAPI(deformable_body_prim): + return False + + # retrieve the physx deformable-body api + physx_deformable_body_api = PhysxSchema.PhysxDeformableBodyAPI(deformable_body_prim) + # retrieve the physx deformable api + physx_deformable_api = PhysxSchema.PhysxDeformableAPI(physx_deformable_body_api) + + # convert to dict + cfg = cfg.to_dict() + # set into deformable body API + attr_kwargs = { + attr_name: cfg.pop(attr_name) + for attr_name in [ + "kinematic_enabled", + "collision_simplification", + "collision_simplification_remeshing", + "collision_simplification_remeshing_resolution", + "collision_simplification_target_triangle_count", + "collision_simplification_force_conforming", + "simulation_hexahedral_resolution", + "solver_position_iteration_count", + "vertex_velocity_damping", + "sleep_damping", + "sleep_threshold", + "settling_threshold", + "self_collision", + "self_collision_filter_distance", + ] + } + status = deformable_utils.add_physx_deformable_body(stage, prim_path=prim_path, **attr_kwargs) + # check if the deformable body was successfully added + if not status: + return False + + # obtain the PhysX collision API (this is set when the deformable body is added) + physx_collision_api = PhysxSchema.PhysxCollisionAPI(deformable_body_prim) + + # set into PhysX API + for attr_name, value in cfg.items(): + if attr_name in ["rest_offset", "contact_offset"]: + safe_set_attribute_on_usd_schema(physx_collision_api, attr_name, value, camel_case=True) + else: + safe_set_attribute_on_usd_schema(physx_deformable_api, attr_name, value, camel_case=True) + + # success + return True
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/schemas/schemas_cfg.html b/_modules/omni/isaac/lab/sim/schemas/schemas_cfg.html new file mode 100644 index 0000000000..f739365d32 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/schemas/schemas_cfg.html @@ -0,0 +1,926 @@ + + + + + + + + + + + omni.isaac.lab.sim.schemas.schemas_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.schemas.schemas_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class ArticulationRootPropertiesCfg: + """Properties to apply to the root of an articulation. + + See :meth:`modify_articulation_root_properties` for more information. + + .. note:: + 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. + """ + + articulation_enabled: bool | None = None + """Whether to enable or disable articulation.""" + + enabled_self_collisions: bool | None = None + """Whether to enable or disable self-collisions.""" + + solver_position_iteration_count: int | None = None + """Solver position iteration counts for the body.""" + + solver_velocity_iteration_count: int | None = None + """Solver position iteration counts for the body.""" + + sleep_threshold: float | None = None + """Mass-normalized kinetic energy threshold below which an actor may go to sleep.""" + + stabilization_threshold: float | None = None + """The mass-normalized kinetic energy threshold below which an articulation may participate in stabilization.""" + + fix_root_link: bool | None = None + """Whether to fix the root link of the articulation. + + * If set to None, the root link is not modified. + * If the articulation already has a fixed root link, this flag will enable or disable the fixed joint. + * If the articulation does not have a fixed root link, this flag will create a fixed joint between the world + frame and the root link. The joint is created with the name "FixedJoint" under the articulation prim. + + .. note:: + This is a non-USD schema property. It is handled by the :meth:`modify_articulation_root_properties` function. + + """
+ + +
[文档]@configclass +class RigidBodyPropertiesCfg: + """Properties to apply to a rigid body. + + See :meth:`modify_rigid_body_properties` for more information. + + .. note:: + 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. + """ + + rigid_body_enabled: bool | None = None + """Whether to enable or disable the rigid body.""" + + kinematic_enabled: bool | None = None + """Determines whether the body is kinematic or not. + + A kinematic body is a body that is moved through animated poses or through user defined poses. The simulation + still derives velocities for the kinematic body based on the external motion. + + For more information on kinematic bodies, please refer to the `documentation <https://openusd.org/release/wp_rigid_body_physics.html#kinematic-bodies>`_. + """ + + disable_gravity: bool | None = None + """Disable gravity for the actor.""" + + linear_damping: float | None = None + """Linear damping for the body.""" + + angular_damping: float | None = None + """Angular damping for the body.""" + + max_linear_velocity: float | None = None + """Maximum linear velocity for rigid bodies (in m/s).""" + + max_angular_velocity: float | None = None + """Maximum angular velocity for rigid bodies (in deg/s).""" + + max_depenetration_velocity: float | None = None + """Maximum depenetration velocity permitted to be introduced by the solver (in m/s).""" + + max_contact_impulse: float | None = None + """The limit on the impulse that may be applied at a contact.""" + + enable_gyroscopic_forces: bool | None = None + """Enables computation of gyroscopic forces on the rigid body.""" + + retain_accelerations: bool | None = None + """Carries over forces/accelerations over sub-steps.""" + + solver_position_iteration_count: int | None = None + """Solver position iteration counts for the body.""" + + solver_velocity_iteration_count: int | None = None + """Solver position iteration counts for the body.""" + + sleep_threshold: float | None = None + """Mass-normalized kinetic energy threshold below which an actor may go to sleep.""" + + stabilization_threshold: float | None = None + """The mass-normalized kinetic energy threshold below which an actor may participate in stabilization."""
+ + +
[文档]@configclass +class CollisionPropertiesCfg: + """Properties to apply to colliders in a rigid body. + + See :meth:`modify_collision_properties` for more information. + + .. note:: + 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. + """ + + collision_enabled: bool | None = None + """Whether to enable or disable collisions.""" + + contact_offset: float | None = None + """Contact offset for the collision shape (in m). + + The collision detector generates contact points as soon as two shapes get closer than the sum of their + contact offsets. This quantity should be non-negative which means that contact generation can potentially start + before the shapes actually penetrate. + """ + + rest_offset: float | None = None + """Rest offset for the collision shape (in m). + + The rest offset quantifies how close a shape gets to others at rest, At rest, the distance between two + vertically stacked objects is the sum of their rest offsets. If a pair of shapes have a positive rest + offset, the shapes will be separated at rest by an air gap. + """ + + torsional_patch_radius: float | None = None + """Radius of the contact patch for applying torsional friction (in m). + + It is used to approximate rotational friction introduced by the compression of contacting surfaces. + If the radius is zero, no torsional friction is applied. + """ + + min_torsional_patch_radius: float | None = None + """Minimum radius of the contact patch for applying torsional friction (in m)."""
+ + +
[文档]@configclass +class MassPropertiesCfg: + """Properties to define explicit mass properties of a rigid body. + + See :meth:`modify_mass_properties` for more information. + + .. note:: + 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. + """ + + mass: float | None = None + """The mass of the rigid body (in kg). + + Note: + If non-zero, the mass is ignored and the density is used to compute the mass. + """ + + density: float | None = None + """The density of the rigid body (in kg/m^3). + + The density indirectly defines the mass of the rigid body. It is generally computed using the collision + approximation of the body. + """
+ + +
[文档]@configclass +class JointDrivePropertiesCfg: + """Properties to define the drive mechanism of a joint. + + See :meth:`modify_joint_drive_properties` for more information. + + .. note:: + 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. + """ + + drive_type: Literal["force", "acceleration"] | None = None + """Joint drive type to apply. + + If the drive type is "force", then the joint is driven by a force. If the drive type is "acceleration", + then the joint is driven by an acceleration (usually used for kinematic joints). + """
+ + +
[文档]@configclass +class FixedTendonPropertiesCfg: + """Properties to define fixed tendons of an articulation. + + See :meth:`modify_fixed_tendon_properties` for more information. + + .. note:: + 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. + """ + + tendon_enabled: bool | None = None + """Whether to enable or disable the tendon.""" + + stiffness: float | None = None + """Spring stiffness term acting on the tendon's length.""" + + damping: float | None = None + """The damping term acting on both the tendon length and the tendon-length limits.""" + + limit_stiffness: float | None = None + """Limit stiffness term acting on the tendon's length limits.""" + + offset: float | None = None + """Length offset term for the tendon. + + It defines an amount to be added to the accumulated length computed for the tendon. This allows the application + to actuate the tendon by shortening or lengthening it. + """ + + rest_length: float | None = None + """Spring rest length of the tendon."""
+ + +
[文档]@configclass +class DeformableBodyPropertiesCfg: + """Properties to apply to a deformable body. + + A deformable body is a body that can deform under forces. The configuration allows users to specify + the properties of the deformable body, such as the solver iteration counts, damping, and self-collision. + + An FEM-based deformable body is created by providing a collision mesh and simulation mesh. The collision mesh + is used for collision detection and the simulation mesh is used for simulation. The collision mesh is usually + a simplified version of the simulation mesh. + + Based on the above, the PhysX team provides APIs to either set the simulation and collision mesh directly + (by specifying the points) or to simplify the collision mesh based on the simulation mesh. The simplification + process involves remeshing the collision mesh and simplifying it based on the target triangle count. + + Since specifying the collision mesh points directly is not a common use case, we only expose the parameters + to simplify the collision mesh based on the simulation mesh. If you want to provide the collision mesh points, + please open an issue on the repository and we can add support for it. + + See :meth:`modify_deformable_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 + the properties and leave the rest as-is. + """ + + deformable_enabled: bool | None = None + """Enables deformable body.""" + + kinematic_enabled: bool = False + """Enables kinematic body. Defaults to False, which means that the body is not kinematic. + + Similar to rigid bodies, this allows setting user-driven motion for the deformable body. For more information, + please refer to the `documentation <https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html#kinematic-soft-bodies>`__. + """ + + self_collision: bool | None = None + """Whether to enable or disable self-collisions for the deformable body based on the rest position distances.""" + + self_collision_filter_distance: float | None = None + """Penetration value that needs to get exceeded before contacts for self collision are generated. + + This parameter must be greater than of equal to twice the :attr:`rest_offset` value. + + This value has an effect only if :attr:`self_collision` is enabled. + """ + + settling_threshold: float | None = None + """Threshold vertex velocity (in m/s) under which sleep damping is applied in addition to velocity damping.""" + + sleep_damping: float | None = None + """Coefficient for the additional damping term if fertex velocity drops below setting threshold.""" + + sleep_threshold: float | None = None + """The velocity threshold (in m/s) under which the vertex becomes a candidate for sleeping in the next step.""" + + solver_position_iteration_count: int | None = None + """Number of the solver positional iterations per step. Range is [1,255]""" + + vertex_velocity_damping: float | None = None + """Coefficient for artificial damping on the vertex velocity. + + This parameter can be used to approximate the effect of air drag on the deformable body. + """ + + simulation_hexahedral_resolution: int = 10 + """The target resolution for the hexahedral mesh used for simulation. Defaults to 10. + + Note: + This value is ignored if the user provides the simulation mesh points directly. However, we assume that + most users will not provide the simulation mesh points directly. If you want to provide the simulation mesh + directly, please set this value to :obj:`None`. + """ + + collision_simplification: bool = True + """Whether or not to simplify the collision mesh before creating a soft body out of it. Defaults to True. + + Note: + This flag is ignored if the user provides the simulation mesh points directly. However, we assume that + most users will not provide the simulation mesh points directly. Hence, this flag is enabled by default. + + If you want to provide the simulation mesh points directly, please set this flag to False. + """ + + collision_simplification_remeshing: bool = True + """Whether or not the collision mesh should be remeshed before simplification. Defaults to True. + + This parameter is ignored if :attr:`collision_simplification` is False. + """ + + collision_simplification_remeshing_resolution: int = 0 + """The resolution used for remeshing. Defaults to 0, which means that a heuristic is used to determine the + resolution. + + This parameter is ignored if :attr:`collision_simplification_remeshing` is False. + """ + + collision_simplification_target_triangle_count: int = 0 + """The target triangle count used for the simplification. Defaults to 0, which means that a heuristic based on + the :attr:`simulation_hexahedral_resolution` is used to determine the target count. + + This parameter is ignored if :attr:`collision_simplification` is False. + """ + + collision_simplification_force_conforming: bool = True + """Whether or not the simplification should force the output mesh to conform to the input mesh. Defaults to True. + + The flag indicates that the tretrahedralizer used to generate the collision mesh should produce tetrahedra + that conform to the triangle mesh. If False, the simplifier uses the output from the tretrahedralizer used. + + This parameter is ignored if :attr:`collision_simplification` is False. + """ + + contact_offset: float | None = None + """Contact offset for the collision shape (in m). + + The collision detector generates contact points as soon as two shapes get closer than the sum of their + contact offsets. This quantity should be non-negative which means that contact generation can potentially start + before the shapes actually penetrate. + """ + + rest_offset: float | None = None + """Rest offset for the collision shape (in m). + + The rest offset quantifies how close a shape gets to others at rest, At rest, the distance between two + vertically stacked objects is the sum of their rest offsets. If a pair of shapes have a positive rest + offset, the shapes will be separated at rest by an air gap. + """ + + max_depenetration_velocity: float | None = None + """Maximum depenetration velocity permitted to be introduced by the solver (in m/s)."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/simulation_cfg.html b/_modules/omni/isaac/lab/sim/simulation_cfg.html new file mode 100644 index 0000000000..574553c8d2 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/simulation_cfg.html @@ -0,0 +1,846 @@ + + + + + + + + + + + omni.isaac.lab.sim.simulation_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.simulation_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Base configuration of the environment.
+
+This module defines the general configuration of the environment. It includes parameters for
+configuring the environment instances, viewer settings, and simulation parameters.
+"""
+
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+from .spawners.materials import RigidBodyMaterialCfg
+
+
+
[文档]@configclass +class PhysxCfg: + """Configuration for PhysX solver-related parameters. + + These parameters are used to configure the PhysX solver. For more information, see the `PhysX 5 SDK + documentation`_. + + PhysX 5 supports GPU-accelerated physics simulation. This is enabled by default, but can be disabled + by setting the :attr:`~SimulationCfg.device` to ``cpu`` in :class:`SimulationCfg`. Unlike CPU PhysX, the GPU + simulation feature is unable to dynamically grow all the buffers. Therefore, it is necessary to provide + a reasonable estimate of the buffer sizes for GPU features. If insufficient buffer sizes are provided, the + simulation will fail with errors and lead to adverse behaviors. The buffer sizes can be adjusted through the + ``gpu_*`` parameters. + + .. _PhysX 5 SDK documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/classPxSceneDesc.html + + """ + + solver_type: Literal[0, 1] = 1 + """The type of solver to use.Default is 1 (TGS). + + Available solvers: + + * :obj:`0`: PGS (Projective Gauss-Seidel) + * :obj:`1`: TGS (Truncated Gauss-Seidel) + """ + + min_position_iteration_count: int = 1 + """Minimum number of solver position iterations (rigid bodies, cloth, particles etc.). Default is 1. + + .. note:: + + Each physics actor in Omniverse specifies its own solver iteration count. The solver takes + the number of iterations specified by the actor with the highest iteration and clamps it to + the range ``[min_position_iteration_count, max_position_iteration_count]``. + """ + + max_position_iteration_count: int = 255 + """Maximum number of solver position iterations (rigid bodies, cloth, particles etc.). Default is 255. + + .. note:: + + Each physics actor in Omniverse specifies its own solver iteration count. The solver takes + the number of iterations specified by the actor with the highest iteration and clamps it to + the range ``[min_position_iteration_count, max_position_iteration_count]``. + """ + + min_velocity_iteration_count: int = 0 + """Minimum number of solver velocity iterations (rigid bodies, cloth, particles etc.). Default is 0. + + .. note:: + + Each physics actor in Omniverse specifies its own solver iteration count. The solver takes + the number of iterations specified by the actor with the highest iteration and clamps it to + the range ``[min_velocity_iteration_count, max_velocity_iteration_count]``. + """ + + max_velocity_iteration_count: int = 255 + """Maximum number of solver velocity iterations (rigid bodies, cloth, particles etc.). Default is 255. + + .. note:: + + Each physics actor in Omniverse specifies its own solver iteration count. The solver takes + the number of iterations specified by the actor with the highest iteration and clamps it to + the range ``[min_velocity_iteration_count, max_velocity_iteration_count]``. + """ + + enable_ccd: bool = False + """Enable a second broad-phase pass that makes it possible to prevent objects from tunneling through each other. + Default is False.""" + + enable_stabilization: bool = True + """Enable/disable additional stabilization pass in solver. Default is True.""" + + enable_enhanced_determinism: bool = False + """Enable/disable improved determinism at the expense of performance. Defaults to False. + + For more information on PhysX determinism, please check `here`_. + + .. _here: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/RigidBodyDynamics.html#enhanced-determinism + """ + + bounce_threshold_velocity: float = 0.5 + """Relative velocity threshold for contacts to bounce (in m/s). Default is 0.5 m/s.""" + + friction_offset_threshold: float = 0.04 + """Threshold for contact point to experience friction force (in m). Default is 0.04 m.""" + + friction_correlation_distance: float = 0.025 + """Distance threshold for merging contacts into a single friction anchor point (in m). Default is 0.025 m.""" + + gpu_max_rigid_contact_count: int = 2**23 + """Size of rigid contact stream buffer allocated in pinned host memory. Default is 2 ** 23.""" + + gpu_max_rigid_patch_count: int = 5 * 2**15 + """Size of the rigid contact patch stream buffer allocated in pinned host memory. Default is 5 * 2 ** 15.""" + + gpu_found_lost_pairs_capacity: int = 2**21 + """Capacity of found and lost buffers allocated in GPU global memory. Default is 2 ** 21. + + This is used for the found/lost pair reports in the BP. + """ + + gpu_found_lost_aggregate_pairs_capacity: int = 2**25 + """Capacity of found and lost buffers in aggregate system allocated in GPU global memory. + Default is 2 ** 25. + + This is used for the found/lost pair reports in AABB manager. + """ + + gpu_total_aggregate_pairs_capacity: int = 2**21 + """Capacity of total number of aggregate pairs allocated in GPU global memory. Default is 2 ** 21.""" + + gpu_collision_stack_size: int = 2**26 + """Size of the collision stack buffer allocated in pinned host memory. Default is 2 ** 26.""" + + gpu_heap_capacity: int = 2**26 + """Initial capacity of the GPU and pinned host memory heaps. Additional memory will be allocated + if more memory is required. Default is 2 ** 26.""" + + gpu_temp_buffer_capacity: int = 2**24 + """Capacity of temp buffer allocated in pinned host memory. Default is 2 ** 24.""" + + gpu_max_num_partitions: int = 8 + """Limitation for the partitions in the GPU dynamics pipeline. Default is 8. + + This variable must be power of 2. A value greater than 32 is currently not supported. Range: (1, 32) + """ + + gpu_max_soft_body_contacts: int = 2**20 + """Size of soft body contacts stream buffer allocated in pinned host memory. Default is 2 ** 20.""" + + gpu_max_particle_contacts: int = 2**20 + """Size of particle contacts stream buffer allocated in pinned host memory. Default is 2 ** 20."""
+ + +
[文档]@configclass +class RenderCfg: + """Configuration for Omniverse RTX Renderer. + + These parameters are used to configure the Omniverse RTX Renderer. + For more information, see the `Omniverse RTX Renderer documentation`_. + + .. _Omniverse RTX Renderer documentation: https://docs.omniverse.nvidia.com/materials-and-rendering/latest/rtx-renderer.html + """ + + enable_translucency: bool = False + """Enables translucency for specular transmissive surfaces such as glass at the cost of some performance. Default is False.""" + + enable_reflections: bool = False + """Enables reflections at the cost of some performance. Default is False.""" + + enable_global_illumination: bool = False + """Enables Diffused Global Illumination at the cost of some performance. Default is False.""" + + antialiasing_mode: Literal["Off", "FXAA", "DLSS", "TAA", "DLAA"] = "DLSS" + """Selects the anti-aliasing mode to use. Defaults to DLSS.""" + + enable_dlssg: bool = False + """"Enables the use of DLSS-G. + DLSS Frame Generation boosts performance by using AI to generate more frames. + DLSS analyzes sequential frames and motion data to create additional high quality frames. + This feature requires an Ada Lovelace architecture GPU. + Enabling this feature also enables additional thread-related activities, which can hurt performance. + Default is False.""" + + dlss_mode: Literal[0, 1, 2, 3] = 0 + """For DLSS anti-aliasing, selects the performance/quality tradeoff mode. + Valid values are 0 (Performance), 1 (Balanced), 2 (Quality), or 3 (Auto). Default is 0.""" + + enable_direct_lighting: bool = True + """Enable direct light contributions from lights.""" + + samples_per_pixel: int = 1 + """Defines the Direct Lighting samples per pixel. + Higher values increase the direct lighting quality at the cost of performance. Default is 1.""" + + enable_shadows: bool = True + """Enables shadows at the cost of performance. When disabled, lights will not cast shadows. Defaults to True.""" + + enable_ambient_occlusion: bool = False + """Enables ambient occlusion at the cost of some performance. Default is False."""
+ + +
[文档]@configclass +class SimulationCfg: + """Configuration for simulation physics.""" + + physics_prim_path: str = "/physicsScene" + """The prim path where the USD PhysicsScene is created. Default is "/physicsScene".""" + + device: str = "cuda:0" + """The device to run the simulation on. Default is ``"cuda:0"``. + + Valid options are: + + - ``"cpu"``: Use CPU. + - ``"cuda"``: Use GPU, where the device ID is inferred from :class:`~omni.isaac.lab.app.AppLauncher`'s config. + - ``"cuda:N"``: Use GPU, where N is the device ID. For example, "cuda:0". + """ + + dt: float = 1.0 / 60.0 + """The physics simulation time-step (in seconds). Default is 0.0167 seconds.""" + + render_interval: int = 1 + """The number of physics simulation steps per rendering step. Default is 1.""" + + gravity: tuple[float, float, float] = (0.0, 0.0, -9.81) + """The gravity vector (in m/s^2). Default is (0.0, 0.0, -9.81). + + If set to (0.0, 0.0, 0.0), gravity is disabled. + """ + + enable_scene_query_support: bool = False + """Enable/disable scene query support for collision shapes. Default is False. + + This flag allows performing collision queries (raycasts, sweeps, and overlaps) on actors and + attached shapes in the scene. This is useful for implementing custom collision detection logic + outside of the physics engine. + + If set to False, the physics engine does not create the scene query manager and the scene query + functionality will not be available. However, this provides some performance speed-up. + + Note: + 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. + """ + + use_fabric: bool = True + """Enable/disable reading of physics buffers directly. Default is True. + + When running the simulation, updates in the states in the scene is normally synchronized with USD. + This leads to an overhead in reading the data and does not scale well with massive parallelization. + This flag allows disabling the synchronization and reading the data directly from the physics buffers. + + It is recommended to set this flag to :obj:`True` when running the simulation with a large number + of primitives in the scene. + + Note: + When enabled, the GUI will not update the physics parameters in real-time. To enable real-time + updates, please set this flag to :obj:`False`. + """ + + disable_contact_processing: bool = False + """Enable/disable contact processing. Default is False. + + By default, the physics engine processes all the contacts in the scene. However, reporting this contact + 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. + + .. note:: + + It is required to set this flag to :obj:`True` when using the TensorAPIs for contact reporting. + """ + + physx: PhysxCfg = PhysxCfg() + """PhysX solver settings. Default is PhysxCfg().""" + + physics_material: RigidBodyMaterialCfg = RigidBodyMaterialCfg() + """Default physics material settings for rigid bodies. Default is RigidBodyMaterialCfg(). + + The physics engine defaults to this physics material for all the rigid body prims that do not have any + physics material specified on them. + + The material is created at the path: ``{physics_prim_path}/defaultMaterial``. + """ + + render: RenderCfg = RenderCfg() + """Render settings. Default is RenderCfg()."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/simulation_context.html b/_modules/omni/isaac/lab/sim/simulation_context.html new file mode 100644 index 0000000000..cd57c692ca --- /dev/null +++ b/_modules/omni/isaac/lab/sim/simulation_context.html @@ -0,0 +1,1354 @@ + + + + + + + + + + + omni.isaac.lab.sim.simulation_context — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.simulation_context 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import builtins
+import enum
+import numpy as np
+import sys
+import torch
+import traceback
+import weakref
+from collections.abc import Iterator
+from contextlib import contextmanager
+from typing import Any
+
+import carb
+import omni.isaac.core.utils.stage as stage_utils
+import omni.log
+import omni.physx
+from omni.isaac.core.simulation_context import SimulationContext as _SimulationContext
+from omni.isaac.core.utils.viewports import set_camera_view
+from omni.isaac.version import get_version
+from pxr import Gf, PhysxSchema, Usd, UsdPhysics
+
+from .simulation_cfg import SimulationCfg
+from .spawners import DomeLightCfg, GroundPlaneCfg
+from .utils import bind_physics_material
+
+
+
[文档]class SimulationContext(_SimulationContext): + """A class to control simulation-related events such as physics stepping and rendering. + + The simulation context helps control various simulation aspects. This includes: + + * configure the simulator with different settings such as the physics time-step, the number of physics substeps, + and the physics solver parameters (for more information, see :class:`omni.isaac.lab.sim.SimulationCfg`) + * playing, pausing, stepping and stopping the simulation + * adding and removing callbacks to different simulation events such as physics stepping, rendering, etc. + + This class inherits from the :class:`omni.isaac.core.simulation_context.SimulationContext` class and + adds additional functionalities such as setting up the simulation context with a configuration object, + exposing other commonly used simulator-related functions, and performing version checks of Isaac Sim + to ensure compatibility between releases. + + The simulation context is a singleton object. This means that there can only be one instance + of the simulation context at any given time. This is enforced by the parent class. Therefore, it is + not possible to create multiple instances of the simulation context. Instead, the simulation context + can be accessed using the ``instance()`` method. + + .. attention:: + Since we only support the `PyTorch <https://pytorch.org/>`_ backend for simulation, the + simulation context is configured to use the ``torch`` backend by default. This means that + all the data structures used in the simulation are ``torch.Tensor`` objects. + + The simulation context can be used in two different modes of operations: + + 1. **Standalone python script**: In this mode, the user has full control over the simulation and + can trigger stepping events synchronously (i.e. as a blocking call). In this case the user + has to manually call :meth:`step` step the physics simulation and :meth:`render` to + render the scene. + 2. **Omniverse extension**: In this mode, the user has limited control over the simulation stepping + and all the simulation events are triggered asynchronously (i.e. as a non-blocking call). In this + case, the user can only trigger the simulation to start, pause, and stop. The simulation takes + care of stepping the physics simulation and rendering the scene. + + Based on above, for most functions in this class there is an equivalent function that is suffixed + with ``_async``. The ``_async`` functions are used in the Omniverse extension mode and + the non-``_async`` functions are used in the standalone python script mode. + """ + +
[文档] class RenderMode(enum.IntEnum): + """Different rendering modes for the simulation. + + Render modes correspond to how the viewport and other UI elements (such as listeners to keyboard or mouse + events) are updated. There are three main components that can be updated when the simulation is rendered: + + 1. **UI elements and other extensions**: These are UI elements (such as buttons, sliders, etc.) and other + extensions that are running in the background that need to be updated when the simulation is running. + 2. **Cameras**: These are typically based on Hydra textures and are used to render the scene from different + viewpoints. They can be attached to a viewport or be used independently to render the scene. + 3. **Viewports**: These are windows where you can see the rendered scene. + + Updating each of the above components has a different overhead. For example, updating the viewports is + computationally expensive compared to updating the UI elements. Therefore, it is useful to be able to + control what is updated when the simulation is rendered. This is where the render mode comes in. There are + four different render modes: + + * :attr:`NO_GUI_OR_RENDERING`: The simulation is running without a GUI and off-screen rendering flag is disabled, + so none of the above are updated. + * :attr:`NO_RENDERING`: No rendering, where only 1 is updated at a lower rate. + * :attr:`PARTIAL_RENDERING`: Partial rendering, where only 1 and 2 are updated. + * :attr:`FULL_RENDERING`: Full rendering, where everything (1, 2, 3) is updated. + + .. _Viewports: https://docs.omniverse.nvidia.com/extensions/latest/ext_viewport.html + """ + + NO_GUI_OR_RENDERING = -1 + """The simulation is running without a GUI and off-screen rendering is disabled.""" + NO_RENDERING = 0 + """No rendering, where only other UI elements are updated at a lower rate.""" + PARTIAL_RENDERING = 1 + """Partial rendering, where the simulation cameras and UI elements are updated.""" + FULL_RENDERING = 2 + """Full rendering, where all the simulation viewports, cameras and UI elements are updated."""
+ +
[文档] def __init__(self, cfg: SimulationCfg | None = None): + """Creates a simulation context to control the simulator. + + Args: + cfg: The configuration of the simulation. Defaults to None, + in which case the default configuration is used. + """ + # store input + if cfg is None: + cfg = SimulationCfg() + # check that the config is valid + cfg.validate() + self.cfg = cfg + # check that simulation is running + if stage_utils.get_current_stage() is None: + raise RuntimeError("The stage has not been created. Did you run the simulator?") + + # set flags for simulator + # acquire settings interface + carb_settings_iface = carb.settings.get_settings() + # enable hydra scene-graph instancing + # note: this allows rendering of instanceable assets on the GUI + carb_settings_iface.set_bool("/persistent/omnihydra/useSceneGraphInstancing", True) + # change dispatcher to use the default dispatcher in PhysX SDK instead of carb tasking + # note: dispatcher handles how threads are launched for multi-threaded physics + carb_settings_iface.set_bool("/physics/physxDispatcher", True) + # disable contact processing in omni.physx if requested + # note: helpful when creating contact reporting over limited number of objects in the scene + if self.cfg.disable_contact_processing: + carb_settings_iface.set_bool("/physics/disableContactProcessing", True) + # enable custom geometry for cylinder and cone collision shapes to allow contact reporting for them + # reason: cylinders and cones aren't natively supported by PhysX so we need to use custom geometry flags + # reference: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/Geometry.html?highlight=capsule#geometry + carb_settings_iface.set_bool("/physics/collisionConeCustomGeometry", False) + carb_settings_iface.set_bool("/physics/collisionCylinderCustomGeometry", False) + # note: we read this once since it is not expected to change during runtime + # read flag for whether a local GUI is enabled + self._local_gui = carb_settings_iface.get("/app/window/enabled") + # read flag for whether livestreaming GUI is enabled + self._livestream_gui = carb_settings_iface.get("/app/livestream/enabled") + + # read flag for whether the Isaac Lab viewport capture pipeline will be used, + # casting None to False if the flag doesn't exist + # this flag is set from the AppLauncher class + self._offscreen_render = bool(carb_settings_iface.get("/isaaclab/render/offscreen")) + # read flag for whether the default viewport should be enabled + self._render_viewport = bool(carb_settings_iface.get("/isaaclab/render/active_viewport")) + # flag for whether any GUI will be rendered (local, livestreamed or viewport) + self._has_gui = self._local_gui or self._livestream_gui + + # apply render settings from render config + carb_settings_iface.set_bool("/rtx/translucency/enabled", self.cfg.render.enable_translucency) + carb_settings_iface.set_bool("/rtx/reflections/enabled", self.cfg.render.enable_reflections) + carb_settings_iface.set_bool("/rtx/indirectDiffuse/enabled", self.cfg.render.enable_global_illumination) + carb_settings_iface.set_bool("/rtx/transient/dlssg/enabled", self.cfg.render.enable_dlssg) + carb_settings_iface.set_int("/rtx/post/dlss/execMode", self.cfg.render.dlss_mode) + carb_settings_iface.set_bool("/rtx/directLighting/enabled", self.cfg.render.enable_direct_lighting) + carb_settings_iface.set_int( + "/rtx/directLighting/sampledLighting/samplesPerPixel", self.cfg.render.samples_per_pixel + ) + carb_settings_iface.set_bool("/rtx/shadows/enabled", self.cfg.render.enable_shadows) + carb_settings_iface.set_bool("/rtx/ambientOcclusion/enabled", self.cfg.render.enable_ambient_occlusion) + # set denoiser mode + try: + import omni.replicator.core as rep + + rep.settings.set_render_rtx_realtime(antialiasing=self.cfg.render.antialiasing_mode) + except Exception: + pass + + # store the default render mode + if not self._has_gui and not self._offscreen_render: + # set default render mode + # note: this is the terminal state: cannot exit from this render mode + self.render_mode = self.RenderMode.NO_GUI_OR_RENDERING + # set viewport context to None + self._viewport_context = None + self._viewport_window = None + elif not self._has_gui and self._offscreen_render: + # set default render mode + # note: this is the terminal state: cannot exit from this render mode + self.render_mode = self.RenderMode.PARTIAL_RENDERING + # set viewport context to None + self._viewport_context = None + self._viewport_window = None + else: + # note: need to import here in case the UI is not available (ex. headless mode) + import omni.ui as ui + from omni.kit.viewport.utility import get_active_viewport + + # set default render mode + # note: this can be changed by calling the `set_render_mode` function + self.render_mode = self.RenderMode.FULL_RENDERING + # acquire viewport context + self._viewport_context = get_active_viewport() + self._viewport_context.updates_enabled = True # pyright: ignore [reportOptionalMemberAccess] + # acquire viewport window + # TODO @mayank: Why not just use get_active_viewport_and_window() directly? + self._viewport_window = ui.Workspace.get_window("Viewport") + # counter for periodic rendering + self._render_throttle_counter = 0 + # rendering frequency in terms of number of render calls + self._render_throttle_period = 5 + + # check the case where we don't need to render the viewport + # since render_viewport can only be False in headless mode, we only need to check for offscreen_render + if not self._render_viewport and self._offscreen_render: + # disable the viewport if offscreen_render is enabled + from omni.kit.viewport.utility import get_active_viewport + + get_active_viewport().updates_enabled = False + + # override enable scene querying if rendering is enabled + # this is needed for some GUI features + if self._has_gui: + self.cfg.enable_scene_query_support = True + # set up flatcache/fabric interface (default is None) + # this is needed to flush the flatcache data into Hydra manually when calling `render()` + # ref: https://docs.omniverse.nvidia.com/prod_extensions/prod_extensions/ext_physics.html + # note: need to do this here because super().__init__ calls render and this variable is needed + self._fabric_iface = None + # read isaac sim version (this includes build tag, release tag etc.) + # note: we do it once here because it reads the VERSION file from disk and is not expected to change. + self._isaacsim_version = get_version() + + # create a tensor for gravity + # note: this line is needed to create a "tensor" in the device to avoid issues with torch 2.1 onwards. + # the issue is with some heap memory corruption when torch tensor is created inside the asset class. + # you can reproduce the issue by commenting out this line and running the test `test_articulation.py`. + self._gravity_tensor = torch.tensor(self.cfg.gravity, dtype=torch.float32, device=self.cfg.device) + + # add callback to deal the simulation app when simulation is stopped. + # this is needed because physics views go invalid once we stop the simulation + if not builtins.ISAAC_LAUNCHED_FROM_TERMINAL: + timeline_event_stream = omni.timeline.get_timeline_interface().get_timeline_event_stream() + self._app_control_on_stop_handle = timeline_event_stream.create_subscription_to_pop_by_type( + int(omni.timeline.TimelineEventType.STOP), + lambda *args, obj=weakref.proxy(self): obj._app_control_on_stop_callback(*args), + order=15, + ) + else: + self._app_control_on_stop_handle = None + + # flatten out the simulation dictionary + sim_params = self.cfg.to_dict() + if sim_params is not None: + if "physx" in sim_params: + physx_params = sim_params.pop("physx") + sim_params.update(physx_params) + # create a simulation context to control the simulator + super().__init__( + stage_units_in_meters=1.0, + physics_dt=self.cfg.dt, + rendering_dt=self.cfg.dt * self.cfg.render_interval, + backend="torch", + sim_params=sim_params, + physics_prim_path=self.cfg.physics_prim_path, + device=self.cfg.device, + )
+ + """ + Operations - New. + """ + +
[文档] def has_gui(self) -> bool: + """Returns whether the simulation has a GUI enabled. + + True if the simulation has a GUI enabled either locally or live-streamed. + """ + return self._has_gui
+ +
[文档] def has_rtx_sensors(self) -> bool: + """Returns whether the simulation has any RTX-rendering related sensors. + + This function returns the value of the simulation parameter ``"/isaaclab/render/rtx_sensors"``. + The parameter is set to True when instances of RTX-related sensors (cameras or LiDARs) are + created using Isaac Lab's sensor classes. + + True if the simulation has RTX sensors (such as USD Cameras or LiDARs). + + For more information, please check `NVIDIA RTX documentation`_. + + .. _NVIDIA RTX documentation: https://developer.nvidia.com/rendering-technologies + """ + return self._settings.get_as_bool("/isaaclab/render/rtx_sensors")
+ +
[文档] def is_fabric_enabled(self) -> bool: + """Returns whether the fabric interface is enabled. + + When fabric interface is enabled, USD read/write operations are disabled. Instead all applications + read and write the simulation state directly from the fabric interface. This reduces a lot of overhead + that occurs during USD read/write operations. + + For more information, please check `Fabric documentation`_. + + .. _Fabric documentation: https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/usd_fabric_usdrt.html + """ + return self._fabric_iface is not None
+ +
[文档] def get_version(self) -> tuple[int, int, int]: + """Returns the version of the simulator. + + This is a wrapper around the ``omni.isaac.version.get_version()`` function. + + The returned tuple contains the following information: + + * Major version (int): This is the year of the release (e.g. 2022). + * Minor version (int): This is the half-year of the release (e.g. 1 or 2). + * Patch version (int): This is the patch number of the release (e.g. 0). + """ + return int(self._isaacsim_version[2]), int(self._isaacsim_version[3]), int(self._isaacsim_version[4])
+ + """ + Operations - New utilities. + """ + +
[文档] def set_camera_view( + self, + eye: tuple[float, float, float], + target: tuple[float, float, float], + camera_prim_path: str = "/OmniverseKit_Persp", + ): + """Set the location and target of the viewport camera in the stage. + + Note: + This is a wrapper around the :math:`omni.isaac.core.utils.viewports.set_camera_view` function. + It is provided here for convenience to reduce the amount of imports needed. + + Args: + eye: The location of the camera eye. + target: The location of the camera target. + camera_prim_path: The path to the camera primitive in the stage. Defaults to + "/OmniverseKit_Persp". + """ + # safe call only if we have a GUI or viewport rendering enabled + if self._has_gui or self._offscreen_render or self._render_viewport: + set_camera_view(eye, target, camera_prim_path)
+ +
[文档] def set_render_mode(self, mode: RenderMode): + """Change the current render mode of the simulation. + + Please see :class:`RenderMode` for more information on the different render modes. + + .. note:: + When no GUI is available (locally or livestreamed), we do not need to choose whether the viewport + needs to render or not (since there is no GUI). Thus, in this case, calling the function will not + change the render mode. + + Args: + mode (RenderMode): The rendering mode. If different than SimulationContext's rendering mode, + SimulationContext's mode is changed to the new mode. + + Raises: + ValueError: If the input mode is not supported. + """ + # check if mode change is possible -- not possible when no GUI is available + if not self._has_gui: + omni.log.warn( + f"Cannot change render mode when GUI is disabled. Using the default render mode: {self.render_mode}." + ) + return + # check if there is a mode change + # note: this is mostly needed for GUI when we want to switch between full rendering and no rendering. + if mode != self.render_mode: + if mode == self.RenderMode.FULL_RENDERING: + # display the viewport and enable updates + self._viewport_context.updates_enabled = True # pyright: ignore [reportOptionalMemberAccess] + self._viewport_window.visible = True # pyright: ignore [reportOptionalMemberAccess] + elif mode == self.RenderMode.PARTIAL_RENDERING: + # hide the viewport and disable updates + self._viewport_context.updates_enabled = False # pyright: ignore [reportOptionalMemberAccess] + self._viewport_window.visible = False # pyright: ignore [reportOptionalMemberAccess] + elif mode == self.RenderMode.NO_RENDERING: + # hide the viewport and disable updates + if self._viewport_context is not None: + self._viewport_context.updates_enabled = False # pyright: ignore [reportOptionalMemberAccess] + self._viewport_window.visible = False # pyright: ignore [reportOptionalMemberAccess] + # reset the throttle counter + self._render_throttle_counter = 0 + else: + raise ValueError(f"Unsupported render mode: {mode}! Please check `RenderMode` for details.") + # update render mode + self.render_mode = mode
+ +
[文档] def set_setting(self, name: str, value: Any): + """Set simulation settings using the Carbonite SDK. + + .. note:: + If the input setting name does not exist, it will be created. If it does exist, the value will be + overwritten. Please make sure to use the correct setting name. + + To understand the settings interface, please refer to the + `Carbonite SDK <https://docs.omniverse.nvidia.com/dev-guide/latest/programmer_ref/settings.html>`_ + documentation. + + Args: + name: The name of the setting. + value: The value of the setting. + """ + self._settings.set(name, value)
+ +
[文档] def get_setting(self, name: str) -> Any: + """Read the simulation setting using the Carbonite SDK. + + Args: + name: The name of the setting. + + Returns: + The value of the setting. + """ + return self._settings.get(name)
+ + """ + Operations - Override (standalone) + """ + + def reset(self, soft: bool = False): + super().reset(soft=soft) + # perform additional rendering steps to warm up replicator buffers + # this is only needed for the first time we set the simulation + if not soft: + for _ in range(2): + self.render() + +
[文档] def step(self, render: bool = True): + """Steps the simulation. + + .. note:: + This function blocks if the timeline is paused. It only returns when the timeline is playing. + + Args: + render: Whether to render the scene after stepping the physics simulation. + If set to False, the scene is not rendered and only the physics simulation is stepped. + """ + # check if the simulation timeline is paused. in that case keep stepping until it is playing + if not self.is_playing(): + # step the simulator (but not the physics) to have UI still active + while not self.is_playing(): + self.render() + # meantime if someone stops, break out of the loop + if self.is_stopped(): + break + # need to do one step to refresh the app + # reason: physics has to parse the scene again and inform other extensions like hydra-delegate. + # without this the app becomes unresponsive. + # FIXME: This steps physics as well, which we is not good in general. + self.app.update() + + # step the simulation + super().step(render=render)
+ +
[文档] def render(self, mode: RenderMode | None = None): + """Refreshes the rendering components including UI elements and view-ports depending on the render mode. + + This function is used to refresh the rendering components of the simulation. This includes updating the + view-ports, UI elements, and other extensions (besides physics simulation) that are running in the + background. The rendering components are refreshed based on the render mode. + + Please see :class:`RenderMode` for more information on the different render modes. + + Args: + mode: The rendering mode. Defaults to None, in which case the current rendering mode is used. + """ + # check if we need to change the render mode + if mode is not None: + self.set_render_mode(mode) + # render based on the render mode + if self.render_mode == self.RenderMode.NO_GUI_OR_RENDERING: + # we never want to render anything here (this is for complete headless mode) + pass + elif self.render_mode == self.RenderMode.NO_RENDERING: + # throttle the rendering frequency to keep the UI responsive + self._render_throttle_counter += 1 + if self._render_throttle_counter % self._render_throttle_period == 0: + self._render_throttle_counter = 0 + # here we don't render viewport so don't need to flush fabric data + # note: we don't call super().render() anymore because they do flush the fabric data + self.set_setting("/app/player/playSimulations", False) + self._app.update() + self.set_setting("/app/player/playSimulations", True) + else: + # manually flush the fabric data to update Hydra textures + if self._fabric_iface is not None: + if self.physics_sim_view is not None and self.is_playing(): + # Update the articulations' link's poses before rendering + self.physics_sim_view.update_articulations_kinematic() + self._update_fabric(0.0, 0.0) + # render the simulation + # note: we don't call super().render() anymore because they do above operation inside + # and we don't want to do it twice. We may remove it once we drop support for Isaac Sim 2022.2. + self.set_setting("/app/player/playSimulations", False) + self._app.update() + self.set_setting("/app/player/playSimulations", True)
+ + """ + Operations - Override (extension) + """ + + async def reset_async(self, soft: bool = False): + # need to load all "physics" information from the USD file + if not soft: + omni.physx.acquire_physx_interface().force_load_physics_from_usd() + # play the simulation + await super().reset_async(soft=soft) + + """ + Initialization/Destruction - Override. + """ + + def _init_stage(self, *args, **kwargs) -> Usd.Stage: + _ = super()._init_stage(*args, **kwargs) + # a stage update here is needed for the case when physics_dt != rendering_dt, otherwise the app crashes + # when in headless mode + self.set_setting("/app/player/playSimulations", False) + self._app.update() + self.set_setting("/app/player/playSimulations", True) + # set additional physx parameters and bind material + self._set_additional_physx_params() + # load flatcache/fabric interface + self._load_fabric_interface() + # return the stage + return self.stage + + async def _initialize_stage_async(self, *args, **kwargs) -> Usd.Stage: + await super()._initialize_stage_async(*args, **kwargs) + # set additional physx parameters and bind material + self._set_additional_physx_params() + # load flatcache/fabric interface + self._load_fabric_interface() + # return the stage + return self.stage + + @classmethod + def clear_instance(cls): + # clear the callback + if cls._instance is not None: + if cls._instance._app_control_on_stop_handle is not None: + cls._instance._app_control_on_stop_handle.unsubscribe() + cls._instance._app_control_on_stop_handle = None + # call parent to clear the instance + super().clear_instance() + + """ + Helper Functions + """ + + def _set_additional_physx_params(self): + """Sets additional PhysX parameters that are not directly supported by the parent class.""" + # obtain the physics scene api + physics_scene: UsdPhysics.Scene = self._physics_context._physics_scene + physx_scene_api: PhysxSchema.PhysxSceneAPI = self._physics_context._physx_scene_api + # assert that scene api is not None + if physx_scene_api is None: + raise RuntimeError("Physics scene API is None! Please create the scene first.") + # set parameters not directly supported by the constructor + # -- Continuous Collision Detection (CCD) + # ref: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/AdvancedCollisionDetection.html?highlight=ccd#continuous-collision-detection + self._physics_context.enable_ccd(self.cfg.physx.enable_ccd) + # -- GPU collision stack size + physx_scene_api.CreateGpuCollisionStackSizeAttr(self.cfg.physx.gpu_collision_stack_size) + # -- Improved determinism by PhysX + physx_scene_api.CreateEnableEnhancedDeterminismAttr(self.cfg.physx.enable_enhanced_determinism) + + # -- Gravity + # note: Isaac sim only takes the "up-axis" as the gravity direction. But physics allows any direction so we + # need to convert the gravity vector to a direction and magnitude pair explicitly. + gravity = np.asarray(self.cfg.gravity) + gravity_magnitude = np.linalg.norm(gravity) + + # Avoid division by zero + if gravity_magnitude != 0.0: + gravity_direction = gravity / gravity_magnitude + else: + gravity_direction = gravity + + physics_scene.CreateGravityDirectionAttr(Gf.Vec3f(*gravity_direction)) + physics_scene.CreateGravityMagnitudeAttr(gravity_magnitude) + + # position iteration count + physx_scene_api.CreateMinPositionIterationCountAttr(self.cfg.physx.min_position_iteration_count) + physx_scene_api.CreateMaxPositionIterationCountAttr(self.cfg.physx.max_position_iteration_count) + # velocity iteration count + physx_scene_api.CreateMinVelocityIterationCountAttr(self.cfg.physx.min_velocity_iteration_count) + physx_scene_api.CreateMaxVelocityIterationCountAttr(self.cfg.physx.max_velocity_iteration_count) + + # create the default physics material + # this material is used when no material is specified for a primitive + # check: https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/simulation-control/physics-settings.html#physics-materials + material_path = f"{self.cfg.physics_prim_path}/defaultMaterial" + self.cfg.physics_material.func(material_path, self.cfg.physics_material) + # bind the physics material to the scene + bind_physics_material(self.cfg.physics_prim_path, material_path) + + def _load_fabric_interface(self): + """Loads the fabric interface if enabled.""" + if self.cfg.use_fabric: + from omni.physxfabric import get_physx_fabric_interface + + # acquire fabric interface + self._fabric_iface = get_physx_fabric_interface() + if hasattr(self._fabric_iface, "force_update"): + # The update method in the fabric interface only performs an update if a physics step has occurred. + # However, for rendering, we need to force an update since any element of the scene might have been + # modified in a reset (which occurs after the physics step) and we want the renderer to be aware of + # these changes. + self._update_fabric = self._fabric_iface.force_update + else: + # Needed for backward compatibility with older Isaac Sim versions + self._update_fabric = self._fabric_iface.update + + """ + Callbacks. + """ + + def _app_control_on_stop_callback(self, event: carb.events.IEvent): + """Callback to deal with the app when the simulation is stopped. + + Once the simulation is stopped, the physics handles go invalid. After that, it is not possible to + resume the simulation from the last state. This leaves the app in an inconsistent state, where + two possible actions can be taken: + + 1. **Keep the app rendering**: In this case, the simulation is kept running and the app is not shutdown. + However, the physics is not updated and the script cannot be resumed from the last state. The + user has to manually close the app to stop the simulation. + 2. **Shutdown the app**: This is the default behavior. In this case, the app is shutdown and + the simulation is stopped. + + Note: + This callback is used only when running the simulation in a standalone python script. In an extension, + it is expected that the user handles the extension shutdown. + """ + # check if the simulation is stopped + if event.type == int(omni.timeline.TimelineEventType.STOP): + # keep running the simulator when configured to not shutdown the app + if self._has_gui and sys.exc_info()[0] is None: + omni.log.warn( + "Simulation is stopped. The app will keep running with physics disabled." + " Press Ctrl+C or close the window to exit the app." + ) + while self.app.is_running(): + self.render() + + # Note: For the following code: + # The method is an exact copy of the implementation in the `omni.isaac.kit.SimulationApp` class. + # We need to remove this method once the SimulationApp class becomes a singleton. + + # make sure that any replicator workflows finish rendering/writing + try: + import omni.replicator.core as rep + + rep_status = rep.orchestrator.get_status() + if rep_status not in [rep.orchestrator.Status.STOPPED, rep.orchestrator.Status.STOPPING]: + rep.orchestrator.stop() + if rep_status != rep.orchestrator.Status.STOPPED: + rep.orchestrator.wait_until_complete() + + # Disable capture on play to avoid replicator engaging on any new timeline events + rep.orchestrator.set_capture_on_play(False) + except Exception: + pass + + # clear the instance and all callbacks + # note: clearing callbacks is important to prevent memory leaks + self.clear_all_callbacks() + + # workaround for exit issues, clean the stage first: + if omni.usd.get_context().can_close_stage(): + omni.usd.get_context().close_stage() + + # print logging information + print("[INFO]: Simulation is stopped. Shutting down the app.") + + # Cleanup any running tracy instances so data is not lost + try: + profiler_tracy = carb.profiler.acquire_profiler_interface(plugin_name="carb.profiler-tracy.plugin") + if profiler_tracy: + profiler_tracy.set_capture_mask(0) + profiler_tracy.end(0) + profiler_tracy.shutdown() + except RuntimeError: + # Tracy plugin was not loaded, so profiler never started - skip checks. + pass + + # Disable logging before shutdown to keep the log clean + # Warnings at this point don't matter as the python process is about to be terminated + logging = carb.logging.acquire_logging() + logging.set_level_threshold(carb.logging.LEVEL_ERROR) + + # App shutdown is disabled to prevent crashes on shutdown. Terminating carb is faster + # self._app.shutdown() + self._framework.unload_all_plugins()
+ + +@contextmanager +def build_simulation_context( + create_new_stage: bool = True, + gravity_enabled: bool = True, + device: str = "cuda:0", + dt: float = 0.01, + sim_cfg: SimulationCfg | None = None, + add_ground_plane: bool = False, + add_lighting: bool = False, + auto_add_lighting: bool = False, +) -> Iterator[SimulationContext]: + """Context manager to build a simulation context with the provided settings. + + This function facilitates the creation of a simulation context and provides flexibility in configuring various + aspects of the simulation, such as time step, gravity, device, and scene elements like ground plane and + lighting. + + If :attr:`sim_cfg` is None, then an instance of :class:`SimulationCfg` is created with default settings, with parameters + overwritten based on arguments to the function. + + An example usage of the context manager function: + + .. code-block:: python + + with build_simulation_context() as sim: + # Design the scene + + # Play the simulation + sim.reset() + while sim.is_playing(): + sim.step() + + Args: + create_new_stage: Whether to create a new stage. Defaults to True. + gravity_enabled: Whether to enable gravity in the simulation. Defaults to True. + device: Device to run the simulation on. Defaults to "cuda:0". + dt: Time step for the simulation: Defaults to 0.01. + sim_cfg: :class:`omni.isaac.lab.sim.SimulationCfg` to use for the simulation. Defaults to None. + add_ground_plane: Whether to add a ground plane to the simulation. Defaults to False. + add_lighting: Whether to add a dome light to the simulation. Defaults to False. + auto_add_lighting: Whether to automatically add a dome light to the simulation if the simulation has a GUI. + Defaults to False. This is useful for debugging tests in the GUI. + + Yields: + The simulation context to use for the simulation. + + """ + try: + if create_new_stage: + stage_utils.create_new_stage() + + if sim_cfg is None: + # Construct one and overwrite the dt, gravity, and device + sim_cfg = SimulationCfg(dt=dt) + + # Set up gravity + if gravity_enabled: + sim_cfg.gravity = (0.0, 0.0, -9.81) + else: + sim_cfg.gravity = (0.0, 0.0, 0.0) + + # Set device + sim_cfg.device = device + + # Construct simulation context + sim = SimulationContext(sim_cfg) + + if add_ground_plane: + # Ground-plane + cfg = GroundPlaneCfg() + cfg.func("/World/defaultGroundPlane", cfg) + + if add_lighting or (auto_add_lighting and sim.has_gui()): + # Lighting + cfg = DomeLightCfg( + color=(0.1, 0.1, 0.1), + enable_color_temperature=True, + color_temperature=5500, + intensity=10000, + ) + # Dome light named specifically to avoid conflicts + cfg.func(prim_path="/World/defaultDomeLight", cfg=cfg, translation=(0.0, 0.0, 10.0)) + + yield sim + + except Exception: + omni.log.error(traceback.format_exc()) + raise + finally: + if not sim.has_gui(): + # Stop simulation only if we aren't rendering otherwise the app will hang indefinitely + sim.stop() + + # Clear the stage + sim.clear_all_callbacks() + sim.clear_instance() +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/from_files/from_files.html b/_modules/omni/isaac/lab/sim/spawners/from_files/from_files.html new file mode 100644 index 0000000000..447d3861b0 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/from_files/from_files.html @@ -0,0 +1,832 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.from_files.from_files — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.from_files.from_files 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+import omni.isaac.core.utils.stage as stage_utils
+import omni.kit.commands
+import omni.log
+from pxr import Gf, Sdf, Usd
+
+from omni.isaac.lab.sim import converters, schemas
+from omni.isaac.lab.sim.utils import bind_physics_material, bind_visual_material, clone, select_usd_variants
+
+if TYPE_CHECKING:
+    from . import from_files_cfg
+
+
+
[文档]@clone +def spawn_from_usd( + prim_path: str, + cfg: from_files_cfg.UsdFileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn an asset from a USD file and override the settings with the given config. + + In the case of a USD file, the asset is spawned at the default prim specified in the USD file. + If a default prim is not specified, then the asset is spawned at the root prim. + + In case a prim already exists at the given prim path, then the function does not create a new prim + or throw an error that the prim already exists. Instead, it just takes the existing prim and overrides + the settings with the given config. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which + case the translation specified in the USD file is used. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case the orientation specified in the USD file is used. + + Returns: + The prim of the spawned asset. + + Raises: + FileNotFoundError: If the USD file does not exist at the given path. + """ + # spawn asset from the given usd file + return _spawn_from_usd_file(prim_path, cfg.usd_path, cfg, translation, orientation)
+ + +
[文档]@clone +def spawn_from_urdf( + prim_path: str, + cfg: from_files_cfg.UrdfFileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn an asset from a URDF file and override the settings with the given config. + + It uses the :class:`UrdfConverter` class to create a USD file from URDF. This file is then imported + at the specified prim path. + + In case a prim already exists at the given prim path, then the function does not create a new prim + or throw an error that the prim already exists. Instead, it just takes the existing prim and overrides + the settings with the given config. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which + case the translation specified in the generated USD file is used. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case the orientation specified in the generated USD file is used. + + Returns: + The prim of the spawned asset. + + Raises: + FileNotFoundError: If the URDF file does not exist at the given path. + """ + # urdf loader to convert urdf to usd + urdf_loader = converters.UrdfConverter(cfg) + # spawn asset from the generated usd file + return _spawn_from_usd_file(prim_path, urdf_loader.usd_path, cfg, translation, orientation)
+ + +
[文档]def spawn_ground_plane( + prim_path: str, + cfg: from_files_cfg.GroundPlaneCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawns a ground plane into the scene. + + This function loads the USD file containing the grid plane asset from Isaac Sim. It may + not work with other assets for ground planes. In those cases, please use the `spawn_from_usd` + function. + + Note: + This function takes keyword arguments to be compatible with other spawners. However, it does not + use any of the kwargs. + + Args: + prim_path: The path to spawn the asset at. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which + case the translation specified in the USD file is used. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case the orientation specified in the USD file is used. + + Returns: + The prim of the spawned asset. + + Raises: + ValueError: If the prim path already exists. + """ + # Spawn Ground-plane + if not prim_utils.is_prim_path_valid(prim_path): + prim_utils.create_prim(prim_path, usd_path=cfg.usd_path, translation=translation, orientation=orientation) + else: + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + + # Create physics material + if cfg.physics_material is not None: + cfg.physics_material.func(f"{prim_path}/physicsMaterial", cfg.physics_material) + # Apply physics material to ground plane + collision_prim_path = prim_utils.get_prim_path( + prim_utils.get_first_matching_child_prim( + prim_path, predicate=lambda x: prim_utils.get_prim_type_name(x) == "Plane" + ) + ) + bind_physics_material(collision_prim_path, f"{prim_path}/physicsMaterial") + + # Scale only the mesh + # Warning: This is specific to the default grid plane asset. + if prim_utils.is_prim_path_valid(f"{prim_path}/Environment"): + # compute scale from size + scale = (cfg.size[0] / 100.0, cfg.size[1] / 100.0, 1.0) + # apply scale to the mesh + prim_utils.set_prim_property(f"{prim_path}/Environment", "xformOp:scale", scale) + + # Change the color of the plane + # Warning: This is specific to the default grid plane asset. + if cfg.color is not None: + prop_path = f"{prim_path}/Looks/theGrid/Shader.inputs:diffuse_tint" + # change the color + omni.kit.commands.execute( + "ChangePropertyCommand", + prop_path=Sdf.Path(prop_path), + value=Gf.Vec3f(*cfg.color), + prev=None, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Color3f, + ) + # Remove the light from the ground plane + # It isn't bright enough and messes up with the user's lighting settings + omni.kit.commands.execute("ToggleVisibilitySelectedPrims", selected_paths=[f"{prim_path}/SphereLight"]) + + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +""" +Helper functions. +""" + + +def _spawn_from_usd_file( + prim_path: str, + usd_path: str, + cfg: from_files_cfg.FileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn an asset from a USD file and override the settings with the given config. + + In case a prim already exists at the given prim path, then the function does not create a new prim + or throw an error that the prim already exists. Instead, it just takes the existing prim and overrides + the settings with the given config. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + usd_path: The path to the USD file to spawn the asset from. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which + case the translation specified in the generated USD file is used. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case the orientation specified in the generated USD file is used. + + Returns: + The prim of the spawned asset. + + Raises: + FileNotFoundError: If the USD file does not exist at the given path. + """ + # check file path exists + stage: Usd.Stage = stage_utils.get_current_stage() + if not stage.ResolveIdentifierToEditTarget(usd_path): + raise FileNotFoundError(f"USD file not found at path: '{usd_path}'.") + # spawn asset if it doesn't exist. + if not prim_utils.is_prim_path_valid(prim_path): + # add prim as reference to stage + prim_utils.create_prim( + prim_path, + usd_path=usd_path, + translation=translation, + orientation=orientation, + scale=cfg.scale, + ) + else: + omni.log.warn(f"A prim already exists at prim path: '{prim_path}'.") + + # modify variants + if hasattr(cfg, "variants") and cfg.variants is not None: + select_usd_variants(prim_path, cfg.variants) + + # modify rigid body properties + if cfg.rigid_props is not None: + schemas.modify_rigid_body_properties(prim_path, cfg.rigid_props) + # modify collision properties + if cfg.collision_props is not None: + schemas.modify_collision_properties(prim_path, cfg.collision_props) + # modify mass properties + if cfg.mass_props is not None: + schemas.modify_mass_properties(prim_path, cfg.mass_props) + + # modify articulation root properties + if cfg.articulation_props is not None: + schemas.modify_articulation_root_properties(prim_path, cfg.articulation_props) + # modify tendon properties + if cfg.fixed_tendons_props is not None: + schemas.modify_fixed_tendon_properties(prim_path, cfg.fixed_tendons_props) + # define drive API on the joints + # note: these are only for setting low-level simulation properties. all others should be set or are + # and overridden by the articulation/actuator properties. + if cfg.joint_drive_props is not None: + schemas.modify_joint_drive_properties(prim_path, cfg.joint_drive_props) + + # modify deformable body properties + if cfg.deformable_props is not None: + schemas.modify_deformable_body_properties(prim_path, cfg.deformable_props) + + # apply visual material + if cfg.visual_material is not None: + if not cfg.visual_material_path.startswith("/"): + material_path = f"{prim_path}/{cfg.visual_material_path}" + else: + material_path = cfg.visual_material_path + # create material + cfg.visual_material.func(material_path, cfg.visual_material) + # apply material + bind_visual_material(prim_path, material_path) + + # return the prim + return prim_utils.get_prim_at_path(prim_path) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/from_files/from_files_cfg.html b/_modules/omni/isaac/lab/sim/spawners/from_files/from_files_cfg.html new file mode 100644 index 0000000000..fdfc2b0c58 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/from_files/from_files_cfg.html @@ -0,0 +1,712 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.from_files.from_files_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.from_files.from_files_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import MISSING
+
+from omni.isaac.lab.sim import converters, schemas
+from omni.isaac.lab.sim.spawners import materials
+from omni.isaac.lab.sim.spawners.spawner_cfg import DeformableObjectSpawnerCfg, RigidObjectSpawnerCfg, SpawnerCfg
+from omni.isaac.lab.utils import configclass
+from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR
+
+from . import from_files
+
+
+@configclass
+class FileCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg):
+    """Configuration parameters for spawning an asset from a file.
+
+    This class is a base class for spawning assets from files. It includes the common parameters
+    for spawning assets from files, such as the path to the file and the function to use for spawning
+    the asset.
+
+    Note:
+        By default, all properties are set to None. This means that no properties will be added or modified
+        to the prim outside of the properties available by default when spawning the prim.
+
+        If they are set to a value, then the properties are modified on the spawned prim in a nested manner.
+        This is done by calling the respective function with the specified properties.
+    """
+
+    scale: tuple[float, float, float] | None = None
+    """Scale of the asset. Defaults to None, in which case the scale is not modified."""
+
+    articulation_props: schemas.ArticulationRootPropertiesCfg | None = None
+    """Properties to apply to the articulation root."""
+
+    fixed_tendons_props: schemas.FixedTendonsPropertiesCfg | None = None
+    """Properties to apply to the fixed tendons (if any)."""
+
+    joint_drive_props: schemas.JointDrivePropertiesCfg | None = None
+    """Properties to apply to a joint."""
+
+    visual_material_path: str = "material"
+    """Path to the visual material to use for the prim. Defaults to "material".
+
+    If the path is relative, then it will be relative to the prim's path.
+    This parameter is ignored if `visual_material` is not None.
+    """
+
+    visual_material: materials.VisualMaterialCfg | None = None
+    """Visual material properties to override the visual material properties in the URDF file.
+
+    Note:
+        If None, then no visual material will be added.
+    """
+
+
+
[文档]@configclass +class UsdFileCfg(FileCfg): + """USD file to spawn asset from. + + USD files are imported directly into the scene. However, given their complexity, there are various different + operations that can be performed on them. For example, selecting variants, applying materials, or modifying + existing properties. + + To prevent the explosion of configuration parameters, the available operations are limited to the most common + ones. These include: + + - **Selecting variants**: This is done by specifying the :attr:`variants` parameter. + - **Creating and applying materials**: This is done by specifying the :attr:`visual_material` and + :attr:`physics_material` parameters. + - **Modifying existing properties**: This is done by specifying the respective properties in the configuration + class. For instance, to modify the scale of the imported prim, set the :attr:`scale` parameter. + + See :meth:`spawn_from_usd` for more information. + + .. note:: + The configuration parameters include various properties. If not `None`, these properties + are modified on the spawned prim in a nested manner. + + If they are set to a value, then the properties are modified on the spawned prim in a nested manner. + This is done by calling the respective function with the specified properties. + """ + + func: Callable = from_files.spawn_from_usd + + usd_path: str = MISSING + """Path to the USD file to spawn asset from.""" + + variants: object | dict[str, str] | None = None + """Variants to select from in the input USD file. Defaults to None, in which case no variants are applied. + + This can either be a configclass object, in which case each attribute is used as a variant set name and its specified value, + or a dictionary mapping between the two. Please check the :meth:`~omni.isaac.lab.sim.utils.select_usd_variants` function + for more information. + """
+ + +
[文档]@configclass +class UrdfFileCfg(FileCfg, converters.UrdfConverterCfg): + """URDF file to spawn asset from. + + It uses the :class:`UrdfConverter` class to create a USD file from URDF and spawns the imported + USD file. Similar to the :class:`UsdFileCfg`, the generated USD file can be modified by specifying + the respective properties in the configuration class. + + See :meth:`spawn_from_urdf` for more information. + + .. note:: + The configuration parameters include various properties. If not `None`, these properties + are modified on the spawned prim in a nested manner. + + If they are set to a value, then the properties are modified on the spawned prim in a nested manner. + This is done by calling the respective function with the specified properties. + + """ + + func: Callable = from_files.spawn_from_urdf
+ + +""" +Spawning ground plane. +""" + + +
[文档]@configclass +class GroundPlaneCfg(SpawnerCfg): + """Create a ground plane prim. + + This uses the USD for the standard grid-world ground plane from Isaac Sim by default. + """ + + func: Callable = from_files.spawn_ground_plane + + usd_path: str = f"{ISAAC_NUCLEUS_DIR}/Environments/Grid/default_environment.usd" + """Path to the USD file to spawn asset from. Defaults to the grid-world ground plane.""" + + color: tuple[float, float, float] | None = (0.0, 0.0, 0.0) + """The color of the ground plane. Defaults to (0.0, 0.0, 0.0). + + If None, then the color remains unchanged. + """ + + size: tuple[float, float] = (100.0, 100.0) + """The size of the ground plane. Defaults to 100 m x 100 m.""" + + physics_material: materials.RigidBodyMaterialCfg = materials.RigidBodyMaterialCfg() + """Physics material properties. Defaults to the default rigid body material."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/lights/lights.html b/_modules/omni/isaac/lab/sim/spawners/lights/lights.html new file mode 100644 index 0000000000..89204ae631 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/lights/lights.html @@ -0,0 +1,639 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.lights.lights — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.lights.lights 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+from pxr import Usd, UsdLux
+
+from omni.isaac.lab.sim.utils import clone, safe_set_attribute_on_usd_prim
+
+if TYPE_CHECKING:
+    from . import lights_cfg
+
+
+
[文档]@clone +def spawn_light( + prim_path: str, + cfg: lights_cfg.LightCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a light prim at the specified prim path with the specified configuration. + + The created prim is based on the `USD.LuxLight <https://openusd.org/dev/api/class_usd_lux_light_a_p_i.html>`_ API. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + 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 + is set to identity. + + Raises: + ValueError: When a prim already exists at the specified prim path. + """ + # check if prim already exists + if prim_utils.is_prim_path_valid(prim_path): + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + # create the prim + prim = prim_utils.create_prim(prim_path, prim_type=cfg.prim_type, translation=translation, orientation=orientation) + + # convert to dict + cfg = cfg.to_dict() + # delete spawner func specific parameters + del cfg["prim_type"] + # delete custom attributes in the config that are not USD parameters + non_usd_cfg_param_names = ["func", "copy_from_source", "visible", "semantic_tags"] + for param_name in non_usd_cfg_param_names: + del cfg[param_name] + # set into USD API + for attr_name, value in cfg.items(): + # special operation for texture properties + # note: this is only used for dome light + if "texture" in attr_name: + light_prim = UsdLux.DomeLight(prim) + if attr_name == "texture_file": + light_prim.CreateTextureFileAttr(value) + elif attr_name == "texture_format": + light_prim.CreateTextureFormatAttr(value) + else: + raise ValueError(f"Unsupported texture attribute: '{attr_name}'.") + else: + if attr_name == "visible_in_primary_ray": + prim_prop_name = attr_name + else: + prim_prop_name = f"inputs:{attr_name}" + # set the attribute + safe_set_attribute_on_usd_prim(prim, prim_prop_name, value, camel_case=True) + # return the prim + return prim
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/lights/lights_cfg.html b/_modules/omni/isaac/lab/sim/spawners/lights/lights_cfg.html new file mode 100644 index 0000000000..52cdb773ee --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/lights/lights_cfg.html @@ -0,0 +1,747 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.lights.lights_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.lights.lights_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.sim.spawners.spawner_cfg import SpawnerCfg
+from omni.isaac.lab.utils import configclass
+
+from . import lights
+
+
+
[文档]@configclass +class LightCfg(SpawnerCfg): + """Configuration parameters for creating a light in the scene. + + Please refer to the documentation on `USD LuxLight <https://openusd.org/dev/api/class_usd_lux_light_a_p_i.html>`_ + for more information. + + .. note:: + The default values for the attributes are those specified in the their official documentation. + """ + + func: Callable = lights.spawn_light + + prim_type: str = MISSING + """The prim type name for the light prim.""" + + color: tuple[float, float, float] = (1.0, 1.0, 1.0) + """The color of emitted light, in energy-linear terms. Defaults to white.""" + + enable_color_temperature: bool = False + """Enables color temperature. Defaults to false.""" + + color_temperature: float = 6500.0 + """Color temperature (in Kelvin) representing the white point. The valid range is [1000, 10000]. Defaults to 6500K. + + The `color temperature <https://en.wikipedia.org/wiki/Color_temperature>`_ corresponds to the warmth + or coolness of light. Warmer light has a lower color temperature, while cooler light has a higher + color temperature. + + Note: + It only takes effect when :attr:`enable_color_temperature` is true. + """ + + normalize: bool = False + """Normalizes power by the surface area of the light. Defaults to false. + + This makes it easier to independently adjust the power and shape of the light, by causing the power + to not vary with the area or angular size of the light. + """ + + exposure: float = 0.0 + """Scales the power of the light exponentially as a power of 2. Defaults to 0.0. + + The result is multiplied against the intensity. + """ + + intensity: float = 1.0 + """Scales the power of the light linearly. Defaults to 1.0."""
+ + +
[文档]@configclass +class DiskLightCfg(LightCfg): + """Configuration parameters for creating a disk light in the scene. + + A disk light is a light source that emits light from a disk. It is useful for simulating + fluorescent lights. For more information, please refer to the documentation on + `USDLux DiskLight <https://openusd.org/dev/api/class_usd_lux_disk_light.html>`_. + + .. note:: + The default values for the attributes are those specified in the their official documentation. + """ + + prim_type = "DiskLight" + + radius: float = 0.5 + """Radius of the disk (in m). Defaults to 0.5m."""
+ + +
[文档]@configclass +class DistantLightCfg(LightCfg): + """Configuration parameters for creating a distant light in the scene. + + A distant light is a light source that is infinitely far away, and emits parallel rays of light. + It is useful for simulating sun/moon light. For more information, please refer to the documentation on + `USDLux DistantLight <https://openusd.org/dev/api/class_usd_lux_distant_light.html>`_. + + .. note:: + The default values for the attributes are those specified in the their official documentation. + """ + + prim_type = "DistantLight" + + angle: float = 0.53 + """Angular size of the light (in degrees). Defaults to 0.53 degrees. + + As an example, the Sun is approximately 0.53 degrees as seen from Earth. + Higher values broaden the light and therefore soften shadow edges. + """
+ + +
[文档]@configclass +class DomeLightCfg(LightCfg): + """Configuration parameters for creating a dome light in the scene. + + A dome light is a light source that emits light inwards from all directions. It is also possible to + attach a texture to the dome light, which will be used to emit light. For more information, please refer + to the documentation on `USDLux DomeLight <https://openusd.org/dev/api/class_usd_lux_dome_light.html>`_. + + .. note:: + The default values for the attributes are those specified in the their official documentation. + """ + + prim_type = "DomeLight" + + texture_file: str | None = None + """A color texture to use on the dome, such as an HDR (high dynamic range) texture intended + for IBL (image based lighting). Defaults to None. + + If None, the dome will emit a uniform color. + """ + + texture_format: Literal["automatic", "latlong", "mirroredBall", "angular", "cubeMapVerticalCross"] = "automatic" + """The parametrization format of the color map file. Defaults to "automatic". + + Valid values are: + + * ``"automatic"``: Tries to determine the layout from the file itself. For example, Renderman texture files embed an explicit parameterization. + * ``"latlong"``: Latitude as X, longitude as Y. + * ``"mirroredBall"``: An image of the environment reflected in a sphere, using an implicitly orthogonal projection. + * ``"angular"``: Similar to mirroredBall but the radial dimension is mapped linearly to the angle, providing better sampling at the edges. + * ``"cubeMapVerticalCross"``: A cube map with faces laid out as a vertical cross. + """ + + visible_in_primary_ray: bool = True + """Whether the dome light is visible in the primary ray. Defaults to True. + + If true, the texture in the sky is visible, otherwise the sky is black. + """
+ + +
[文档]@configclass +class CylinderLightCfg(LightCfg): + """Configuration parameters for creating a cylinder light in the scene. + + A cylinder light is a light source that emits light from a cylinder. It is useful for simulating + fluorescent lights. For more information, please refer to the documentation on + `USDLux CylinderLight <https://openusd.org/dev/api/class_usd_lux_cylinder_light.html>`_. + + .. note:: + The default values for the attributes are those specified in the their official documentation. + """ + + prim_type = "CylinderLight" + + length: float = 1.0 + """Length of the cylinder (in m). Defaults to 1.0m.""" + + radius: float = 0.5 + """Radius of the cylinder (in m). Defaults to 0.5m.""" + + treat_as_line: bool = False + """Treats the cylinder as a line source, i.e. a zero-radius cylinder. Defaults to false."""
+ + +
[文档]@configclass +class SphereLightCfg(LightCfg): + """Configuration parameters for creating a sphere light in the scene. + + A sphere light is a light source that emits light outward from a sphere. For more information, + please refer to the documentation on + `USDLux SphereLight <https://openusd.org/dev/api/class_usd_lux_sphere_light.html>`_. + + .. note:: + The default values for the attributes are those specified in the their official documentation. + """ + + prim_type = "SphereLight" + + radius: float = 0.5 + """Radius of the sphere. Defaults to 0.5m.""" + + treat_as_point: bool = False + """Treats the sphere as a point source, i.e. a zero-radius sphere. Defaults to false."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/materials/physics_materials.html b/_modules/omni/isaac/lab/sim/spawners/materials/physics_materials.html new file mode 100644 index 0000000000..f1cb6ad753 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/materials/physics_materials.html @@ -0,0 +1,682 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.materials.physics_materials — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.materials.physics_materials 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+import omni.isaac.core.utils.stage as stage_utils
+from pxr import PhysxSchema, Usd, UsdPhysics, UsdShade
+
+from omni.isaac.lab.sim.utils import clone, safe_set_attribute_on_usd_schema
+
+if TYPE_CHECKING:
+    from . import physics_materials_cfg
+
+
+
[文档]@clone +def spawn_rigid_body_material(prim_path: str, cfg: physics_materials_cfg.RigidBodyMaterialCfg) -> Usd.Prim: + """Create material with rigid-body physics properties. + + Rigid body materials are used to define the physical properties to meshes of a rigid body. These + include the friction, restitution, and their respective combination modes. For more information on + rigid body material, please refer to the `documentation on PxMaterial <https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/classPxBaseMaterial.html>`_. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration for the physics material. + + Returns: + The spawned rigid body material prim. + + Raises: + ValueError: When a prim already exists at the specified prim path and is not a material. + """ + # create material prim if no prim exists + if not prim_utils.is_prim_path_valid(prim_path): + _ = UsdShade.Material.Define(stage_utils.get_current_stage(), prim_path) + + # obtain prim + prim = prim_utils.get_prim_at_path(prim_path) + # check if prim is a material + if not prim.IsA(UsdShade.Material): + raise ValueError(f"A prim already exists at path: '{prim_path}' but is not a material.") + # retrieve the USD rigid-body api + usd_physics_material_api = UsdPhysics.MaterialAPI(prim) + if not usd_physics_material_api: + usd_physics_material_api = UsdPhysics.MaterialAPI.Apply(prim) + # retrieve the collision api + physx_material_api = PhysxSchema.PhysxMaterialAPI(prim) + if not physx_material_api: + physx_material_api = PhysxSchema.PhysxMaterialAPI.Apply(prim) + + # convert to dict + cfg = cfg.to_dict() + del cfg["func"] + # set into USD API + for attr_name in ["static_friction", "dynamic_friction", "restitution"]: + value = cfg.pop(attr_name, None) + safe_set_attribute_on_usd_schema(usd_physics_material_api, attr_name, value, camel_case=True) + # set into PhysX API + for attr_name, value in cfg.items(): + safe_set_attribute_on_usd_schema(physx_material_api, attr_name, value, camel_case=True) + # return the prim + return prim
+ + +
[文档]@clone +def spawn_deformable_body_material(prim_path: str, cfg: physics_materials_cfg.DeformableBodyMaterialCfg) -> Usd.Prim: + """Create material with deformable-body physics properties. + + Deformable body materials are used to define the physical properties to meshes of a deformable body. These + include the friction and deformable body properties. For more information on deformable body material, + please refer to the documentation on `PxFEMSoftBodyMaterial`_. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration for the physics material. + + Returns: + The spawned deformable body material prim. + + Raises: + ValueError: When a prim already exists at the specified prim path and is not a material. + + .. _PxFEMSoftBodyMaterial: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/structPxFEMSoftBodyMaterialModel.html + """ + # create material prim if no prim exists + if not prim_utils.is_prim_path_valid(prim_path): + _ = UsdShade.Material.Define(stage_utils.get_current_stage(), prim_path) + + # obtain prim + prim = prim_utils.get_prim_at_path(prim_path) + # check if prim is a material + if not prim.IsA(UsdShade.Material): + raise ValueError(f"A prim already exists at path: '{prim_path}' but is not a material.") + # retrieve the deformable-body api + physx_deformable_body_material_api = PhysxSchema.PhysxDeformableBodyMaterialAPI(prim) + if not physx_deformable_body_material_api: + physx_deformable_body_material_api = PhysxSchema.PhysxDeformableBodyMaterialAPI.Apply(prim) + + # convert to dict + cfg = cfg.to_dict() + del cfg["func"] + # set into PhysX API + for attr_name, value in cfg.items(): + safe_set_attribute_on_usd_schema(physx_deformable_body_material_api, attr_name, value, camel_case=True) + # return the prim + return prim
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/materials/physics_materials_cfg.html b/_modules/omni/isaac/lab/sim/spawners/materials/physics_materials_cfg.html new file mode 100644 index 0000000000..580a48f4f4 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/materials/physics_materials_cfg.html @@ -0,0 +1,689 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.materials.physics_materials_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.materials.physics_materials_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+from . import physics_materials
+
+
+
[文档]@configclass +class PhysicsMaterialCfg: + """Configuration parameters for creating a physics material. + + Physics material are PhysX schemas that can be applied to a USD material prim to define the + physical properties related to the material. For example, the friction coefficient, restitution + coefficient, etc. For more information on physics material, please refer to the + `PhysX documentation <https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/classPxBaseMaterial.html>`__. + """ + + func: Callable = MISSING + """Function to use for creating the material."""
+ + +
[文档]@configclass +class RigidBodyMaterialCfg(PhysicsMaterialCfg): + """Physics material parameters for rigid bodies. + + See :meth:`spawn_rigid_body_material` for more information. + + Note: + The default values are the `default values used by PhysX 5 + <https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/rigid-bodies.html#rigid-body-materials>`__. + """ + + func: Callable = physics_materials.spawn_rigid_body_material + + static_friction: float = 0.5 + """The static friction coefficient. Defaults to 0.5.""" + + dynamic_friction: float = 0.5 + """The dynamic friction coefficient. Defaults to 0.5.""" + + restitution: float = 0.0 + """The restitution coefficient. Defaults to 0.0.""" + + improve_patch_friction: bool = True + """Whether to enable patch friction. Defaults to True.""" + + friction_combine_mode: Literal["average", "min", "multiply", "max"] = "average" + """Determines the way friction will be combined during collisions. Defaults to `"average"`. + + .. attention:: + + When two physics materials with different combine modes collide, the combine mode with the higher + priority will be used. The priority order is provided `here + <https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/structPxCombineMode.html>`__. + """ + + restitution_combine_mode: Literal["average", "min", "multiply", "max"] = "average" + """Determines the way restitution coefficient will be combined during collisions. Defaults to `"average"`. + + .. attention:: + + When two physics materials with different combine modes collide, the combine mode with the higher + priority will be used. The priority order is provided `here + <https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/_api_build/structPxCombineMode.html>`__. + """ + + compliant_contact_stiffness: float = 0.0 + """Spring stiffness for a compliant contact model using implicit springs. Defaults to 0.0. + + A higher stiffness results in behavior closer to a rigid contact. The compliant contact model is only enabled + if the stiffness is larger than 0. + """ + + compliant_contact_damping: float = 0.0 + """Damping coefficient for a compliant contact model using implicit springs. Defaults to 0.0. + + Irrelevant if compliant contacts are disabled when :obj:`compliant_contact_stiffness` is set to zero and + rigid contacts are active. + """
+ + +
[文档]@configclass +class DeformableBodyMaterialCfg(PhysicsMaterialCfg): + """Physics material parameters for deformable bodies. + + See :meth:`spawn_deformable_body_material` for more information. + + Note: + The default values are the `default values used by PhysX 5 + <https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/deformable-bodies.html#deformable-body-material>`__. + """ + + func: Callable = physics_materials.spawn_deformable_body_material + + density: float | None = None + """The material density. Defaults to None, in which case the simulation decides the default density.""" + + dynamic_friction: float = 0.25 + """The dynamic friction. Defaults to 0.25.""" + + youngs_modulus: float = 50000000.0 + """The Young's modulus, which defines the body's stiffness. Defaults to 50000000.0. + + The Young's modulus is a measure of the material's ability to deform under stress. It is measured in Pascals (Pa). + """ + + poissons_ratio: float = 0.45 + """The Poisson's ratio which defines the body's volume preservation. Defaults to 0.45. + + The Poisson's ratio is a measure of the material's ability to expand in the lateral direction when compressed + in the axial direction. It is a dimensionless number between 0 and 0.5. Using a value of 0.5 will make the + material incompressible. + """ + + elasticity_damping: float = 0.005 + """The elasticity damping for the deformable material. Defaults to 0.005.""" + + damping_scale: float = 1.0 + """The damping scale for the deformable material. Defaults to 1.0. + + A scale of 1 corresponds to default damping. A value of 0 will only apply damping to certain motions leading + to special effects that look similar to water filled soft bodies. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/materials/visual_materials.html b/_modules/omni/isaac/lab/sim/spawners/materials/visual_materials.html new file mode 100644 index 0000000000..fc426f9c52 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/materials/visual_materials.html @@ -0,0 +1,675 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.materials.visual_materials — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.materials.visual_materials 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+import omni.kit.commands
+from pxr import Usd
+
+from omni.isaac.lab.sim.utils import clone, safe_set_attribute_on_usd_prim
+from omni.isaac.lab.utils.assets import NVIDIA_NUCLEUS_DIR
+
+if TYPE_CHECKING:
+    from . import visual_materials_cfg
+
+
+
[文档]@clone +def spawn_preview_surface(prim_path: str, cfg: visual_materials_cfg.PreviewSurfaceCfg) -> Usd.Prim: + """Create a preview surface prim and override the settings with the given config. + + 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 <https://openusd.org/release/spec_usdpreviewsurface.html>`__. + + The function calls the USD command `CreatePreviewSurfaceMaterialPrim`_ to create the prim. + + .. _CreatePreviewSurfaceMaterialPrim: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.CreatePreviewSurfaceMaterialPrimCommand.html + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn material if it doesn't exist. + if not prim_utils.is_prim_path_valid(prim_path): + omni.kit.commands.execute("CreatePreviewSurfaceMaterialPrim", mtl_path=prim_path, select_new_prim=False) + else: + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + # obtain prim + prim = prim_utils.get_prim_at_path(f"{prim_path}/Shader") + # apply properties + cfg = cfg.to_dict() + del cfg["func"] + for attr_name, attr_value in cfg.items(): + safe_set_attribute_on_usd_prim(prim, f"inputs:{attr_name}", attr_value, camel_case=True) + # return prim + return prim
+ + +
[文档]@clone +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) <https://www.nvidia.com/en-us/design-visualization/technologies/material-definition-language/>`__ + 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 <https://docs.omniverse.nvidia.com/materials-and-rendering/latest/materials.html>`_. + + The function calls the USD command `CreateMdlMaterialPrim`_ to create the prim. + + .. _CreateMdlMaterialPrim: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.CreateMdlMaterialPrimCommand.html + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn material if it doesn't exist. + if not prim_utils.is_prim_path_valid(prim_path): + # extract material name from path + material_name = cfg.mdl_path.split("/")[-1].split(".")[0] + omni.kit.commands.execute( + "CreateMdlMaterialPrim", + mtl_url=cfg.mdl_path.format(NVIDIA_NUCLEUS_DIR=NVIDIA_NUCLEUS_DIR), + mtl_name=material_name, + mtl_path=prim_path, + select_new_prim=False, + ) + else: + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + # obtain prim + prim = prim_utils.get_prim_at_path(f"{prim_path}/Shader") + # apply properties + cfg = cfg.to_dict() + del cfg["func"] + del cfg["mdl_path"] + for attr_name, attr_value in cfg.items(): + safe_set_attribute_on_usd_prim(prim, f"inputs:{attr_name}", attr_value, camel_case=False) + # return prim + return prim
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/materials/visual_materials_cfg.html b/_modules/omni/isaac/lab/sim/spawners/materials/visual_materials_cfg.html new file mode 100644 index 0000000000..0cc040ff98 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/materials/visual_materials_cfg.html @@ -0,0 +1,669 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.materials.visual_materials_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.materials.visual_materials_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from collections.abc import Callable
+from dataclasses import MISSING
+
+from omni.isaac.lab.utils import configclass
+
+from . import visual_materials
+
+
+
[文档]@configclass +class VisualMaterialCfg: + """Configuration parameters for creating a visual material.""" + + func: Callable = MISSING + """The function to use for creating the material."""
+ + +
[文档]@configclass +class PreviewSurfaceCfg(VisualMaterialCfg): + """Configuration parameters for creating a preview surface. + + See :meth:`spawn_preview_surface` for more information. + """ + + func: Callable = visual_materials.spawn_preview_surface + + diffuse_color: tuple[float, float, float] = (0.18, 0.18, 0.18) + """The RGB diffusion color. This is the base color of the surface. Defaults to a dark gray.""" + emissive_color: tuple[float, float, float] = (0.0, 0.0, 0.0) + """The RGB emission component of the surface. Defaults to black.""" + roughness: float = 0.5 + """The roughness for specular lobe. Ranges from 0 (smooth) to 1 (rough). Defaults to 0.5.""" + metallic: float = 0.0 + """The metallic component. Ranges from 0 (dielectric) to 1 (metal). Defaults to 0.""" + opacity: float = 1.0 + """The opacity of the surface. Ranges from 0 (transparent) to 1 (opaque). Defaults to 1. + + Note: + Opacity only affects the surface's appearance during interactive rendering. + """
+ + +
[文档]@configclass +class MdlFileCfg(VisualMaterialCfg): + """Configuration parameters for loading an MDL material from a file. + + See :meth:`spawn_from_mdl_file` for more information. + """ + + func: Callable = visual_materials.spawn_from_mdl_file + + mdl_path: str = MISSING + """The path to the MDL material. + + NVIDIA Omniverse provides various MDL materials in the NVIDIA Nucleus. + To use these materials, you can set the path of the material in the nucleus directory + using the ``{NVIDIA_NUCLEUS_DIR}`` variable. This is internally resolved to the path of the + NVIDIA Nucleus directory on the host machine through the attribute + :attr:`omni.isaac.lab.utils.assets.NVIDIA_NUCLEUS_DIR`. + + For example, to use the "Aluminum_Anodized" material, you can set the path to: + ``{NVIDIA_NUCLEUS_DIR}/Materials/Base/Metals/Aluminum_Anodized.mdl``. + """ + project_uvw: bool | None = None + """Whether to project the UVW coordinates of the material. Defaults to None. + + If None, then the default setting in the MDL material will be used. + """ + albedo_brightness: float | None = None + """Multiplier for the diffuse color of the material. Defaults to None. + + If None, then the default setting in the MDL material will be used. + """ + texture_scale: tuple[float, float] | None = None + """The scale of the texture. Defaults to None. + + If None, then the default setting in the MDL material will be used. + """
+ + +
[文档]@configclass +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 <https://docs.omniverse.nvidia.com/materials-and-rendering/latest/materials.html#omniglass>`__. + + .. note:: + The default values are taken from the glass material in the NVIDIA Nucleus. + """ + + func: Callable = visual_materials.spawn_from_mdl_file + + mdl_path: str = "OmniGlass.mdl" + """The path to the MDL material. Defaults to the glass material in the NVIDIA Nucleus.""" + glass_color: tuple[float, float, float] = (1.0, 1.0, 1.0) + """The RGB color or tint of the glass. Defaults to white.""" + frosting_roughness: float = 0.0 + """The amount of reflectivity of the surface. Ranges from 0 (perfectly clear) to 1 (frosted). + Defaults to 0.""" + thin_walled: bool = False + """Whether to perform thin-walled refraction. Defaults to False.""" + glass_ior: float = 1.491 + """The incidence of refraction to control how much light is bent when passing through the glass. + Defaults to 1.491, which is the IOR of glass. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/meshes/meshes.html b/_modules/omni/isaac/lab/sim/spawners/meshes/meshes.html new file mode 100644 index 0000000000..34e2224112 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/meshes/meshes.html @@ -0,0 +1,923 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.meshes.meshes — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.meshes.meshes 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import numpy as np
+import trimesh
+import trimesh.transformations
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+from pxr import Usd, UsdPhysics
+
+from omni.isaac.lab.sim import schemas
+from omni.isaac.lab.sim.utils import bind_physics_material, bind_visual_material, clone
+
+from ..materials import DeformableBodyMaterialCfg, RigidBodyMaterialCfg
+
+if TYPE_CHECKING:
+    from . import meshes_cfg
+
+
+
[文档]@clone +def spawn_mesh_sphere( + prim_path: str, + cfg: meshes_cfg.MeshSphereCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USD-Mesh sphere prim with the given attributes. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # create a trimesh sphere + sphere = trimesh.creation.uv_sphere(radius=cfg.radius) + # spawn the sphere as a mesh + _spawn_mesh_geom_from_mesh(prim_path, cfg, sphere, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_mesh_cuboid( + prim_path: str, + cfg: meshes_cfg.MeshCuboidCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USD-Mesh cuboid prim with the given attributes. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ # create a trimesh box + box = trimesh.creation.box(cfg.size) + # spawn the cuboid as a mesh + _spawn_mesh_geom_from_mesh(prim_path, cfg, box, translation, orientation, None) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_mesh_cylinder( + prim_path: str, + cfg: meshes_cfg.MeshCylinderCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USD-Mesh cylinder prim with the given attributes. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # align axis from "Z" to input by rotating the cylinder + axis = cfg.axis.upper() + if axis == "X": + transform = trimesh.transformations.rotation_matrix(np.pi / 2, [0, 1, 0]) + elif axis == "Y": + transform = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0]) + else: + transform = None + # create a trimesh cylinder + cylinder = trimesh.creation.cylinder(radius=cfg.radius, height=cfg.height, transform=transform) + # spawn the cylinder as a mesh + _spawn_mesh_geom_from_mesh(prim_path, cfg, cylinder, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_mesh_capsule( + prim_path: str, + cfg: meshes_cfg.MeshCapsuleCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USD-Mesh capsule prim with the given attributes. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # align axis from "Z" to input by rotating the cylinder + axis = cfg.axis.upper() + if axis == "X": + transform = trimesh.transformations.rotation_matrix(np.pi / 2, [0, 1, 0]) + elif axis == "Y": + transform = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0]) + else: + transform = None + # create a trimesh capsule + capsule = trimesh.creation.capsule(radius=cfg.radius, height=cfg.height, transform=transform) + # spawn capsule if it doesn't exist. + _spawn_mesh_geom_from_mesh(prim_path, cfg, capsule, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_mesh_cone( + prim_path: str, + cfg: meshes_cfg.MeshConeCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USD-Mesh cone prim with the given attributes. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # align axis from "Z" to input by rotating the cylinder + axis = cfg.axis.upper() + if axis == "X": + transform = trimesh.transformations.rotation_matrix(np.pi / 2, [0, 1, 0]) + elif axis == "Y": + transform = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0]) + else: + transform = None + # create a trimesh cone + cone = trimesh.creation.cone(radius=cfg.radius, height=cfg.height, transform=transform) + # spawn cone if it doesn't exist. + _spawn_mesh_geom_from_mesh(prim_path, cfg, cone, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +""" +Helper functions. +""" + + +def _spawn_mesh_geom_from_mesh( + prim_path: str, + cfg: meshes_cfg.MeshCfg, + mesh: trimesh.Trimesh, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, + scale: tuple[float, float, float] | None = None, +): + """Create a `USDGeomMesh`_ prim from the given mesh. + + This function is similar to :func:`shapes._spawn_geom_from_prim_type` but spawns the prim from a given mesh. + In case of the mesh, it is spawned as a USDGeomMesh prim with the given vertices and faces. + + There is a difference in how the properties are applied to the prim based on the type of object: + + - Deformable body properties: The properties are applied to the mesh prim: ``{prim_path}/geometry/mesh``. + - Collision properties: The properties are applied to the mesh prim: ``{prim_path}/geometry/mesh``. + - Rigid body properties: The properties are applied to the parent prim: ``{prim_path}``. + + Args: + prim_path: The prim path to spawn the asset at. + cfg: The config containing the properties to apply. + mesh: The mesh to spawn the prim from. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + scale: The scale to apply to the prim. Defaults to None, in which case this is set to identity. + + Raises: + ValueError: If a prim already exists at the given path. + ValueError: If both deformable and rigid properties are used. + ValueError: If both deformable and collision properties are used. + ValueError: If the physics material is not of the correct type. Deformable properties require a deformable + physics material, and rigid properties require a rigid physics material. + + .. _USDGeomMesh: https://openusd.org/dev/api/class_usd_geom_mesh.html + """ + # spawn geometry if it doesn't exist. + if not prim_utils.is_prim_path_valid(prim_path): + prim_utils.create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation) + else: + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + + # check that invalid schema types are not used + if cfg.deformable_props is not None and cfg.rigid_props is not None: + raise ValueError("Cannot use both deformable and rigid properties at the same time.") + if cfg.deformable_props is not None and cfg.collision_props is not None: + raise ValueError("Cannot use both deformable and collision properties at the same time.") + # check material types are correct + if cfg.deformable_props is not None and cfg.physics_material is not None: + if not isinstance(cfg.physics_material, DeformableBodyMaterialCfg): + raise ValueError("Deformable properties require a deformable physics material.") + if cfg.rigid_props is not None and cfg.physics_material is not None: + if not isinstance(cfg.physics_material, RigidBodyMaterialCfg): + raise ValueError("Rigid properties require a rigid physics material.") + + # create all the paths we need for clarity + geom_prim_path = prim_path + "/geometry" + mesh_prim_path = geom_prim_path + "/mesh" + + # create the mesh prim + mesh_prim = prim_utils.create_prim( + mesh_prim_path, + prim_type="Mesh", + scale=scale, + attributes={ + "points": mesh.vertices, + "faceVertexIndices": mesh.faces.flatten(), + "faceVertexCounts": np.asarray([3] * len(mesh.faces)), + "subdivisionScheme": "bilinear", + }, + ) + + # note: in case of deformable objects, we need to apply the deformable properties to the mesh prim. + # this is different from rigid objects where we apply the properties to the parent prim. + if cfg.deformable_props is not None: + # apply mass properties + if cfg.mass_props is not None: + schemas.define_mass_properties(mesh_prim_path, cfg.mass_props) + # apply deformable body properties + schemas.define_deformable_body_properties(mesh_prim_path, cfg.deformable_props) + elif cfg.collision_props is not None: + # decide on type of collision approximation based on the mesh + if cfg.__class__.__name__ == "MeshSphereCfg": + collision_approximation = "boundingSphere" + elif cfg.__class__.__name__ == "MeshCuboidCfg": + collision_approximation = "boundingCube" + else: + # for: MeshCylinderCfg, MeshCapsuleCfg, MeshConeCfg + collision_approximation = "convexHull" + # apply collision approximation to mesh + # note: for primitives, we use the convex hull approximation -- this should be sufficient for most cases. + mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(mesh_prim) + mesh_collision_api.GetApproximationAttr().Set(collision_approximation) + # apply collision properties + schemas.define_collision_properties(mesh_prim_path, cfg.collision_props) + + # apply visual material + if cfg.visual_material is not None: + if not cfg.visual_material_path.startswith("/"): + material_path = f"{geom_prim_path}/{cfg.visual_material_path}" + else: + material_path = cfg.visual_material_path + # create material + cfg.visual_material.func(material_path, cfg.visual_material) + # apply material + bind_visual_material(mesh_prim_path, material_path) + + # apply physics material + if cfg.physics_material is not None: + if not cfg.physics_material_path.startswith("/"): + material_path = f"{geom_prim_path}/{cfg.physics_material_path}" + else: + material_path = cfg.physics_material_path + # create material + cfg.physics_material.func(material_path, cfg.physics_material) + # apply material + bind_physics_material(mesh_prim_path, material_path) + + # note: we apply the rigid properties to the parent prim in case of rigid objects. + if cfg.rigid_props is not None: + # apply mass properties + if cfg.mass_props is not None: + schemas.define_mass_properties(prim_path, cfg.mass_props) + # apply rigid properties + schemas.define_rigid_body_properties(prim_path, cfg.rigid_props) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/meshes/meshes_cfg.html b/_modules/omni/isaac/lab/sim/spawners/meshes/meshes_cfg.html new file mode 100644 index 0000000000..35dee1a0cb --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/meshes/meshes_cfg.html @@ -0,0 +1,703 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.meshes.meshes_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.meshes.meshes_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.sim.spawners import materials
+from omni.isaac.lab.sim.spawners.spawner_cfg import DeformableObjectSpawnerCfg, RigidObjectSpawnerCfg
+from omni.isaac.lab.utils import configclass
+
+from . import meshes
+
+
+
[文档]@configclass +class MeshCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg): + """Configuration parameters for a USD Geometry or Geom prim. + + This class is similar to :class:`ShapeCfg` but is specifically for meshes. + + Meshes support both rigid and deformable properties. However, their schemas are applied at + different levels in the USD hierarchy based on the type of the object. These are described below: + + - Deformable body properties: Applied to the mesh prim: ``{prim_path}/geometry/mesh``. + - Collision properties: Applied to the mesh prim: ``{prim_path}/geometry/mesh``. + - Rigid body properties: Applied to the parent prim: ``{prim_path}``. + + where ``{prim_path}`` is the path to the prim in the USD stage and ``{prim_path}/geometry/mesh`` + is the path to the mesh prim. + + .. note:: + There are mututally exclusive parameters for rigid and deformable properties. If both are set, + then an error will be raised. This also holds if collision and deformable properties are set together. + + """ + + visual_material_path: str = "material" + """Path to the visual material to use for the prim. Defaults to "material". + + If the path is relative, then it will be relative to the prim's path. + This parameter is ignored if `visual_material` is not None. + """ + + visual_material: materials.VisualMaterialCfg | None = None + """Visual material properties. + + Note: + If None, then no visual material will be added. + """ + + physics_material_path: str = "material" + """Path to the physics material to use for the prim. Defaults to "material". + + If the path is relative, then it will be relative to the prim's path. + This parameter is ignored if `physics_material` is not None. + """ + + physics_material: materials.PhysicsMaterialCfg | None = None + """Physics material properties. + + Note: + If None, then no physics material will be added. + """
+ + +
[文档]@configclass +class MeshSphereCfg(MeshCfg): + """Configuration parameters for a sphere mesh prim with deformable properties. + + See :meth:`spawn_mesh_sphere` for more information. + """ + + func: Callable = meshes.spawn_mesh_sphere + + radius: float = MISSING + """Radius of the sphere (in m)."""
+ + +
[文档]@configclass +class MeshCuboidCfg(MeshCfg): + """Configuration parameters for a cuboid mesh prim with deformable properties. + + See :meth:`spawn_mesh_cuboid` for more information. + """ + + func: Callable = meshes.spawn_mesh_cuboid + + size: tuple[float, float, float] = MISSING + """Size of the cuboid (in m)."""
+ + +
[文档]@configclass +class MeshCylinderCfg(MeshCfg): + """Configuration parameters for a cylinder mesh prim with deformable properties. + + See :meth:`spawn_cylinder` for more information. + """ + + func: Callable = meshes.spawn_mesh_cylinder + + radius: float = MISSING + """Radius of the cylinder (in m).""" + height: float = MISSING + """Height of the cylinder (in m).""" + axis: Literal["X", "Y", "Z"] = "Z" + """Axis of the cylinder. Defaults to "Z"."""
+ + +
[文档]@configclass +class MeshCapsuleCfg(MeshCfg): + """Configuration parameters for a capsule mesh prim. + + See :meth:`spawn_capsule` for more information. + """ + + func: Callable = meshes.spawn_mesh_capsule + + radius: float = MISSING + """Radius of the capsule (in m).""" + height: float = MISSING + """Height of the capsule (in m).""" + axis: Literal["X", "Y", "Z"] = "Z" + """Axis of the capsule. Defaults to "Z"."""
+ + +
[文档]@configclass +class MeshConeCfg(MeshCfg): + """Configuration parameters for a cone mesh prim. + + See :meth:`spawn_cone` for more information. + """ + + func: Callable = meshes.spawn_mesh_cone + + radius: float = MISSING + """Radius of the cone (in m).""" + height: float = MISSING + """Height of the v (in m).""" + axis: Literal["X", "Y", "Z"] = "Z" + """Axis of the cone. Defaults to "Z"."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/sensors/sensors.html b/_modules/omni/isaac/lab/sim/spawners/sensors/sensors.html new file mode 100644 index 0000000000..ed259e9b76 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/sensors/sensors.html @@ -0,0 +1,701 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.sensors.sensors — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.sensors.sensors 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+import omni.kit.commands
+import omni.log
+from pxr import Sdf, Usd
+
+from omni.isaac.lab.sim.utils import clone
+from omni.isaac.lab.utils import to_camel_case
+
+if TYPE_CHECKING:
+    from . import sensors_cfg
+
+
+CUSTOM_PINHOLE_CAMERA_ATTRIBUTES = {
+    "projection_type": ("cameraProjectionType", Sdf.ValueTypeNames.Token),
+}
+"""Custom attributes for pinhole camera model.
+
+The dictionary maps the attribute name in the configuration to the attribute name in the USD prim.
+"""
+
+
+CUSTOM_FISHEYE_CAMERA_ATTRIBUTES = {
+    "projection_type": ("cameraProjectionType", Sdf.ValueTypeNames.Token),
+    "fisheye_nominal_width": ("fthetaWidth", Sdf.ValueTypeNames.Float),
+    "fisheye_nominal_height": ("fthetaHeight", Sdf.ValueTypeNames.Float),
+    "fisheye_optical_centre_x": ("fthetaCx", Sdf.ValueTypeNames.Float),
+    "fisheye_optical_centre_y": ("fthetaCy", Sdf.ValueTypeNames.Float),
+    "fisheye_max_fov": ("fthetaMaxFov", Sdf.ValueTypeNames.Float),
+    "fisheye_polynomial_a": ("fthetaPolyA", Sdf.ValueTypeNames.Float),
+    "fisheye_polynomial_b": ("fthetaPolyB", Sdf.ValueTypeNames.Float),
+    "fisheye_polynomial_c": ("fthetaPolyC", Sdf.ValueTypeNames.Float),
+    "fisheye_polynomial_d": ("fthetaPolyD", Sdf.ValueTypeNames.Float),
+    "fisheye_polynomial_e": ("fthetaPolyE", Sdf.ValueTypeNames.Float),
+    "fisheye_polynomial_f": ("fthetaPolyF", Sdf.ValueTypeNames.Float),
+}
+"""Custom attributes for fisheye camera model.
+
+The dictionary maps the attribute name in the configuration to the attribute name in the USD prim.
+"""
+
+
+
[文档]@clone +def spawn_camera( + prim_path: str, + cfg: sensors_cfg.PinholeCameraCfg | sensors_cfg.FisheyeCameraCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USD camera prim with given projection type. + + The function creates various attributes on the camera prim that specify the camera's properties. + These are later used by ``omni.replicator.core`` to render the scene with the given camera. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn camera if it doesn't exist. + if not prim_utils.is_prim_path_valid(prim_path): + prim_utils.create_prim(prim_path, "Camera", translation=translation, orientation=orientation) + else: + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + + # lock camera from viewport (this disables viewport movement for camera) + if cfg.lock_camera: + omni.kit.commands.execute( + "ChangePropertyCommand", + prop_path=Sdf.Path(f"{prim_path}.omni:kit:cameraLock"), + value=True, + prev=None, + type_to_create_if_not_exist=Sdf.ValueTypeNames.Bool, + ) + # decide the custom attributes to add + if cfg.projection_type == "pinhole": + attribute_types = CUSTOM_PINHOLE_CAMERA_ATTRIBUTES + else: + attribute_types = CUSTOM_FISHEYE_CAMERA_ATTRIBUTES + + # TODO: Adjust to handle aperture offsets once supported by omniverse + # Internal ticket from rendering team: OM-42611 + if cfg.horizontal_aperture_offset > 1e-4 or cfg.vertical_aperture_offset > 1e-4: + omni.log.warn("Camera aperture offsets are not supported by Omniverse. These parameters will be ignored.") + + # custom attributes in the config that are not USD Camera parameters + non_usd_cfg_param_names = [ + "func", + "copy_from_source", + "lock_camera", + "visible", + "semantic_tags", + "from_intrinsic_matrix", + ] + # get camera prim + prim = prim_utils.get_prim_at_path(prim_path) + # create attributes for the fisheye camera model + # note: for pinhole those are already part of the USD camera prim + for attr_name, attr_type in attribute_types.values(): + # check if attribute does not exist + if prim.GetAttribute(attr_name).Get() is None: + # create attribute based on type + prim.CreateAttribute(attr_name, attr_type) + # set attribute values + for param_name, param_value in cfg.__dict__.items(): + # check if value is valid + if param_value is None or param_name in non_usd_cfg_param_names: + continue + # obtain prim property name + if param_name in attribute_types: + # check custom attributes + prim_prop_name = attribute_types[param_name][0] + else: + # convert attribute name in prim to cfg name + prim_prop_name = to_camel_case(param_name, to="cC") + # get attribute from the class + prim.GetAttribute(prim_prop_name).Set(param_value) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.html b/_modules/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.html new file mode 100644 index 0000000000..acb28018a4 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/sensors/sensors_cfg.html @@ -0,0 +1,790 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.sensors.sensors_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.sensors.sensors_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from typing import Literal
+
+from omni.isaac.lab.sim.spawners.spawner_cfg import SpawnerCfg
+from omni.isaac.lab.utils import configclass
+
+from . import sensors
+
+
+
[文档]@configclass +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 <https://docs.omniverse.nvidia.com/materials-and-rendering/latest/cameras.html>`__. + + ..note :: + Focal length as well as the aperture sizes and offsets are set as a tenth of the world unit. In our case, the + world unit is Meter s.t. all of these values are set in cm. + + .. note:: + The default values are taken from the `Replicator camera <https://docs.omniverse.nvidia.com/py/replicator/1.9.8/source/extensions/omni.replicator.core/docs/API.html#omni.replicator.core.create.camera>`__ + function. + """ + + func: Callable = sensors.spawn_camera + + projection_type: str = "pinhole" + """Type of projection to use for the camera. Defaults to "pinhole". + + Note: + Currently only "pinhole" is supported. + """ + + clipping_range: tuple[float, float] = (0.01, 1e6) + """Near and far clipping distances (in m). Defaults to (0.01, 1e6). + + The minimum clipping range will shift the camera forward by the specified distance. Don't set it too high to + avoid issues for distance related data types (e.g., ``distance_to_image_plane``). + """ + + focal_length: float = 24.0 + """Perspective focal length (in cm). Defaults to 24.0cm. + + Longer lens lengths narrower FOV, shorter lens lengths wider FOV. + """ + + focus_distance: float = 400.0 + """Distance from the camera to the focus plane (in m). Defaults to 400.0. + + The distance at which perfect sharpness is achieved. + """ + + f_stop: float = 0.0 + """Lens aperture. Defaults to 0.0, which turns off focusing. + + Controls Distance Blurring. Lower Numbers decrease focus range, larger numbers increase it. + """ + + horizontal_aperture: float = 20.955 + """Horizontal aperture (in cm). Defaults to 20.955 cm. + + Emulates sensor/film width on a camera. + + Note: + The default value is the horizontal aperture of a 35 mm spherical projector. + """ + + vertical_aperture: float | None = None + r"""Vertical aperture (in mm). Defaults to None. + + Emulates sensor/film height on a camera. If None, then the vertical aperture is calculated based on the + horizontal aperture and the aspect ratio of the image to maintain squared pixels. This is calculated as: + + .. math:: + \text{vertical aperture} = \text{horizontal aperture} \times \frac{\text{height}}{\text{width}} + """ + + horizontal_aperture_offset: float = 0.0 + """Offsets Resolution/Film gate horizontally. Defaults to 0.0.""" + + vertical_aperture_offset: float = 0.0 + """Offsets Resolution/Film gate vertically. Defaults to 0.0.""" + + lock_camera: bool = True + """Locks the camera in the Omniverse viewport. Defaults to True. + + If True, then the camera remains fixed at its configured transform. This is useful when wanting to view + the camera output on the GUI and not accidentally moving the camera through the GUI interactions. + """ + +
[文档] @classmethod + def from_intrinsic_matrix( + cls, + intrinsic_matrix: list[float], + width: int, + height: int, + clipping_range: tuple[float, float] = (0.01, 1e6), + focal_length: float = 24.0, + focus_distance: float = 400.0, + f_stop: float = 0.0, + projection_type: str = "pinhole", + lock_camera: bool = True, + ) -> PinholeCameraCfg: + r"""Create a :class:`PinholeCameraCfg` class instance from an intrinsic matrix. + + The intrinsic matrix is a 3x3 matrix that defines the mapping between the 3D world coordinates and + the 2D image. The matrix is defined as: + + .. math:: + I_{cam} = \begin{bmatrix} + f_x & 0 & c_x \\ + 0 & f_y & c_y \\ + 0 & 0 & 1 + \\end{bmatrix}, + + where :math:`f_x` and :math:`f_y` are the focal length along x and y direction, while :math:`c_x` and :math:`c_y` are the + principle point offsets along x and y direction respectively. + + Args: + intrinsic_matrix: Intrinsic matrix of the camera in row-major format. + The matrix is defined as [f_x, 0, c_x, 0, f_y, c_y, 0, 0, 1]. Shape is (9,). + width: Width of the image (in pixels). + height: Height of the image (in pixels). + clipping_range: Near and far clipping distances (in m). Defaults to (0.01, 1e6). + focal_length: Perspective focal length (in cm). Defaults to 24.0 cm. + focus_distance: Distance from the camera to the focus plane (in m). Defaults to 400.0 m. + f_stop: Lens aperture. Defaults to 0.0, which turns off focusing. + projection_type: Type of projection to use for the camera. Defaults to "pinhole". + lock_camera: Locks the camera in the Omniverse viewport. Defaults to True. + + Returns: + An instance of the :class:`PinholeCameraCfg` class. + """ + # raise not implemented error is projection type is not pinhole + if projection_type != "pinhole": + raise NotImplementedError("Only pinhole projection type is supported.") + + # extract parameters from matrix + f_x = intrinsic_matrix[0] + c_x = intrinsic_matrix[2] + f_y = intrinsic_matrix[4] + c_y = intrinsic_matrix[5] + # resolve parameters for usd camera + horizontal_aperture = width * focal_length / f_x + vertical_aperture = height * focal_length / f_y + horizontal_aperture_offset = (c_x - width / 2) / f_x + vertical_aperture_offset = (c_y - height / 2) / f_y + + return cls( + projection_type=projection_type, + clipping_range=clipping_range, + focal_length=focal_length, + focus_distance=focus_distance, + f_stop=f_stop, + horizontal_aperture=horizontal_aperture, + vertical_aperture=vertical_aperture, + horizontal_aperture_offset=horizontal_aperture_offset, + vertical_aperture_offset=vertical_aperture_offset, + lock_camera=lock_camera, + )
+ + +
[文档]@configclass +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 <https://docs.omniverse.nvidia.com/materials-and-rendering/latest/cameras.html#fisheye-properties>`__. + + .. note:: + The default values are taken from the `Replicator camera <https://docs.omniverse.nvidia.com/py/replicator/1.9.8/source/extensions/omni.replicator.core/docs/API.html#omni.replicator.core.create.camera>`__ + function. + + .. _fish-eye camera: https://en.wikipedia.org/wiki/Fisheye_lens + """ + + func: Callable = sensors.spawn_camera + + projection_type: Literal[ + "fisheye_orthographic", "fisheye_equidistant", "fisheye_equisolid", "fisheye_polynomial", "fisheye_spherical" + ] = "fisheye_polynomial" + r"""Type of projection to use for the camera. Defaults to "fisheye_polynomial". + + Available options: + + - ``"fisheye_orthographic"``: Fisheye camera model using orthographic correction. + - ``"fisheye_equidistant"``: Fisheye camera model using equidistant correction. + - ``"fisheye_equisolid"``: Fisheye camera model using equisolid correction. + - ``"fisheye_polynomial"``: Fisheye camera model with :math:`360^{\circ}` spherical projection. + - ``"fisheye_spherical"``: Fisheye camera model with :math:`360^{\circ}` full-frame projection. + """ + + fisheye_nominal_width: float = 1936.0 + """Nominal width of fisheye lens model (in pixels). Defaults to 1936.0.""" + + fisheye_nominal_height: float = 1216.0 + """Nominal height of fisheye lens model (in pixels). Defaults to 1216.0.""" + + fisheye_optical_centre_x: float = 970.94244 + """Horizontal optical centre position of fisheye lens model (in pixels). Defaults to 970.94244.""" + + fisheye_optical_centre_y: float = 600.37482 + """Vertical optical centre position of fisheye lens model (in pixels). Defaults to 600.37482.""" + + fisheye_max_fov: float = 200.0 + """Maximum field of view of fisheye lens model (in degrees). Defaults to 200.0 degrees.""" + + fisheye_polynomial_a: float = 0.0 + """First component of fisheye polynomial. Defaults to 0.0.""" + + fisheye_polynomial_b: float = 0.00245 + """Second component of fisheye polynomial. Defaults to 0.00245.""" + + fisheye_polynomial_c: float = 0.0 + """Third component of fisheye polynomial. Defaults to 0.0.""" + + fisheye_polynomial_d: float = 0.0 + """Fourth component of fisheye polynomial. Defaults to 0.0.""" + + fisheye_polynomial_e: float = 0.0 + """Fifth component of fisheye polynomial. Defaults to 0.0.""" + + fisheye_polynomial_f: float = 0.0 + """Sixth component of fisheye polynomial. Defaults to 0.0."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/shapes/shapes.html b/_modules/omni/isaac/lab/sim/spawners/shapes/shapes.html new file mode 100644 index 0000000000..f189ccb4f8 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/shapes/shapes.html @@ -0,0 +1,860 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.shapes.shapes — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.shapes.shapes 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import omni.isaac.core.utils.prims as prim_utils
+from pxr import Usd
+
+from omni.isaac.lab.sim import schemas
+from omni.isaac.lab.sim.utils import bind_physics_material, bind_visual_material, clone
+
+if TYPE_CHECKING:
+    from . import shapes_cfg
+
+
+
[文档]@clone +def spawn_sphere( + prim_path: str, + cfg: shapes_cfg.SphereCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USDGeom-based sphere prim with the given attributes. + + For more information, see `USDGeomSphere <https://openusd.org/dev/api/class_usd_geom_sphere.html>`_. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn sphere if it doesn't exist. + attributes = {"radius": cfg.radius} + _spawn_geom_from_prim_type(prim_path, cfg, "Sphere", attributes, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_cuboid( + prim_path: str, + cfg: shapes_cfg.CuboidCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USDGeom-based cuboid prim with the given attributes. + + For more information, see `USDGeomCube <https://openusd.org/dev/api/class_usd_geom_cube.html>`_. + + Note: + Since USD only supports cubes, we set the size of the cube to the minimum of the given size and + scale the cube accordingly. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + If a prim already exists at the given path. + """ + # resolve the scale + size = min(cfg.size) + scale = [dim / size for dim in cfg.size] + # spawn cuboid if it doesn't exist. + attributes = {"size": size} + _spawn_geom_from_prim_type(prim_path, cfg, "Cube", attributes, translation, orientation, scale) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_cylinder( + prim_path: str, + cfg: shapes_cfg.CylinderCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USDGeom-based cylinder prim with the given attributes. + + For more information, see `USDGeomCylinder <https://openusd.org/dev/api/class_usd_geom_cylinder.html>`_. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn cylinder if it doesn't exist. + attributes = {"radius": cfg.radius, "height": cfg.height, "axis": cfg.axis.upper()} + _spawn_geom_from_prim_type(prim_path, cfg, "Cylinder", attributes, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_capsule( + prim_path: str, + cfg: shapes_cfg.CapsuleCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USDGeom-based capsule prim with the given attributes. + + For more information, see `USDGeomCapsule <https://openusd.org/dev/api/class_usd_geom_capsule.html>`_. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn capsule if it doesn't exist. + attributes = {"radius": cfg.radius, "height": cfg.height, "axis": cfg.axis.upper()} + _spawn_geom_from_prim_type(prim_path, cfg, "Capsule", attributes, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +
[文档]@clone +def spawn_cone( + prim_path: str, + cfg: shapes_cfg.ConeCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Create a USDGeom-based cone prim with the given attributes. + + For more information, see `USDGeomCone <https://openusd.org/dev/api/class_usd_geom_cone.html>`_. + + .. note:: + This function is decorated with :func:`clone` that resolves prim path into list of paths + if the input prim path is a regex pattern. This is done to support spawning multiple assets + from a single and cloning the USD prim at the given path expression. + + Args: + prim_path: The prim path or pattern to spawn the asset at. If the prim path is a regex pattern, + then the asset is spawned at all the matching prim paths. + cfg: The configuration instance. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + + Returns: + The created prim. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn cone if it doesn't exist. + attributes = {"radius": cfg.radius, "height": cfg.height, "axis": cfg.axis.upper()} + _spawn_geom_from_prim_type(prim_path, cfg, "Cone", attributes, translation, orientation) + # return the prim + return prim_utils.get_prim_at_path(prim_path)
+ + +""" +Helper functions. +""" + + +def _spawn_geom_from_prim_type( + prim_path: str, + cfg: shapes_cfg.ShapeCfg, + prim_type: str, + attributes: dict, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, + scale: tuple[float, float, float] | None = None, +): + """Create a USDGeom-based prim with the given attributes. + + To make the asset instanceable, we must follow a certain structure dictated by how USD scene-graph + instancing and physics work. The rigid body component must be added to each instance and not the + referenced asset (i.e. the prototype prim itself). This is because the rigid body component defines + properties that are specific to each instance and cannot be shared under the referenced asset. For + more information, please check the `documentation <https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/rigid-bodies.html#instancing-rigid-bodies>`_. + + Due to the above, we follow the following structure: + + * ``{prim_path}`` - The root prim that is an Xform with the rigid body and mass APIs if configured. + * ``{prim_path}/geometry`` - The prim that contains the mesh and optionally the materials if configured. + If instancing is enabled, this prim will be an instanceable reference to the prototype prim. + + Args: + prim_path: The prim path to spawn the asset at. + cfg: The config containing the properties to apply. + prim_type: The type of prim to create. + attributes: The attributes to apply to the prim. + translation: The translation to apply to the prim w.r.t. its parent prim. Defaults to None, in which case + this is set to the origin. + orientation: The orientation in (w, x, y, z) to apply to the prim w.r.t. its parent prim. Defaults to None, + in which case this is set to identity. + scale: The scale to apply to the prim. Defaults to None, in which case this is set to identity. + + Raises: + ValueError: If a prim already exists at the given path. + """ + # spawn geometry if it doesn't exist. + if not prim_utils.is_prim_path_valid(prim_path): + prim_utils.create_prim(prim_path, prim_type="Xform", translation=translation, orientation=orientation) + else: + raise ValueError(f"A prim already exists at path: '{prim_path}'.") + + # create all the paths we need for clarity + geom_prim_path = prim_path + "/geometry" + mesh_prim_path = geom_prim_path + "/mesh" + + # create the geometry prim + prim_utils.create_prim(mesh_prim_path, prim_type, scale=scale, attributes=attributes) + # apply collision properties + if cfg.collision_props is not None: + schemas.define_collision_properties(mesh_prim_path, cfg.collision_props) + # apply visual material + if cfg.visual_material is not None: + if not cfg.visual_material_path.startswith("/"): + material_path = f"{geom_prim_path}/{cfg.visual_material_path}" + else: + material_path = cfg.visual_material_path + # create material + cfg.visual_material.func(material_path, cfg.visual_material) + # apply material + bind_visual_material(mesh_prim_path, material_path) + # apply physics material + if cfg.physics_material is not None: + if not cfg.physics_material_path.startswith("/"): + material_path = f"{geom_prim_path}/{cfg.physics_material_path}" + else: + material_path = cfg.physics_material_path + # create material + cfg.physics_material.func(material_path, cfg.physics_material) + # apply material + bind_physics_material(mesh_prim_path, material_path) + + # note: we apply rigid properties in the end to later make the instanceable prim + # apply mass properties + if cfg.mass_props is not None: + schemas.define_mass_properties(prim_path, cfg.mass_props) + # apply rigid body properties + if cfg.rigid_props is not None: + schemas.define_rigid_body_properties(prim_path, cfg.rigid_props) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/shapes/shapes_cfg.html b/_modules/omni/isaac/lab/sim/spawners/shapes/shapes_cfg.html new file mode 100644 index 0000000000..9003b9a1e9 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/shapes/shapes_cfg.html @@ -0,0 +1,681 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.shapes.shapes_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.shapes.shapes_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.sim.spawners import materials
+from omni.isaac.lab.sim.spawners.spawner_cfg import RigidObjectSpawnerCfg
+from omni.isaac.lab.utils import configclass
+
+from . import shapes
+
+
+
[文档]@configclass +class ShapeCfg(RigidObjectSpawnerCfg): + """Configuration parameters for a USD Geometry or Geom prim.""" + + visual_material_path: str = "material" + """Path to the visual material to use for the prim. Defaults to "material". + + If the path is relative, then it will be relative to the prim's path. + This parameter is ignored if `visual_material` is not None. + """ + visual_material: materials.VisualMaterialCfg | None = None + """Visual material properties. + + Note: + If None, then no visual material will be added. + """ + + physics_material_path: str = "material" + """Path to the physics material to use for the prim. Defaults to "material". + + If the path is relative, then it will be relative to the prim's path. + This parameter is ignored if `physics_material` is not None. + """ + physics_material: materials.PhysicsMaterialCfg | None = None + """Physics material properties. + + Note: + If None, then no physics material will be added. + """
+ + +
[文档]@configclass +class SphereCfg(ShapeCfg): + """Configuration parameters for a sphere prim. + + See :meth:`spawn_sphere` for more information. + """ + + func: Callable = shapes.spawn_sphere + + radius: float = MISSING + """Radius of the sphere (in m)."""
+ + +
[文档]@configclass +class CuboidCfg(ShapeCfg): + """Configuration parameters for a cuboid prim. + + See :meth:`spawn_cuboid` for more information. + """ + + func: Callable = shapes.spawn_cuboid + + size: tuple[float, float, float] = MISSING + """Size of the cuboid."""
+ + +
[文档]@configclass +class CylinderCfg(ShapeCfg): + """Configuration parameters for a cylinder prim. + + See :meth:`spawn_cylinder` for more information. + """ + + func: Callable = shapes.spawn_cylinder + + radius: float = MISSING + """Radius of the cylinder (in m).""" + height: float = MISSING + """Height of the cylinder (in m).""" + axis: Literal["X", "Y", "Z"] = "Z" + """Axis of the cylinder. Defaults to "Z"."""
+ + +
[文档]@configclass +class CapsuleCfg(ShapeCfg): + """Configuration parameters for a capsule prim. + + See :meth:`spawn_capsule` for more information. + """ + + func: Callable = shapes.spawn_capsule + + radius: float = MISSING + """Radius of the capsule (in m).""" + height: float = MISSING + """Height of the capsule (in m).""" + axis: Literal["X", "Y", "Z"] = "Z" + """Axis of the capsule. Defaults to "Z"."""
+ + +
[文档]@configclass +class ConeCfg(ShapeCfg): + """Configuration parameters for a cone prim. + + See :meth:`spawn_cone` for more information. + """ + + func: Callable = shapes.spawn_cone + + radius: float = MISSING + """Radius of the cone (in m).""" + height: float = MISSING + """Height of the v (in m).""" + axis: Literal["X", "Y", "Z"] = "Z" + """Axis of the cone. Defaults to "Z"."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/spawner_cfg.html b/_modules/omni/isaac/lab/sim/spawners/spawner_cfg.html new file mode 100644 index 0000000000..e7dc2d605d --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/spawner_cfg.html @@ -0,0 +1,677 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.spawner_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.spawner_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import MISSING
+
+from pxr import Usd
+
+from omni.isaac.lab.sim import schemas
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class SpawnerCfg: + """Configuration parameters for spawning an asset. + + Spawning an asset is done by calling the :attr:`func` function. The function takes in the + prim path to spawn the asset at, the configuration instance and transformation, and returns the + prim path of the spawned asset. + + The function is typically decorated with :func:`omni.isaac.lab.sim.spawner.utils.clone` decorator + that checks if input prim path is a regex expression and spawns the asset at all matching prims. + For this, the decorator uses the Cloner API from Isaac Sim and handles the :attr:`copy_from_source` + parameter. + """ + + func: Callable[..., Usd.Prim] = MISSING + """Function to use for spawning the asset. + + The function takes in the prim path (or expression) to spawn the asset at, the configuration instance + and transformation, and returns the source prim spawned. + """ + + visible: bool = True + """Whether the spawned asset should be visible. Defaults to True.""" + + semantic_tags: list[tuple[str, str]] | None = None + """List of semantic tags to add to the spawned asset. Defaults to None, + which means no semantic tags will be added. + + The semantic tags follow the `Replicator Semantic` tagging system. Each tag is a tuple of the + form ``(type, data)``, where ``type`` is the type of the tag and ``data`` is the semantic label + associated with the tag. For example, to annotate a spawned asset in the class avocado, the semantic + tag would be ``[("class", "avocado")]``. + + You can specify multiple semantic tags by passing in a list of tags. For example, to annotate a + spawned asset in the class avocado and the color green, the semantic tags would be + ``[("class", "avocado"), ("color", "green")]``. + + .. seealso:: + + For more information on the semantics filter, see the documentation for the `semantics schema editor`_. + + .. _semantics schema editor: https://docs.omniverse.nvidia.com/extensions/latest/ext_replicator/semantics_schema_editor.html#semantics-filtering + + """ + + copy_from_source: bool = True + """Whether to copy the asset from the source prim or inherit it. Defaults to True. + + This parameter is only used when cloning prims. If False, then the asset will be inherited from + the source prim, i.e. all USD changes to the source prim will be reflected in the cloned prims. + """
+ + +
[文档]@configclass +class RigidObjectSpawnerCfg(SpawnerCfg): + """Configuration parameters for spawning a rigid asset. + + Note: + By default, all properties are set to None. This means that no properties will be added or modified + to the prim outside of the properties available by default when spawning the prim. + """ + + mass_props: schemas.MassPropertiesCfg | None = None + """Mass properties.""" + + rigid_props: schemas.RigidBodyPropertiesCfg | None = None + """Rigid body properties. + + For making a rigid object static, set the :attr:`schemas.RigidBodyPropertiesCfg.kinematic_enabled` + as True. This will make the object static and will not be affected by gravity or other forces. + """ + + collision_props: schemas.CollisionPropertiesCfg | None = None + """Properties to apply to all collision meshes.""" + + activate_contact_sensors: bool = False + """Activate contact reporting on all rigid bodies. Defaults to False. + + This adds the PhysxContactReporter API to all the rigid bodies in the given prim path and its children. + """
+ + +
[文档]@configclass +class DeformableObjectSpawnerCfg(SpawnerCfg): + """Configuration parameters for spawning a deformable asset. + + Unlike rigid objects, deformable objects are affected by forces and can deform when subjected to + external forces. This class is used to configure the properties of the deformable object. + + Deformable bodies don't have a separate collision mesh. The collision mesh is the same as the visual mesh. + The collision properties such as rest and collision offsets are specified in the :attr:`deformable_props`. + + Note: + By default, all properties are set to None. This means that no properties will be added or modified + to the prim outside of the properties available by default when spawning the prim. + """ + + mass_props: schemas.MassPropertiesCfg | None = None + """Mass properties.""" + + deformable_props: schemas.DeformableBodyPropertiesCfg | None = None + """Deformable body properties."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/wrappers/wrappers.html b/_modules/omni/isaac/lab/sim/spawners/wrappers/wrappers.html new file mode 100644 index 0000000000..727671829a --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/wrappers/wrappers.html @@ -0,0 +1,728 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.wrappers.wrappers — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.wrappers.wrappers 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import random
+import re
+from typing import TYPE_CHECKING
+
+import carb
+import omni.isaac.core.utils.prims as prim_utils
+import omni.isaac.core.utils.stage as stage_utils
+from pxr import Sdf, Usd
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.sim.spawners.from_files import UsdFileCfg
+
+if TYPE_CHECKING:
+    from . import wrappers_cfg
+
+
+
[文档]def spawn_multi_asset( + prim_path: str, + cfg: wrappers_cfg.MultiAssetSpawnerCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn multiple assets based on the provided configurations. + + This function spawns multiple assets based on the provided configurations. The assets are spawned + in the order they are provided in the list. If the :attr:`~MultiAssetSpawnerCfg.random_choice` parameter is + set to True, a random asset configuration is selected for each spawn. + + Args: + prim_path: The prim path to spawn the assets. + cfg: The configuration for spawning the assets. + translation: The translation of the spawned assets. Default is None. + orientation: The orientation of the spawned assets in (w, x, y, z) order. Default is None. + + Returns: + The created prim at the first prim path. + """ + # resolve: {SPAWN_NS}/AssetName + # note: this assumes that the spawn namespace already exists in the stage + root_path, asset_path = prim_path.rsplit("/", 1) + # check if input is a regex expression + # note: a valid prim path can only contain alphanumeric characters, underscores, and forward slashes + is_regex_expression = re.match(r"^[a-zA-Z0-9/_]+$", root_path) is None + + # resolve matching prims for source prim path expression + if is_regex_expression and root_path != "": + source_prim_paths = sim_utils.find_matching_prim_paths(root_path) + # if no matching prims are found, raise an error + if len(source_prim_paths) == 0: + raise RuntimeError( + f"Unable to find source prim path: '{root_path}'. Please create the prim before spawning." + ) + else: + source_prim_paths = [root_path] + + # find a free prim path to hold all the template prims + template_prim_path = stage_utils.get_next_free_path("/World/Template") + prim_utils.create_prim(template_prim_path, "Scope") + + # spawn everything first in a "Dataset" prim + proto_prim_paths = list() + for index, asset_cfg in enumerate(cfg.assets_cfg): + # append semantic tags if specified + if cfg.semantic_tags is not None: + if asset_cfg.semantic_tags is None: + asset_cfg.semantic_tags = cfg.semantic_tags + else: + asset_cfg.semantic_tags += cfg.semantic_tags + # override settings for properties + attr_names = ["mass_props", "rigid_props", "collision_props", "activate_contact_sensors", "deformable_props"] + for attr_name in attr_names: + attr_value = getattr(cfg, attr_name) + if hasattr(asset_cfg, attr_name) and attr_value is not None: + setattr(asset_cfg, attr_name, attr_value) + # spawn single instance + proto_prim_path = f"{template_prim_path}/Asset_{index:04d}" + asset_cfg.func(proto_prim_path, asset_cfg, translation=translation, orientation=orientation) + # append to proto prim paths + proto_prim_paths.append(proto_prim_path) + + # resolve prim paths for spawning and cloning + prim_paths = [f"{source_prim_path}/{asset_path}" for source_prim_path in source_prim_paths] + + # acquire stage + stage = stage_utils.get_current_stage() + + # manually clone prims if the source prim path is a regex expression + # note: unlike in the cloner API from Isaac Sim, we do not "reset" xforms on the copied prims. + # This is because the "spawn" calls during the creation of the proto prims already handles this operation. + with Sdf.ChangeBlock(): + for index, prim_path in enumerate(prim_paths): + # spawn single instance + env_spec = Sdf.CreatePrimInLayer(stage.GetRootLayer(), prim_path) + # randomly select an asset configuration + if cfg.random_choice: + proto_path = random.choice(proto_prim_paths) + else: + proto_path = proto_prim_paths[index % len(proto_prim_paths)] + # copy the proto prim + Sdf.CopySpec(env_spec.layer, Sdf.Path(proto_path), env_spec.layer, Sdf.Path(prim_path)) + + # delete the dataset prim after spawning + prim_utils.delete_prim(template_prim_path) + + # set carb setting to indicate Isaac Lab's environments that different prims have been spawned + # at varying prim paths. In this case, PhysX parser shouldn't optimize the stage parsing. + # the flag is mainly used to inform the user that they should disable `InteractiveScene.replicate_physics` + carb_settings_iface = carb.settings.get_settings() + carb_settings_iface.set_bool("/isaaclab/spawn/multi_assets", True) + + # return the prim + return prim_utils.get_prim_at_path(prim_paths[0])
+ + +
[文档]def spawn_multi_usd_file( + prim_path: str, + cfg: wrappers_cfg.MultiUsdFileCfg, + translation: tuple[float, float, float] | None = None, + orientation: tuple[float, float, float, float] | None = None, +) -> Usd.Prim: + """Spawn multiple USD files based on the provided configurations. + + This function creates configuration instances corresponding the individual USD files and + calls the :meth:`spawn_multi_asset` method to spawn them into the scene. + + Args: + prim_path: The prim path to spawn the assets. + cfg: The configuration for spawning the assets. + translation: The translation of the spawned assets. Default is None. + orientation: The orientation of the spawned assets in (w, x, y, z) order. Default is None. + + Returns: + The created prim at the first prim path. + """ + # needed here to avoid circular imports + from .wrappers_cfg import MultiAssetSpawnerCfg + + # parse all the usd files + if isinstance(cfg.usd_path, str): + usd_paths = [cfg.usd_path] + else: + usd_paths = cfg.usd_path + + # make a template usd config + usd_template_cfg = UsdFileCfg() + for attr_name, attr_value in cfg.__dict__.items(): + # skip names we know are not present + if attr_name in ["func", "usd_path", "random_choice"]: + continue + # set the attribute into the template + setattr(usd_template_cfg, attr_name, attr_value) + + # create multi asset configuration of USD files + multi_asset_cfg = MultiAssetSpawnerCfg(assets_cfg=[]) + for usd_path in usd_paths: + usd_cfg = usd_template_cfg.replace(usd_path=usd_path) + multi_asset_cfg.assets_cfg.append(usd_cfg) + # set random choice + multi_asset_cfg.random_choice = cfg.random_choice + + # call the original function + return spawn_multi_asset(prim_path, multi_asset_cfg, translation, orientation)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.html b/_modules/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.html new file mode 100644 index 0000000000..b4d1b64022 --- /dev/null +++ b/_modules/omni/isaac/lab/sim/spawners/wrappers/wrappers_cfg.html @@ -0,0 +1,626 @@ + + + + + + + + + + + omni.isaac.lab.sim.spawners.wrappers.wrappers_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.spawners.wrappers.wrappers_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.sim.spawners.from_files import UsdFileCfg
+from omni.isaac.lab.sim.spawners.spawner_cfg import DeformableObjectSpawnerCfg, RigidObjectSpawnerCfg, SpawnerCfg
+from omni.isaac.lab.utils import configclass
+
+from . import wrappers
+
+
+
[文档]@configclass +class MultiAssetSpawnerCfg(RigidObjectSpawnerCfg, DeformableObjectSpawnerCfg): + """Configuration parameters for loading multiple assets from their individual configurations. + + Specifying values for any properties at the configuration level will override the settings of + individual assets' configuration. For instance if the attribute + :attr:`MultiAssetSpawnerCfg.mass_props` is specified, its value will overwrite the values of the + mass properties in each configuration inside :attr:`assets_cfg` (wherever applicable). + This is done to simplify configuring similar properties globally. By default, all properties are set to None. + + The following is an exception to the above: + + * :attr:`visible`: This parameter is ignored. Its value for the individual assets is used. + * :attr:`semantic_tags`: If specified, it will be appended to each individual asset's semantic tags. + + """ + + func = wrappers.spawn_multi_asset + + assets_cfg: list[SpawnerCfg] = MISSING + """List of asset configurations to spawn.""" + + random_choice: bool = True + """Whether to randomly select an asset configuration. Default is True. + + If False, the asset configurations are spawned in the order they are provided in the list. + If True, a random asset configuration is selected for each spawn. + """
+ + +
[文档]@configclass +class MultiUsdFileCfg(UsdFileCfg): + """Configuration parameters for loading multiple USD files. + + Specifying values for any properties at the configuration level is applied to all the assets + imported from their USD files. + + .. tip:: + It is recommended that all the USD based assets follow a similar prim-hierarchy. + + """ + + func = wrappers.spawn_multi_usd_file + + usd_path: str | list[str] = MISSING + """Path or a list of paths to the USD files to spawn asset from.""" + + random_choice: bool = True + """Whether to randomly select an asset configuration. Default is True. + + If False, the asset configurations are spawned in the order they are provided in the list. + If True, a random asset configuration is selected for each spawn. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/sim/utils.html b/_modules/omni/isaac/lab/sim/utils.html new file mode 100644 index 0000000000..fed6e4c95b --- /dev/null +++ b/_modules/omni/isaac/lab/sim/utils.html @@ -0,0 +1,1423 @@ + + + + + + + + + + + omni.isaac.lab.sim.utils — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.sim.utils 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module with USD-related utilities."""
+
+from __future__ import annotations
+
+import functools
+import inspect
+import re
+from collections.abc import Callable
+from typing import TYPE_CHECKING, Any
+
+import omni.isaac.core.utils.stage as stage_utils
+import omni.kit.commands
+import omni.log
+from omni.isaac.cloner import Cloner
+from pxr import PhysxSchema, Sdf, Usd, UsdGeom, UsdPhysics, UsdShade
+
+# from Isaac Sim 4.2 onwards, pxr.Semantics is deprecated
+try:
+    import Semantics
+except ModuleNotFoundError:
+    from pxr import Semantics
+
+from omni.isaac.lab.utils.string import to_camel_case
+
+from . import schemas
+
+if TYPE_CHECKING:
+    from .spawners.spawner_cfg import SpawnerCfg
+
+"""
+Attribute - Setters.
+"""
+
+
+
[文档]def safe_set_attribute_on_usd_schema(schema_api: Usd.APISchemaBase, name: str, value: Any, camel_case: bool): + """Set the value of an attribute on its USD schema if it exists. + + A USD API schema serves as an interface or API for authoring and extracting a set of attributes. + They typically derive from the :class:`pxr.Usd.SchemaBase` class. This function checks if the + attribute exists on the schema and sets the value of the attribute if it exists. + + Args: + schema_api: The USD schema to set the attribute on. + name: The name of the attribute. + value: The value to set the attribute to. + camel_case: Whether to convert the attribute name to camel case. + + Raises: + TypeError: When the input attribute name does not exist on the provided schema API. + """ + # if value is None, do nothing + if value is None: + return + # convert attribute name to camel case + if camel_case: + attr_name = to_camel_case(name, to="CC") + else: + attr_name = name + # retrieve the attribute + # reference: https://openusd.org/dev/api/_usd__page__common_idioms.html#Usd_Create_Or_Get_Property + attr = getattr(schema_api, f"Create{attr_name}Attr", None) + # check if attribute exists + if attr is not None: + attr().Set(value) + else: + # think: do we ever need to create the attribute if it doesn't exist? + # currently, we are not doing this since the schemas are already created with some defaults. + omni.log.error(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.") + raise TypeError(f"Attribute '{attr_name}' does not exist on prim '{schema_api.GetPath()}'.")
+ + +
[文档]def safe_set_attribute_on_usd_prim(prim: Usd.Prim, attr_name: str, value: Any, camel_case: bool): + """Set the value of a attribute on its USD prim. + + The function creates a new attribute if it does not exist on the prim. This is because in some cases (such + as with shaders), their attributes are not exposed as USD prim properties that can be altered. This function + allows us to set the value of the attributes in these cases. + + Args: + prim: The USD prim to set the attribute on. + attr_name: The name of the attribute. + value: The value to set the attribute to. + camel_case: Whether to convert the attribute name to camel case. + """ + # if value is None, do nothing + if value is None: + return + # convert attribute name to camel case + if camel_case: + attr_name = to_camel_case(attr_name, to="cC") + # resolve sdf type based on value + if isinstance(value, bool): + sdf_type = Sdf.ValueTypeNames.Bool + elif isinstance(value, int): + sdf_type = Sdf.ValueTypeNames.Int + elif isinstance(value, float): + sdf_type = Sdf.ValueTypeNames.Float + elif isinstance(value, (tuple, list)) and len(value) == 3 and any(isinstance(v, float) for v in value): + sdf_type = Sdf.ValueTypeNames.Float3 + elif isinstance(value, (tuple, list)) and len(value) == 2 and any(isinstance(v, float) for v in value): + sdf_type = Sdf.ValueTypeNames.Float2 + else: + raise NotImplementedError( + f"Cannot set attribute '{attr_name}' with value '{value}'. Please modify the code to support this type." + ) + # change property + omni.kit.commands.execute( + "ChangePropertyCommand", + prop_path=Sdf.Path(f"{prim.GetPath()}.{attr_name}"), + value=value, + prev=None, + type_to_create_if_not_exist=sdf_type, + usd_context_name=prim.GetStage(), + )
+ + +""" +Decorators. +""" + + +
[文档]def apply_nested(func: Callable) -> Callable: + """Decorator to apply a function to all prims under a specified prim-path. + + The function iterates over the provided prim path and all its children to apply input function + to all prims under the specified prim path. + + If the function succeeds to apply to a prim, it will not look at the children of that prim. + This is based on the physics behavior that nested schemas are not allowed. For example, a parent prim + and its child prim cannot both have a rigid-body schema applied on them, or it is not possible to + have nested articulations. + + While traversing the prims under the specified prim path, the function will throw a warning if it + does not succeed to apply the function to any prim. This is because the user may have intended to + apply the function to a prim that does not have valid attributes, or the prim may be an instanced prim. + + Args: + func: The function to apply to all prims under a specified prim-path. The function + must take the prim-path and other arguments. It should return a boolean indicating whether + the function succeeded or not. + + Returns: + The wrapped function that applies the function to all prims under a specified prim-path. + + Raises: + ValueError: If the prim-path does not exist on the stage. + """ + + @functools.wraps(func) + def wrapper(prim_path: str | Sdf.Path, *args, **kwargs): + # map args and kwargs to function signature so we can get the stage + # note: we do this to check if stage is given in arg or kwarg + sig = inspect.signature(func) + bound_args = sig.bind(prim_path, *args, **kwargs) + # get current stage + stage = bound_args.arguments.get("stage") + if stage is None: + stage = stage_utils.get_current_stage() + # get USD prim + prim: Usd.Prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # add iterable to check if property was applied on any of the prims + count_success = 0 + instanced_prim_paths = [] + # iterate over all prims under prim-path + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + child_prim_path = child_prim.GetPath().pathString # type: ignore + # check if prim is a prototype + if child_prim.IsInstance(): + instanced_prim_paths.append(child_prim_path) + continue + # set properties + success = func(child_prim_path, *args, **kwargs) + # if successful, do not look at children + # this is based on the physics behavior that nested schemas are not allowed + if not success: + all_prims += child_prim.GetChildren() + else: + count_success += 1 + # check if we were successful in applying the function to any prim + if count_success == 0: + omni.log.warn( + f"Could not perform '{func.__name__}' on any prims under: '{prim_path}'." + " This might be because of the following reasons:" + "\n\t(1) The desired attribute does not exist on any of the prims." + "\n\t(2) The desired attribute exists on an instanced prim." + f"\n\t\tDiscovered list of instanced prim paths: {instanced_prim_paths}" + ) + + return wrapper
+ + +
[文档]def clone(func: Callable) -> Callable: + """Decorator for cloning a prim based on matching prim paths of the prim's parent. + + The decorator checks if the parent prim path matches any prim paths in the stage. If so, it clones the + spawned prim at each matching prim path. For example, if the input prim path is: ``/World/Table_[0-9]/Bottle``, + the decorator will clone the prim at each matching prim path of the parent prim: ``/World/Table_0/Bottle``, + ``/World/Table_1/Bottle``, etc. + + Note: + For matching prim paths, the decorator assumes that valid prims exist for all matching prim paths. + In case no matching prim paths are found, the decorator raises a ``RuntimeError``. + + Args: + func: The function to decorate. + + Returns: + The decorated function that spawns the prim and clones it at each matching prim path. + It returns the spawned source prim, i.e., the first prim in the list of matching prim paths. + """ + + @functools.wraps(func) + def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs): + # cast prim_path to str type in case its an Sdf.Path + prim_path = str(prim_path) + # check prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # resolve: {SPAWN_NS}/AssetName + # note: this assumes that the spawn namespace already exists in the stage + root_path, asset_path = prim_path.rsplit("/", 1) + # check if input is a regex expression + # note: a valid prim path can only contain alphanumeric characters, underscores, and forward slashes + is_regex_expression = re.match(r"^[a-zA-Z0-9/_]+$", root_path) is None + + # resolve matching prims for source prim path expression + if is_regex_expression and root_path != "": + source_prim_paths = find_matching_prim_paths(root_path) + # if no matching prims are found, raise an error + if len(source_prim_paths) == 0: + raise RuntimeError( + f"Unable to find source prim path: '{root_path}'. Please create the prim before spawning." + ) + else: + source_prim_paths = [root_path] + + # resolve prim paths for spawning and cloning + prim_paths = [f"{source_prim_path}/{asset_path}" for source_prim_path in source_prim_paths] + # spawn single instance + prim = func(prim_paths[0], cfg, *args, **kwargs) + # set the prim visibility + if hasattr(cfg, "visible"): + imageable = UsdGeom.Imageable(prim) + if cfg.visible: + imageable.MakeVisible() + else: + imageable.MakeInvisible() + # set the semantic annotations + if hasattr(cfg, "semantic_tags") and cfg.semantic_tags is not None: + # note: taken from replicator scripts.utils.utils.py + for semantic_type, semantic_value in cfg.semantic_tags: + # deal with spaces by replacing them with underscores + semantic_type_sanitized = semantic_type.replace(" ", "_") + semantic_value_sanitized = semantic_value.replace(" ", "_") + # set the semantic API for the instance + instance_name = f"{semantic_type_sanitized}_{semantic_value_sanitized}" + sem = Semantics.SemanticsAPI.Apply(prim, instance_name) + # create semantic type and data attributes + sem.CreateSemanticTypeAttr() + sem.CreateSemanticDataAttr() + sem.GetSemanticTypeAttr().Set(semantic_type) + sem.GetSemanticDataAttr().Set(semantic_value) + # activate rigid body contact sensors + if hasattr(cfg, "activate_contact_sensors") and cfg.activate_contact_sensors: + schemas.activate_contact_sensors(prim_paths[0], cfg.activate_contact_sensors) + # clone asset using cloner API + if len(prim_paths) > 1: + cloner = Cloner() + # clone the prim + cloner.clone(prim_paths[0], prim_paths[1:], replicate_physics=False, copy_from_source=cfg.copy_from_source) + # return the source prim + return prim + + return wrapper
+ + +""" +Material bindings. +""" + + +
[文档]@apply_nested +def bind_visual_material( + prim_path: str | Sdf.Path, + material_path: str | Sdf.Path, + stage: Usd.Stage | None = None, + stronger_than_descendants: bool = True, +): + """Bind a visual material to a prim. + + This function is a wrapper around the USD command `BindMaterialCommand`_. + + .. note:: + The function is decorated with :meth:`apply_nested` to allow applying the function to a prim path + and all its descendants. + + .. _BindMaterialCommand: https://docs.omniverse.nvidia.com/kit/docs/omni.usd/latest/omni.usd.commands/omni.usd.commands.BindMaterialCommand.html + + Args: + prim_path: The prim path where to apply the material. + material_path: The prim path of the material to apply. + stage: The stage where the prim and material exist. + Defaults to None, in which case the current stage is used. + stronger_than_descendants: Whether the material should override the material of its descendants. + Defaults to True. + + Raises: + ValueError: If the provided prim paths do not exist on stage. + """ + # resolve stage + if stage is None: + stage = stage_utils.get_current_stage() + # check if prim and material exists + if not stage.GetPrimAtPath(prim_path).IsValid(): + raise ValueError(f"Target prim '{material_path}' does not exist.") + if not stage.GetPrimAtPath(material_path).IsValid(): + raise ValueError(f"Visual material '{material_path}' does not exist.") + + # resolve token for weaker than descendants + if stronger_than_descendants: + binding_strength = "strongerThanDescendants" + else: + binding_strength = "weakerThanDescendants" + # obtain material binding API + # note: we prefer using the command here as it is more robust than the USD API + success, _ = omni.kit.commands.execute( + "BindMaterialCommand", + prim_path=prim_path, + material_path=material_path, + strength=binding_strength, + stage=stage, + ) + # return success + return success
+ + +
[文档]@apply_nested +def bind_physics_material( + prim_path: str | Sdf.Path, + material_path: str | Sdf.Path, + stage: Usd.Stage | None = None, + stronger_than_descendants: bool = True, +): + """Bind a physics material to a prim. + + `Physics material`_ can be applied only to a prim with physics-enabled on them. This includes having + collision APIs, or deformable body APIs, or being a particle system. In case the prim does not have + any of these APIs, the function will not apply the material and return False. + + .. note:: + The function is decorated with :meth:`apply_nested` to allow applying the function to a prim path + and all its descendants. + + .. _Physics material: https://docs.omniverse.nvidia.com/extensions/latest/ext_physics/simulation-control/physics-settings.html#physics-materials + + Args: + prim_path: The prim path where to apply the material. + material_path: The prim path of the material to apply. + stage: The stage where the prim and material exist. + Defaults to None, in which case the current stage is used. + stronger_than_descendants: Whether the material should override the material of its descendants. + Defaults to True. + + Raises: + ValueError: If the provided prim paths do not exist on stage. + """ + # resolve stage + if stage is None: + stage = stage_utils.get_current_stage() + # check if prim and material exists + if not stage.GetPrimAtPath(prim_path).IsValid(): + raise ValueError(f"Target prim '{material_path}' does not exist.") + if not stage.GetPrimAtPath(material_path).IsValid(): + raise ValueError(f"Physics material '{material_path}' does not exist.") + # get USD prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim has collision applied on it + has_physics_scene_api = prim.HasAPI(PhysxSchema.PhysxSceneAPI) + has_collider = prim.HasAPI(UsdPhysics.CollisionAPI) + has_deformable_body = prim.HasAPI(PhysxSchema.PhysxDeformableBodyAPI) + has_particle_system = prim.IsA(PhysxSchema.PhysxParticleSystem) + if not (has_physics_scene_api or has_collider or has_deformable_body or has_particle_system): + omni.log.verbose( + f"Cannot apply physics material '{material_path}' on prim '{prim_path}'. It is neither a" + " PhysX scene, collider, a deformable body, nor a particle system." + ) + return False + + # obtain material binding API + if prim.HasAPI(UsdShade.MaterialBindingAPI): + material_binding_api = UsdShade.MaterialBindingAPI(prim) + else: + material_binding_api = UsdShade.MaterialBindingAPI.Apply(prim) + # obtain the material prim + material = UsdShade.Material(stage.GetPrimAtPath(material_path)) + # resolve token for weaker than descendants + if stronger_than_descendants: + binding_strength = UsdShade.Tokens.strongerThanDescendants + else: + binding_strength = UsdShade.Tokens.weakerThanDescendants + # apply the material + material_binding_api.Bind(material, bindingStrength=binding_strength, materialPurpose="physics") # type: ignore + # return success + return True
+ + +""" +Exporting. +""" + + +
[文档]def export_prim_to_file( + path: str | Sdf.Path, + source_prim_path: str | Sdf.Path, + target_prim_path: str | Sdf.Path | None = None, + stage: Usd.Stage | None = None, +): + """Exports a prim from a given stage to a USD file. + + The function creates a new layer at the provided path and copies the prim to the layer. + It sets the copied prim as the default prim in the target layer. Additionally, it updates + the stage up-axis and meters-per-unit to match the current stage. + + Args: + path: The filepath path to export the prim to. + source_prim_path: The prim path to export. + target_prim_path: The prim path to set as the default prim in the target layer. + Defaults to None, in which case the source prim path is used. + stage: The stage where the prim exists. Defaults to None, in which case the + current stage is used. + + Raises: + ValueError: If the prim paths are not global (i.e: do not start with '/'). + """ + # automatically casting to str in case args + # are path types + path = str(path) + source_prim_path = str(source_prim_path) + if target_prim_path is not None: + target_prim_path = str(target_prim_path) + + if not source_prim_path.startswith("/"): + raise ValueError(f"Source prim path '{source_prim_path}' is not global. It must start with '/'.") + if target_prim_path is not None and not target_prim_path.startswith("/"): + raise ValueError(f"Target prim path '{target_prim_path}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage: Usd.Stage = omni.usd.get_context().get_stage() + # get root layer + source_layer = stage.GetRootLayer() + + # only create a new layer if it doesn't exist already + target_layer = Sdf.Find(path) + if target_layer is None: + target_layer = Sdf.Layer.CreateNew(path) + # open the target stage + target_stage = Usd.Stage.Open(target_layer) + + # update stage data + UsdGeom.SetStageUpAxis(target_stage, UsdGeom.GetStageUpAxis(stage)) + UsdGeom.SetStageMetersPerUnit(target_stage, UsdGeom.GetStageMetersPerUnit(stage)) + + # specify the prim to copy + source_prim_path = Sdf.Path(source_prim_path) + if target_prim_path is None: + target_prim_path = source_prim_path + + # copy the prim + Sdf.CreatePrimInLayer(target_layer, target_prim_path) + Sdf.CopySpec(source_layer, source_prim_path, target_layer, target_prim_path) + # set the default prim + target_layer.defaultPrim = Sdf.Path(target_prim_path).name + # resolve all paths relative to layer path + omni.usd.resolve_paths(source_layer.identifier, target_layer.identifier) + # save the stage + target_layer.Save()
+ + +""" +USD Prim properties. +""" + + +
[文档]def make_uninstanceable(prim_path: str | Sdf.Path, stage: Usd.Stage | None = None): + """Check if a prim and its descendants are instanced and make them uninstanceable. + + This function checks if the prim at the specified prim path and its descendants are instanced. + If so, it makes the respective prim uninstanceable by disabling instancing on the prim. + + This is useful when we want to modify the properties of a prim that is instanced. For example, if we + want to apply a different material on an instanced prim, we need to make the prim uninstanceable first. + + Args: + prim_path: The prim path to check. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage = stage_utils.get_current_stage() + # get prim + prim: Usd.Prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # iterate over all prims under prim-path + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + # check if prim is instanced + if child_prim.IsInstance(): + # make the prim uninstanceable + child_prim.SetInstanceable(False) + # add children to list + all_prims += child_prim.GetChildren()
+ + +""" +USD Stage traversal. +""" + + +
[文档]def get_first_matching_child_prim( + prim_path: str | Sdf.Path, predicate: Callable[[Usd.Prim], bool], stage: Usd.Stage | None = None +) -> Usd.Prim | None: + """Recursively get the first USD Prim at the path string that passes the predicate function + + Args: + prim_path: The path of the prim in the stage. + predicate: The function to test the prims against. It takes a prim as input and returns a boolean. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + The first prim on the path that passes the predicate. If no prim passes the predicate, it returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage = stage_utils.get_current_stage() + # get prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # iterate over all prims under prim-path + all_prims = [prim] + while len(all_prims) > 0: + # get current prim + child_prim = all_prims.pop(0) + # check if prim passes predicate + if predicate(child_prim): + return child_prim + # add children to list + all_prims += child_prim.GetChildren() + return None
+ + +
[文档]def get_all_matching_child_prims( + prim_path: str | Sdf.Path, + predicate: Callable[[Usd.Prim], bool] = lambda _: True, + depth: int | None = None, + stage: Usd.Stage | None = None, +) -> list[Usd.Prim]: + """Performs a search starting from the root and returns all the prims matching the predicate. + + Args: + prim_path: The root prim path to start the search from. + predicate: The predicate that checks if the prim matches the desired criteria. It takes a prim as input + and returns a boolean. Defaults to a function that always returns True. + depth: The maximum depth for traversal, should be bigger than zero if specified. + Defaults to None (i.e: traversal happens till the end of the tree). + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + A list containing all the prims matching the predicate. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # make paths str type if they aren't already + prim_path = str(prim_path) + # check if prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage = stage_utils.get_current_stage() + # get prim + prim = stage.GetPrimAtPath(prim_path) + # check if prim is valid + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # check if depth is valid + if depth is not None and depth <= 0: + raise ValueError(f"Depth must be bigger than zero, got {depth}.") + + # iterate over all prims under prim-path + # list of tuples (prim, current_depth) + all_prims_queue = [(prim, 0)] + output_prims = [] + while len(all_prims_queue) > 0: + # get current prim + child_prim, current_depth = all_prims_queue.pop(0) + # check if prim passes predicate + if predicate(child_prim): + output_prims.append(child_prim) + # add children to list + if depth is None or current_depth < depth: + all_prims_queue += [(child, current_depth + 1) for child in child_prim.GetChildren()] + + return output_prims
+ + +
[文档]def find_first_matching_prim(prim_path_regex: str, stage: Usd.Stage | None = None) -> Usd.Prim | None: + """Find the first matching prim in the stage based on input regex expression. + + Args: + prim_path_regex: The regex expression for prim path. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + The first prim that matches input expression. If no prim matches, returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # check prim path is global + if not prim_path_regex.startswith("/"): + raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage = stage_utils.get_current_stage() + # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string + pattern = f"^{prim_path_regex}$" + compiled_pattern = re.compile(pattern) + # obtain matching prim (depth-first search) + for prim in stage.Traverse(): + # check if prim passes predicate + if compiled_pattern.match(prim.GetPath().pathString) is not None: + return prim + return None
+ + +
[文档]def find_matching_prims(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[Usd.Prim]: + """Find all the matching prims in the stage based on input regex expression. + + Args: + prim_path_regex: The regex expression for prim path. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + A list of prims that match input expression. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # check prim path is global + if not prim_path_regex.startswith("/"): + raise ValueError(f"Prim path '{prim_path_regex}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage = stage_utils.get_current_stage() + # need to wrap the token patterns in '^' and '$' to prevent matching anywhere in the string + tokens = prim_path_regex.split("/")[1:] + tokens = [f"^{token}$" for token in tokens] + # iterate over all prims in stage (breath-first search) + all_prims = [stage.GetPseudoRoot()] + output_prims = [] + for index, token in enumerate(tokens): + token_compiled = re.compile(token) + for prim in all_prims: + for child in prim.GetAllChildren(): + if token_compiled.match(child.GetName()) is not None: + output_prims.append(child) + if index < len(tokens) - 1: + all_prims = output_prims + output_prims = [] + return output_prims
+ + +
[文档]def find_matching_prim_paths(prim_path_regex: str, stage: Usd.Stage | None = None) -> list[str]: + """Find all the matching prim paths in the stage based on input regex expression. + + Args: + prim_path_regex: The regex expression for prim path. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + A list of prim paths that match input expression. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + """ + # obtain matching prims + output_prims = find_matching_prims(prim_path_regex, stage) + # convert prims to prim paths + output_prim_paths = [] + for prim in output_prims: + output_prim_paths.append(prim.GetPath().pathString) + return output_prim_paths
+ + +
[文档]def find_global_fixed_joint_prim( + prim_path: str | Sdf.Path, check_enabled_only: bool = False, stage: Usd.Stage | None = None +) -> UsdPhysics.Joint | None: + """Find the fixed joint prim under the specified prim path that connects the target to the simulation world. + + A joint is a connection between two bodies. A fixed joint is a joint that does not allow relative motion + between the two bodies. When a fixed joint has only one target body, it is considered to attach the body + to the simulation world. + + This function finds the fixed joint prim that has only one target under the specified prim path. If no such + fixed joint prim exists, it returns None. + + Args: + prim_path: The prim path to search for the fixed joint prim. + check_enabled_only: Whether to consider only enabled fixed joints. Defaults to False. + If False, then all joints (enabled or disabled) are considered. + stage: The stage where the prim exists. Defaults to None, in which case the current stage is used. + + Returns: + The fixed joint prim that has only one target. If no such fixed joint prim exists, it returns None. + + Raises: + ValueError: If the prim path is not global (i.e: does not start with '/'). + ValueError: If the prim path does not exist on the stage. + """ + # check prim path is global + if not prim_path.startswith("/"): + raise ValueError(f"Prim path '{prim_path}' is not global. It must start with '/'.") + # get current stage + if stage is None: + stage = stage_utils.get_current_stage() + + # check if prim exists + prim = stage.GetPrimAtPath(prim_path) + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + + fixed_joint_prim = None + # we check all joints under the root prim and classify the asset as fixed base if there exists + # a fixed joint that has only one target (i.e. the root link). + for prim in Usd.PrimRange(prim): + # note: ideally checking if it is FixedJoint would have been enough, but some assets use "Joint" as the + # schema name which makes it difficult to distinguish between the two. + joint_prim = UsdPhysics.Joint(prim) + if joint_prim: + # if check_enabled_only is True, we only consider enabled joints + if check_enabled_only and not joint_prim.GetJointEnabledAttr().Get(): + continue + # check body 0 and body 1 exist + body_0_exist = joint_prim.GetBody0Rel().GetTargets() != [] + body_1_exist = joint_prim.GetBody1Rel().GetTargets() != [] + # if either body 0 or body 1 does not exist, we have a fixed joint that connects to the world + if not (body_0_exist and body_1_exist): + fixed_joint_prim = joint_prim + break + + return fixed_joint_prim
+ + +""" +USD Variants. +""" + + +
[文档]def select_usd_variants(prim_path: str, variants: object | dict[str, str], stage: Usd.Stage | None = None): + """Sets the variant selections from the specified variant sets on a USD prim. + + `USD Variants`_ are a very powerful tool in USD composition that allows prims to have different options on + a single asset. This can be done by modifying variations of the same prim parameters per variant option in a set. + This function acts as a script-based utility to set the variant selections for the specified variant sets on a + USD prim. + + The function takes a dictionary or a config class mapping variant set names to variant selections. For instance, + if we have a prim at ``"/World/Table"`` with two variant sets: "color" and "size", we can set the variant + selections as follows: + + .. code-block:: python + + select_usd_variants( + prim_path="/World/Table", + variants={ + "color": "red", + "size": "large", + }, + ) + + Alternatively, we can use a config class to define the variant selections: + + .. code-block:: python + + @configclass + class TableVariants: + color: Literal["blue", "red"] = "red" + size: Literal["small", "large"] = "large" + + select_usd_variants( + prim_path="/World/Table", + variants=TableVariants(), + ) + + Args: + prim_path: The path of the USD prim. + variants: A dictionary or config class mapping variant set names to variant selections. + stage: The USD stage. Defaults to None, in which case, the current stage is used. + + Raises: + ValueError: If the prim at the specified path is not valid. + + .. _USD Variants: https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-Variant + """ + # Resolve stage + if stage is None: + stage = stage_utils.get_current_stage() + # Obtain prim + prim = stage.GetPrimAtPath(prim_path) + if not prim.IsValid(): + raise ValueError(f"Prim at path '{prim_path}' is not valid.") + # Convert to dict if we have a configclass object. + if not isinstance(variants, dict): + variants = variants.to_dict() + + existing_variant_sets = prim.GetVariantSets() + for variant_set_name, variant_selection in variants.items(): + # Check if the variant set exists on the prim. + if not existing_variant_sets.HasVariantSet(variant_set_name): + omni.log.warn(f"Variant set '{variant_set_name}' does not exist on prim '{prim_path}'.") + continue + + variant_set = existing_variant_sets.GetVariantSet(variant_set_name) + # Only set the variant selection if it is different from the current selection. + if variant_set.GetVariantSelection() != variant_selection: + variant_set.SetVariantSelection(variant_selection) + omni.log.info( + f"Setting variant selection '{variant_selection}' for variant set '{variant_set_name}' on" + f" prim '{prim_path}'." + )
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/height_field/hf_terrains.html b/_modules/omni/isaac/lab/terrains/height_field/hf_terrains.html new file mode 100644 index 0000000000..f39c2de9c0 --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/height_field/hf_terrains.html @@ -0,0 +1,995 @@ + + + + + + + + + + + omni.isaac.lab.terrains.height_field.hf_terrains — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.height_field.hf_terrains 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Functions to generate height fields for different terrains."""
+
+from __future__ import annotations
+
+import numpy as np
+import scipy.interpolate as interpolate
+from typing import TYPE_CHECKING
+
+from .utils import height_field_to_mesh
+
+if TYPE_CHECKING:
+    from . import hf_terrains_cfg
+
+
+
[文档]@height_field_to_mesh +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 + :width: 40% + :align: center + + Note: + The :obj:`difficulty` parameter is ignored for this terrain. + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + The height field of the terrain as a 2D numpy array with discretized heights. + The shape of the array is (width, length), where width and length are the number of points + along the x and y axis, respectively. + + Raises: + ValueError: When the downsampled scale is smaller than the horizontal scale. + """ + # check parameters + # -- horizontal scale + if cfg.downsampled_scale is None: + cfg.downsampled_scale = cfg.horizontal_scale + elif cfg.downsampled_scale < cfg.horizontal_scale: + raise ValueError( + "Downsampled scale must be larger than or equal to the horizontal scale:" + f" {cfg.downsampled_scale} < {cfg.horizontal_scale}." + ) + + # switch parameters to discrete units + # -- horizontal scale + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + # -- downsampled scale + width_downsampled = int(cfg.size[0] / cfg.downsampled_scale) + length_downsampled = int(cfg.size[1] / cfg.downsampled_scale) + # -- height + height_min = int(cfg.noise_range[0] / cfg.vertical_scale) + height_max = int(cfg.noise_range[1] / cfg.vertical_scale) + height_step = int(cfg.noise_step / cfg.vertical_scale) + + # create range of heights possible + height_range = np.arange(height_min, height_max + height_step, height_step) + # sample heights randomly from the range along a grid + height_field_downsampled = np.random.choice(height_range, size=(width_downsampled, length_downsampled)) + # create interpolation function for the sampled heights + x = np.linspace(0, cfg.size[0] * cfg.horizontal_scale, width_downsampled) + y = np.linspace(0, cfg.size[1] * cfg.horizontal_scale, length_downsampled) + func = interpolate.RectBivariateSpline(x, y, height_field_downsampled) + + # interpolate the sampled heights to obtain the height field + x_upsampled = np.linspace(0, cfg.size[0] * cfg.horizontal_scale, width_pixels) + y_upsampled = np.linspace(0, cfg.size[1] * cfg.horizontal_scale, length_pixels) + z_upsampled = func(x_upsampled, y_upsampled) + # round off the interpolated heights to the nearest vertical step + return np.rint(z_upsampled).astype(np.int16)
+ + +
[文档]@height_field_to_mesh +def pyramid_sloped_terrain(difficulty: float, cfg: hf_terrains_cfg.HfPyramidSlopedTerrainCfg) -> np.ndarray: + """Generate a terrain with a truncated pyramid structure. + + The terrain is a pyramid-shaped sloped surface with a slope of :obj:`slope` that trims into a flat platform + at the center. The slope is defined as the ratio of the height change along the x axis to the width along the + x axis. For example, a slope of 1.0 means that the height changes by 1 unit for every 1 unit of width. + + 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 + :width: 40% + + .. image:: ../../_static/terrains/height_field/inverted_pyramid_sloped_terrain.jpg + :width: 40% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + The height field of the terrain as a 2D numpy array with discretized heights. + The shape of the array is (width, length), where width and length are the number of points + along the x and y axis, respectively. + """ + # resolve terrain configuration + if cfg.inverted: + slope = -cfg.slope_range[0] - difficulty * (cfg.slope_range[1] - cfg.slope_range[0]) + else: + slope = cfg.slope_range[0] + difficulty * (cfg.slope_range[1] - cfg.slope_range[0]) + + # switch parameters to discrete units + # -- horizontal scale + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + # -- height + # we want the height to be 1/2 of the width since the terrain is a pyramid + height_max = int(slope * cfg.size[0] / 2 / cfg.vertical_scale) + # -- center of the terrain + center_x = int(width_pixels / 2) + center_y = int(length_pixels / 2) + + # create a meshgrid of the terrain + x = np.arange(0, width_pixels) + y = np.arange(0, length_pixels) + xx, yy = np.meshgrid(x, y, sparse=True) + # offset the meshgrid to the center of the terrain + xx = (center_x - np.abs(center_x - xx)) / center_x + yy = (center_y - np.abs(center_y - yy)) / center_y + # reshape the meshgrid to be 2D + xx = xx.reshape(width_pixels, 1) + yy = yy.reshape(1, length_pixels) + # create a sloped surface + hf_raw = np.zeros((width_pixels, length_pixels)) + hf_raw = height_max * xx * yy + + # create a flat platform at the center of the terrain + platform_width = int(cfg.platform_width / cfg.horizontal_scale / 2) + # get the height of the platform at the corner of the platform + x_pf = width_pixels // 2 - platform_width + y_pf = length_pixels // 2 - platform_width + z_pf = hf_raw[x_pf, y_pf] + hf_raw = np.clip(hf_raw, min(0, z_pf), max(0, z_pf)) + + # round off the heights to the nearest vertical step + return np.rint(hf_raw).astype(np.int16)
+ + +
[文档]@height_field_to_mesh +def pyramid_stairs_terrain(difficulty: float, cfg: hf_terrains_cfg.HfPyramidStairsTerrainCfg) -> np.ndarray: + """Generate a terrain with a pyramid stair pattern. + + The terrain is a pyramid stair pattern which trims to a flat platform at the center of the terrain. + + 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 + :width: 40% + + .. image:: ../../_static/terrains/height_field/inverted_pyramid_stairs_terrain.jpg + :width: 40% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + The height field of the terrain as a 2D numpy array with discretized heights. + The shape of the array is (width, length), where width and length are the number of points + along the x and y axis, respectively. + """ + # resolve terrain configuration + step_height = cfg.step_height_range[0] + difficulty * (cfg.step_height_range[1] - cfg.step_height_range[0]) + if cfg.inverted: + step_height *= -1 + # switch parameters to discrete units + # -- terrain + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + # -- stairs + step_width = int(cfg.step_width / cfg.horizontal_scale) + step_height = int(step_height / cfg.vertical_scale) + # -- platform + platform_width = int(cfg.platform_width / cfg.horizontal_scale) + + # create a terrain with a flat platform at the center + hf_raw = np.zeros((width_pixels, length_pixels)) + # add the steps + current_step_height = 0 + start_x, start_y = 0, 0 + stop_x, stop_y = width_pixels, length_pixels + while (stop_x - start_x) > platform_width and (stop_y - start_y) > platform_width: + # increment position + # -- x + start_x += step_width + stop_x -= step_width + # -- y + start_y += step_width + stop_y -= step_width + # increment height + current_step_height += step_height + # add the step + hf_raw[start_x:stop_x, start_y:stop_y] = current_step_height + + # round off the heights to the nearest vertical step + return np.rint(hf_raw).astype(np.int16)
+ + +
[文档]@height_field_to_mesh +def discrete_obstacles_terrain(difficulty: float, cfg: hf_terrains_cfg.HfDiscreteObstaclesTerrainCfg) -> np.ndarray: + """Generate a terrain with randomly generated obstacles as pillars with positive and negative heights. + + The terrain is a flat platform at the center of the terrain with randomly generated obstacles as pillars + with positive and negative height. The obstacles are randomly generated cuboids with a random width and + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + The height field of the terrain as a 2D numpy array with discretized heights. + The shape of the array is (width, length), where width and length are the number of points + along the x and y axis, respectively. + """ + # resolve terrain configuration + obs_height = cfg.obstacle_height_range[0] + difficulty * ( + cfg.obstacle_height_range[1] - cfg.obstacle_height_range[0] + ) + + # switch parameters to discrete units + # -- terrain + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + # -- obstacles + obs_height = int(obs_height / cfg.vertical_scale) + obs_width_min = int(cfg.obstacle_width_range[0] / cfg.horizontal_scale) + obs_width_max = int(cfg.obstacle_width_range[1] / cfg.horizontal_scale) + # -- center of the terrain + platform_width = int(cfg.platform_width / cfg.horizontal_scale) + + # create discrete ranges for the obstacles + # -- shape + obs_width_range = np.arange(obs_width_min, obs_width_max, 4) + obs_length_range = np.arange(obs_width_min, obs_width_max, 4) + # -- position + obs_x_range = np.arange(0, width_pixels, 4) + obs_y_range = np.arange(0, length_pixels, 4) + + # create a terrain with a flat platform at the center + hf_raw = np.zeros((width_pixels, length_pixels)) + # generate the obstacles + for _ in range(cfg.num_obstacles): + # sample size + if cfg.obstacle_height_mode == "choice": + height = np.random.choice([-obs_height, -obs_height // 2, obs_height // 2, obs_height]) + elif cfg.obstacle_height_mode == "fixed": + height = obs_height + else: + raise ValueError(f"Unknown obstacle height mode '{cfg.obstacle_height_mode}'. Must be 'choice' or 'fixed'.") + width = int(np.random.choice(obs_width_range)) + length = int(np.random.choice(obs_length_range)) + # sample position + x_start = int(np.random.choice(obs_x_range)) + y_start = int(np.random.choice(obs_y_range)) + # clip start position to the terrain + if x_start + width > width_pixels: + x_start = width_pixels - width + if y_start + length > length_pixels: + y_start = length_pixels - length + # add to terrain + hf_raw[x_start : x_start + width, y_start : y_start + length] = height + # clip the terrain to the platform + x1 = (width_pixels - platform_width) // 2 + x2 = (width_pixels + platform_width) // 2 + y1 = (length_pixels - platform_width) // 2 + y2 = (length_pixels + platform_width) // 2 + hf_raw[x1:x2, y1:y2] = 0 + # round off the heights to the nearest vertical step + return np.rint(hf_raw).astype(np.int16)
+ + +
[文档]@height_field_to_mesh +def wave_terrain(difficulty: float, cfg: hf_terrains_cfg.HfWaveTerrainCfg) -> np.ndarray: + r"""Generate a terrain with a wave pattern. + + The terrain is a flat platform at the center of the terrain with a wave pattern. The wave pattern + is generated by adding sinusoidal waves based on the number of waves and the amplitude of the waves. + + The height of the terrain at a point :math:`(x, y)` is given by: + + .. math:: + + h(x, y) = A \left(\sin\left(\frac{2 \pi x}{\lambda}\right) + \cos\left(\frac{2 \pi y}{\lambda}\right) \right) + + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + The height field of the terrain as a 2D numpy array with discretized heights. + The shape of the array is (width, length), where width and length are the number of points + along the x and y axis, respectively. + + Raises: + ValueError: When the number of waves is non-positive. + """ + # check number of waves + if cfg.num_waves < 0: + raise ValueError(f"Number of waves must be a positive integer. Got: {cfg.num_waves}.") + + # resolve terrain configuration + amplitude = cfg.amplitude_range[0] + difficulty * (cfg.amplitude_range[1] - cfg.amplitude_range[0]) + # switch parameters to discrete units + # -- terrain + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + amplitude_pixels = int(0.5 * amplitude / cfg.vertical_scale) + + # compute the wave number: nu = 2 * pi / lambda + wave_length = length_pixels / cfg.num_waves + wave_number = 2 * np.pi / wave_length + # create meshgrid for the terrain + x = np.arange(0, width_pixels) + y = np.arange(0, length_pixels) + xx, yy = np.meshgrid(x, y, sparse=True) + xx = xx.reshape(width_pixels, 1) + yy = yy.reshape(1, length_pixels) + + # create a terrain with a flat platform at the center + hf_raw = np.zeros((width_pixels, length_pixels)) + # add the waves + hf_raw += amplitude_pixels * (np.cos(yy * wave_number) + np.sin(xx * wave_number)) + # round off the heights to the nearest vertical step + return np.rint(hf_raw).astype(np.int16)
+ + +
[文档]@height_field_to_mesh +def stepping_stones_terrain(difficulty: float, cfg: hf_terrains_cfg.HfSteppingStonesTerrainCfg) -> np.ndarray: + """Generate a terrain with a stepping stones pattern. + + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + The height field of the terrain as a 2D numpy array with discretized heights. + The shape of the array is (width, length), where width and length are the number of points + along the x and y axis, respectively. + """ + # resolve terrain configuration + stone_width = cfg.stone_width_range[1] - difficulty * (cfg.stone_width_range[1] - cfg.stone_width_range[0]) + stone_distance = cfg.stone_distance_range[0] + difficulty * ( + cfg.stone_distance_range[1] - cfg.stone_distance_range[0] + ) + + # switch parameters to discrete units + # -- terrain + width_pixels = int(cfg.size[0] / cfg.horizontal_scale) + length_pixels = int(cfg.size[1] / cfg.horizontal_scale) + # -- stones + stone_distance = int(stone_distance / cfg.horizontal_scale) + stone_width = int(stone_width / cfg.horizontal_scale) + stone_height_max = int(cfg.stone_height_max / cfg.vertical_scale) + # -- holes + holes_depth = int(cfg.holes_depth / cfg.vertical_scale) + # -- platform + platform_width = int(cfg.platform_width / cfg.horizontal_scale) + # create range of heights + stone_height_range = np.arange(-stone_height_max - 1, stone_height_max, step=1) + + # create a terrain with a flat platform at the center + hf_raw = np.full((width_pixels, length_pixels), holes_depth) + # add the stones + start_x, start_y = 0, 0 + # -- if the terrain is longer than it is wide then fill the terrain column by column + if length_pixels >= width_pixels: + while start_y < length_pixels: + # ensure that stone stops along y-axis + stop_y = min(length_pixels, start_y + stone_width) + # randomly sample x-position + start_x = np.random.randint(0, stone_width) + stop_x = max(0, start_x - stone_distance) + # fill first stone + hf_raw[0:stop_x, start_y:stop_y] = np.random.choice(stone_height_range) + # fill row with stones + while start_x < width_pixels: + stop_x = min(width_pixels, start_x + stone_width) + hf_raw[start_x:stop_x, start_y:stop_y] = np.random.choice(stone_height_range) + start_x += stone_width + stone_distance + # update y-position + start_y += stone_width + stone_distance + elif width_pixels > length_pixels: + while start_x < width_pixels: + # ensure that stone stops along x-axis + stop_x = min(width_pixels, start_x + stone_width) + # randomly sample y-position + start_y = np.random.randint(0, stone_width) + stop_y = max(0, start_y - stone_distance) + # fill first stone + hf_raw[start_x:stop_x, 0:stop_y] = np.random.choice(stone_height_range) + # fill column with stones + while start_y < length_pixels: + stop_y = min(length_pixels, start_y + stone_width) + hf_raw[start_x:stop_x, start_y:stop_y] = np.random.choice(stone_height_range) + start_y += stone_width + stone_distance + # update x-position + start_x += stone_width + stone_distance + # add the platform in the center + x1 = (width_pixels - platform_width) // 2 + x2 = (width_pixels + platform_width) // 2 + y1 = (length_pixels - platform_width) // 2 + y2 = (length_pixels + platform_width) // 2 + hf_raw[x1:x2, y1:y2] = 0 + # round off the heights to the nearest vertical step + return np.rint(hf_raw).astype(np.int16)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/height_field/hf_terrains_cfg.html b/_modules/omni/isaac/lab/terrains/height_field/hf_terrains_cfg.html new file mode 100644 index 0000000000..7e73827b44 --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/height_field/hf_terrains_cfg.html @@ -0,0 +1,726 @@ + + + + + + + + + + + omni.isaac.lab.terrains.height_field.hf_terrains_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.height_field.hf_terrains_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+
+from omni.isaac.lab.utils import configclass
+
+from ..terrain_generator_cfg import SubTerrainBaseCfg
+from . import hf_terrains
+
+
+
[文档]@configclass +class HfTerrainBaseCfg(SubTerrainBaseCfg): + """The base configuration for height field terrains.""" + + border_width: float = 0.0 + """The width of the border/padding around the terrain (in m). Defaults to 0.0. + + The border width is subtracted from the :obj:`size` of the terrain. If non-zero, it must be + greater than or equal to the :obj:`horizontal scale`. + """ + horizontal_scale: float = 0.1 + """The discretization of the terrain along the x and y axes (in m). Defaults to 0.1.""" + 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 None, + in which case no correction is applied."""
+ + +""" +Different height field terrain configurations. +""" + + +
[文档]@configclass +class HfRandomUniformTerrainCfg(HfTerrainBaseCfg): + """Configuration for a random uniform height field terrain.""" + + function = hf_terrains.random_uniform_terrain + + noise_range: tuple[float, float] = MISSING + """The minimum and maximum height noise (i.e. along z) of the terrain (in m).""" + noise_step: float = MISSING + """The minimum height (in m) change between two points.""" + downsampled_scale: float | None = None + """The distance between two randomly sampled points on the terrain. Defaults to None, + in which case the :obj:`horizontal scale` is used. + + The heights are sampled at this resolution and interpolation is performed for intermediate points. + This must be larger than or equal to the :obj:`horizontal scale`. + """
+ + +
[文档]@configclass +class HfPyramidSlopedTerrainCfg(HfTerrainBaseCfg): + """Configuration for a pyramid sloped height field terrain.""" + + function = hf_terrains.pyramid_sloped_terrain + + slope_range: tuple[float, float] = MISSING + """The slope of the terrain (in radians).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0.""" + inverted: bool = False + """Whether the pyramid is inverted. Defaults to False. + + If True, the terrain is inverted such that the platform is at the bottom and the slopes are upwards. + """
+ + +
[文档]@configclass +class HfInvertedPyramidSlopedTerrainCfg(HfPyramidSlopedTerrainCfg): + """Configuration for an inverted pyramid sloped height field terrain. + + Note: + This is a subclass of :class:`HfPyramidSlopedTerrainCfg` with :obj:`inverted` set to True. + We make it as a separate class to make it easier to distinguish between the two and match + the naming convention of the other terrains. + """ + + inverted: bool = True
+ + +
[文档]@configclass +class HfPyramidStairsTerrainCfg(HfTerrainBaseCfg): + """Configuration for a pyramid stairs height field terrain.""" + + function = hf_terrains.pyramid_stairs_terrain + + step_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the steps (in m).""" + step_width: float = MISSING + """The width of the steps (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0.""" + inverted: bool = False + """Whether the pyramid stairs is inverted. Defaults to False. + + If True, the terrain is inverted such that the platform is at the bottom and the stairs are upwards. + """
+ + +
[文档]@configclass +class HfInvertedPyramidStairsTerrainCfg(HfPyramidStairsTerrainCfg): + """Configuration for an inverted pyramid stairs height field terrain. + + Note: + This is a subclass of :class:`HfPyramidStairsTerrainCfg` with :obj:`inverted` set to True. + We make it as a separate class to make it easier to distinguish between the two and match + the naming convention of the other terrains. + """ + + inverted: bool = True
+ + +
[文档]@configclass +class HfDiscreteObstaclesTerrainCfg(HfTerrainBaseCfg): + """Configuration for a discrete obstacles height field terrain.""" + + function = hf_terrains.discrete_obstacles_terrain + + obstacle_height_mode: str = "choice" + """The mode to use for the obstacle height. Defaults to "choice". + + The following modes are supported: "choice", "fixed". + """ + obstacle_width_range: tuple[float, float] = MISSING + """The minimum and maximum width of the obstacles (in m).""" + obstacle_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the obstacles (in m).""" + num_obstacles: int = MISSING + """The number of obstacles to generate.""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0."""
+ + +
[文档]@configclass +class HfWaveTerrainCfg(HfTerrainBaseCfg): + """Configuration for a wave height field terrain.""" + + function = hf_terrains.wave_terrain + + amplitude_range: tuple[float, float] = MISSING + """The minimum and maximum amplitude of the wave (in m).""" + num_waves: int = 1.0 + """The number of waves to generate. Defaults to 1.0."""
+ + +
[文档]@configclass +class HfSteppingStonesTerrainCfg(HfTerrainBaseCfg): + """Configuration for a stepping stones height field terrain.""" + + function = hf_terrains.stepping_stones_terrain + + stone_height_max: float = MISSING + """The maximum height of the stones (in m).""" + stone_width_range: tuple[float, float] = MISSING + """The minimum and maximum width of the stones (in m).""" + stone_distance_range: tuple[float, float] = MISSING + """The minimum and maximum distance between stones (in m).""" + holes_depth: float = -10.0 + """The depth of the holes (negative obstacles). Defaults to -10.0.""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/terrain_generator.html b/_modules/omni/isaac/lab/terrains/terrain_generator.html new file mode 100644 index 0000000000..a5307bb92a --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/terrain_generator.html @@ -0,0 +1,946 @@ + + + + + + + + + + + omni.isaac.lab.terrains.terrain_generator — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.terrain_generator 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import numpy as np
+import os
+import torch
+import trimesh
+
+import omni.log
+
+from omni.isaac.lab.utils.dict import dict_to_md5_hash
+from omni.isaac.lab.utils.io import dump_yaml
+from omni.isaac.lab.utils.timer import Timer
+from omni.isaac.lab.utils.warp import convert_to_warp_mesh
+
+from .height_field import HfTerrainBaseCfg
+from .terrain_generator_cfg import FlatPatchSamplingCfg, SubTerrainBaseCfg, TerrainGeneratorCfg
+from .trimesh.utils import make_border
+from .utils import color_meshes_by_height, find_flat_patches
+
+
+
[文档]class TerrainGenerator: + r"""Terrain generator to handle different terrain generation functions. + + The terrains are represented as meshes. These are obtained either from height fields or by using the + `trimesh <https://trimsh.org/trimesh.html>`__ library. The height field representation is more + flexible, but it is less computationally and memory efficient than the trimesh representation. + + All terrain generation functions take in the argument :obj:`difficulty` which determines the complexity + of the terrain. The difficulty is a number between 0 and 1, where 0 is the easiest and 1 is the hardest. + In most cases, the difficulty is used for linear interpolation between different terrain parameters. + For example, in a pyramid stairs terrain the step height is interpolated between the specified minimum + and maximum step height. + + Each sub-terrain has a corresponding configuration class that can be used to specify the parameters + of the terrain. The configuration classes are inherited from the :class:`SubTerrainBaseCfg` class + which contains the common parameters for all terrains. + + If a curriculum is used, the terrains are generated based on their difficulty parameter. + The difficulty is varied linearly over the number of rows (i.e. along x) with a small random value + added to the difficulty to ensure that the columns with the same sub-terrain type are not exactly + the same. The difficulty parameter for a sub-terrain at a given row is calculated as: + + .. math:: + + \text{difficulty} = \frac{\text{row_id} + \eta}{\text{num_rows}} \times (\text{upper} - \text{lower}) + \text{lower} + + where :math:`\eta\sim\mathcal{U}(0, 1)` is a random perturbation to the difficulty, and + :math:`(\text{lower}, \text{upper})` is the range of the difficulty parameter, specified using the + :attr:`~TerrainGeneratorCfg.difficulty_range` parameter. + + If a curriculum is not used, the terrains are generated randomly. In this case, the difficulty parameter + is randomly sampled from the specified range, given by the :attr:`~TerrainGeneratorCfg.difficulty_range` parameter: + + .. math:: + + \text{difficulty} \sim \mathcal{U}(\text{lower}, \text{upper}) + + If the :attr:`~TerrainGeneratorCfg.flat_patch_sampling` is specified for a sub-terrain, flat patches are sampled + on the terrain. These can be used for spawning robots, targets, etc. The sampled patches are stored + in the :obj:`flat_patches` dictionary. The key specifies the intention of the flat patches and the + value is a tensor containing the flat patches for each sub-terrain. + + If the flag :attr:`~TerrainGeneratorCfg.use_cache` is set to True, the terrains are cached based on their + sub-terrain configurations. This means that if the same sub-terrain configuration is used + multiple times, the terrain is only generated once and then reused. This is useful when + generating complex sub-terrains that take a long time to generate. + + .. attention:: + + The terrain generation has its own seed parameter. This is set using the :attr:`TerrainGeneratorCfg.seed` + parameter. If the seed is not set and the caching is disabled, the terrain generation may not be + completely reproducible. + + """ + + terrain_mesh: trimesh.Trimesh + """A single trimesh.Trimesh object for all the generated sub-terrains.""" + terrain_meshes: list[trimesh.Trimesh] + """List of trimesh.Trimesh objects for all the generated sub-terrains.""" + terrain_origins: np.ndarray + """The origin of each sub-terrain. Shape is (num_rows, num_cols, 3).""" + flat_patches: dict[str, torch.Tensor] + """A dictionary of sampled valid (flat) patches for each sub-terrain. + + The dictionary keys are the names of the flat patch sampling configurations. This maps to a + tensor containing the flat patches for each sub-terrain. The shape of the tensor is + (num_rows, num_cols, num_patches, 3). + + For instance, the key "root_spawn" maps to a tensor containing the flat patches for spawning an asset. + Similarly, the key "target_spawn" maps to a tensor containing the flat patches for setting targets. + """ + +
[文档] def __init__(self, cfg: TerrainGeneratorCfg, device: str = "cpu"): + """Initialize the terrain generator. + + Args: + cfg: Configuration for the terrain generator. + device: The device to use for the flat patches tensor. + """ + # check inputs + if len(cfg.sub_terrains) == 0: + raise ValueError("No sub-terrains specified! Please add at least one sub-terrain.") + # store inputs + self.cfg = cfg + self.device = device + + # set common values to all sub-terrains config + for sub_cfg in self.cfg.sub_terrains.values(): + # size of all terrains + sub_cfg.size = self.cfg.size + # params for height field terrains + if isinstance(sub_cfg, HfTerrainBaseCfg): + sub_cfg.horizontal_scale = self.cfg.horizontal_scale + sub_cfg.vertical_scale = self.cfg.vertical_scale + sub_cfg.slope_threshold = self.cfg.slope_threshold + + # throw a warning if the cache is enabled but the seed is not set + if self.cfg.use_cache and self.cfg.seed is None: + omni.log.warn( + "Cache is enabled but the seed is not set. The terrain generation will not be reproducible." + " Please set the seed in the terrain generator configuration to make the generation reproducible." + ) + + # if the seed is not set, we assume there is a global seed set and use that. + # this ensures that the terrain is reproducible if the seed is set at the beginning of the program. + if self.cfg.seed is not None: + seed = self.cfg.seed + else: + seed = np.random.get_state()[1][0] + # set the seed for reproducibility + # note: we create a new random number generator to avoid affecting the global state + # in the other places where random numbers are used. + self.np_rng = np.random.default_rng(seed) + + # buffer for storing valid patches + self.flat_patches = {} + # create a list of all sub-terrains + self.terrain_meshes = list() + self.terrain_origins = np.zeros((self.cfg.num_rows, self.cfg.num_cols, 3)) + + # parse configuration and add sub-terrains + # create terrains based on curriculum or randomly + if self.cfg.curriculum: + with Timer("[INFO] Generating terrains based on curriculum took"): + self._generate_curriculum_terrains() + else: + with Timer("[INFO] Generating terrains randomly took"): + self._generate_random_terrains() + # add a border around the terrains + self._add_terrain_border() + # combine all the sub-terrains into a single mesh + self.terrain_mesh = trimesh.util.concatenate(self.terrain_meshes) + + # color the terrain mesh + if self.cfg.color_scheme == "height": + self.terrain_mesh = color_meshes_by_height(self.terrain_mesh) + elif self.cfg.color_scheme == "random": + self.terrain_mesh.visual.vertex_colors = self.np_rng.choice( + range(256), size=(len(self.terrain_mesh.vertices), 4) + ) + elif self.cfg.color_scheme == "none": + pass + else: + raise ValueError(f"Invalid color scheme: {self.cfg.color_scheme}.") + + # offset the entire terrain and origins so that it is centered + # -- terrain mesh + transform = np.eye(4) + transform[:2, -1] = -self.cfg.size[0] * self.cfg.num_rows * 0.5, -self.cfg.size[1] * self.cfg.num_cols * 0.5 + self.terrain_mesh.apply_transform(transform) + # -- terrain origins + self.terrain_origins += transform[:3, -1] + # -- valid patches + terrain_origins_torch = torch.tensor(self.terrain_origins, dtype=torch.float, device=self.device).unsqueeze(2) + for name, value in self.flat_patches.items(): + self.flat_patches[name] = value + terrain_origins_torch
+ + def __str__(self): + """Return a string representation of the terrain generator.""" + msg = "Terrain Generator:" + msg += f"\n\tSeed: {self.cfg.seed}" + msg += f"\n\tNumber of rows: {self.cfg.num_rows}" + msg += f"\n\tNumber of columns: {self.cfg.num_cols}" + msg += f"\n\tSub-terrain size: {self.cfg.size}" + msg += f"\n\tSub-terrain types: {list(self.cfg.sub_terrains.keys())}" + msg += f"\n\tCurriculum: {self.cfg.curriculum}" + msg += f"\n\tDifficulty range: {self.cfg.difficulty_range}" + msg += f"\n\tColor scheme: {self.cfg.color_scheme}" + msg += f"\n\tUse cache: {self.cfg.use_cache}" + if self.cfg.use_cache: + msg += f"\n\tCache directory: {self.cfg.cache_dir}" + + return msg + + """ + Terrain generator functions. + """ + + def _generate_random_terrains(self): + """Add terrains based on randomly sampled difficulty parameter.""" + # normalize the proportions of the sub-terrains + proportions = np.array([sub_cfg.proportion for sub_cfg in self.cfg.sub_terrains.values()]) + proportions /= np.sum(proportions) + # create a list of all terrain configs + sub_terrains_cfgs = list(self.cfg.sub_terrains.values()) + + # randomly sample sub-terrains + for index in range(self.cfg.num_rows * self.cfg.num_cols): + # coordinate index of the sub-terrain + (sub_row, sub_col) = np.unravel_index(index, (self.cfg.num_rows, self.cfg.num_cols)) + # randomly sample terrain index + sub_index = self.np_rng.choice(len(proportions), p=proportions) + # randomly sample difficulty parameter + difficulty = self.np_rng.uniform(*self.cfg.difficulty_range) + # generate terrain + mesh, origin = self._get_terrain_mesh(difficulty, sub_terrains_cfgs[sub_index]) + # add to sub-terrains + self._add_sub_terrain(mesh, origin, sub_row, sub_col, sub_terrains_cfgs[sub_index]) + + def _generate_curriculum_terrains(self): + """Add terrains based on the difficulty parameter.""" + # normalize the proportions of the sub-terrains + proportions = np.array([sub_cfg.proportion for sub_cfg in self.cfg.sub_terrains.values()]) + proportions /= np.sum(proportions) + + # find the sub-terrain index for each column + # we generate the terrains based on their proportion (not randomly sampled) + sub_indices = [] + for index in range(self.cfg.num_cols): + sub_index = np.min(np.where(index / self.cfg.num_cols + 0.001 < np.cumsum(proportions))[0]) + sub_indices.append(sub_index) + sub_indices = np.array(sub_indices, dtype=np.int32) + # create a list of all terrain configs + sub_terrains_cfgs = list(self.cfg.sub_terrains.values()) + + # curriculum-based sub-terrains + for sub_col in range(self.cfg.num_cols): + for sub_row in range(self.cfg.num_rows): + # vary the difficulty parameter linearly over the number of rows + # note: based on the proportion, multiple columns can have the same sub-terrain type. + # Thus to increase the diversity along the rows, we add a small random value to the difficulty. + # This ensures that the terrains are not exactly the same. For example, if the + # the row index is 2 and the number of rows is 10, the nominal difficulty is 0.2. + # We add a small random value to the difficulty to make it between 0.2 and 0.3. + lower, upper = self.cfg.difficulty_range + difficulty = (sub_row + self.np_rng.uniform()) / self.cfg.num_rows + difficulty = lower + (upper - lower) * difficulty + # generate terrain + mesh, origin = self._get_terrain_mesh(difficulty, sub_terrains_cfgs[sub_indices[sub_col]]) + # add to sub-terrains + self._add_sub_terrain(mesh, origin, sub_row, sub_col, sub_terrains_cfgs[sub_indices[sub_col]]) + + """ + Internal helper functions. + """ + + def _add_terrain_border(self): + """Add a surrounding border over all the sub-terrains into the terrain meshes.""" + # border parameters + border_size = ( + self.cfg.num_rows * self.cfg.size[0] + 2 * self.cfg.border_width, + self.cfg.num_cols * self.cfg.size[1] + 2 * self.cfg.border_width, + ) + inner_size = (self.cfg.num_rows * self.cfg.size[0], self.cfg.num_cols * self.cfg.size[1]) + border_center = ( + self.cfg.num_rows * self.cfg.size[0] / 2, + self.cfg.num_cols * self.cfg.size[1] / 2, + -self.cfg.border_height / 2, + ) + # border mesh + border_meshes = make_border(border_size, inner_size, height=self.cfg.border_height, position=border_center) + border = trimesh.util.concatenate(border_meshes) + # update the faces to have minimal triangles + selector = ~(np.asarray(border.triangles)[:, :, 2] < -0.1).any(1) + border.update_faces(selector) + # add the border to the list of meshes + self.terrain_meshes.append(border) + + def _add_sub_terrain( + self, mesh: trimesh.Trimesh, origin: np.ndarray, row: int, col: int, sub_terrain_cfg: SubTerrainBaseCfg + ): + """Add input sub-terrain to the list of sub-terrains. + + This function adds the input sub-terrain mesh to the list of sub-terrains and updates the origin + of the sub-terrain in the list of origins. It also samples flat patches if specified. + + Args: + mesh: The mesh of the sub-terrain. + origin: The origin of the sub-terrain. + row: The row index of the sub-terrain. + col: The column index of the sub-terrain. + """ + # sample flat patches if specified + if sub_terrain_cfg.flat_patch_sampling is not None: + omni.log.info(f"Sampling flat patches for sub-terrain at (row, col): ({row}, {col})") + # convert the mesh to warp mesh + wp_mesh = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=self.device) + # sample flat patches based on each patch configuration for that sub-terrain + for name, patch_cfg in sub_terrain_cfg.flat_patch_sampling.items(): + patch_cfg: FlatPatchSamplingCfg + # create the flat patches tensor (if not already created) + if name not in self.flat_patches: + self.flat_patches[name] = torch.zeros( + (self.cfg.num_rows, self.cfg.num_cols, patch_cfg.num_patches, 3), device=self.device + ) + # add the flat patches to the tensor + self.flat_patches[name][row, col] = find_flat_patches( + wp_mesh=wp_mesh, + origin=origin, + num_patches=patch_cfg.num_patches, + patch_radius=patch_cfg.patch_radius, + x_range=patch_cfg.x_range, + y_range=patch_cfg.y_range, + z_range=patch_cfg.z_range, + max_height_diff=patch_cfg.max_height_diff, + ) + + # transform the mesh to the correct position + transform = np.eye(4) + transform[0:2, -1] = (row + 0.5) * self.cfg.size[0], (col + 0.5) * self.cfg.size[1] + mesh.apply_transform(transform) + # add mesh to the list + self.terrain_meshes.append(mesh) + # add origin to the list + self.terrain_origins[row, col] = origin + transform[:3, -1] + + def _get_terrain_mesh(self, difficulty: float, cfg: SubTerrainBaseCfg) -> tuple[trimesh.Trimesh, np.ndarray]: + """Generate a sub-terrain mesh based on the input difficulty parameter. + + If caching is enabled, the sub-terrain is cached and loaded from the cache if it exists. + The cache is stored in the cache directory specified in the configuration. + + .. Note: + This function centers the 2D center of the mesh and its specified origin such that the + 2D center becomes :math:`(0, 0)` instead of :math:`(size[0] / 2, size[1] / 2). + + Args: + difficulty: The difficulty parameter. + cfg: The configuration of the sub-terrain. + + Returns: + The sub-terrain mesh and origin. + """ + # copy the configuration + cfg = cfg.copy() + # add other parameters to the sub-terrain configuration + cfg.difficulty = float(difficulty) + cfg.seed = self.cfg.seed + # generate hash for the sub-terrain + sub_terrain_hash = dict_to_md5_hash(cfg.to_dict()) + # generate the file name + sub_terrain_cache_dir = os.path.join(self.cfg.cache_dir, sub_terrain_hash) + sub_terrain_obj_filename = os.path.join(sub_terrain_cache_dir, "mesh.obj") + sub_terrain_csv_filename = os.path.join(sub_terrain_cache_dir, "origin.csv") + sub_terrain_meta_filename = os.path.join(sub_terrain_cache_dir, "cfg.yaml") + + # check if hash exists - if true, load the mesh and origin and return + if self.cfg.use_cache and os.path.exists(sub_terrain_obj_filename): + # load existing mesh + mesh = trimesh.load_mesh(sub_terrain_obj_filename, process=False) + origin = np.loadtxt(sub_terrain_csv_filename, delimiter=",") + # return the generated mesh + return mesh, origin + + # generate the terrain + meshes, origin = cfg.function(difficulty, cfg) + mesh = trimesh.util.concatenate(meshes) + # offset mesh such that they are in their center + transform = np.eye(4) + transform[0:2, -1] = -cfg.size[0] * 0.5, -cfg.size[1] * 0.5 + mesh.apply_transform(transform) + # change origin to be in the center of the sub-terrain + origin += transform[0:3, -1] + + # if caching is enabled, save the mesh and origin + if self.cfg.use_cache: + # create the cache directory + os.makedirs(sub_terrain_cache_dir, exist_ok=True) + # save the data + mesh.export(sub_terrain_obj_filename) + np.savetxt(sub_terrain_csv_filename, origin, delimiter=",", header="x,y,z") + dump_yaml(sub_terrain_meta_filename, cfg) + # return the generated mesh + return mesh, origin
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/terrain_generator_cfg.html b/_modules/omni/isaac/lab/terrains/terrain_generator_cfg.html new file mode 100644 index 0000000000..9e8e4e3ce7 --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/terrain_generator_cfg.html @@ -0,0 +1,759 @@ + + + + + + + + + + + omni.isaac.lab.terrains.terrain_generator_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.terrain_generator_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""
+Configuration classes defining the different terrains available. Each configuration class must
+inherit from ``omni.isaac.lab.terrains.terrains_cfg.TerrainConfig`` and define the following attributes:
+
+- ``name``: Name of the terrain. This is used for the prim name in the USD stage.
+- ``function``: Function to generate the terrain. This function must take as input the terrain difficulty
+  and the configuration parameters and return a `tuple with the `trimesh`` mesh object and terrain origin.
+"""
+
+from __future__ import annotations
+
+import numpy as np
+import trimesh
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+
+@configclass
+class FlatPatchSamplingCfg:
+    """Configuration for sampling flat patches on the sub-terrain.
+
+    For a given sub-terrain, this configuration specifies how to sample flat patches on the terrain.
+    The sampled flat patches can be used for spawning robots, targets, etc.
+
+    Please check the function :meth:`~omni.isaac.lab.terrains.utils.find_flat_patches` for more details.
+    """
+
+    num_patches: int = MISSING
+    """Number of patches to sample."""
+
+    patch_radius: float | list[float] = MISSING
+    """Radius of the patches.
+
+    A list of radii can be provided to check for patches of different sizes. This is useful to deal with
+    cases where the terrain may have holes or obstacles in some areas.
+    """
+
+    x_range: tuple[float, float] = (-1e6, 1e6)
+    """The range of x-coordinates to sample from. Defaults to (-1e6, 1e6).
+
+    This range is internally clamped to the size of the terrain mesh.
+    """
+
+    y_range: tuple[float, float] = (-1e6, 1e6)
+    """The range of y-coordinates to sample from. Defaults to (-1e6, 1e6).
+
+    This range is internally clamped to the size of the terrain mesh.
+    """
+
+    z_range: tuple[float, float] = (-1e6, 1e6)
+    """Allowed range of z-coordinates for the sampled patch. Defaults to (-1e6, 1e6)."""
+
+    max_height_diff: float = MISSING
+    """Maximum allowed height difference between the highest and lowest points on the patch."""
+
+
+
[文档]@configclass +class SubTerrainBaseCfg: + """Base class for terrain configurations. + + All the sub-terrain configurations must inherit from this class. + + The :attr:`size` attribute is the size of the generated sub-terrain. Based on this, the terrain must + extend from :math:`(0, 0)` to :math:`(size[0], size[1])`. + """ + + function: Callable[[float, SubTerrainBaseCfg], tuple[list[trimesh.Trimesh], np.ndarray]] = MISSING + """Function to generate the terrain. + + This function must take as input the terrain difficulty and the configuration parameters and + return a tuple with a list of ``trimesh`` mesh objects and the terrain origin. + """ + + proportion: float = 1.0 + """Proportion of the terrain to generate. Defaults to 1.0. + + This is used to generate a mix of terrains. The proportion corresponds to the probability of sampling + the particular terrain. For example, if there are two terrains, A and B, with proportions 0.3 and 0.7, + respectively, then the probability of sampling terrain A is 0.3 and the probability of sampling terrain B + is 0.7. + """ + + size: tuple[float, float] = (10.0, 10.0) + """The width (along x) and length (along y) of the terrain (in m). Defaults to (10.0, 10.0). + + In case the :class:`~omni.isaac.lab.terrains.TerrainImporterCfg` is used, this parameter gets overridden by + :attr:`omni.isaac.lab.scene.TerrainImporterCfg.size` attribute. + """ + + flat_patch_sampling: dict[str, FlatPatchSamplingCfg] | None = None + """Dictionary of configurations for sampling flat patches on the sub-terrain. Defaults to None, + in which case no flat patch sampling is performed. + + The keys correspond to the name of the flat patch sampling configuration and the values are the + corresponding configurations. + """
+ + +
[文档]@configclass +class TerrainGeneratorCfg: + """Configuration for the terrain generator.""" + + seed: int | None = None + """The seed for the random number generator. Defaults to None, in which case the seed from the + current NumPy's random state is used. + + When the seed is set, the random number generator is initialized with the given seed. This ensures + that the generated terrains are deterministic across different runs. If the seed is not set, the + seed from the current NumPy's random state is used. This assumes that the seed is set elsewhere in + the code. + """ + + curriculum: bool = False + """Whether to use the curriculum mode. Defaults to False. + + If True, the terrains are generated based on their difficulty parameter. Otherwise, + they are randomly generated. + """ + + size: tuple[float, float] = MISSING + """The width (along x) and length (along y) of each sub-terrain (in m). + + Note: + This value is passed on to all the sub-terrain configurations. + """ + + border_width: float = 0.0 + """The width of the border around the terrain (in m). Defaults to 0.0.""" + + border_height: float = 1.0 + """The height of the border around the terrain (in m). Defaults to 1.0.""" + + num_rows: int = 1 + """Number of rows of sub-terrains to generate. Defaults to 1.""" + + num_cols: int = 1 + """Number of columns of sub-terrains to generate. Defaults to 1.""" + + color_scheme: Literal["height", "random", "none"] = "none" + """Color scheme to use for the terrain. Defaults to "none". + + The available color schemes are: + + - "height": Color based on the height of the terrain. + - "random": Random color scheme. + - "none": No color scheme. + """ + + horizontal_scale: float = 0.1 + """The discretization of the terrain along the x and y axes (in m). Defaults to 0.1. + + This value is passed on to all the height field sub-terrain configurations. + """ + + vertical_scale: float = 0.005 + """The discretization of the terrain along the z axis (in m). Defaults to 0.005. + + This value is passed on to all the height field sub-terrain configurations. + """ + + slope_threshold: float | None = 0.75 + """The slope threshold above which surfaces are made vertical. Defaults to 0.75. + + If None no correction is applied. + + This value is passed on to all the height field sub-terrain configurations. + """ + + sub_terrains: dict[str, SubTerrainBaseCfg] = MISSING + """Dictionary of sub-terrain configurations. + + The keys correspond to the name of the sub-terrain configuration and the values are the corresponding + configurations. + """ + + difficulty_range: tuple[float, float] = (0.0, 1.0) + """The range of difficulty values for the sub-terrains. Defaults to (0.0, 1.0). + + If curriculum is enabled, the terrains will be generated based on this range in ascending order + of difficulty. Otherwise, the terrains will be generated based on this range in a random order. + """ + + use_cache: bool = False + """Whether to load the sub-terrain from cache if it exists. Defaults to True. + + If enabled, the generated terrains are stored in the cache directory. When generating terrains, the cache + is checked to see if the terrain already exists. If it does, the terrain is loaded from the cache. Otherwise, + the terrain is generated and stored in the cache. Caching can be used to speed up terrain generation. + """ + + cache_dir: str = "/tmp/isaaclab/terrains" + """The directory where the terrain cache is stored. Defaults to "/tmp/isaaclab/terrains"."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/terrain_importer.html b/_modules/omni/isaac/lab/terrains/terrain_importer.html new file mode 100644 index 0000000000..2c15cc6eb4 --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/terrain_importer.html @@ -0,0 +1,923 @@ + + + + + + + + + + + omni.isaac.lab.terrains.terrain_importer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.terrain_importer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import numpy as np
+import torch
+import trimesh
+from typing import TYPE_CHECKING
+
+import warp
+from pxr import UsdGeom
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.markers import VisualizationMarkers
+from omni.isaac.lab.markers.config import FRAME_MARKER_CFG
+from omni.isaac.lab.utils.warp import convert_to_warp_mesh
+
+from .terrain_generator import TerrainGenerator
+from .trimesh.utils import make_plane
+from .utils import create_prim_from_mesh
+
+if TYPE_CHECKING:
+    from .terrain_importer_cfg import TerrainImporterCfg
+
+
+
[文档]class TerrainImporter: + r"""A class to handle terrain meshes and import them into the simulator. + + We assume that a terrain mesh comprises of sub-terrains that are arranged in a grid with + rows ``num_rows`` and columns ``num_cols``. The terrain origins are the positions of the sub-terrains + where the robot should be spawned. + + Based on the configuration, the terrain importer handles computing the environment origins from the sub-terrain + origins. In a typical setup, the number of sub-terrains (:math:`num\_rows \times num\_cols`) is smaller than + the number of environments (:math:`num\_envs`). In this case, the environment origins are computed by + sampling the sub-terrain origins. + + If a curriculum is used, it is possible to update the environment origins to terrain origins that correspond + to a harder difficulty. This is done by calling :func:`update_terrain_levels`. The idea comes from game-based + curriculum. For example, in a game, the player starts with easy levels and progresses to harder levels. + """ + + meshes: dict[str, trimesh.Trimesh] + """A dictionary containing the names of the meshes and their keys.""" + warp_meshes: dict[str, warp.Mesh] + """A dictionary containing the names of the warp meshes and their keys.""" + terrain_origins: torch.Tensor | None + """The origins of the sub-terrains in the added terrain mesh. Shape is (num_rows, num_cols, 3). + + 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).""" + +
[文档] def __init__(self, cfg: TerrainImporterCfg): + """Initialize the terrain importer. + + Args: + cfg: The configuration for the terrain importer. + + Raises: + ValueError: If input terrain type is not supported. + ValueError: If terrain type is 'generator' and no configuration provided for ``terrain_generator``. + ValueError: If terrain type is 'usd' and no configuration provided for ``usd_path``. + ValueError: If terrain type is 'usd' or 'plane' and no configuration provided for ``env_spacing``. + """ + # check that the config is valid + cfg.validate() + # store inputs + self.cfg = cfg + self.device = sim_utils.SimulationContext.instance().device # type: ignore + + # create a dict of meshes + self.meshes = dict() + self.warp_meshes = dict() + self.env_origins = None + self.terrain_origins = None + # private variables + self._terrain_flat_patches = dict() + + # auto-import the terrain based on the config + if self.cfg.terrain_type == "generator": + # check config is provided + if self.cfg.terrain_generator is None: + raise ValueError("Input terrain type is 'generator' but no value provided for 'terrain_generator'.") + # generate the terrain + terrain_generator = TerrainGenerator(cfg=self.cfg.terrain_generator, device=self.device) + self.import_mesh("terrain", terrain_generator.terrain_mesh) + # configure the terrain origins based on the terrain generator + self.configure_env_origins(terrain_generator.terrain_origins) + # refer to the flat patches + self._terrain_flat_patches = terrain_generator.flat_patches + elif self.cfg.terrain_type == "usd": + # check if config is provided + if self.cfg.usd_path is None: + raise ValueError("Input terrain type is 'usd' but no value provided for 'usd_path'.") + # import the terrain + self.import_usd("terrain", self.cfg.usd_path) + # configure the origins in a grid + self.configure_env_origins() + elif self.cfg.terrain_type == "plane": + # load the plane + self.import_ground_plane("terrain") + # configure the origins in a grid + self.configure_env_origins() + else: + raise ValueError(f"Terrain type '{self.cfg.terrain_type}' not available.") + + # set initial state of debug visualization + self.set_debug_vis(self.cfg.debug_vis)
+ + """ + Properties. + """ + + @property + def has_debug_vis_implementation(self) -> bool: + """Whether the terrain importer has a debug visualization implemented. + + This always returns True. + """ + return True + + @property + def flat_patches(self) -> dict[str, torch.Tensor]: + """A dictionary containing the sampled valid (flat) patches for the terrain. + + This is only available if the terrain type is 'generator'. For other terrain types, this feature + is not available and the function returns an empty dictionary. + + Please refer to the :attr:`TerrainGenerator.flat_patches` for more information. + """ + return self._terrain_flat_patches + + """ + Operations - Visibility. + """ + +
[文档] def set_debug_vis(self, debug_vis: bool) -> bool: + """Set the debug visualization of the terrain importer. + + Args: + debug_vis: Whether to visualize the terrain origins. + + Returns: + Whether the debug visualization was successfully set. False if the terrain + importer does not support debug visualization. + + Raises: + RuntimeError: If terrain origins are not configured. + """ + # create a marker if necessary + if debug_vis: + if not hasattr(self, "origin_visualizer"): + self.origin_visualizer = VisualizationMarkers( + cfg=FRAME_MARKER_CFG.replace(prim_path="/Visuals/TerrainOrigin") + ) + if self.terrain_origins is not None: + self.origin_visualizer.visualize(self.terrain_origins.reshape(-1, 3)) + elif self.env_origins is not None: + self.origin_visualizer.visualize(self.env_origins.reshape(-1, 3)) + else: + raise RuntimeError("Terrain origins are not configured.") + # set visibility + self.origin_visualizer.set_visibility(True) + else: + if hasattr(self, "origin_visualizer"): + self.origin_visualizer.set_visibility(False) + # report success + return True
+ + """ + Operations - Import. + """ + +
[文档] def import_ground_plane(self, key: str, size: tuple[float, float] = (2.0e6, 2.0e6)): + """Add a plane to the terrain importer. + + Args: + key: The key to store the mesh. + size: The size of the plane. Defaults to (2.0e6, 2.0e6). + + Raises: + ValueError: If a terrain with the same key already exists. + """ + # check if key exists + if key in self.meshes: + raise ValueError(f"Mesh with key {key} already exists. Existing keys: {self.meshes.keys()}.") + # create a plane + mesh = make_plane(size, height=0.0, center_zero=True) + # store the mesh + self.meshes[key] = mesh + # create a warp mesh + device = "cuda" if "cuda" in self.device else "cpu" + self.warp_meshes[key] = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=device) + + # get the mesh + ground_plane_cfg = sim_utils.GroundPlaneCfg(physics_material=self.cfg.physics_material, size=size) + ground_plane_cfg.func(self.cfg.prim_path, ground_plane_cfg)
+ +
[文档] def import_mesh(self, key: str, mesh: trimesh.Trimesh): + """Import a mesh into the simulator. + + The mesh is imported into the simulator under the prim path ``cfg.prim_path/{key}``. The created path + contains the mesh as a :class:`pxr.UsdGeom` instance along with visual or physics material prims. + + Args: + key: The key to store the mesh. + mesh: The mesh to import. + + Raises: + ValueError: If a terrain with the same key already exists. + """ + # check if key exists + if key in self.meshes: + raise ValueError(f"Mesh with key {key} already exists. Existing keys: {self.meshes.keys()}.") + # store the mesh + self.meshes[key] = mesh + # create a warp mesh + device = "cuda" if "cuda" in self.device else "cpu" + self.warp_meshes[key] = convert_to_warp_mesh(mesh.vertices, mesh.faces, device=device) + + # get the mesh + mesh = self.meshes[key] + mesh_prim_path = self.cfg.prim_path + f"/{key}" + # import the mesh + create_prim_from_mesh( + mesh_prim_path, + mesh, + visual_material=self.cfg.visual_material, + physics_material=self.cfg.physics_material, + )
+ +
[文档] def import_usd(self, key: str, usd_path: str): + """Import a mesh from a USD file. + + We assume that the USD file contains a single mesh. If the USD file contains multiple meshes, then + the first mesh is used. The function mainly helps in registering the mesh into the warp meshes + and the meshes dictionary. + + Note: + We do not apply any material properties to the mesh. The material properties should + be defined in the USD file. + + Args: + key: The key to store the mesh. + usd_path: The path to the USD file. + + Raises: + ValueError: If a terrain with the same key already exists. + """ + # add mesh to the dict + if key in self.meshes: + raise ValueError(f"Mesh with key {key} already exists. Existing keys: {self.meshes.keys()}.") + # add the prim path + cfg = sim_utils.UsdFileCfg(usd_path=usd_path) + cfg.func(self.cfg.prim_path + f"/{key}", cfg) + + # traverse the prim and get the collision mesh + # THINK: Should the user specify the collision mesh? + mesh_prim = sim_utils.get_first_matching_child_prim( + self.cfg.prim_path + f"/{key}", lambda prim: prim.GetTypeName() == "Mesh" + ) + # check if the mesh is valid + if mesh_prim is None: + raise ValueError(f"Could not find any collision mesh in {usd_path}. Please check asset.") + # cast into UsdGeomMesh + mesh_prim = UsdGeom.Mesh(mesh_prim) + # store the mesh + vertices = np.asarray(mesh_prim.GetPointsAttr().Get()) + faces = np.asarray(mesh_prim.GetFaceVertexIndicesAttr().Get()).reshape(-1, 3) + self.meshes[key] = trimesh.Trimesh(vertices=vertices, faces=faces) + # create a warp mesh + device = "cuda" if "cuda" in self.device else "cpu" + self.warp_meshes[key] = convert_to_warp_mesh(vertices, faces, device=device)
+ + """ + Operations - Origins. + """ + +
[文档] def configure_env_origins(self, origins: np.ndarray | None = None): + """Configure the origins of the environments based on the added terrain. + + Args: + origins: The origins of the sub-terrains. Shape is (num_rows, num_cols, 3). + """ + # decide whether to compute origins in a grid or based on curriculum + if origins is not None: + # convert to numpy + if isinstance(origins, np.ndarray): + origins = torch.from_numpy(origins) + # store the origins + self.terrain_origins = origins.to(self.device, dtype=torch.float) + # compute environment origins + self.env_origins = self._compute_env_origins_curriculum(self.cfg.num_envs, self.terrain_origins) + else: + self.terrain_origins = None + # check if env spacing is valid + if self.cfg.env_spacing is None: + raise ValueError("Environment spacing must be specified for configuring grid-like origins.") + # compute environment origins + self.env_origins = self._compute_env_origins_grid(self.cfg.num_envs, self.cfg.env_spacing)
+ +
[文档] def update_env_origins(self, env_ids: torch.Tensor, move_up: torch.Tensor, move_down: torch.Tensor): + """Update the environment origins based on the terrain levels.""" + # check if grid-like spawning + if self.terrain_origins is None: + return + # update terrain level for the envs + self.terrain_levels[env_ids] += 1 * move_up - 1 * move_down + # robots that solve the last level are sent to a random one + # the minimum level is zero + self.terrain_levels[env_ids] = torch.where( + self.terrain_levels[env_ids] >= self.max_terrain_level, + torch.randint_like(self.terrain_levels[env_ids], self.max_terrain_level), + torch.clip(self.terrain_levels[env_ids], 0), + ) + # update the env origins + self.env_origins[env_ids] = self.terrain_origins[self.terrain_levels[env_ids], self.terrain_types[env_ids]]
+ + """ + Internal helpers. + """ + + def _compute_env_origins_curriculum(self, num_envs: int, origins: torch.Tensor) -> torch.Tensor: + """Compute the origins of the environments defined by the sub-terrains origins.""" + # extract number of rows and cols + num_rows, num_cols = origins.shape[:2] + # maximum initial level possible for the terrains + if self.cfg.max_init_terrain_level is None: + max_init_level = num_rows - 1 + else: + max_init_level = min(self.cfg.max_init_terrain_level, num_rows - 1) + # store maximum terrain level possible + self.max_terrain_level = num_rows + # define all terrain levels and types available + self.terrain_levels = torch.randint(0, max_init_level + 1, (num_envs,), device=self.device) + self.terrain_types = torch.div( + torch.arange(num_envs, device=self.device), + (num_envs / num_cols), + rounding_mode="floor", + ).to(torch.long) + # create tensor based on number of environments + env_origins = torch.zeros(num_envs, 3, device=self.device) + env_origins[:] = origins[self.terrain_levels, self.terrain_types] + return env_origins + + def _compute_env_origins_grid(self, num_envs: int, env_spacing: float) -> torch.Tensor: + """Compute the origins of the environments in a grid based on configured spacing.""" + # create tensor based on number of environments + env_origins = torch.zeros(num_envs, 3, device=self.device) + # create a grid of origins + num_rows = np.ceil(num_envs / int(np.sqrt(num_envs))) + num_cols = np.ceil(num_envs / num_rows) + ii, jj = torch.meshgrid( + torch.arange(num_rows, device=self.device), torch.arange(num_cols, device=self.device), indexing="ij" + ) + env_origins[:, 0] = -(ii.flatten()[:num_envs] - (num_rows - 1) / 2) * env_spacing + env_origins[:, 1] = (jj.flatten()[:num_envs] - (num_cols - 1) / 2) * env_spacing + env_origins[:, 2] = 0.0 + return env_origins
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/terrain_importer_cfg.html b/_modules/omni/isaac/lab/terrains/terrain_importer_cfg.html new file mode 100644 index 0000000000..394f87681c --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/terrain_importer_cfg.html @@ -0,0 +1,662 @@ + + + + + + + + + + + omni.isaac.lab.terrains.terrain_importer_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.terrain_importer_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from dataclasses import MISSING
+from typing import TYPE_CHECKING, Literal
+
+import omni.isaac.lab.sim as sim_utils
+from omni.isaac.lab.utils import configclass
+
+from .terrain_importer import TerrainImporter
+
+if TYPE_CHECKING:
+    from .terrain_generator_cfg import TerrainGeneratorCfg
+
+
+
[文档]@configclass +class TerrainImporterCfg: + """Configuration for the terrain manager.""" + + class_type: type = TerrainImporter + """The class to use for the terrain importer. + + Defaults to :class:`omni.isaac.lab.terrains.terrain_importer.TerrainImporter`. + """ + + collision_group: int = -1 + """The collision group of the terrain. Defaults to -1.""" + + prim_path: str = MISSING + """The absolute path of the USD terrain prim. + + All sub-terrains are imported relative to this prim path. + """ + + num_envs: int = 1 + """The number of environment origins to consider. Defaults to 1. + + In case, the :class:`~omni.isaac.lab.scene.InteractiveSceneCfg` is used, this parameter gets overridden by + :attr:`omni.isaac.lab.scene.InteractiveSceneCfg.num_envs` attribute. + """ + + terrain_type: Literal["generator", "plane", "usd"] = "generator" + """The type of terrain to generate. Defaults to "generator". + + Available options are "plane", "usd", and "generator". + """ + + terrain_generator: TerrainGeneratorCfg | None = None + """The terrain generator configuration. + + Only used if ``terrain_type`` is set to "generator". + """ + + usd_path: str | None = None + """The path to the USD file containing the terrain. + + Only used if ``terrain_type`` is set to "usd". + """ + + env_spacing: float | None = None + """The spacing between environment origins when defined in a grid. Defaults to None. + + Note: + This parameter is used only when the ``terrain_type`` is ``"plane"`` or ``"usd"``. + """ + + visual_material: sim_utils.VisualMaterialCfg | None = sim_utils.PreviewSurfaceCfg( + diffuse_color=(0.065, 0.0725, 0.080) + ) + """The visual material of the terrain. Defaults to a dark gray color material. + + The material is created at the path: ``{prim_path}/visualMaterial``. If `None`, then no material is created. + + .. note:: + This parameter is used only when the ``terrain_type`` is ``"generator"``. + """ + + physics_material: sim_utils.RigidBodyMaterialCfg = sim_utils.RigidBodyMaterialCfg() + """The physics material of the terrain. Defaults to a default physics material. + + The material is created at the path: ``{prim_path}/physicsMaterial``. + + .. note:: + This parameter is used only when the ``terrain_type`` is ``"generator"`` or ``"plane"``. + """ + + max_init_terrain_level: int | None = None + """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 None, then the initial terrain level is set to the maximum + terrain level available (``num_rows - 1``). + + Note: + This parameter is used only when sub-terrain origins are defined. + """ + + debug_vis: bool = False + """Whether to enable visualization of terrain origins for the terrain. Defaults to False."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/trimesh/mesh_terrains.html b/_modules/omni/isaac/lab/terrains/trimesh/mesh_terrains.html new file mode 100644 index 0000000000..07e856e88b --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/trimesh/mesh_terrains.html @@ -0,0 +1,1411 @@ + + + + + + + + + + + omni.isaac.lab.terrains.trimesh.mesh_terrains — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.trimesh.mesh_terrains 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Functions to generate different terrains using the ``trimesh`` library."""
+
+from __future__ import annotations
+
+import numpy as np
+import scipy.spatial.transform as tf
+import torch
+import trimesh
+from typing import TYPE_CHECKING
+
+from .utils import *  # noqa: F401, F403
+from .utils import make_border, make_plane
+
+if TYPE_CHECKING:
+    from . import mesh_terrains_cfg
+
+
+
[文档]def flat_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshPlaneTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a flat terrain as a plane. + + .. image:: ../../_static/terrains/trimesh/flat_terrain.jpg + :width: 45% + :align: center + + Note: + The :obj:`difficulty` parameter is ignored for this terrain. + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # compute the position of the terrain + origin = (cfg.size[0] / 2.0, cfg.size[1] / 2.0, 0.0) + # compute the vertices of the terrain + plane_mesh = make_plane(cfg.size, 0.0, center_zero=False) + # return the tri-mesh and the position + return [plane_mesh], np.array(origin)
+ + +
[文档]def pyramid_stairs_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshPyramidStairsTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a pyramid stair pattern. + + The terrain is a pyramid stair pattern which trims to a flat platform at the center of the terrain. + + If :obj:`cfg.holes` is True, the terrain will have pyramid stairs of length or width + :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 + :width: 45% + + .. image:: ../../_static/terrains/trimesh/pyramid_stairs_terrain_with_holes.jpg + :width: 45% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + step_height = cfg.step_height_range[0] + difficulty * (cfg.step_height_range[1] - cfg.step_height_range[0]) + + # compute number of steps in x and y direction + num_steps_x = (cfg.size[0] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1 + num_steps_y = (cfg.size[1] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1 + # we take the minimum number of steps in x and y direction + num_steps = int(min(num_steps_x, num_steps_y)) + + # initialize list of meshes + meshes_list = list() + + # generate the border if needed + if cfg.border_width > 0.0 and not cfg.holes: + # obtain a list of meshes for the border + border_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], -step_height / 2] + border_inner_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width) + make_borders = make_border(cfg.size, border_inner_size, step_height, border_center) + # add the border meshes to the list of meshes + meshes_list += make_borders + + # generate the terrain + # -- compute the position of the center of the terrain + terrain_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.0] + terrain_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width) + # -- generate the stair pattern + for k in range(num_steps): + # check if we need to add holes around the steps + if cfg.holes: + box_size = (cfg.platform_width, cfg.platform_width) + else: + box_size = (terrain_size[0] - 2 * k * cfg.step_width, terrain_size[1] - 2 * k * cfg.step_width) + # compute the quantities of the box + # -- location + box_z = terrain_center[2] + k * step_height / 2.0 + box_offset = (k + 0.5) * cfg.step_width + # -- dimensions + box_height = (k + 2) * step_height + # generate the boxes + # top/bottom + box_dims = (box_size[0], cfg.step_width, box_height) + # -- top + box_pos = (terrain_center[0], terrain_center[1] + terrain_size[1] / 2.0 - box_offset, box_z) + box_top = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # -- bottom + box_pos = (terrain_center[0], terrain_center[1] - terrain_size[1] / 2.0 + box_offset, box_z) + box_bottom = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # right/left + if cfg.holes: + box_dims = (cfg.step_width, box_size[1], box_height) + else: + box_dims = (cfg.step_width, box_size[1] - 2 * cfg.step_width, box_height) + # -- right + box_pos = (terrain_center[0] + terrain_size[0] / 2.0 - box_offset, terrain_center[1], box_z) + box_right = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # -- left + box_pos = (terrain_center[0] - terrain_size[0] / 2.0 + box_offset, terrain_center[1], box_z) + box_left = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # add the boxes to the list of meshes + meshes_list += [box_top, box_bottom, box_right, box_left] + + # generate final box for the middle of the terrain + box_dims = ( + terrain_size[0] - 2 * num_steps * cfg.step_width, + terrain_size[1] - 2 * num_steps * cfg.step_width, + (num_steps + 2) * step_height, + ) + box_pos = (terrain_center[0], terrain_center[1], terrain_center[2] + num_steps * step_height / 2) + box_middle = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + meshes_list.append(box_middle) + # origin of the terrain + origin = np.array([terrain_center[0], terrain_center[1], (num_steps + 1) * step_height]) + + return meshes_list, origin
+ + +
[文档]def inverted_pyramid_stairs_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshInvertedPyramidStairsTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a inverted pyramid stair pattern. + + The terrain is an inverted pyramid stair pattern which trims to a flat platform at the center of the terrain. + + If :obj:`cfg.holes` is True, the terrain will have pyramid stairs of length or width + :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 + :width: 45% + + .. image:: ../../_static/terrains/trimesh/inverted_pyramid_stairs_terrain_with_holes.jpg + :width: 45% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + step_height = cfg.step_height_range[0] + difficulty * (cfg.step_height_range[1] - cfg.step_height_range[0]) + + # compute number of steps in x and y direction + num_steps_x = (cfg.size[0] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1 + num_steps_y = (cfg.size[1] - 2 * cfg.border_width - cfg.platform_width) // (2 * cfg.step_width) + 1 + # we take the minimum number of steps in x and y direction + num_steps = int(min(num_steps_x, num_steps_y)) + # total height of the terrain + total_height = (num_steps + 1) * step_height + + # initialize list of meshes + meshes_list = list() + + # generate the border if needed + if cfg.border_width > 0.0 and not cfg.holes: + # obtain a list of meshes for the border + border_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], -0.5 * step_height] + border_inner_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width) + make_borders = make_border(cfg.size, border_inner_size, step_height, border_center) + # add the border meshes to the list of meshes + meshes_list += make_borders + # generate the terrain + # -- compute the position of the center of the terrain + terrain_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.0] + terrain_size = (cfg.size[0] - 2 * cfg.border_width, cfg.size[1] - 2 * cfg.border_width) + # -- generate the stair pattern + for k in range(num_steps): + # check if we need to add holes around the steps + if cfg.holes: + box_size = (cfg.platform_width, cfg.platform_width) + else: + box_size = (terrain_size[0] - 2 * k * cfg.step_width, terrain_size[1] - 2 * k * cfg.step_width) + # compute the quantities of the box + # -- location + box_z = terrain_center[2] - total_height / 2 - (k + 1) * step_height / 2.0 + box_offset = (k + 0.5) * cfg.step_width + # -- dimensions + box_height = total_height - (k + 1) * step_height + # generate the boxes + # top/bottom + box_dims = (box_size[0], cfg.step_width, box_height) + # -- top + box_pos = (terrain_center[0], terrain_center[1] + terrain_size[1] / 2.0 - box_offset, box_z) + box_top = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # -- bottom + box_pos = (terrain_center[0], terrain_center[1] - terrain_size[1] / 2.0 + box_offset, box_z) + box_bottom = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # right/left + if cfg.holes: + box_dims = (cfg.step_width, box_size[1], box_height) + else: + box_dims = (cfg.step_width, box_size[1] - 2 * cfg.step_width, box_height) + # -- right + box_pos = (terrain_center[0] + terrain_size[0] / 2.0 - box_offset, terrain_center[1], box_z) + box_right = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # -- left + box_pos = (terrain_center[0] - terrain_size[0] / 2.0 + box_offset, terrain_center[1], box_z) + box_left = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + # add the boxes to the list of meshes + meshes_list += [box_top, box_bottom, box_right, box_left] + # generate final box for the middle of the terrain + box_dims = ( + terrain_size[0] - 2 * num_steps * cfg.step_width, + terrain_size[1] - 2 * num_steps * cfg.step_width, + step_height, + ) + box_pos = (terrain_center[0], terrain_center[1], terrain_center[2] - total_height - step_height / 2) + box_middle = trimesh.creation.box(box_dims, trimesh.transformations.translation_matrix(box_pos)) + meshes_list.append(box_middle) + # origin of the terrain + origin = np.array([terrain_center[0], terrain_center[1], -(num_steps + 1) * step_height]) + + return meshes_list, origin
+ + +
[文档]def random_grid_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshRandomGridTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with cells of random heights and fixed width. + + The terrain is generated in the x-y plane and has a height of 1.0. It is then divided into a grid of the + specified size :obj:`cfg.grid_width`. Each grid cell is then randomly shifted in the z-direction by a value uniformly + sampled between :obj:`cfg.grid_height_range`. At the center of the terrain, a platform of the specified width + :obj:`cfg.platform_width` is generated. + + 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 + :width: 45% + + .. image:: ../../_static/terrains/trimesh/random_grid_terrain_with_holes.jpg + :width: 45% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + + Raises: + ValueError: If the terrain is not square. This method only supports square terrains. + RuntimeError: If the grid width is large such that the border width is negative. + """ + # check to ensure square terrain + if cfg.size[0] != cfg.size[1]: + raise ValueError(f"The terrain must be square. Received size: {cfg.size}.") + # resolve the terrain configuration + grid_height = cfg.grid_height_range[0] + difficulty * (cfg.grid_height_range[1] - cfg.grid_height_range[0]) + + # initialize list of meshes + meshes_list = list() + # compute the number of boxes in each direction + num_boxes_x = int(cfg.size[0] / cfg.grid_width) + num_boxes_y = int(cfg.size[1] / cfg.grid_width) + # constant parameters + terrain_height = 1.0 + device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") + + # generate the border + border_width = cfg.size[0] - min(num_boxes_x, num_boxes_y) * cfg.grid_width + if border_width > 0: + # compute parameters for the border + border_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2) + border_inner_size = (cfg.size[0] - border_width, cfg.size[1] - border_width) + # create border meshes + make_borders = make_border(cfg.size, border_inner_size, terrain_height, border_center) + meshes_list += make_borders + else: + raise RuntimeError("Border width must be greater than 0! Adjust the parameter 'cfg.grid_width'.") + + # create a template grid of terrain height + grid_dim = [cfg.grid_width, cfg.grid_width, terrain_height] + grid_position = [0.5 * cfg.grid_width, 0.5 * cfg.grid_width, -terrain_height / 2] + template_box = trimesh.creation.box(grid_dim, trimesh.transformations.translation_matrix(grid_position)) + # extract vertices and faces of the box to create a template + template_vertices = template_box.vertices # (8, 3) + template_faces = template_box.faces + + # repeat the template box vertices to span the terrain (num_boxes_x * num_boxes_y, 8, 3) + vertices = torch.tensor(template_vertices, device=device).repeat(num_boxes_x * num_boxes_y, 1, 1) + # create a meshgrid to offset the vertices + x = torch.arange(0, num_boxes_x, device=device) + y = torch.arange(0, num_boxes_y, device=device) + xx, yy = torch.meshgrid(x, y, indexing="ij") + xx = xx.flatten().view(-1, 1) + yy = yy.flatten().view(-1, 1) + xx_yy = torch.cat((xx, yy), dim=1) + # offset the vertices + offsets = cfg.grid_width * xx_yy + border_width / 2 + vertices[:, :, :2] += offsets.unsqueeze(1) + # mask the vertices to create holes, s.t. only grids along the x and y axis are present + if cfg.holes: + # -- x-axis + mask_x = torch.logical_and( + (vertices[:, :, 0] > (cfg.size[0] - border_width - cfg.platform_width) / 2).all(dim=1), + (vertices[:, :, 0] < (cfg.size[0] + border_width + cfg.platform_width) / 2).all(dim=1), + ) + vertices_x = vertices[mask_x] + # -- y-axis + mask_y = torch.logical_and( + (vertices[:, :, 1] > (cfg.size[1] - border_width - cfg.platform_width) / 2).all(dim=1), + (vertices[:, :, 1] < (cfg.size[1] + border_width + cfg.platform_width) / 2).all(dim=1), + ) + vertices_y = vertices[mask_y] + # -- combine these vertices + vertices = torch.cat((vertices_x, vertices_y)) + # add noise to the vertices to have a random height over each grid cell + num_boxes = len(vertices) + # create noise for the z-axis + h_noise = torch.zeros((num_boxes, 3), device=device) + h_noise[:, 2].uniform_(-grid_height, grid_height) + # reshape noise to match the vertices (num_boxes, 4, 3) + # only the top vertices of the box are affected + vertices_noise = torch.zeros((num_boxes, 4, 3), device=device) + vertices_noise += h_noise.unsqueeze(1) + # add height only to the top vertices of the box + vertices[vertices[:, :, 2] == 0] += vertices_noise.view(-1, 3) + # move to numpy + vertices = vertices.reshape(-1, 3).cpu().numpy() + + # create faces for boxes (num_boxes, 12, 3). Each box has 6 faces, each face has 2 triangles. + faces = torch.tensor(template_faces, device=device).repeat(num_boxes, 1, 1) + face_offsets = torch.arange(0, num_boxes, device=device).unsqueeze(1).repeat(1, 12) * 8 + faces += face_offsets.unsqueeze(2) + # move to numpy + faces = faces.view(-1, 3).cpu().numpy() + # convert to trimesh + grid_mesh = trimesh.Trimesh(vertices=vertices, faces=faces) + meshes_list.append(grid_mesh) + + # add a platform in the center of the terrain that is accessible from all sides + dim = (cfg.platform_width, cfg.platform_width, terrain_height + grid_height) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2 + grid_height / 2) + box_platform = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(box_platform) + + # specify the origin of the terrain + origin = np.array([0.5 * cfg.size[0], 0.5 * cfg.size[1], grid_height]) + + return meshes_list, origin
+ + +
[文档]def rails_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshRailsTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with box rails as extrusions. + + The terrain contains two sets of box rails created as extrusions. The first set (inner rails) is extruded from + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. this is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + rail_height = cfg.rail_height_range[1] - difficulty * (cfg.rail_height_range[1] - cfg.rail_height_range[0]) + + # initialize list of meshes + meshes_list = list() + # extract quantities + rail_1_thickness, rail_2_thickness = cfg.rail_thickness_range + rail_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], rail_height * 0.5) + # constants for terrain generation + terrain_height = 1.0 + rail_2_ratio = 0.6 + + # generate first set of rails + rail_1_inner_size = (cfg.platform_width, cfg.platform_width) + rail_1_outer_size = (cfg.platform_width + 2.0 * rail_1_thickness, cfg.platform_width + 2.0 * rail_1_thickness) + meshes_list += make_border(rail_1_outer_size, rail_1_inner_size, rail_height, rail_center) + # generate second set of rails + rail_2_inner_x = cfg.platform_width + (cfg.size[0] - cfg.platform_width) * rail_2_ratio + rail_2_inner_y = cfg.platform_width + (cfg.size[1] - cfg.platform_width) * rail_2_ratio + rail_2_inner_size = (rail_2_inner_x, rail_2_inner_y) + rail_2_outer_size = (rail_2_inner_x + 2.0 * rail_2_thickness, rail_2_inner_y + 2.0 * rail_2_thickness) + meshes_list += make_border(rail_2_outer_size, rail_2_inner_size, rail_height, rail_center) + # generate the ground + dim = (cfg.size[0], cfg.size[1], terrain_height) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2) + ground_meshes = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(ground_meshes) + + # specify the origin of the terrain + origin = np.array([pos[0], pos[1], 0.0]) + + return meshes_list, origin
+ + +
[文档]def pit_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshPitTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a pit with levels (stairs) leading out of the pit. + + The terrain contains a platform at the center and a staircase leading out of the pit. + The staircase is a series of steps that are aligned along the x- and y- axis. The steps are + 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 + :width: 40% + + .. image:: ../../_static/terrains/trimesh/pit_terrain_with_two_levels.jpg + :width: 40% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + pit_depth = cfg.pit_depth_range[0] + difficulty * (cfg.pit_depth_range[1] - cfg.pit_depth_range[0]) + + # initialize list of meshes + meshes_list = list() + # extract quantities + inner_pit_size = (cfg.platform_width, cfg.platform_width) + total_depth = pit_depth + # constants for terrain generation + terrain_height = 1.0 + ring_2_ratio = 0.6 + + # if the pit is double, the inner ring is smaller to fit the second level + if cfg.double_pit: + # increase the total height of the pit + total_depth *= 2.0 + # reduce the size of the inner ring + inner_pit_x = cfg.platform_width + (cfg.size[0] - cfg.platform_width) * ring_2_ratio + inner_pit_y = cfg.platform_width + (cfg.size[1] - cfg.platform_width) * ring_2_ratio + inner_pit_size = (inner_pit_x, inner_pit_y) + + # generate the pit (outer ring) + pit_center = [0.5 * cfg.size[0], 0.5 * cfg.size[1], -total_depth * 0.5] + meshes_list += make_border(cfg.size, inner_pit_size, total_depth, pit_center) + # generate the second level of the pit (inner ring) + if cfg.double_pit: + pit_center[2] = -total_depth + meshes_list += make_border(inner_pit_size, (cfg.platform_width, cfg.platform_width), total_depth, pit_center) + # generate the ground + dim = (cfg.size[0], cfg.size[1], terrain_height) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -total_depth - terrain_height / 2) + ground_meshes = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(ground_meshes) + + # specify the origin of the terrain + origin = np.array([pos[0], pos[1], -total_depth]) + + return meshes_list, origin
+ + +
[文档]def box_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshBoxTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with boxes (similar to a pyramid). + + The terrain has a ground with boxes on top of it that are stacked on top of each other. + 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 + :width: 40% + + .. image:: ../../_static/terrains/trimesh/box_terrain_with_two_boxes.jpg + :width: 40% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + box_height = cfg.box_height_range[0] + difficulty * (cfg.box_height_range[1] - cfg.box_height_range[0]) + + # initialize list of meshes + meshes_list = list() + # extract quantities + total_height = box_height + if cfg.double_box: + total_height *= 2.0 + # constants for terrain generation + terrain_height = 1.0 + box_2_ratio = 0.6 + + # Generate the top box + dim = (cfg.platform_width, cfg.platform_width, terrain_height + total_height) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], (total_height - terrain_height) / 2) + box_mesh = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(box_mesh) + # Generate the lower box + if cfg.double_box: + # calculate the size of the lower box + outer_box_x = cfg.platform_width + (cfg.size[0] - cfg.platform_width) * box_2_ratio + outer_box_y = cfg.platform_width + (cfg.size[1] - cfg.platform_width) * box_2_ratio + # create the lower box + dim = (outer_box_x, outer_box_y, terrain_height + total_height / 2) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], (total_height - terrain_height) / 2 - total_height / 4) + box_mesh = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(box_mesh) + # Generate the ground + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2) + dim = (cfg.size[0], cfg.size[1], terrain_height) + ground_mesh = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(ground_mesh) + + # specify the origin of the terrain + origin = np.array([pos[0], pos[1], total_height]) + + return meshes_list, origin
+ + +
[文档]def gap_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshGapTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a gap around the platform. + + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + gap_width = cfg.gap_width_range[0] + difficulty * (cfg.gap_width_range[1] - cfg.gap_width_range[0]) + + # initialize list of meshes + meshes_list = list() + # constants for terrain generation + terrain_height = 1.0 + terrain_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2) + + # Generate the outer ring + inner_size = (cfg.platform_width + 2 * gap_width, cfg.platform_width + 2 * gap_width) + meshes_list += make_border(cfg.size, inner_size, terrain_height, terrain_center) + # Generate the inner box + box_dim = (cfg.platform_width, cfg.platform_width, terrain_height) + box = trimesh.creation.box(box_dim, trimesh.transformations.translation_matrix(terrain_center)) + meshes_list.append(box) + + # specify the origin of the terrain + origin = np.array([terrain_center[0], terrain_center[1], 0.0]) + + return meshes_list, origin
+ + +
[文档]def floating_ring_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshFloatingRingTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a floating square ring. + + The terrain has a ground with a floating ring in the middle. The ring extends from the center from + :obj:`platform_width` to :obj:`platform_width` + :obj:`ring_width` in the x and y directions. + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + """ + # resolve the terrain configuration + ring_height = cfg.ring_height_range[1] - difficulty * (cfg.ring_height_range[1] - cfg.ring_height_range[0]) + ring_width = cfg.ring_width_range[0] + difficulty * (cfg.ring_width_range[1] - cfg.ring_width_range[0]) + + # initialize list of meshes + meshes_list = list() + # constants for terrain generation + terrain_height = 1.0 + + # Generate the floating ring + ring_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], ring_height + 0.5 * cfg.ring_thickness) + ring_outer_size = (cfg.platform_width + 2 * ring_width, cfg.platform_width + 2 * ring_width) + ring_inner_size = (cfg.platform_width, cfg.platform_width) + meshes_list += make_border(ring_outer_size, ring_inner_size, cfg.ring_thickness, ring_center) + # Generate the ground + dim = (cfg.size[0], cfg.size[1], terrain_height) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -terrain_height / 2) + ground = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(ground) + + # specify the origin of the terrain + origin = np.asarray([pos[0], pos[1], 0.0]) + + return meshes_list, origin
+ + +
[文档]def star_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshStarTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a star. + + The terrain has a ground with a cylinder in the middle. The star is made of :obj:`num_bars` bars + 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 + :width: 40% + :align: center + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + + Raises: + ValueError: If :obj:`num_bars` is less than 2. + """ + # check the number of bars + if cfg.num_bars < 2: + raise ValueError(f"The number of bars in the star must be greater than 2. Received: {cfg.num_bars}") + + # resolve the terrain configuration + bar_height = cfg.bar_height_range[0] + difficulty * (cfg.bar_height_range[1] - cfg.bar_height_range[0]) + bar_width = cfg.bar_width_range[1] - difficulty * (cfg.bar_width_range[1] - cfg.bar_width_range[0]) + + # initialize list of meshes + meshes_list = list() + # Generate a platform in the middle + platform_center = (0.5 * cfg.size[0], 0.5 * cfg.size[1], -bar_height / 2) + platform_transform = trimesh.transformations.translation_matrix(platform_center) + platform = trimesh.creation.cylinder( + cfg.platform_width * 0.5, bar_height, sections=2 * cfg.num_bars, transform=platform_transform + ) + meshes_list.append(platform) + # Generate bars to connect the platform to the terrain + transform = np.eye(4) + transform[:3, -1] = np.asarray(platform_center) + yaw = 0.0 + for _ in range(cfg.num_bars): + # compute the length of the bar based on the yaw + # length changes since the bar is connected to a square border + bar_length = cfg.size[0] + if yaw < 0.25 * np.pi: + bar_length /= np.math.cos(yaw) + elif yaw < 0.75 * np.pi: + bar_length /= np.math.sin(yaw) + else: + bar_length /= np.math.cos(np.pi - yaw) + # compute the transform of the bar + transform[0:3, 0:3] = tf.Rotation.from_euler("z", yaw).as_matrix() + # add the bar to the mesh + dim = [bar_length - bar_width, bar_width, bar_height] + bar = trimesh.creation.box(dim, transform) + meshes_list.append(bar) + # increment the yaw + yaw += np.pi / cfg.num_bars + # Generate the exterior border + inner_size = (cfg.size[0] - 2 * bar_width, cfg.size[1] - 2 * bar_width) + meshes_list += make_border(cfg.size, inner_size, bar_height, platform_center) + # Generate the ground + ground = make_plane(cfg.size, -bar_height, center_zero=False) + meshes_list.append(ground) + # specify the origin of the terrain + origin = np.asarray([0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.0]) + + return meshes_list, origin
+ + +
[文档]def repeated_objects_terrain( + difficulty: float, cfg: mesh_terrains_cfg.MeshRepeatedObjectsTerrainCfg +) -> tuple[list[trimesh.Trimesh], np.ndarray]: + """Generate a terrain with a set of repeated objects. + + The terrain has a ground with a platform in the middle. The objects are randomly placed on the + terrain s.t. they do not overlap with the platform. + + Depending on the object type, the objects are generated with different parameters. The objects + The types of objects that can be generated are: ``"cylinder"``, ``"box"``, ``"cone"``. + + 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 + :width: 30% + + .. image:: ../../_static/terrains/trimesh/repeated_objects_box_terrain.jpg + :width: 30% + + .. image:: ../../_static/terrains/trimesh/repeated_objects_pyramid_terrain.jpg + :width: 30% + + Args: + difficulty: The difficulty of the terrain. This is a value between 0 and 1. + cfg: The configuration for the terrain. + + Returns: + A tuple containing the tri-mesh of the terrain and the origin of the terrain (in m). + + Raises: + ValueError: If the object type is not supported. It must be either a string or a callable. + """ + # import the object functions -- this is done here to avoid circular imports + from .mesh_terrains_cfg import ( + MeshRepeatedBoxesTerrainCfg, + MeshRepeatedCylindersTerrainCfg, + MeshRepeatedPyramidsTerrainCfg, + ) + + # if object type is a string, get the function: make_{object_type} + if isinstance(cfg.object_type, str): + object_func = globals().get(f"make_{cfg.object_type}") + else: + object_func = cfg.object_type + if not callable(object_func): + raise ValueError(f"The attribute 'object_type' must be a string or a callable. Received: {object_func}") + + # Resolve the terrain configuration + # -- pass parameters to make calling simpler + cp_0 = cfg.object_params_start + cp_1 = cfg.object_params_end + # -- common parameters + num_objects = cp_0.num_objects + int(difficulty * (cp_1.num_objects - cp_0.num_objects)) + height = cp_0.height + difficulty * (cp_1.height - cp_0.height) + # -- object specific parameters + # note: SIM114 requires duplicated logical blocks under a single body. + if isinstance(cfg, MeshRepeatedBoxesTerrainCfg): + cp_0: MeshRepeatedBoxesTerrainCfg.ObjectCfg + cp_1: MeshRepeatedBoxesTerrainCfg.ObjectCfg + object_kwargs = { + "length": cp_0.size[0] + difficulty * (cp_1.size[0] - cp_0.size[0]), + "width": cp_0.size[1] + difficulty * (cp_1.size[1] - cp_0.size[1]), + "max_yx_angle": cp_0.max_yx_angle + difficulty * (cp_1.max_yx_angle - cp_0.max_yx_angle), + "degrees": cp_0.degrees, + } + elif isinstance(cfg, MeshRepeatedPyramidsTerrainCfg): # noqa: SIM114 + cp_0: MeshRepeatedPyramidsTerrainCfg.ObjectCfg + cp_1: MeshRepeatedPyramidsTerrainCfg.ObjectCfg + object_kwargs = { + "radius": cp_0.radius + difficulty * (cp_1.radius - cp_0.radius), + "max_yx_angle": cp_0.max_yx_angle + difficulty * (cp_1.max_yx_angle - cp_0.max_yx_angle), + "degrees": cp_0.degrees, + } + elif isinstance(cfg, MeshRepeatedCylindersTerrainCfg): # noqa: SIM114 + cp_0: MeshRepeatedCylindersTerrainCfg.ObjectCfg + cp_1: MeshRepeatedCylindersTerrainCfg.ObjectCfg + object_kwargs = { + "radius": cp_0.radius + difficulty * (cp_1.radius - cp_0.radius), + "max_yx_angle": cp_0.max_yx_angle + difficulty * (cp_1.max_yx_angle - cp_0.max_yx_angle), + "degrees": cp_0.degrees, + } + else: + raise ValueError(f"Unknown terrain configuration: {cfg}") + # constants for the terrain + platform_clearance = 0.1 + + # initialize list of meshes + meshes_list = list() + # compute quantities + origin = np.asarray((0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.5 * height)) + platform_corners = np.asarray([ + [origin[0] - cfg.platform_width / 2, origin[1] - cfg.platform_width / 2], + [origin[0] + cfg.platform_width / 2, origin[1] + cfg.platform_width / 2], + ]) + platform_corners[0, :] *= 1 - platform_clearance + platform_corners[1, :] *= 1 + platform_clearance + # sample center for objects + while True: + object_centers = np.zeros((num_objects, 3)) + object_centers[:, 0] = np.random.uniform(0, cfg.size[0], num_objects) + object_centers[:, 1] = np.random.uniform(0, cfg.size[1], num_objects) + # filter out the centers that are on the platform + is_within_platform_x = np.logical_and( + object_centers[:, 0] >= platform_corners[0, 0], object_centers[:, 0] <= platform_corners[1, 0] + ) + is_within_platform_y = np.logical_and( + object_centers[:, 1] >= platform_corners[0, 1], object_centers[:, 1] <= platform_corners[1, 1] + ) + masks = np.logical_and(is_within_platform_x, is_within_platform_y) + # if there are no objects on the platform, break + if not np.any(masks): + break + + # generate obstacles (but keep platform clean) + for index in range(len(object_centers)): + # randomize the height of the object + ob_height = height + np.random.uniform(-cfg.max_height_noise, cfg.max_height_noise) + if ob_height > 0.0: + object_mesh = object_func(center=object_centers[index], height=ob_height, **object_kwargs) + meshes_list.append(object_mesh) + + # generate a ground plane for the terrain + ground_plane = make_plane(cfg.size, height=0.0, center_zero=False) + meshes_list.append(ground_plane) + # generate a platform in the middle + dim = (cfg.platform_width, cfg.platform_width, 0.5 * height) + pos = (0.5 * cfg.size[0], 0.5 * cfg.size[1], 0.25 * height) + platform = trimesh.creation.box(dim, trimesh.transformations.translation_matrix(pos)) + meshes_list.append(platform) + + return meshes_list, origin
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/trimesh/mesh_terrains_cfg.html b/_modules/omni/isaac/lab/terrains/trimesh/mesh_terrains_cfg.html new file mode 100644 index 0000000000..fe3ca2a01a --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/trimesh/mesh_terrains_cfg.html @@ -0,0 +1,828 @@ + + + + + + + + + + + omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+from typing import Literal
+
+import omni.isaac.lab.terrains.trimesh.mesh_terrains as mesh_terrains
+import omni.isaac.lab.terrains.trimesh.utils as mesh_utils_terrains
+from omni.isaac.lab.utils import configclass
+
+from ..terrain_generator_cfg import SubTerrainBaseCfg
+
+"""
+Different trimesh terrain configurations.
+"""
+
+
+
[文档]@configclass +class MeshPlaneTerrainCfg(SubTerrainBaseCfg): + """Configuration for a plane mesh terrain.""" + + function = mesh_terrains.flat_terrain
+ + +
[文档]@configclass +class MeshPyramidStairsTerrainCfg(SubTerrainBaseCfg): + """Configuration for a pyramid stair mesh terrain.""" + + function = mesh_terrains.pyramid_stairs_terrain + + border_width: float = 0.0 + """The width of the border around the terrain (in m). Defaults to 0.0. + + The border is a flat terrain with the same height as the terrain. + """ + step_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the steps (in m).""" + step_width: float = MISSING + """The width of the steps (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0.""" + holes: bool = False + """If True, the terrain will have holes in the steps. Defaults to False. + + If :obj:`holes` is True, the terrain will have pyramid stairs of length or width + :obj:`platform_width` (depending on the direction) with no steps in the remaining area. Additionally, + no border will be added. + """
+ + +
[文档]@configclass +class MeshInvertedPyramidStairsTerrainCfg(MeshPyramidStairsTerrainCfg): + """Configuration for an inverted pyramid stair mesh terrain. + + Note: + This is the same as :class:`MeshPyramidStairsTerrainCfg` except that the steps are inverted. + """ + + function = mesh_terrains.inverted_pyramid_stairs_terrain
+ + +
[文档]@configclass +class MeshRandomGridTerrainCfg(SubTerrainBaseCfg): + """Configuration for a random grid mesh terrain.""" + + function = mesh_terrains.random_grid_terrain + + grid_width: float = MISSING + """The width of the grid cells (in m).""" + grid_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the grid cells (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0.""" + holes: bool = False + """If True, the terrain will have holes in the steps. Defaults to False. + + If :obj:`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. + """
+ + +
[文档]@configclass +class MeshRailsTerrainCfg(SubTerrainBaseCfg): + """Configuration for a terrain with box rails as extrusions.""" + + function = mesh_terrains.rails_terrain + + rail_thickness_range: tuple[float, float] = MISSING + """The thickness of the inner and outer rails (in m).""" + rail_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the rails (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0."""
+ + +
[文档]@configclass +class MeshPitTerrainCfg(SubTerrainBaseCfg): + """Configuration for a terrain with a pit that leads out of the pit.""" + + function = mesh_terrains.pit_terrain + + pit_depth_range: tuple[float, float] = MISSING + """The minimum and maximum height of the pit (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0.""" + double_pit: bool = False + """If True, the pit contains two levels of stairs. Defaults to False."""
+ + +
[文档]@configclass +class MeshBoxTerrainCfg(SubTerrainBaseCfg): + """Configuration for a terrain with boxes (similar to a pyramid).""" + + function = mesh_terrains.box_terrain + + box_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the box (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0.""" + double_box: bool = False + """If True, the pit contains two levels of stairs/boxes. Defaults to False."""
+ + +
[文档]@configclass +class MeshGapTerrainCfg(SubTerrainBaseCfg): + """Configuration for a terrain with a gap around the platform.""" + + function = mesh_terrains.gap_terrain + + gap_width_range: tuple[float, float] = MISSING + """The minimum and maximum width of the gap (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0."""
+ + +
[文档]@configclass +class MeshFloatingRingTerrainCfg(SubTerrainBaseCfg): + """Configuration for a terrain with a floating ring around the center.""" + + function = mesh_terrains.floating_ring_terrain + + ring_width_range: tuple[float, float] = MISSING + """The minimum and maximum width of the ring (in m).""" + ring_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the ring (in m).""" + ring_thickness: float = MISSING + """The thickness (along z) of the ring (in m).""" + platform_width: float = 1.0 + """The width of the square platform at the center of the terrain. Defaults to 1.0."""
+ + +
[文档]@configclass +class MeshStarTerrainCfg(SubTerrainBaseCfg): + """Configuration for a terrain with a star pattern.""" + + function = mesh_terrains.star_terrain + + num_bars: int = MISSING + """The number of bars per-side the star. Must be greater than 2.""" + bar_width_range: tuple[float, float] = MISSING + """The minimum and maximum width of the bars in the star (in m).""" + bar_height_range: tuple[float, float] = MISSING + """The minimum and maximum height of the bars in the star (in m).""" + platform_width: float = 1.0 + """The width of the cylindrical platform at the center of the terrain. Defaults to 1.0."""
+ + +
[文档]@configclass +class MeshRepeatedObjectsTerrainCfg(SubTerrainBaseCfg): + """Base configuration for a terrain with repeated objects.""" + +
[文档] @configclass + class ObjectCfg: + """Configuration of repeated objects.""" + + num_objects: int = MISSING + """The number of objects to add to the terrain.""" + height: float = MISSING + """The height (along z) of the object (in m)."""
+ + function = mesh_terrains.repeated_objects_terrain + + object_type: Literal["cylinder", "box", "cone"] | callable = MISSING + """The type of object to generate. + + The type can be a string or a callable. If it is a string, the function will look for a function called + ``make_{object_type}`` in the current module scope. If it is a callable, the function will + use the callable to generate the object. + """ + object_params_start: ObjectCfg = MISSING + """The object curriculum parameters at the start of the curriculum.""" + object_params_end: ObjectCfg = MISSING + """The object curriculum parameters at the end of the curriculum.""" + + max_height_noise: float = 0.0 + """The maximum amount of noise to add to the height of the objects (in m). Defaults to 0.0.""" + platform_width: float = 1.0 + """The width of the cylindrical platform at the center of the terrain. Defaults to 1.0."""
+ + +
[文档]@configclass +class MeshRepeatedPyramidsTerrainCfg(MeshRepeatedObjectsTerrainCfg): + """Configuration for a terrain with repeated pyramids.""" + +
[文档] @configclass + class ObjectCfg(MeshRepeatedObjectsTerrainCfg.ObjectCfg): + """Configuration for a curriculum of repeated pyramids.""" + + radius: float = MISSING + """The radius of the pyramids (in m).""" + max_yx_angle: float = 0.0 + """The maximum angle along the y and x axis. Defaults to 0.0.""" + degrees: bool = True + """Whether the angle is in degrees. Defaults to True."""
+ + object_type = mesh_utils_terrains.make_cone + + object_params_start: ObjectCfg = MISSING + """The object curriculum parameters at the start of the curriculum.""" + object_params_end: ObjectCfg = MISSING + """The object curriculum parameters at the end of the curriculum."""
+ + +
[文档]@configclass +class MeshRepeatedBoxesTerrainCfg(MeshRepeatedObjectsTerrainCfg): + """Configuration for a terrain with repeated boxes.""" + +
[文档] @configclass + class ObjectCfg(MeshRepeatedObjectsTerrainCfg.ObjectCfg): + """Configuration for repeated boxes.""" + + size: tuple[float, float] = MISSING + """The width (along x) and length (along y) of the box (in m).""" + max_yx_angle: float = 0.0 + """The maximum angle along the y and x axis. Defaults to 0.0.""" + degrees: bool = True + """Whether the angle is in degrees. Defaults to True."""
+ + object_type = mesh_utils_terrains.make_box + + object_params_start: ObjectCfg = MISSING + """The box curriculum parameters at the start of the curriculum.""" + object_params_end: ObjectCfg = MISSING + """The box curriculum parameters at the end of the curriculum."""
+ + +
[文档]@configclass +class MeshRepeatedCylindersTerrainCfg(MeshRepeatedObjectsTerrainCfg): + """Configuration for a terrain with repeated cylinders.""" + +
[文档] @configclass + class ObjectCfg(MeshRepeatedObjectsTerrainCfg.ObjectCfg): + """Configuration for repeated cylinder.""" + + radius: float = MISSING + """The radius of the pyramids (in m).""" + max_yx_angle: float = 0.0 + """The maximum angle along the y and x axis. Defaults to 0.0.""" + degrees: bool = True + """Whether the angle is in degrees. Defaults to True."""
+ + object_type = mesh_utils_terrains.make_cylinder + + object_params_start: ObjectCfg = MISSING + """The box curriculum parameters at the start of the curriculum.""" + object_params_end: ObjectCfg = MISSING + """The box curriculum parameters at the end of the curriculum."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/terrains/utils.html b/_modules/omni/isaac/lab/terrains/utils.html new file mode 100644 index 0000000000..b7bbd16120 --- /dev/null +++ b/_modules/omni/isaac/lab/terrains/utils.html @@ -0,0 +1,834 @@ + + + + + + + + + + + omni.isaac.lab.terrains.utils — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.terrains.utils 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# needed to import for allowing type-hinting: np.ndarray | torch.Tensor | None
+from __future__ import annotations
+
+import numpy as np
+import torch
+import trimesh
+
+import warp as wp
+
+from omni.isaac.lab.utils.warp import raycast_mesh
+
+
+
[文档]def color_meshes_by_height(meshes: list[trimesh.Trimesh], **kwargs) -> trimesh.Trimesh: + """ + Color the vertices of a trimesh object based on the z-coordinate (height) of each vertex, + using the Turbo colormap. If the z-coordinates are all the same, the vertices will be colored + with a single color. + + Args: + meshes: A list of trimesh objects. + + Keyword Args: + color: A list of 3 integers in the range [0,255] representing the RGB + color of the mesh. Used when the z-coordinates of all vertices are the same. + Defaults to [172, 216, 230]. + color_map: The name of the color map to be used. Defaults to "turbo". + + Returns: + A trimesh object with the vertices colored based on the z-coordinate (height) of each vertex. + """ + # Combine all meshes into a single mesh + mesh = trimesh.util.concatenate(meshes) + # Get the z-coordinates of each vertex + heights = mesh.vertices[:, 2] + # Check if the z-coordinates are all the same + if np.max(heights) == np.min(heights): + # Obtain a single color: light blue + color = kwargs.pop("color", (172, 216, 230)) + color = np.asarray(color, dtype=np.uint8) + # Set the color for all vertices + mesh.visual.vertex_colors = color + else: + # Normalize the heights to [0,1] + heights_normalized = (heights - np.min(heights)) / (np.max(heights) - np.min(heights)) + # clip lower and upper bounds to have better color mapping + heights_normalized = np.clip(heights_normalized, 0.1, 0.9) + # Get the color for each vertex based on the height + color_map = kwargs.pop("color_map", "turbo") + colors = trimesh.visual.color.interpolate(heights_normalized, color_map=color_map) + # Set the vertex colors + mesh.visual.vertex_colors = colors + # Return the mesh + return mesh
+ + +
[文档]def create_prim_from_mesh(prim_path: str, mesh: trimesh.Trimesh, **kwargs): + """Create a USD prim with mesh defined from vertices and triangles. + + The function creates a USD prim with a mesh defined from vertices and triangles. It performs the + following steps: + + - Create a USD Xform prim at the path :obj:`prim_path`. + - Create a USD prim with a mesh defined from the input vertices and triangles at the path :obj:`{prim_path}/mesh`. + - Assign a physics material to the mesh at the path :obj:`{prim_path}/physicsMaterial`. + - Assign a visual material to the mesh at the path :obj:`{prim_path}/visualMaterial`. + + Args: + prim_path: The path to the primitive to be created. + mesh: The mesh to be used for the primitive. + + Keyword Args: + translation: The translation of the terrain. Defaults to None. + orientation: The orientation of the terrain. Defaults to None. + visual_material: The visual material to apply. Defaults to None. + physics_material: The physics material to apply. Defaults to None. + """ + # need to import these here to prevent isaacsim launching when importing this module + import omni.isaac.core.utils.prims as prim_utils + from pxr import UsdGeom + + import omni.isaac.lab.sim as sim_utils + + # create parent prim + prim_utils.create_prim(prim_path, "Xform") + # create mesh prim + prim = prim_utils.create_prim( + f"{prim_path}/mesh", + "Mesh", + translation=kwargs.get("translation"), + orientation=kwargs.get("orientation"), + attributes={ + "points": mesh.vertices, + "faceVertexIndices": mesh.faces.flatten(), + "faceVertexCounts": np.asarray([3] * len(mesh.faces)), + "subdivisionScheme": "bilinear", + }, + ) + # apply collider properties + collider_cfg = sim_utils.CollisionPropertiesCfg(collision_enabled=True) + sim_utils.define_collision_properties(prim.GetPrimPath(), collider_cfg) + # add rgba color to the mesh primvars + if mesh.visual.vertex_colors is not None: + # obtain color from the mesh + rgba_colors = np.asarray(mesh.visual.vertex_colors).astype(np.float32) / 255.0 + # displayColor is a primvar attribute that is used to color the mesh + color_prim_attr = prim.GetAttribute("primvars:displayColor") + color_prim_var = UsdGeom.Primvar(color_prim_attr) + color_prim_var.SetInterpolation(UsdGeom.Tokens.vertex) + color_prim_attr.Set(rgba_colors[:, :3]) + # displayOpacity is a primvar attribute that is used to set the opacity of the mesh + display_prim_attr = prim.GetAttribute("primvars:displayOpacity") + display_prim_var = UsdGeom.Primvar(display_prim_attr) + display_prim_var.SetInterpolation(UsdGeom.Tokens.vertex) + display_prim_var.Set(rgba_colors[:, 3]) + + # create visual material + if kwargs.get("visual_material") is not None: + visual_material_cfg: sim_utils.VisualMaterialCfg = kwargs.get("visual_material") + # spawn the material + visual_material_cfg.func(f"{prim_path}/visualMaterial", visual_material_cfg) + sim_utils.bind_visual_material(prim.GetPrimPath(), f"{prim_path}/visualMaterial") + # create physics material + if kwargs.get("physics_material") is not None: + physics_material_cfg: sim_utils.RigidBodyMaterialCfg = kwargs.get("physics_material") + # spawn the material + physics_material_cfg.func(f"{prim_path}/physicsMaterial", physics_material_cfg) + sim_utils.bind_physics_material(prim.GetPrimPath(), f"{prim_path}/physicsMaterial")
+ + +
[文档]def find_flat_patches( + wp_mesh: wp.Mesh, + num_patches: int, + patch_radius: float | list[float], + origin: np.ndarray | torch.Tensor | tuple[float, float, float], + x_range: tuple[float, float], + y_range: tuple[float, float], + z_range: tuple[float, float], + max_height_diff: float, +) -> torch.Tensor: + """Finds flat patches of given radius in the input mesh. + + The function finds flat patches of given radius based on the search space defined by the input ranges. + The search space is characterized by origin in the mesh frame, and the x, y, and z ranges. The x and y + ranges are used to sample points in the 2D region around the origin, and the z range is used to filter + patches based on the height of the points. + + The function performs rejection sampling to find the patches based on the following steps: + + 1. Sample patch locations in the 2D region around the origin. + 2. Define a ring of points around each patch location to query the height of the points using ray-casting. + 3. Reject patches that are outside the z range or have a height difference that is too large. + 4. Keep sampling until all patches are valid. + + Args: + wp_mesh: The warp mesh to find patches in. + num_patches: The desired number of patches to find. + patch_radius: The radii used to form patches. If a list is provided, multiple patch sizes are checked. + This is useful to deal with holes or other artifacts in the mesh. + origin: The origin defining the center of the search space. This is specified in the mesh frame. + x_range: The range of X coordinates to sample from. + y_range: The range of Y coordinates to sample from. + z_range: The range of valid Z coordinates used for filtering patches. + max_height_diff: The maximum allowable distance between the lowest and highest points + on a patch to consider it as valid. If the difference is greater than this value, + the patch is rejected. + + Returns: + A tensor of shape (num_patches, 3) containing the flat patches. The patches are defined in the mesh frame. + + Raises: + RuntimeError: If the function fails to find valid patches. This can happen if the input parameters + are not suitable for finding valid patches and maximum number of iterations is reached. + """ + # set device to warp mesh device + device = wp.device_to_torch(wp_mesh.device) + + # resolve inputs to consistent type + # -- patch radii + if isinstance(patch_radius, float): + patch_radius = [patch_radius] + # -- origin + if isinstance(origin, np.ndarray): + origin = torch.from_numpy(origin).to(torch.float).to(device) + elif isinstance(origin, torch.Tensor): + origin = origin.to(device) + else: + origin = torch.tensor(origin, dtype=torch.float, device=device) + + # create ranges for the x and y coordinates around the origin. + # The provided ranges are bounded by the mesh's bounding box. + x_range = ( + max(x_range[0] + origin[0].item(), wp_mesh.points.numpy()[:, 0].min()), + min(x_range[1] + origin[0].item(), wp_mesh.points.numpy()[:, 0].max()), + ) + y_range = ( + max(y_range[0] + origin[1].item(), wp_mesh.points.numpy()[:, 1].min()), + min(y_range[1] + origin[1].item(), wp_mesh.points.numpy()[:, 1].max()), + ) + z_range = ( + z_range[0] + origin[2].item(), + z_range[1] + origin[2].item(), + ) + + # create a circle of points around (0, 0) to query validity of the patches + # the ring of points is uniformly distributed around the circle + angle = torch.linspace(0, 2 * np.pi, 10, device=device) + query_x = [] + query_y = [] + for radius in patch_radius: + query_x.append(radius * torch.cos(angle)) + query_y.append(radius * torch.sin(angle)) + query_x = torch.cat(query_x).unsqueeze(1) # dim: (num_radii * 10, 1) + query_y = torch.cat(query_y).unsqueeze(1) # dim: (num_radii * 10, 1) + # dim: (num_radii * 10, 3) + query_points = torch.cat([query_x, query_y, torch.zeros_like(query_x)], dim=-1) + + # create buffers + # -- a buffer to store indices of points that are not valid + points_ids = torch.arange(num_patches, device=device) + # -- a buffer to store the flat patches locations + flat_patches = torch.zeros(num_patches, 3, device=device) + + # sample points and raycast to find the height. + # 1. Reject points that are outside the z_range or have a height difference that is too large. + # 2. Keep sampling until all points are valid. + iter_count = 0 + while len(points_ids) > 0 and iter_count < 10000: + # sample points in the 2D region around the origin + pos_x = torch.empty(len(points_ids), device=device).uniform_(*x_range) + pos_y = torch.empty(len(points_ids), device=device).uniform_(*y_range) + flat_patches[points_ids, :2] = torch.stack([pos_x, pos_y], dim=-1) + + # define the query points to check validity of the patch + # dim: (num_patches, num_radii * 10, 3) + points = flat_patches[points_ids].unsqueeze(1) + query_points + points[..., 2] = 100.0 + # ray-cast direction is downwards + dirs = torch.zeros_like(points) + dirs[..., 2] = -1.0 + + # ray-cast to find the height of the patches + ray_hits = raycast_mesh(points.view(-1, 3), dirs.view(-1, 3), wp_mesh)[0] + heights = ray_hits.view(points.shape)[..., 2] + # set the height of the patches + # note: for invalid patches, they would be overwritten in the next iteration + # so it's safe to set the height to the last value + flat_patches[points_ids, 2] = heights[..., -1] + + # check validity + # -- height is within the z range + not_valid = torch.any(torch.logical_or(heights < z_range[0], heights > z_range[1]), dim=1) + # -- height difference is within the max height difference + not_valid = torch.logical_or(not_valid, (heights.max(dim=1)[0] - heights.min(dim=1)[0]) > max_height_diff) + + # remove invalid patches indices + points_ids = points_ids[not_valid] + # increment count + iter_count += 1 + + # check all patches are valid + if len(points_ids) > 0: + raise RuntimeError( + "Failed to find valid patches! Please check the input parameters." + f"\n\tMaximum number of iterations reached: {iter_count}" + f"\n\tNumber of invalid patches: {len(points_ids)}" + f"\n\tMaximum height difference: {max_height_diff}" + ) + + # return the flat patches (in the mesh frame) + return flat_patches - origin
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/array.html b/_modules/omni/isaac/lab/utils/array.html new file mode 100644 index 0000000000..40da9c3a7d --- /dev/null +++ b/_modules/omni/isaac/lab/utils/array.html @@ -0,0 +1,654 @@ + + + + + + + + + + + omni.isaac.lab.utils.array — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.array 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module containing utilities for working with different array backends."""
+
+# needed to import for allowing type-hinting: torch.device | str | None
+from __future__ import annotations
+
+import numpy as np
+import torch
+from typing import Union
+
+import warp as wp
+
+TensorData = Union[np.ndarray, torch.Tensor, wp.array]
+"""Type definition for a tensor data.
+
+Union of numpy, torch, and warp arrays.
+"""
+
+TENSOR_TYPES = {
+    "numpy": np.ndarray,
+    "torch": torch.Tensor,
+    "warp": wp.array,
+}
+"""A dictionary containing the types for each backend.
+
+The keys are the name of the backend ("numpy", "torch", "warp") and the values are the corresponding type
+(``np.ndarray``, ``torch.Tensor``, ``wp.array``).
+"""
+
+TENSOR_TYPE_CONVERSIONS = {
+    "numpy": {wp.array: lambda x: x.numpy(), torch.Tensor: lambda x: x.detach().cpu().numpy()},
+    "torch": {wp.array: lambda x: wp.torch.to_torch(x), np.ndarray: lambda x: torch.from_numpy(x)},
+    "warp": {np.array: lambda x: wp.array(x), torch.Tensor: lambda x: wp.torch.from_torch(x)},
+}
+"""A nested dictionary containing the conversion functions for each backend.
+
+The keys of the outer dictionary are the name of target backend ("numpy", "torch", "warp"). The keys of the
+inner dictionary are the source backend (``np.ndarray``, ``torch.Tensor``, ``wp.array``).
+"""
+
+
+
[文档]def convert_to_torch( + array: TensorData, + dtype: torch.dtype = None, + device: torch.device | str | None = None, +) -> torch.Tensor: + """Converts a given array into a torch tensor. + + The function tries to convert the array to a torch tensor. If the array is a numpy/warp arrays, or python + list/tuples, it is converted to a torch tensor. If the array is already a torch tensor, it is returned + directly. + + 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: + Since PyTorch does not support unsigned integer types, unsigned integer arrays are converted to + signed integer arrays. This is done by casting the array to the corresponding signed integer type. + + Args: + array: The input array. It can be a numpy array, warp array, python list/tuple, or torch tensor. + dtype: Target data-type for the tensor. + device: The target device for the tensor. Defaults to None. + + Returns: + The converted array as torch tensor. + """ + # Convert array to tensor + # if the datatype is not currently supported by torch we need to improvise + # supported types are: https://pytorch.org/docs/stable/tensors.html + if isinstance(array, torch.Tensor): + tensor = array + elif isinstance(array, np.ndarray): + if array.dtype == np.uint32: + array = array.astype(np.int32) + # need to deal with object arrays (np.void) separately + tensor = torch.from_numpy(array) + elif isinstance(array, wp.array): + if array.dtype == wp.uint32: + array = array.view(wp.int32) + tensor = wp.to_torch(array) + else: + tensor = torch.Tensor(array) + # Convert tensor to the right device + if device is not None and str(tensor.device) != str(device): + tensor = tensor.to(device) + # Convert dtype of tensor if requested + if dtype is not None and tensor.dtype != dtype: + tensor = tensor.type(dtype) + + return tensor
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/assets.html b/_modules/omni/isaac/lab/utils/assets.html new file mode 100644 index 0000000000..3b764fe620 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/assets.html @@ -0,0 +1,687 @@ + + + + + + + + + + + omni.isaac.lab.utils.assets — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.assets 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""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.
+
+For more information, please check information on `Omniverse Nucleus`_.
+
+.. _Omniverse Nucleus: https://docs.omniverse.nvidia.com/nucleus/latest/overview/overview.html
+"""
+
+import io
+import os
+import tempfile
+from typing import Literal
+
+import carb
+import omni.client
+
+NUCLEUS_ASSET_ROOT_DIR = carb.settings.get_settings().get("/persistent/isaac/asset_root/cloud")
+"""Path to the root directory on the Nucleus Server."""
+
+NVIDIA_NUCLEUS_DIR = f"{NUCLEUS_ASSET_ROOT_DIR}/NVIDIA"
+"""Path to the root directory on the NVIDIA Nucleus Server."""
+
+ISAAC_NUCLEUS_DIR = f"{NUCLEUS_ASSET_ROOT_DIR}/Isaac"
+"""Path to the ``Isaac`` directory on the NVIDIA Nucleus Server."""
+
+ISAACLAB_NUCLEUS_DIR = f"{ISAAC_NUCLEUS_DIR}/IsaacLab"
+"""Path to the ``Isaac/IsaacLab`` directory on the NVIDIA Nucleus Server."""
+
+
+
[文档]def check_file_path(path: str) -> Literal[0, 1, 2]: + """Checks if a file exists on the Nucleus Server or locally. + + Args: + path: The path to the file. + + Returns: + The status of the file. Possible values are listed below. + + * :obj:`0` if the file does not exist + * :obj:`1` if the file exists locally + * :obj:`2` if the file exists on the Nucleus Server + """ + if os.path.isfile(path): + return 1 + elif omni.client.stat(path)[0] == omni.client.Result.OK: + return 2 + else: + return 0
+ + +
[文档]def retrieve_file_path(path: str, download_dir: str | None = None, force_download: bool = True) -> str: + """Retrieves the path to a file on the Nucleus Server or locally. + + If the file exists locally, then the absolute path to the file is returned. + If the file exists on the Nucleus Server, then the file is downloaded to the local machine + and the absolute path to the file is returned. + + Args: + path: The path to the file. + download_dir: The directory where the file should be downloaded. Defaults to None, in which + case the file is downloaded to the system's temporary directory. + force_download: Whether to force download the file from the Nucleus Server. This will overwrite + the local file if it exists. Defaults to True. + + Returns: + The path to the file on the local machine. + + Raises: + FileNotFoundError: When the file not found locally or on Nucleus Server. + RuntimeError: When the file cannot be copied from the Nucleus Server to the local machine. This + can happen when the file already exists locally and :attr:`force_download` is set to False. + """ + # check file status + file_status = check_file_path(path) + if file_status == 1: + return os.path.abspath(path) + elif file_status == 2: + # resolve download directory + if download_dir is None: + download_dir = tempfile.gettempdir() + else: + download_dir = os.path.abspath(download_dir) + # create download directory if it does not exist + if not os.path.exists(download_dir): + os.makedirs(download_dir) + # download file in temp directory using os + file_name = os.path.basename(omni.client.break_url(path).path) + target_path = os.path.join(download_dir, file_name) + # check if file already exists locally + if not os.path.isfile(target_path) or force_download: + # copy file to local machine + result = omni.client.copy(path, target_path) + if result != omni.client.Result.OK and force_download: + raise RuntimeError(f"Unable to copy file: '{path}'. Is the Nucleus Server running?") + return os.path.abspath(target_path) + else: + raise FileNotFoundError(f"Unable to find the file: {path}")
+ + +
[文档]def read_file(path: str) -> io.BytesIO: + """Reads a file from the Nucleus Server or locally. + + Args: + path: The path to the file. + + Raises: + FileNotFoundError: When the file not found locally or on Nucleus Server. + + Returns: + The content of the file. + """ + # check file status + file_status = check_file_path(path) + if file_status == 1: + with open(path, "rb") as f: + return io.BytesIO(f.read()) + elif file_status == 2: + file_content = omni.client.read_file(path)[2] + return io.BytesIO(memoryview(file_content).tobytes()) + else: + raise FileNotFoundError(f"Unable to find the file: {path}")
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/buffers/circular_buffer.html b/_modules/omni/isaac/lab/utils/buffers/circular_buffer.html new file mode 100644 index 0000000000..2efb10a301 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/buffers/circular_buffer.html @@ -0,0 +1,708 @@ + + + + + + + + + + + omni.isaac.lab.utils.buffers.circular_buffer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.buffers.circular_buffer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from collections.abc import Sequence
+
+
+
[文档]class CircularBuffer: + """Circular buffer for storing a history of batched tensor data. + + This class implements a circular buffer for storing a history of batched tensor data. The buffer is + initialized with a maximum length and a batch size. The data is stored in a circular fashion, and the + data can be retrieved in a LIFO (Last-In-First-Out) fashion. The buffer is designed to be used in + multi-environment settings, where each environment has its own data. + + The shape of the appended data is expected to be (batch_size, ...), where the first dimension is the + batch dimension. Correspondingly, the shape of the ring buffer is (max_len, batch_size, ...). + """ + +
[文档] def __init__(self, max_len: int, batch_size: int, device: str): + """Initialize the circular buffer. + + Args: + max_len: The maximum length of the circular buffer. The minimum allowed value is 1. + batch_size: The batch dimension of the data. + device: The device used for processing. + + Raises: + ValueError: If the buffer size is less than one. + """ + if max_len < 1: + raise ValueError(f"The buffer size should be greater than zero. However, it is set to {max_len}!") + # set the parameters + self._batch_size = batch_size + self._device = device + self._ALL_INDICES = torch.arange(batch_size, device=device) + + # max length tensor for comparisons + self._max_len = torch.full((batch_size,), max_len, dtype=torch.int, device=device) + # number of data pushes passed since the last call to :meth:`reset` + self._num_pushes = torch.zeros(batch_size, dtype=torch.long, device=device) + # the pointer to the current head of the circular buffer (-1 means not initialized) + self._pointer: int = -1 + # the actual buffer for data storage + # note: this is initialized on the first call to :meth:`append` + self._buffer: torch.Tensor = None # type: ignore
+ + """ + Properties. + """ + + @property + def batch_size(self) -> int: + """The batch size of the ring buffer.""" + return self._batch_size + + @property + def device(self) -> str: + """The device used for processing.""" + return self._device + + @property + def max_length(self) -> int: + """The maximum length of the ring buffer.""" + return int(self._max_len[0].item()) + + @property + def current_length(self) -> torch.Tensor: + """The current length of the buffer. Shape is (batch_size,). + + Since the buffer is circular, the current length is the minimum of the number of pushes + and the maximum length. + """ + return torch.minimum(self._num_pushes, self._max_len) + + """ + Operations. + """ + +
[文档] def reset(self, batch_ids: Sequence[int] | None = None): + """Reset the circular buffer at the specified batch indices. + + Args: + batch_ids: Elements to reset in the batch dimension. Default is None, which resets all the batch indices. + """ + # resolve all indices + if batch_ids is None: + batch_ids = slice(None) + # reset the number of pushes for the specified batch indices + # note: we don't need to reset the buffer since it will be overwritten. The pointer handles this. + self._num_pushes[batch_ids] = 0
+ +
[文档] def append(self, data: torch.Tensor): + """Append the data to the circular buffer. + + Args: + data: The data to append to the circular buffer. The first dimension should be the batch dimension. + Shape is (batch_size, ...). + + Raises: + ValueError: If the input data has a different batch size than the buffer. + """ + # check the batch size + if data.shape[0] != self.batch_size: + raise ValueError(f"The input data has {data.shape[0]} environments while expecting {self.batch_size}") + + # at the fist call, initialize the buffer + if self._buffer is None: + self._pointer = -1 + self._buffer = torch.empty((self.max_length, *data.shape), dtype=data.dtype, device=self._device) + # move the head to the next slot + self._pointer = (self._pointer + 1) % self.max_length + # add the new data to the last layer + self._buffer[self._pointer] = data.to(self._device) + # increment number of number of pushes + self._num_pushes += 1
+ + def __getitem__(self, key: torch.Tensor) -> torch.Tensor: + """Retrieve the data from the circular buffer in last-in-first-out (LIFO) fashion. + + If the requested index is larger than the number of pushes since the last call to :meth:`reset`, + the oldest stored data is returned. + + Args: + key: The index to retrieve from the circular buffer. The index should be less than the number of pushes + since the last call to :meth:`reset`. Shape is (batch_size,). + + Returns: + The data from the circular buffer. Shape is (batch_size, ...). + + Raises: + ValueError: If the input key has a different batch size than the buffer. + RuntimeError: If the buffer is empty. + """ + # check the batch size + if len(key) != self.batch_size: + raise ValueError(f"The argument 'key' has length {key.shape[0]}, while expecting {self.batch_size}") + # check if the buffer is empty + if torch.any(self._num_pushes == 0) or self._buffer is None: + raise RuntimeError("Attempting to retrieve data on an empty circular buffer. Please append data first.") + + # admissible lag + valid_keys = torch.minimum(key, self._num_pushes - 1) + # the index in the circular buffer (pointer points to the last+1 index) + index_in_buffer = torch.remainder(self._pointer - valid_keys, self.max_length) + # return output + return self._buffer[index_in_buffer, self._ALL_INDICES]
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/buffers/delay_buffer.html b/_modules/omni/isaac/lab/utils/buffers/delay_buffer.html new file mode 100644 index 0000000000..83f19597d2 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/buffers/delay_buffer.html @@ -0,0 +1,736 @@ + + + + + + + + + + + omni.isaac.lab.utils.buffers.delay_buffer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.buffers.delay_buffer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+# needed because we concatenate int and torch.Tensor in the type hints
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+
+from .circular_buffer import CircularBuffer
+
+
+
[文档]class DelayBuffer: + """Delay buffer that allows retrieving stored data with delays. + + This class uses a batched circular buffer to store input data. Different to a standard circular buffer, + which uses the LIFO (last-in-first-out) principle to retrieve the data, the delay buffer class allows + retrieving data based on the lag set by the user. For instance, if the delay set inside the buffer + is 1, then the second last entry from the stream is retrieved. If it is 2, then the third last entry + and so on. + + The class supports storing a batched tensor data. This means that the shape of the appended data + is expected to be (batch_size, ...), where the first dimension is the batch dimension. Correspondingly, + the delay can be set separately for each batch index. If the requested delay is larger than the current + length of the underlying buffer, the most recent entry is returned. + + .. note:: + By default, the delay buffer has no delay, meaning that the data is returned as is. + """ + +
[文档] def __init__(self, history_length: int, batch_size: int, device: str): + """Initialize the delay buffer. + + Args: + history_length: The history of the buffer, i.e., the number of time steps in the past that the data + will be buffered. It is recommended to set this value equal to the maximum time-step lag that + is expected. The minimum acceptable value is zero, which means only the latest data is stored. + batch_size: The batch dimension of the data. + device: The device used for processing. + """ + # set the parameters + self._history_length = max(0, history_length) + + # the buffer size: current data plus the history length + self._circular_buffer = CircularBuffer(self._history_length + 1, batch_size, device) + + # the minimum and maximum lags across all environments. + self._min_time_lag = 0 + self._max_time_lag = 0 + # the lags for each environment. + self._time_lags = torch.zeros(batch_size, dtype=torch.int, device=device)
+ + """ + Properties. + """ + + @property + def batch_size(self) -> int: + """The batch size of the ring buffer.""" + return self._circular_buffer.batch_size + + @property + def device(self) -> str: + """The device used for processing.""" + return self._circular_buffer.device + + @property + def history_length(self) -> int: + """The history length of the delay buffer. + + If zero, only the latest data is stored. If one, the latest and the previous data are stored, and so on. + """ + return self._history_length + + @property + def min_time_lag(self) -> int: + """Minimum amount of time steps that can be delayed. + + This value cannot be negative or larger than :attr:`max_time_lag`. + """ + return self._min_time_lag + + @property + def max_time_lag(self) -> int: + """Maximum amount of time steps that can be delayed. + + This value cannot be greater than :attr:`history_length`. + """ + return self._max_time_lag + + @property + def time_lags(self) -> torch.Tensor: + """The time lag across each batch index. + + The shape of the tensor is (batch_size, ). The value at each index represents the delay for that index. + This value is used to retrieve the data from the buffer. + """ + return self._time_lags + + """ + Operations. + """ + +
[文档] def set_time_lag(self, time_lag: int | torch.Tensor, batch_ids: Sequence[int] | None = None): + """Sets the time lag for the delay buffer across the provided batch indices. + + Args: + time_lag: The desired delay for the buffer. + + * If an integer is provided, the same delay is set for the provided batch indices. + * If a tensor is provided, the delay is set for each batch index separately. The shape of the tensor + should be (len(batch_ids),). + + batch_ids: The batch indices for which the time lag is set. Default is None, which sets the time lag + for all batch indices. + + Raises: + TypeError: If the type of the :attr:`time_lag` is not int or integer tensor. + ValueError: If the minimum time lag is negative or the maximum time lag is larger than the history length. + """ + # resolve batch indices + if batch_ids is None: + batch_ids = slice(None) + + # parse requested time_lag + if isinstance(time_lag, int): + # set the time lags across provided batch indices + self._time_lags[batch_ids] = time_lag + elif isinstance(time_lag, torch.Tensor): + # check valid dtype for time_lag: must be int or long + if time_lag.dtype not in [torch.int, torch.long]: + raise TypeError(f"Invalid dtype for time_lag: {time_lag.dtype}. Expected torch.int or torch.long.") + # set the time lags + self._time_lags[batch_ids] = time_lag.to(device=self.device) + else: + raise TypeError(f"Invalid type for time_lag: {type(time_lag)}. Expected int or integer tensor.") + + # compute the min and max time lag + self._min_time_lag = int(torch.min(self._time_lags).item()) + self._max_time_lag = int(torch.max(self._time_lags).item()) + # check that time_lag is feasible + if self._min_time_lag < 0: + raise ValueError(f"The minimum time lag cannot be negative. Received: {self._min_time_lag}") + if self._max_time_lag > self._history_length: + raise ValueError( + f"The maximum time lag cannot be larger than the history length. Received: {self._max_time_lag}" + )
+ +
[文档] def reset(self, batch_ids: Sequence[int] | None = None): + """Reset the data in the delay buffer at the specified batch indices. + + Args: + batch_ids: Elements to reset in the batch dimension. Default is None, which resets all the batch indices. + """ + self._circular_buffer.reset(batch_ids)
+ +
[文档] def compute(self, data: torch.Tensor) -> torch.Tensor: + """Append the input data to the buffer and returns a stale version of the data based on time lag delay. + + If the requested delay is larger than the number of buffered data points since the last reset, + the function returns the latest data. For instance, if the delay is set to 2 and only one data point + is stored in the buffer, the function will return the latest data. If the delay is set to 2 and three + data points are stored, the function will return the first data point. + + Args: + data: The input data. Shape is (batch_size, ...). + + Returns: + The delayed version of the data from the stored buffer. Shape is (batch_size, ...). + """ + # add the new data to the last layer + self._circular_buffer.append(data) + # return output + delayed_data = self._circular_buffer[self._time_lags] + return delayed_data.clone()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/buffers/timestamped_buffer.html b/_modules/omni/isaac/lab/utils/buffers/timestamped_buffer.html new file mode 100644 index 0000000000..e2a09fba87 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/buffers/timestamped_buffer.html @@ -0,0 +1,587 @@ + + + + + + + + + + + omni.isaac.lab.utils.buffers.timestamped_buffer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.buffers.timestamped_buffer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from dataclasses import dataclass
+
+
+
[文档]@dataclass +class TimestampedBuffer: + """A buffer class containing data and its timestamp. + + This class is a simple data container that stores a tensor and its timestamp. The timestamp is used to + track the last update of the buffer. The timestamp is set to -1.0 by default, indicating that the buffer + has not been updated yet. The timestamp should be updated whenever the data in the buffer is updated. This + way the buffer can be used to check whether the data is outdated and needs to be refreshed. + + The buffer is useful for creating lazy buffers that only update the data when it is outdated. This can be + useful when the data is expensive to compute or retrieve. For example usage, refer to the data classes in + the :mod:`omni.isaac.lab.assets` module. + """ + + data: torch.Tensor = None # type: ignore + """The data stored in the buffer. Default is None, indicating that the buffer is empty.""" + + timestamp: float = -1.0 + """Timestamp at the last update of the buffer. Default is -1.0, indicating that the buffer has not been updated."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/configclass.html b/_modules/omni/isaac/lab/utils/configclass.html new file mode 100644 index 0000000000..654b2cc5f2 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/configclass.html @@ -0,0 +1,1046 @@ + + + + + + + + + + + omni.isaac.lab.utils.configclass — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.configclass 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module that provides a wrapper around the Python 3.7 onwards ``dataclasses`` module."""
+
+import inspect
+import types
+from collections.abc import Callable
+from copy import deepcopy
+from dataclasses import MISSING, Field, dataclass, field, replace
+from typing import Any, ClassVar
+
+from .dict import class_to_dict, update_class_from_dict
+
+_CONFIGCLASS_METHODS = ["to_dict", "from_dict", "replace", "copy", "validate"]
+"""List of class methods added at runtime to dataclass."""
+
+"""
+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 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 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 + + from omni.isaac.lab.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 as a dictionary + print(env_cfg.to_dict()) + + # create a copy of the configuration + env_cfg_copy = env_cfg.copy() + + # 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) + # add field factory + _process_mutable_types(cls) + # copy mutable members + # note: we check if user defined __post_init__ function exists and augment it with our own + if hasattr(cls, "__post_init__"): + setattr(cls, "__post_init__", _combined_function(cls.__post_init__, _custom_post_init)) + else: + 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) + setattr(cls, "replace", _replace_class_with_kwargs) + setattr(cls, "copy", _copy_class) + setattr(cls, "validate", _validate) + # 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. + + Args: + obj: The object to convert. + + 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: + obj: The object to update. + 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. + """ + update_class_from_dict(obj, data, _ns="") + + +def _replace_class_with_kwargs(obj: object, **kwargs) -> object: + """Return a new object replacing specified fields with new values. + + This is especially useful for frozen classes. Example usage: + + .. 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 + + Args: + obj: The object to replace. + **kwargs: The fields to replace and their new values. + + Returns: + The new object. + """ + return replace(obj, **kwargs) + + +def _copy_class(obj: object) -> object: + """Return a new object with the same fields as the original.""" + return replace(obj) + + +""" +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 + """ + # get type hints + hints = {} + # iterate over class inheritance + # we add annotations from base classes first + for base in reversed(cls.__mro__): + # check if base is object + if base is object: + continue + # get base class annotations + ann = base.__dict__.get("__annotations__", {}) + # directly add all annotations from base class + hints.update(ann) + # iterate over base class members + # Note: Do not change this to dir(base) since it orders the members alphabetically. + # This is not desirable since the order of the members is important in some cases. + for key in base.__dict__: + # get class member + value = getattr(base, key) + # skip members + if _skippable_class_member(key, value, hints): + continue + # add type annotations for members that don't have explicit type annotations + # for these, we deduce the type from the default value + if not isinstance(value, type): + if key not in hints: + # check if var type is not MISSING + # we cannot deduce type from MISSING! + if value is MISSING: + raise TypeError( + f"Missing type annotation for '{key}' in class '{cls.__name__}'." + " Please add a type annotation or set a default value." + ) + # add type annotation + hints[key] = type(value) + elif key != value.__name__: + # note: we don't want to add type annotations for nested configclass. Thus, we check if + # the name of the type matches the name of the variable. + # since Python 3.10, type hints are stored as strings + hints[key] = f"type[{value.__name__}]" + + # 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__ = hints + + +def _validate(obj: object, prefix: str = "") -> list[str]: + """Check the validity of configclass object. + + This function checks if the object is a valid configclass object. A valid configclass object contains no MISSING + entries. + + Args: + obj: The object to check. + prefix: The prefix to add to the missing fields. Defaults to ''. + + Returns: + A list of missing fields. + + Raises: + TypeError: When the object is not a valid configuration object. + """ + missing_fields = [] + + if type(obj) is type(MISSING): + missing_fields.append(prefix) + return missing_fields + elif isinstance(obj, (list, tuple)): + for index, item in enumerate(obj): + current_path = f"{prefix}[{index}]" + missing_fields.extend(_validate(item, prefix=current_path)) + return missing_fields + elif isinstance(obj, dict): + obj_dict = obj + elif hasattr(obj, "__dict__"): + obj_dict = obj.__dict__ + else: + return missing_fields + + for key, value in obj_dict.items(): + # disregard builtin attributes + if key.startswith("__"): + continue + current_path = f"{prefix}.{key}" if prefix else key + missing_fields.extend(_validate(value, prefix=current_path)) + + # raise an error only once at the top-level call + if prefix == "" and missing_fields: + formatted_message = "\n".join(f" - {field}" for field in missing_fields) + raise TypeError( + f"Missing values detected in object {obj.__class__.__name__} for the following" + f" fields:\n{formatted_message}\n" + ) + return missing_fields + + +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 <class 'list'> for field pos is not allowed: use default_factory + """ + # note: Need to set this up in the same order as annotations. Otherwise, it + # complains about missing positional arguments. + ann = cls.__dict__.get("__annotations__", {}) + + # iterate over all class members and store them in a dictionary + class_members = {} + for base in reversed(cls.__mro__): + # check if base is object + if base is object: + continue + # iterate over base class members + for key in base.__dict__: + # get class member + f = getattr(base, key) + # skip members + if _skippable_class_member(key, f): + continue + # store class member if it is not a type or if it is already present in annotations + if not isinstance(f, type) or key in ann: + class_members[key] = f + # iterate over base class data fields + # in previous call, things that became a dataclass field were removed from class members + # so we need to add them back here as a dataclass field directly + for key, f in base.__dict__.get("__dataclass_fields__", {}).items(): + # store class member + if not isinstance(f, type): + class_members[key] = f + + # check that all annotations are present in class members + # note: mainly for debugging purposes + if len(class_members) != len(ann): + raise ValueError( + f"In class '{cls.__name__}', number of annotations ({len(ann)}) does not match number of class members" + f" ({len(class_members)}). Please check that all class members have type annotations and/or a default" + " value. If you don't want to specify a default value, please use the literal `dataclasses.MISSING`." + ) + # iterate over annotations and add field factory for mutable types + for key in ann: + # find matching field in class + value = class_members.get(key, MISSING) + # check if key belongs to ClassVar + # in that case, we cannot use default_factory! + origin = getattr(ann[key], "__origin__", None) + if origin is ClassVar: + continue + # check if f is MISSING + # note: commented out for now since it causes issue with inheritance + # of dataclasses when parent have some positional and some keyword arguments. + # Ref: https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses + # TODO: check if this is fixed in Python 3.10 + # if f is MISSING: + # continue + if isinstance(value, Field): + setattr(cls, key, value) + elif not isinstance(value, type): + # create field factory for mutable types + value = field(default_factory=_return_f(value)) + setattr(cls, key, value) + + +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 + # get data member + value = getattr(obj, key) + # check annotation + ann = obj.__class__.__dict__.get(key) + # duplicate data members that are mutable + if not callable(value) and not isinstance(ann, property): + setattr(obj, key, deepcopy(value)) + + +def _combined_function(f1: Callable, f2: Callable) -> Callable: + """Combine two functions into one. + + Args: + f1: The first function. + f2: The second function. + + Returns: + The combined function. + """ + + def _combined(*args, **kwargs): + # call both functions + f1(*args, **kwargs) + f2(*args, **kwargs) + + return _combined + + +""" +Helper functions +""" + + +def _skippable_class_member(key: str, value: Any, hints: dict | None = None) -> bool: + """Check if the class member should be skipped in configclass processing. + + The following members are skipped: + + * Dunder members: ``__name__``, ``__module__``, ``__qualname__``, ``__annotations__``, ``__dict__``. + * Manually-added special class functions: From :obj:`_CONFIGCLASS_METHODS`. + * Members that are already present in the type annotations. + * Functions bounded to class object or class. + * Properties bounded to class object. + + Args: + key: The class member name. + value: The class member value. + hints: The type hints for the class. Defaults to None, in which case, the + members existence in type hints are not checked. + + Returns: + True if the class member should be skipped, False otherwise. + """ + # skip dunder members + if key.startswith("__"): + return True + # skip manually-added special class functions + if key in _CONFIGCLASS_METHODS: + return True + # check if key is already present + if hints is not None and key in hints: + return True + # skip functions bounded to class + if callable(value): + # FIXME: This doesn't yet work for static methods because they are essentially seen as function types. + # check for class methods + if isinstance(value, types.MethodType): + return True + # check for instance methods + signature = inspect.signature(value) + if "self" in signature.parameters or "cls" in signature.parameters: + return True + # skip property methods + if isinstance(value, property): + return True + # Otherwise, don't skip + return False + + +def _return_f(f: Any) -> Callable[[], Any]: + """Returns default factory function for creating mutable/immutable variables. + + This function should be used to create default factory functions for variables. + + Example: + + .. code-block:: python + + value = field(default_factory=_return_f(value)) + setattr(cls, key, value) + """ + + def _wrap(): + if isinstance(f, Field): + if f.default_factory is MISSING: + return deepcopy(f.default) + else: + return f.default_factory + else: + return deepcopy(f) + + return _wrap +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/dict.html b/_modules/omni/isaac/lab/utils/dict.html new file mode 100644 index 0000000000..265055dd04 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/dict.html @@ -0,0 +1,860 @@ + + + + + + + + + + + omni.isaac.lab.utils.dict — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.dict 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module for utilities for working with dictionaries."""
+
+import collections.abc
+import hashlib
+import json
+from collections.abc import Iterable, Mapping
+from typing import Any
+
+from .array import TENSOR_TYPE_CONVERSIONS, TENSOR_TYPES
+from .string import callable_to_string, string_to_callable, string_to_slice
+
+"""
+Dictionary <-> Class operations.
+"""
+
+
+
[文档]def class_to_dict(obj: object) -> dict[str, Any]: + """Convert an object into dictionary recursively. + + Note: + Ignores all names starting with "__" (i.e. built-in methods). + + Args: + obj: An instance of a class to convert. + + Raises: + ValueError: When input argument is not an object. + + Returns: + Converted dictionary mapping. + """ + # check that input data is class instance + if not hasattr(obj, "__class__"): + raise ValueError(f"Expected a class instance. Received: {type(obj)}.") + # convert object to dictionary + if isinstance(obj, dict): + obj_dict = obj + elif hasattr(obj, "__dict__"): + obj_dict = obj.__dict__ + else: + return obj + + # convert to dictionary + data = dict() + for key, value in obj_dict.items(): + # disregard builtin attributes + if key.startswith("__"): + continue + # check if attribute is callable -- function + if callable(value): + data[key] = callable_to_string(value) + # check if attribute is a dictionary + elif hasattr(value, "__dict__") or isinstance(value, dict): + data[key] = class_to_dict(value) + elif isinstance(value, (list, tuple)): + data[key] = type(value)([class_to_dict(v) for v in value]) + else: + data[key] = value + return data
+ + +
[文档]def update_class_from_dict(obj, data: dict[str, Any], _ns: str = "") -> None: + """Reads a dictionary and sets object variables recursively. + + This function performs in-place update of the class member attributes. + + Args: + obj: An instance of a class to update. + data: Input dictionary to update from. + _ns: Namespace of the current object. This is useful for nested configuration + classes or dictionaries. Defaults to "". + + 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. + """ + for key, value in data.items(): + # key_ns is the full namespace of the key + key_ns = _ns + "/" + key + # check if key is present in the object + if hasattr(obj, key) or isinstance(obj, dict): + obj_mem = obj[key] if isinstance(obj, dict) else getattr(obj, key) + if isinstance(value, Mapping): + # recursively call if it is a dictionary + update_class_from_dict(obj_mem, value, _ns=key_ns) + continue + if isinstance(value, Iterable) and not isinstance(value, str): + # check length of value to be safe + if len(obj_mem) != len(value) and obj_mem is not None: + raise ValueError( + f"[Config]: Incorrect length under namespace: {key_ns}." + f" Expected: {len(obj_mem)}, Received: {len(value)}." + ) + if isinstance(obj_mem, tuple): + value = tuple(value) + else: + set_obj = True + # recursively call if iterable contains dictionaries + for i in range(len(obj_mem)): + if isinstance(value[i], dict): + update_class_from_dict(obj_mem[i], value[i], _ns=key_ns) + set_obj = False + # do not set value to obj, otherwise it overwrites the cfg class with the dict + if not set_obj: + continue + elif callable(obj_mem): + # update function name + value = string_to_callable(value) + elif isinstance(value, type(obj_mem)) or value is None: + pass + else: + raise ValueError( + f"[Config]: Incorrect type under namespace: {key_ns}." + f" Expected: {type(obj_mem)}, Received: {type(value)}." + ) + # set value + if isinstance(obj, dict): + obj[key] = value + else: + setattr(obj, key, value) + else: + raise KeyError(f"[Config]: Key not found under namespace: {key_ns}.")
+ + +""" +Dictionary <-> Hashable operations. +""" + + +
[文档]def dict_to_md5_hash(data: object) -> str: + """Convert a dictionary into a hashable key using MD5 hash. + + Args: + data: Input dictionary or configuration object to convert. + + Returns: + A string object of double length containing only hexadecimal digits. + """ + # convert to dictionary + if isinstance(data, dict): + encoded_buffer = json.dumps(data, sort_keys=True).encode() + else: + encoded_buffer = json.dumps(class_to_dict(data), sort_keys=True).encode() + # compute hash using MD5 + data_hash = hashlib.md5() + data_hash.update(encoded_buffer) + # return the hash key + return data_hash.hexdigest()
+ + +""" +Dictionary operations. +""" + + +
[文档]def convert_dict_to_backend( + data: dict, backend: str = "numpy", array_types: Iterable[str] = ("numpy", "torch", "warp") +) -> dict: + """Convert all arrays or tensors in a dictionary to a given backend. + + This function iterates over the dictionary, converts all arrays or tensors with the given types to + the desired backend, and stores them in a new dictionary. It also works with nested dictionaries. + + Currently supported backends are "numpy", "torch", and "warp". + + Note: + This function only converts arrays or tensors. Other types of data are left unchanged. Mutable types + (e.g. lists) are referenced by the new dictionary, so they are not copied. + + Args: + data: An input dict containing array or tensor data as values. + backend: The backend ("numpy", "torch", "warp") to which arrays in this dict should be converted. + Defaults to "numpy". + array_types: A list containing the types of arrays that should be converted to + the desired backend. Defaults to ("numpy", "torch", "warp"). + + Raises: + ValueError: If the specified ``backend`` or ``array_types`` are unknown, i.e. not in the list of supported + backends ("numpy", "torch", "warp"). + + Returns: + The updated dict with the data converted to the desired backend. + """ + # THINK: Should we also support converting to a specific device, e.g. "cuda:0"? + # Check the backend is valid. + if backend not in TENSOR_TYPE_CONVERSIONS: + raise ValueError(f"Unknown backend '{backend}'. Supported backends are 'numpy', 'torch', and 'warp'.") + # Define the conversion functions for each backend. + tensor_type_conversions = TENSOR_TYPE_CONVERSIONS[backend] + + # Parse the array types and convert them to the corresponding types: "numpy" -> np.ndarray, etc. + parsed_types = list() + for t in array_types: + # Check type is valid. + if t not in TENSOR_TYPES: + raise ValueError(f"Unknown array type: '{t}'. Supported array types are 'numpy', 'torch', and 'warp'.") + # Exclude types that match the backend, since we do not need to convert these. + if t == backend: + continue + # Convert the string types to the corresponding types. + parsed_types.append(TENSOR_TYPES[t]) + + # Convert the data to the desired backend. + output_dict = dict() + for key, value in data.items(): + # Obtain the data type of the current value. + data_type = type(value) + # -- arrays + if data_type in parsed_types: + # check if we have a known conversion. + if data_type not in tensor_type_conversions: + raise ValueError(f"No registered conversion for data type: {data_type} to {backend}!") + # convert the data to the desired backend. + output_dict[key] = tensor_type_conversions[data_type](value) + # -- nested dictionaries + elif isinstance(data[key], dict): + output_dict[key] = convert_dict_to_backend(value) + # -- everything else + else: + output_dict[key] = value + + return output_dict
+ + +
[文档]def update_dict(orig_dict: dict, new_dict: collections.abc.Mapping) -> dict: + """Updates existing dictionary with values from a new dictionary. + + This function mimics the dict.update() function. However, it works for + nested dictionaries as well. + + Args: + orig_dict: The original dictionary to insert items to. + new_dict: The new dictionary to insert items from. + + Returns: + The updated dictionary. + """ + for keyname, value in new_dict.items(): + if isinstance(value, collections.abc.Mapping): + orig_dict[keyname] = update_dict(orig_dict.get(keyname, {}), value) + else: + orig_dict[keyname] = value + return orig_dict
+ + +
[文档]def replace_slices_with_strings(data: dict) -> dict: + """Replace slice objects with their string representations in a dictionary. + + Args: + data: The dictionary to process. + + Returns: + The dictionary with slice objects replaced by their string representations. + """ + if isinstance(data, dict): + return {k: replace_slices_with_strings(v) for k, v in data.items()} + elif isinstance(data, slice): + return f"slice({data.start},{data.stop},{data.step})" + else: + return data
+ + +
[文档]def replace_strings_with_slices(data: dict) -> dict: + """Replace string representations of slices with slice objects in a dictionary. + + Args: + data: The dictionary to process. + + Returns: + The dictionary with string representations of slices replaced by slice objects. + """ + if isinstance(data, dict): + return {k: replace_strings_with_slices(v) for k, v in data.items()} + elif isinstance(data, str) and data.startswith("slice("): + return string_to_slice(data) + else: + return data
+ + + +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/interpolation/linear_interpolation.html b/_modules/omni/isaac/lab/utils/interpolation/linear_interpolation.html new file mode 100644 index 0000000000..f0a1f377db --- /dev/null +++ b/_modules/omni/isaac/lab/utils/interpolation/linear_interpolation.html @@ -0,0 +1,644 @@ + + + + + + + + + + + omni.isaac.lab.utils.interpolation.linear_interpolation — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.interpolation.linear_interpolation 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+
+
+
[文档]class LinearInterpolation: + """Linearly interpolates a sampled scalar function for arbitrary query points. + + This class implements a linear interpolation for a scalar function. The function maps from real values, x, to + real values, y. It expects a set of samples from the function's domain, x, and the corresponding values, y. + The class allows querying the function's values at any arbitrary point. + + The interpolation is done by finding the two closest points in x to the query point and then linearly + interpolating between the corresponding y values. For the query points that are outside the input points, + the class does a zero-order-hold extrapolation based on the boundary values. This means that the class + returns the value of the closest point in x. + """ + +
[文档] def __init__(self, x: torch.Tensor, y: torch.Tensor, device: str): + """Initializes the linear interpolation. + + The scalar function maps from real values, x, to real values, y. The input to the class is a set of samples + from the function's domain, x, and the corresponding values, y. + + Note: + The input tensor x should be sorted in ascending order. + + Args: + x: An vector of samples from the function's domain. The values should be sorted in ascending order. + Shape is (num_samples,) + y: The function's values associated to the input x. Shape is (num_samples,) + device: The device used for processing. + + Raises: + ValueError: If the input tensors are empty or have different sizes. + ValueError: If the input tensor x is not sorted in ascending order. + """ + # make sure that input tensors are 1D of size (num_samples,) + self._x = x.view(-1).clone().to(device=device) + self._y = y.view(-1).clone().to(device=device) + + # make sure sizes are correct + if self._x.numel() == 0: + raise ValueError("Input tensor x is empty!") + if self._x.numel() != self._y.numel(): + raise ValueError(f"Input tensors x and y have different sizes: {self._x.numel()} != {self._y.numel()}") + # make sure that x is sorted + if torch.any(self._x[1:] < self._x[:-1]): + raise ValueError("Input tensor x is not sorted in ascending order!")
+ +
[文档] def compute(self, q: torch.Tensor) -> torch.Tensor: + """Calculates a linearly interpolated values for the query points. + + Args: + q: The query points. It can have any arbitrary shape. + + Returns: + The interpolated values at query points. It has the same shape as the input tensor. + """ + # serialized q + q_1d = q.view(-1) + # Number of elements in the x that are strictly smaller than query points (use int32 instead of int64) + num_smaller_elements = torch.sum(self._x.unsqueeze(1) < q_1d.unsqueeze(0), dim=0, dtype=torch.int) + + # The index pointing to the first element in x such that x[lower_bound_i] < q_i + # If a point is smaller that all x elements, it will assign 0 + lower_bound = torch.clamp(num_smaller_elements - 1, min=0) + # The index pointing to the first element in x such that x[upper_bound_i] >= q_i + # If a point is greater than all x elements, it will assign the last elements' index + upper_bound = torch.clamp(num_smaller_elements, max=self._x.numel() - 1) + + # compute the weight as: (q_i - x_lb) / (x_ub - x_lb) + weight = (q_1d - self._x[lower_bound]) / (self._x[upper_bound] - self._x[lower_bound]) + # If a point is out of bounds assign weight 0.0 + weight[upper_bound == lower_bound] = 0.0 + + # Perform linear interpolation + fq = self._y[lower_bound] + weight * (self._y[upper_bound] - self._y[lower_bound]) + + # deserialized fq + fq = fq.view(q.shape) + return fq
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/io/pkl.html b/_modules/omni/isaac/lab/utils/io/pkl.html new file mode 100644 index 0000000000..5398d78f18 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/io/pkl.html @@ -0,0 +1,609 @@ + + + + + + + + + + + omni.isaac.lab.utils.io.pkl — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.io.pkl 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Utilities for file I/O with pickle."""
+
+import os
+import pickle
+from typing import Any
+
+
+
[文档]def load_pickle(filename: str) -> Any: + """Loads an input PKL file safely. + + Args: + filename: The path to pickled file. + + Raises: + FileNotFoundError: When the specified file does not exist. + + Returns: + The data read from the input file. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f"File not found: {filename}") + with open(filename, "rb") as f: + data = pickle.load(f) + return data
+ + +
[文档]def dump_pickle(filename: str, data: Any): + """Saves data into a pickle file safely. + + Note: + The function creates any missing directory along the file's path. + + Args: + filename: The path to save the file at. + data: The data to save. + """ + # check ending + if not filename.endswith("pkl"): + filename += ".pkl" + # create directory + if not os.path.exists(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename), exist_ok=True) + # save data + with open(filename, "wb") as f: + pickle.dump(data, f)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/io/yaml.html b/_modules/omni/isaac/lab/utils/io/yaml.html new file mode 100644 index 0000000000..4db6ccd56f --- /dev/null +++ b/_modules/omni/isaac/lab/utils/io/yaml.html @@ -0,0 +1,614 @@ + + + + + + + + + + + omni.isaac.lab.utils.io.yaml — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.io.yaml 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Utilities for file I/O with yaml."""
+
+import os
+import yaml
+
+from omni.isaac.lab.utils import class_to_dict
+
+
+
[文档]def load_yaml(filename: str) -> dict: + """Loads an input PKL file safely. + + Args: + filename: The path to pickled file. + + Raises: + FileNotFoundError: When the specified file does not exist. + + Returns: + The data read from the input file. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f"File not found: {filename}") + with open(filename) as f: + data = yaml.full_load(f) + return data
+ + +
[文档]def dump_yaml(filename: str, data: dict | object, sort_keys: bool = False): + """Saves data into a YAML file safely. + + Note: + The function creates any missing directory along the file's path. + + Args: + filename: The path to save the file at. + data: The data to save either a dictionary or class object. + sort_keys: Whether to sort the keys in the output file. Defaults to False. + """ + # check ending + if not filename.endswith("yaml"): + filename += ".yaml" + # create directory + if not os.path.exists(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename), exist_ok=True) + # convert data into dictionary + if not isinstance(data, dict): + data = class_to_dict(data) + # save data + with open(filename, "w") as f: + yaml.dump(data, f, default_flow_style=False, sort_keys=sort_keys)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/math.html b/_modules/omni/isaac/lab/utils/math.html new file mode 100644 index 0000000000..f26fa7d879 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/math.html @@ -0,0 +1,2120 @@ + + + + + + + + + + + omni.isaac.lab.utils.math — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.math 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module containing utilities for various math operations."""
+
+# needed to import for allowing type-hinting: torch.Tensor | np.ndarray
+from __future__ import annotations
+
+import math
+import numpy as np
+import torch
+import torch.nn.functional
+from typing import Literal
+
+"""
+General
+"""
+
+
+@torch.jit.script
+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:
+        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:
+        Normalized transform of the tensor. Shape is (N, dims).
+    """
+    # 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
+def saturate(x: torch.Tensor, lower: torch.Tensor, upper: torch.Tensor) -> torch.Tensor:
+    """Clamps a given input tensor to (lower, upper).
+
+    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:
+        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:
+    r"""Wraps input angles (in radians) to the range :math:`[-\pi, \pi]`.
+
+    This function wraps angles in radians to the range :math:`[-\pi, \pi]`, such that
+    :math:`\pi` maps to :math:`\pi`, and :math:`-\pi` maps to :math:`-\pi`. In general,
+    odd positive multiples of :math:`\pi` are mapped to :math:`\pi`, and odd negative
+    multiples of :math:`\pi` are mapped to :math:`-\pi`.
+
+    The function behaves similar to MATLAB's `wrapToPi <https://www.mathworks.com/help/map/ref/wraptopi.html>`_
+    function.
+
+    Args:
+        angles: Input angles of any shape.
+
+    Returns:
+        Angles in the range :math:`[-\pi, \pi]`.
+    """
+    # wrap to [0, 2*pi)
+    wrapped_angle = (angles + torch.pi) % (2 * torch.pi)
+    # map to [-pi, pi]
+    # we check for zero in wrapped angle to make it go to pi when input angle is odd multiple of pi
+    return torch.where((wrapped_angle == 0) & (angles > 0), torch.pi, wrapped_angle - torch.pi)
+
+
+@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.
+
+    Note:
+        The implementation follows from `torch.copysign`. The function allows a scalar magnitude.
+
+    Args:
+        mag: The magnitude scalar.
+        other: The tensor containing values whose signbits are applied to magnitude.
+
+    Returns:
+        The output tensor.
+    """
+    mag_torch = torch.tensor(mag, device=other.device, dtype=torch.float).repeat(other.shape[0])
+    return torch.abs(mag_torch) * torch.sign(other)
+
+
+"""
+Rotation
+"""
+
+
+@torch.jit.script
+def matrix_from_quat(quaternions: torch.Tensor) -> torch.Tensor:
+    """Convert rotations given as quaternions to rotation matrices.
+
+    Args:
+        quaternions: The quaternion orientation in (w, x, y, z). Shape is (..., 4).
+
+    Returns:
+        Rotation matrices. The shape is (..., 3, 3).
+
+    Reference:
+        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`.
+    two_s = 2.0 / (quaternions * quaternions).sum(-1)
+
+    o = torch.stack(
+        (
+            1 - two_s * (j * j + k * k),
+            two_s * (i * j - k * r),
+            two_s * (i * k + j * r),
+            two_s * (i * j + k * r),
+            1 - two_s * (i * i + k * k),
+            two_s * (j * k - i * r),
+            two_s * (i * k - j * r),
+            two_s * (j * k + i * r),
+            1 - two_s * (i * i + j * j),
+        ),
+        -1,
+    )
+    return o.reshape(quaternions.shape[:-1] + (3, 3))
+
+
+
[文档]def convert_quat(quat: torch.Tensor | np.ndarray, to: Literal["xyzw", "wxyz"] = "xyzw") -> torch.Tensor | np.ndarray: + """Converts quaternion from one convention to another. + + The convention to convert TO is specified as an optional argument. If to == 'xyzw', + then the input is in 'wxyz' format, and vice-versa. + + Args: + quat: The quaternion of shape (..., 4). + to: Convention to convert quaternion to.. Defaults to "xyzw". + + Returns: + The converted quaternion in specified convention. + + Raises: + ValueError: Invalid input argument `to`, i.e. not "xyzw" or "wxyz". + ValueError: Invalid shape of input `quat`, i.e. not (..., 4,). + """ + # check input is correct + if quat.shape[-1] != 4: + msg = f"Expected input quaternion shape mismatch: {quat.shape} != (..., 4)." + raise ValueError(msg) + if to not in ["xyzw", "wxyz"]: + msg = f"Expected input argument `to` to be 'xyzw' or 'wxyz'. Received: {to}." + raise ValueError(msg) + # check if input is numpy array (we support this backend since some classes use numpy) + if isinstance(quat, np.ndarray): + # use numpy functions + if to == "xyzw": + # wxyz -> xyzw + return np.roll(quat, -1, axis=-1) + else: + # xyzw -> wxyz + return np.roll(quat, 1, axis=-1) + else: + # convert to torch (sanity check) + if not isinstance(quat, torch.Tensor): + quat = torch.tensor(quat, dtype=float) + # convert to specified quaternion type + if to == "xyzw": + # wxyz -> xyzw + return quat.roll(-1, dims=-1) + else: + # 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 quaternion orientation in (w, x, y, z). Shape is (N, 4). + + Returns: + The inverse quaternion in (w, x, y, z). Shape is (N, 4). + """ + return normalize(quat_conjugate(q)) + + +@torch.jit.script +def quat_from_euler_xyz(roll: torch.Tensor, pitch: torch.Tensor, yaw: torch.Tensor) -> torch.Tensor: + """Convert rotations given as Euler angles in radians to Quaternions. + + Note: + 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,). + + Returns: + The quaternion in (w, x, y, z). Shape is (N, 4). + """ + cy = torch.cos(yaw * 0.5) + sy = torch.sin(yaw * 0.5) + cr = torch.cos(roll * 0.5) + sr = torch.sin(roll * 0.5) + cp = torch.cos(pitch * 0.5) + sp = torch.sin(pitch * 0.5) + # compute quaternion + qw = cy * cr * cp + sy * sr * sp + qx = cy * sr * cp - sy * cr * sp + qy = cy * cr * sp + sy * sr * cp + qz = sy * cr * cp - cy * sr * sp + + return torch.stack([qw, qx, qy, qz], dim=-1) + + +@torch.jit.script +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: + https://github.com/facebookresearch/pytorch3d/blob/main/pytorch3d/transforms/rotation_conversions.py#L91-L99 + """ + ret = torch.zeros_like(x) + positive_mask = x > 0 + ret[positive_mask] = torch.sqrt(x[positive_mask]) + return ret + + +@torch.jit.script +def quat_from_matrix(matrix: torch.Tensor) -> torch.Tensor: + """Convert rotations given as rotation matrices to quaternions. + + Args: + matrix: The rotation matrices. Shape is (..., 3, 3). + + Returns: + The quaternion in (w, x, y, z). Shape is (..., 4). + + Reference: + 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}.") + + batch_dim = matrix.shape[:-2] + m00, m01, m02, m10, m11, m12, m20, m21, m22 = torch.unbind(matrix.reshape(batch_dim + (9,)), dim=-1) + + q_abs = _sqrt_positive_part( + torch.stack( + [ + 1.0 + m00 + m11 + m22, + 1.0 + m00 - m11 - m22, + 1.0 - m00 + m11 - m22, + 1.0 - m00 - m11 + m22, + ], + dim=-1, + ) + ) + + # 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`. + 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`. + 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`. + 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`. + torch.stack([m10 - m01, m20 + m02, m21 + m12, q_abs[..., 3] ** 2], dim=-1), + ], + dim=-2, + ) + + # We floor here at 0.1 but the exact level is not important; if q_abs is small, + # the candidate won't be picked. + flr = torch.tensor(0.1).to(dtype=q_abs.dtype, device=q_abs.device) + quat_candidates = quat_by_rijk / (2.0 * q_abs[..., None].max(flr)) + + # 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,) + ) + + +def _axis_angle_rotation(axis: Literal["X", "Y", "Z"], angle: torch.Tensor) -> torch.Tensor: + """Return the rotation matrices for one of the rotations about an axis of which Euler angles describe, + for each value of the angle given. + + Args: + axis: Axis label "X" or "Y or "Z". + angle: Euler angles in radians of any shape. + + Returns: + Rotation matrices. Shape is (..., 3, 3). + + Reference: + 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) + zero = torch.zeros_like(angle) + + if axis == "X": + R_flat = (one, zero, zero, zero, cos, -sin, zero, sin, cos) + elif axis == "Y": + R_flat = (cos, zero, sin, zero, one, zero, -sin, zero, cos) + elif axis == "Z": + R_flat = (cos, -sin, zero, sin, cos, zero, zero, zero, one) + else: + raise ValueError("letter must be either X, Y or Z.") + + return torch.stack(R_flat, -1).reshape(angle.shape + (3, 3)) + + +
[文档]def matrix_from_euler(euler_angles: torch.Tensor, convention: str) -> torch.Tensor: + """ + Convert rotations given as Euler angles in radians to rotation matrices. + + Args: + 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. Shape is (..., 3, 3). + + Reference: + 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.") + if len(convention) != 3: + raise ValueError("Convention must have 3 letters.") + if convention[1] in (convention[0], convention[2]): + raise ValueError(f"Invalid convention {convention}.") + for letter in convention: + if letter not in ("X", "Y", "Z"): + raise ValueError(f"Invalid letter {letter} in convention string.") + matrices = [_axis_angle_rotation(c, e) for c, e in zip(convention, torch.unbind(euler_angles, -1))] + # return functools.reduce(torch.matmul, matrices) + return torch.matmul(torch.matmul(matrices[0], matrices[1]), matrices[2])
+ + +@torch.jit.script +def euler_xyz_from_quat(quat: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Convert rotations given as quaternions to Euler angles in radians. + + Note: + The euler angles are assumed in XYZ convention. + + Args: + quat: The quaternion orientation in (w, x, y, z). Shape is (N, 4). + + Returns: + 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) + sin_roll = 2.0 * (q_w * q_x + q_y * q_z) + cos_roll = 1 - 2 * (q_x * q_x + q_y * q_y) + roll = torch.atan2(sin_roll, cos_roll) + + # pitch (y-axis rotation) + sin_pitch = 2.0 * (q_w * q_y - q_z * q_x) + pitch = torch.where(torch.abs(sin_pitch) >= 1, copysign(torch.pi / 2.0, sin_pitch), torch.asin(sin_pitch)) + + # yaw (z-axis rotation) + sin_yaw = 2.0 * (q_w * q_z + q_x * q_y) + cos_yaw = 1 - 2 * (q_y * q_y + q_z * q_z) + yaw = torch.atan2(sin_yaw, cos_yaw) + + 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_unique(q: torch.Tensor) -> torch.Tensor: + """Convert a unit quaternion to a standard form where the real part is non-negative. + + Quaternion representations have a singularity since ``q`` and ``-q`` represent the same + rotation. This function ensures the real part of the quaternion is non-negative. + + Args: + q: The quaternion orientation in (w, x, y, z). Shape is (..., 4). + + Returns: + Standardized quaternions. Shape is (..., 4). + """ + return torch.where(q[..., 0:1] < 0, -q, q) + + +@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). + """ + 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.atan2(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: 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] + qy = quat_yaw[:, 2] + qz = quat_yaw[:, 3] + yaw = torch.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz)) + quat_yaw[:] = 0.0 + quat_yaw[:, 3] = torch.sin(yaw / 2) + quat_yaw[:, 0] = torch.cos(yaw / 2) + quat_yaw = normalize(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 +def quat_apply_yaw(quat: torch.Tensor, vec: torch.Tensor) -> torch.Tensor: + """Rotate a vector only around the yaw-direction. + + Args: + quat: The orientation in (w, x, y, z). Shape is (N, 4). + vec: The vector in (x, y, z). Shape is (N, 3). + + Returns: + 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_rotate(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + """Rotate a vector by a quaternion along the last dimension of q and v. + + Args: + q: The quaternion in (w, x, y, z). Shape is (..., 4). + v: The vector in (x, y, z). Shape is (..., 3). + + Returns: + The rotated vector in (x, y, z). Shape is (..., 3). + """ + 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 + # for two-dimensional tensors, bmm is faster than einsum + if q_vec.dim() == 2: + c = q_vec * torch.bmm(q_vec.view(q.shape[0], 1, 3), v.view(q.shape[0], 3, 1)).squeeze(-1) * 2.0 + else: + c = q_vec * torch.einsum("...i,...i->...", q_vec, v).unsqueeze(-1) * 2.0 + return a + b + c + + +@torch.jit.script +def quat_rotate_inverse(q: torch.Tensor, v: torch.Tensor) -> torch.Tensor: + """Rotate a vector by the inverse of a quaternion along the last dimension of q and v. + + Args: + q: The quaternion in (w, x, y, z). Shape is (..., 4). + v: The vector in (x, y, z). Shape is (..., 3). + + Returns: + The rotated vector in (x, y, z). Shape is (..., 3). + """ + 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 + # for two-dimensional tensors, bmm is faster than einsum + if q_vec.dim() == 2: + c = q_vec * torch.bmm(q_vec.view(q.shape[0], 1, 3), v.view(q.shape[0], 3, 1)).squeeze(-1) * 2.0 + else: + c = q_vec * torch.einsum("...i,...i->...", q_vec, v).unsqueeze(-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 +def axis_angle_from_quat(quat: torch.Tensor, eps: float = 1.0e-6) -> torch.Tensor: + """Convert rotations given as quaternions to axis/angle. + + Args: + 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. Shape is (..., 3). + The vector's magnitude is the angle turned anti-clockwise in radians around the vector's direction. + + Reference: + 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)] + # Axis-angle is [a_x, a_y, a_z] = [theta * n_x, theta * n_y, theta * n_z] + # Thus, axis-angle is [q_x, q_y, q_z] / (sin(theta/2) / theta) + # When theta = 0, (sin(theta/2) / theta) is undefined + # However, as theta --> 0, we can use the Taylor approximation 1/2 - theta^2 / 48 + quat = quat * (1.0 - 2.0 * (quat[..., 0:1] < 0.0)) + mag = torch.linalg.norm(quat[..., 1:], dim=-1) + half_angle = torch.atan2(mag, quat[..., 0]) + angle = 2.0 * half_angle + # check whether to apply Taylor approximation + sin_half_angles_over_angles = torch.where( + angle.abs() > eps, torch.sin(half_angle) / angle, 0.5 - angle * angle / 48 + ) + return quat[..., 1:4] / sin_half_angles_over_angles.unsqueeze(-1) + + +@torch.jit.script +def quat_error_magnitude(q1: torch.Tensor, q2: torch.Tensor) -> torch.Tensor: + """Computes the rotation difference between two quaternions. + + 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: + Angular error between input quaternions in radians. + """ + quat_diff = quat_mul(q1, quat_conjugate(q2)) + return torch.norm(axis_angle_from_quat(quat_diff), dim=-1) + + +@torch.jit.script +def skew_symmetric_matrix(vec: torch.Tensor) -> torch.Tensor: + """Computes the skew-symmetric matrix of a vector. + + Args: + vec: The input vector. Shape is (3,) or (N, 3). + + Returns: + The skew-symmetric matrix. Shape is (1, 3, 3) or (N, 3, 3). + + Raises: + ValueError: If input tensor is not of shape (..., 3). + """ + # check input is correct + if vec.shape[-1] != 3: + raise ValueError(f"Expected input vector shape mismatch: {vec.shape} != (..., 3).") + # unsqueeze the last dimension + if vec.ndim == 1: + vec = vec.unsqueeze(0) + # create a skew-symmetric matrix + skew_sym_mat = torch.zeros(vec.shape[0], 3, 3, device=vec.device, dtype=vec.dtype) + skew_sym_mat[:, 0, 1] = -vec[:, 2] + skew_sym_mat[:, 0, 2] = vec[:, 1] + skew_sym_mat[:, 1, 2] = -vec[:, 0] + skew_sym_mat[:, 1, 0] = vec[:, 2] + skew_sym_mat[:, 2, 0] = -vec[:, 1] + skew_sym_mat[:, 2, 1] = vec[:, 0] + + return skew_sym_mat + + +""" +Transformations +""" + + +
[文档]def is_identity_pose(pos: torch.tensor, rot: torch.tensor) -> bool: + """Checks if input poses are identity transforms. + + The function checks if the input position and orientation are close to zero and + identity respectively using L2-norm. It does NOT check the error in the orientation. + + Args: + pos: The cartesian position. Shape is (N, 3). + 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. + """ + # create identity transformations + pos_identity = torch.zeros_like(pos) + rot_identity = torch.zeros_like(rot) + rot_identity[..., 0] = 1 + # compare input to identity + return torch.allclose(pos, pos_identity) and torch.allclose(rot, rot_identity)
+ + +# @torch.jit.script +
[文档]def combine_frame_transforms( + t01: torch.Tensor, q01: torch.Tensor, t12: torch.Tensor | None = None, q12: torch.Tensor | None = None +) -> tuple[torch.Tensor, torch.Tensor]: + r"""Combine transformations between two reference frames into a stationary frame. + + It performs the following transformation operation: :math:`T_{02} = T_{01} \times T_{12}`, + 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. 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). + Defaults to None, in which case the position is assumed to be zero. + q12: Quaternion orientation of frame 2 w.r.t. frame 1 in (w, x, y, z). Shape is (N, 4). + Defaults to None, in which case the orientation is assumed to be identity. + + 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: + q02 = quat_mul(q01, q12) + else: + q02 = q01 + # compute translation + if t12 is not None: + t02 = t01 + quat_apply(q01, t12) + else: + t02 = t01 + + return t02, q02
+ + +# @torch.jit.script +
[文档]def subtract_frame_transforms( + t01: torch.Tensor, q01: torch.Tensor, t02: torch.Tensor | None = None, q02: torch.Tensor | None = None +) -> tuple[torch.Tensor, torch.Tensor]: + r"""Subtract transformations between two reference frames into a stationary frame. + + It performs the following transformation operation: :math:`T_{12} = T_{01}^{-1} \times T_{02}`, + 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. 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). + Defaults to None, in which case the position is assumed to be zero. + q02: Quaternion orientation of frame 2 w.r.t. frame 0 in (w, x, y, z). Shape is (N, 4). + Defaults to None, in which case the orientation is assumed to be identity. + + 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) + if q02 is not None: + q12 = quat_mul(q10, q02) + else: + q12 = q10 + # compute translation + if t02 is not None: + t12 = quat_apply(q10, t02 - t01) + else: + t12 = quat_apply(q10, -t01) + return t12, q12
+ + +# @torch.jit.script +
[文档]def compute_pose_error( + t01: torch.Tensor, + q01: torch.Tensor, + t02: torch.Tensor, + q02: torch.Tensor, + rot_error_type: Literal["quat", "axis_angle"] = "axis_angle", +) -> tuple[torch.Tensor, torch.Tensor]: + """Compute the position and orientation error between source and target frames. + + Args: + 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. 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. 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. + """ + # Compute quaternion error (i.e., difference quaternion) + # Reference: https://personal.utdallas.edu/~sxb027100/dock/quaternion.html + # q_current_norm = q_current * q_current_conj + source_quat_norm = quat_mul(q01, quat_conjugate(q01))[:, 0] + # q_current_inv = q_current_conj / q_current_norm + source_quat_inv = quat_conjugate(q01) / source_quat_norm.unsqueeze(-1) + # q_error = q_target * q_current_inv + quat_error = quat_mul(q02, source_quat_inv) + + # Compute position error + pos_error = t02 - t01 + + # return error based on specified type + if rot_error_type == "quat": + return pos_error, quat_error + elif rot_error_type == "axis_angle": + # Convert to axis-angle error + axis_angle_error = axis_angle_from_quat(quat_error) + return pos_error, axis_angle_error + else: + raise ValueError(f"Unsupported orientation error type: {rot_error_type}. Valid: 'quat', 'axis_angle'.")
+ + +@torch.jit.script +def apply_delta_pose( + source_pos: torch.Tensor, source_rot: torch.Tensor, delta_pose: torch.Tensor, eps: float = 1.0e-6 +) -> tuple[torch.Tensor, torch.Tensor]: + """Applies delta pose transformation on source pose. + + The first three elements of `delta_pose` are interpreted as cartesian position displacement. + The remaining three elements of `delta_pose` are interpreted as orientation displacement + in the angle-axis format. + + Args: + source_pos: Position of source frame. Shape is (N, 3). + source_rot: Quaternion orientation of source frame in (w, x, y, z). Shape is (N, 4).. + delta_pose: Position and orientation displacements. Shape is (N, 6). + eps: The tolerance to consider orientation displacement as zero. Defaults to 1.0e-6. + + 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] + device = source_pos.device + + # interpret delta_pose[:, 0:3] as target position displacements + target_pos = source_pos + delta_pose[:, 0:3] + # interpret delta_pose[:, 3:6] as target rotation displacements + rot_actions = delta_pose[:, 3:6] + angle = torch.linalg.vector_norm(rot_actions, dim=1) + axis = rot_actions / angle.unsqueeze(-1) + # change from axis-angle to quat convention + identity_quat = torch.tensor([1.0, 0.0, 0.0, 0.0], device=device).repeat(num_poses, 1) + rot_delta_quat = torch.where( + angle.unsqueeze(-1).repeat(1, 4) > eps, quat_from_angle_axis(angle, axis), identity_quat + ) + # TODO: Check if this is the correct order for this multiplication. + target_rot = quat_mul(rot_delta_quat, source_rot) + + return target_pos, target_rot + + +# @torch.jit.script +
[文档]def transform_points( + points: torch.Tensor, pos: torch.Tensor | None = None, quat: torch.Tensor | None = None +) -> torch.Tensor: + r"""Transform input points in a given frame to a target frame. + + This function transform points from a source frame to a target frame. The transformation is defined by the + position :math:`t` and orientation :math:`R` of the target frame in the source frame. + + .. math:: + p_{target} = R_{target} \times p_{source} + t_{target} + + If the input `points` is a batch of points, the inputs `pos` and `quat` must be either a batch of + 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 :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). + pos: Position of the target frame. Shape is (N, 3) or (3,). + Defaults to None, in which case the position is assumed to be zero. + quat: Quaternion orientation of the target frame in (w, x, y, z). Shape is (N, 4) or (4,). + Defaults to None, in which case the orientation is assumed to be identity. + + Returns: + Transformed points in the target frame. Shape is (N, P, 3) or (P, 3). + + Raises: + ValueError: If the inputs `points` is not of shape (N, P, 3) or (P, 3). + ValueError: If the inputs `pos` is not of shape (N, 3) or (3,). + ValueError: If the inputs `quat` is not of shape (N, 4) or (4,). + """ + points_batch = points.clone() + # check if inputs are batched + is_batched = points_batch.dim() == 3 + # -- check inputs + if points_batch.dim() == 2: + points_batch = points_batch[None] # (P, 3) -> (1, P, 3) + if points_batch.dim() != 3: + raise ValueError(f"Expected points to have dim = 2 or dim = 3: got shape {points.shape}") + if not (pos is None or pos.dim() == 1 or pos.dim() == 2): + raise ValueError(f"Expected pos to have dim = 1 or dim = 2: got shape {pos.shape}") + if not (quat is None or quat.dim() == 1 or quat.dim() == 2): + raise ValueError(f"Expected quat to have dim = 1 or dim = 2: got shape {quat.shape}") + # -- rotation + if quat is not None: + # convert to batched rotation matrix + rot_mat = matrix_from_quat(quat) + if rot_mat.dim() == 2: + rot_mat = rot_mat[None] # (3, 3) -> (1, 3, 3) + # convert points to matching batch size (N, P, 3) -> (N, 3, P) + # and apply rotation + points_batch = torch.matmul(rot_mat, points_batch.transpose_(1, 2)) + # (N, 3, P) -> (N, P, 3) + points_batch = points_batch.transpose_(1, 2) + # -- translation + if pos is not None: + # convert to batched translation vector + if pos.dim() == 1: + pos = pos[None, None, :] # (3,) -> (1, 1, 3) + else: + pos = pos[:, None, :] # (N, 3) -> (N, 1, 3) + # apply translation + points_batch += pos + # -- return points in same shape as input + if not is_batched: + points_batch = points_batch.squeeze(0) # (1, P, 3) -> (P, 3) + + return points_batch
+ + +""" +Projection operations. +""" + + +@torch.jit.script +def orthogonalize_perspective_depth(depth: torch.Tensor, intrinsics: torch.Tensor) -> torch.Tensor: + """Converts perspective depth image to orthogonal depth image. + + Perspective depth images contain distances measured from the camera's optical center. + Meanwhile, orthogonal depth images provide the distance from the camera's image plane. + This method uses the camera geometry to convert perspective depth to orthogonal depth image. + + The function assumes that the width and height are both greater than 1. + + Args: + depth: The perspective depth images. Shape is (H, W) or or (H, W, 1) or (N, H, W) or (N, H, W, 1). + intrinsics: The camera's calibration matrix. If a single matrix is provided, the same + calibration matrix is used across all the depth images in the batch. + Shape is (3, 3) or (N, 3, 3). + + Returns: + The orthogonal depth images. Shape matches the input shape of depth images. + + 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). + """ + # Clone inputs to avoid in-place modifications + perspective_depth_batch = depth.clone() + intrinsics_batch = intrinsics.clone() + + # Check if inputs are batched + is_batched = perspective_depth_batch.dim() == 4 or ( + perspective_depth_batch.dim() == 3 and perspective_depth_batch.shape[-1] != 1 + ) + + # Track whether the last dimension was singleton + add_last_dim = False + if perspective_depth_batch.dim() == 4 and perspective_depth_batch.shape[-1] == 1: + add_last_dim = True + perspective_depth_batch = perspective_depth_batch.squeeze(dim=3) # (N, H, W, 1) -> (N, H, W) + if perspective_depth_batch.dim() == 3 and perspective_depth_batch.shape[-1] == 1: + add_last_dim = True + perspective_depth_batch = perspective_depth_batch.squeeze(dim=2) # (H, W, 1) -> (H, W) + + if perspective_depth_batch.dim() == 2: + perspective_depth_batch = perspective_depth_batch[None] # (H, W) -> (1, H, W) + + if intrinsics_batch.dim() == 2: + intrinsics_batch = intrinsics_batch[None] # (3, 3) -> (1, 3, 3) + + if is_batched and intrinsics_batch.shape[0] == 1: + intrinsics_batch = intrinsics_batch.expand(perspective_depth_batch.shape[0], -1, -1) # (1, 3, 3) -> (N, 3, 3) + + # Validate input shapes + if perspective_depth_batch.dim() != 3: + raise ValueError(f"Expected depth images to have 2, 3, or 4 dimensions; got {depth.shape}.") + if intrinsics_batch.dim() != 3: + raise ValueError(f"Expected intrinsics to have shape (3, 3) or (N, 3, 3); got {intrinsics.shape}.") + + # Image dimensions + im_height, im_width = perspective_depth_batch.shape[1:] + + # Get the intrinsics parameters + fx = intrinsics_batch[:, 0, 0].view(-1, 1, 1) + fy = intrinsics_batch[:, 1, 1].view(-1, 1, 1) + cx = intrinsics_batch[:, 0, 2].view(-1, 1, 1) + cy = intrinsics_batch[:, 1, 2].view(-1, 1, 1) + + # Create meshgrid of pixel coordinates + u_grid = torch.arange(im_width, device=depth.device, dtype=depth.dtype) + v_grid = torch.arange(im_height, device=depth.device, dtype=depth.dtype) + u_grid, v_grid = torch.meshgrid(u_grid, v_grid, indexing="xy") + + # Expand the grids for batch processing + u_grid = u_grid.unsqueeze(0).expand(perspective_depth_batch.shape[0], -1, -1) + v_grid = v_grid.unsqueeze(0).expand(perspective_depth_batch.shape[0], -1, -1) + + # Compute the squared terms for efficiency + x_term = ((u_grid - cx) / fx) ** 2 + y_term = ((v_grid - cy) / fy) ** 2 + + # Calculate the orthogonal (normal) depth + orthogonal_depth = perspective_depth_batch / torch.sqrt(1 + x_term + y_term) + + # Restore the last dimension if it was present in the input + if add_last_dim: + orthogonal_depth = orthogonal_depth.unsqueeze(-1) + + # Return to original shape if input was not batched + if not is_batched: + orthogonal_depth = orthogonal_depth.squeeze(0) + + return orthogonal_depth + + +@torch.jit.script +def unproject_depth(depth: torch.Tensor, intrinsics: torch.Tensor, is_ortho: bool = True) -> torch.Tensor: + r"""Un-project depth image into a pointcloud. + + This function converts orthogonal or perspective depth images into points given the calibration matrix + of the camera. It uses the following transformation based on camera geometry: + + .. math:: + p_{3D} = K^{-1} \times [u, v, 1]^T \times d + + where :math:`p_{3D}` is the 3D point, :math:`d` is the depth value (measured from the image plane), + :math:`u` and :math:`v` are the pixel coordinates and :math:`K` is the intrinsic matrix. + + The function assumes that the width and height are both greater than 1. This makes the function + deal with many possible shapes of depth images and intrinsics matrices. + + .. note:: + If :attr:`is_ortho` is False, the input depth images are transformed to orthogonal depth images + by using the :meth:`orthogonalize_perspective_depth` method. + + Args: + depth: The depth measurement. Shape is (H, W) or or (H, W, 1) or (N, H, W) or (N, H, W, 1). + intrinsics: The camera's calibration matrix. If a single matrix is provided, the same + calibration matrix is used across all the depth images in the batch. + Shape is (3, 3) or (N, 3, 3). + is_ortho: Whether the input depth image is orthogonal or perspective depth image. If True, the input + depth image is considered as the *orthogonal* type, where the measurements are from the camera's + image plane. If False, the depth image is considered as the *perspective* type, where the + measurements are from the camera's optical center. Defaults to True. + + Returns: + The 3D coordinates of points. Shape is (P, 3) or (N, P, 3). + + 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). + """ + # clone inputs to avoid in-place modifications + intrinsics_batch = intrinsics.clone() + # convert depth image to orthogonal if needed + if not is_ortho: + depth_batch = orthogonalize_perspective_depth(depth, intrinsics) + else: + depth_batch = depth.clone() + + # check if inputs are batched + is_batched = depth_batch.dim() == 4 or (depth_batch.dim() == 3 and depth_batch.shape[-1] != 1) + # make sure inputs are batched + if depth_batch.dim() == 3 and depth_batch.shape[-1] == 1: + depth_batch = depth_batch.squeeze(dim=2) # (H, W, 1) -> (H, W) + if depth_batch.dim() == 2: + depth_batch = depth_batch[None] # (H, W) -> (1, H, W) + if depth_batch.dim() == 4 and depth_batch.shape[-1] == 1: + depth_batch = depth_batch.squeeze(dim=3) # (N, H, W, 1) -> (N, H, W) + if intrinsics_batch.dim() == 2: + intrinsics_batch = intrinsics_batch[None] # (3, 3) -> (1, 3, 3) + # check shape of inputs + if depth_batch.dim() != 3: + raise ValueError(f"Expected depth images to have dim = 2 or 3 or 4: got shape {depth.shape}") + if intrinsics_batch.dim() != 3: + raise ValueError(f"Expected intrinsics to have shape (3, 3) or (N, 3, 3): got shape {intrinsics.shape}") + + # get image height and width + im_height, im_width = depth_batch.shape[1:] + # create image points in homogeneous coordinates (3, H x W) + indices_u = torch.arange(im_width, device=depth.device, dtype=depth.dtype) + indices_v = torch.arange(im_height, device=depth.device, dtype=depth.dtype) + img_indices = torch.stack(torch.meshgrid([indices_u, indices_v], indexing="ij"), dim=0).reshape(2, -1) + pixels = torch.nn.functional.pad(img_indices, (0, 0, 0, 1), mode="constant", value=1.0) + pixels = pixels.unsqueeze(0) # (3, H x W) -> (1, 3, H x W) + + # unproject points into 3D space + points = torch.matmul(torch.inverse(intrinsics_batch), pixels) # (N, 3, H x W) + points = points / points[:, -1, :].unsqueeze(1) # normalize by last coordinate + # flatten depth image (N, H, W) -> (N, H x W) + depth_batch = depth_batch.transpose_(1, 2).reshape(depth_batch.shape[0], -1).unsqueeze(2) + depth_batch = depth_batch.expand(-1, -1, 3) + # scale points by depth + points_xyz = points.transpose_(1, 2) * depth_batch # (N, H x W, 3) + + # return points in same shape as input + if not is_batched: + points_xyz = points_xyz.squeeze(0) + + return points_xyz + + +@torch.jit.script +def project_points(points: torch.Tensor, intrinsics: torch.Tensor) -> torch.Tensor: + r"""Projects 3D points into 2D image plane. + + This project 3D points into a 2D image plane. The transformation is defined by the intrinsic + matrix of the camera. + + .. math:: + + \begin{align} + p &= K \times p_{3D} = \\ + p_{2D} &= \begin{pmatrix} u \\ v \\ d \end{pmatrix} + = \begin{pmatrix} p[0] / p[2] \\ p[1] / p[2] \\ Z \end{pmatrix} + \end{align} + + where :math:`p_{2D} = (u, v, d)` is the projected 3D point, :math:`p_{3D} = (X, Y, Z)` is the + 3D point and :math:`K \in \mathbb{R}^{3 \times 3}` is the intrinsic matrix. + + If `points` is a batch of 3D points and `intrinsics` is a single intrinsic matrix, the same + calibration matrix is applied to all points in the batch. + + Args: + points: The 3D coordinates of points. Shape is (P, 3) or (N, P, 3). + intrinsics: Camera's calibration matrix. Shape is (3, 3) or (N, 3, 3). + + Returns: + Projected 3D coordinates of points. Shape is (P, 3) or (N, P, 3). + """ + # clone the inputs to avoid in-place operations modifying the original data + points_batch = points.clone() + intrinsics_batch = intrinsics.clone() + + # check if inputs are batched + is_batched = points_batch.dim() == 2 + # make sure inputs are batched + if points_batch.dim() == 2: + points_batch = points_batch[None] # (P, 3) -> (1, P, 3) + if intrinsics_batch.dim() == 2: + intrinsics_batch = intrinsics_batch[None] # (3, 3) -> (1, 3, 3) + # check shape of inputs + if points_batch.dim() != 3: + raise ValueError(f"Expected points to have dim = 3: got shape {points.shape}.") + if intrinsics_batch.dim() != 3: + raise ValueError(f"Expected intrinsics to have shape (3, 3) or (N, 3, 3): got shape {intrinsics.shape}.") + + # project points into 2D image plane + points_2d = torch.matmul(intrinsics_batch, points_batch.transpose(1, 2)) + points_2d = points_2d / points_2d[:, -1, :].unsqueeze(1) # normalize by last coordinate + points_2d = points_2d.transpose_(1, 2) # (N, 3, P) -> (N, P, 3) + # replace last coordinate with depth + points_2d[:, :, -1] = points_batch[:, :, -1] + + # return points in same shape as input + if not is_batched: + points_2d = points_2d.squeeze(0) # (1, 3, P) -> (3, P) + + return points_2d + + +""" +Sampling +""" + + +@torch.jit.script +def default_orientation(num: int, device: str) -> torch.Tensor: + """Returns identity rotation transform. + + Args: + num: The number of rotations to sample. + device: Device to create tensor on. + + Returns: + 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 + + return quat + + +@torch.jit.script +def random_orientation(num: int, device: str) -> torch.Tensor: + """Returns sampled rotation in 3D as quaternion. + + Args: + num: The number of rotations to sample. + device: Device to create tensor on. + + Returns: + 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) + # normalize the quaternion + return torch.nn.functional.normalize(quat, p=2.0, dim=-1, eps=1e-12) + + +@torch.jit.script +def random_yaw_orientation(num: int, device: str) -> torch.Tensor: + """Returns sampled rotation around z-axis. + + Args: + num: The number of rotations to sample. + device: Device to create tensor on. + + Returns: + 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) + yaw = 2 * torch.pi * torch.rand(num, dtype=torch.float, device=device) + + return quat_from_euler_xyz(roll, pitch, yaw) + + +
[文档]def sample_triangle(lower: float, upper: float, size: int | tuple[int, ...], device: str) -> torch.Tensor: + """Randomly samples tensor from a triangular distribution. + + Args: + lower: The lower range of the sampled tensor. + upper: The upper range of the sampled tensor. + size: The shape of the tensor. + device: Device to create tensor on. + + Returns: + Sampled tensor. Shape is based on :attr:`size`. + """ + # convert to tuple + if isinstance(size, int): + size = (size,) + # create random tensor in the range [-1, 1] + r = 2 * torch.rand(*size, device=device) - 1 + # convert to triangular distribution + r = torch.where(r < 0.0, -torch.sqrt(-r), torch.sqrt(r)) + # rescale back to [0, 1] + r = (r + 1.0) / 2.0 + # rescale to range [lower, upper] + return (upper - lower) * r + lower
+ + +
[文档]def sample_uniform( + lower: torch.Tensor | float, upper: torch.Tensor | float, size: int | tuple[int, ...], device: str +) -> torch.Tensor: + """Sample uniformly within a range. + + Args: + lower: Lower bound of uniform range. + upper: Upper bound of uniform range. + size: The shape of the tensor. + device: Device to create tensor on. + + Returns: + Sampled tensor. Shape is based on :attr:`size`. + """ + # convert to tuple + if isinstance(size, int): + size = (size,) + # return tensor + return torch.rand(*size, device=device) * (upper - lower) + lower
+ + +
[文档]def sample_log_uniform( + lower: torch.Tensor | float, upper: torch.Tensor | float, size: int | tuple[int, ...], device: str +) -> torch.Tensor: + r"""Sample using log-uniform distribution within a range. + + The log-uniform distribution is defined as a uniform distribution in the log-space. It + is useful for sampling values that span several orders of magnitude. The sampled values + are uniformly distributed in the log-space and then exponentiated to get the final values. + + .. math:: + + x = \exp(\text{uniform}(\log(\text{lower}), \log(\text{upper}))) + + Args: + lower: Lower bound of uniform range. + upper: Upper bound of uniform range. + size: The shape of the tensor. + device: Device to create tensor on. + + Returns: + Sampled tensor. Shape is based on :attr:`size`. + """ + # cast to tensor if not already + if not isinstance(lower, torch.Tensor): + lower = torch.tensor(lower, dtype=torch.float, device=device) + if not isinstance(upper, torch.Tensor): + upper = torch.tensor(upper, dtype=torch.float, device=device) + # sample in log-space and exponentiate + return torch.exp(sample_uniform(torch.log(lower), torch.log(upper), size, device))
+ + +
[文档]def sample_gaussian( + mean: torch.Tensor | float, std: torch.Tensor | float, size: int | tuple[int, ...], device: str +) -> torch.Tensor: + """Sample using gaussian distribution. + + Args: + mean: Mean of the gaussian. + std: Std of the gaussian. + size: The shape of the tensor. + device: Device to create tensor on. + + Returns: + Sampled tensor. + """ + if isinstance(mean, float): + if isinstance(size, int): + size = (size,) + return torch.normal(mean=mean, std=std, size=size).to(device=device) + else: + return torch.normal(mean=mean, std=std).to(device=device)
+ + +
[文档]def sample_cylinder( + radius: float, h_range: tuple[float, float], size: int | tuple[int, ...], device: str +) -> torch.Tensor: + """Sample 3D points uniformly on a cylinder's surface. + + The cylinder is centered at the origin and aligned with the z-axis. The height of the cylinder is + sampled uniformly from the range :obj:`h_range`, while the radius is fixed to :obj:`radius`. + + The sampled points are returned as a tensor of shape :obj:`(*size, 3)`, i.e. the last dimension + contains the x, y, and z coordinates of the sampled points. + + Args: + radius: The radius of the cylinder. + h_range: The minimum and maximum height of the cylinder. + size: The shape of the tensor. + device: Device to create tensor on. + + Returns: + Sampled tensor. Shape is :obj:`(*size, 3)`. + """ + # sample angles + angles = (torch.rand(size, device=device) * 2 - 1) * torch.pi + h_min, h_max = h_range + # add shape + if isinstance(size, int): + size = (size, 3) + else: + size += (3,) + # allocate a tensor + xyz = torch.zeros(size, device=device) + xyz[..., 0] = radius * torch.cos(angles) + xyz[..., 1] = radius * torch.sin(angles) + xyz[..., 2].uniform_(h_min, h_max) + # return positions + return xyz
+ + +""" +Orientation Conversions +""" + + +
[文档]def convert_camera_frame_orientation_convention( + orientation: torch.Tensor, + origin: Literal["opengl", "ros", "world"] = "opengl", + target: Literal["opengl", "ros", "world"] = "ros", +) -> torch.Tensor: + r"""Converts a quaternion representing a rotation from one convention to another. + + In USD, the camera follows the ``"opengl"`` convention. Thus, it is always in **Y up** convention. + This means that the camera is looking down the -Z axis with the +Y axis pointing up , and +X axis pointing right. + However, in ROS, the camera is looking down the +Z axis with the +Y axis pointing down, and +X axis pointing right. + Thus, the camera needs to be rotated by :math:`180^{\circ}` around the X axis to follow the ROS convention. + + .. math:: + + T_{ROS} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & -1 & 0 & 0 \\ 0 & 0 & -1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} + + On the other hand, the typical world coordinate system is with +X pointing forward, +Y pointing left, + and +Z pointing up. The camera can also be set in this convention by rotating the camera by :math:`90^{\circ}` + around the X axis and :math:`-90^{\circ}` around the Y axis. + + .. math:: + + T_{WORLD} = \begin{bmatrix} 0 & 0 & -1 & 0 \\ -1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} T_{USD} + + Thus, based on their application, cameras follow different conventions for their orientation. This function + converts a quaternion from one convention to another. + + Possible conventions are: + + - :obj:`"opengl"` - forward axis: -Z - up axis +Y - Offset is applied in the OpenGL (Usd.Camera) convention + - :obj:`"ros"` - forward axis: +Z - up axis -Y - Offset is applied in the ROS convention + - :obj:`"world"` - forward axis: +X - up axis +Z - Offset is applied in the World Frame convention + + Args: + orientation: Quaternion of form `(w, x, y, z)` with shape (..., 4) in source convention. + origin: Convention to convert from. Defaults to "opengl". + target: Convention to convert to. Defaults to "ros". + + Returns: + Quaternion of form `(w, x, y, z)` with shape (..., 4) in target convention + """ + if target == origin: + return orientation.clone() + + # -- unify input type + if origin == "ros": + # convert from ros to opengl convention + rotm = matrix_from_quat(orientation) + rotm[:, :, 2] = -rotm[:, :, 2] + rotm[:, :, 1] = -rotm[:, :, 1] + # convert to opengl convention + quat_gl = quat_from_matrix(rotm) + elif origin == "world": + # convert from world (x forward and z up) to opengl convention + rotm = matrix_from_quat(orientation) + rotm = torch.matmul( + rotm, + matrix_from_euler(torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ"), + ) + # convert to isaac-sim convention + quat_gl = quat_from_matrix(rotm) + else: + quat_gl = orientation + + # -- convert to target convention + if target == "ros": + # convert from opengl to ros convention + rotm = matrix_from_quat(quat_gl) + rotm[:, :, 2] = -rotm[:, :, 2] + rotm[:, :, 1] = -rotm[:, :, 1] + return quat_from_matrix(rotm) + elif target == "world": + # convert from opengl to world (x forward and z up) convention + rotm = matrix_from_quat(quat_gl) + rotm = torch.matmul( + rotm, + matrix_from_euler(torch.tensor([math.pi / 2, -math.pi / 2, 0], device=orientation.device), "XYZ").T, + ) + return quat_from_matrix(rotm) + else: + return quat_gl.clone()
+ + +
[文档]def create_rotation_matrix_from_view( + eyes: torch.Tensor, + targets: torch.Tensor, + up_axis: Literal["Y", "Z"] = "Z", + device: str = "cpu", +) -> torch.Tensor: + """Compute the rotation matrix from world to view coordinates. + + This function takes a vector ''eyes'' which specifies the location + of the camera in world coordinates and the vector ''targets'' which + indicate the position of the object. + The output is a rotation matrix representing the transformation + from world coordinates -> view coordinates. + + The inputs eyes and targets can each be a + - 3 element tuple/list + - torch tensor of shape (1, 3) + - torch tensor of shape (N, 3) + + Args: + eyes: Position of the camera in world coordinates. + targets: Position of the object in world coordinates. + up_axis: The up axis of the camera. Defaults to "Z". + device: The device to create torch tensors on. Defaults to "cpu". + + The vectors are broadcast against each other so they all have shape (N, 3). + + Returns: + R: (N, 3, 3) batched rotation matrices + + Reference: + Based on PyTorch3D (https://github.com/facebookresearch/pytorch3d/blob/eaf0709d6af0025fe94d1ee7cec454bc3054826a/pytorch3d/renderer/cameras.py#L1635-L1685) + """ + if up_axis == "Y": + up_axis_vec = torch.tensor((0, 1, 0), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1) + elif up_axis == "Z": + up_axis_vec = torch.tensor((0, 0, 1), device=device, dtype=torch.float32).repeat(eyes.shape[0], 1) + else: + raise ValueError(f"Invalid up axis: {up_axis}. Valid options are 'Y' and 'Z'.") + + # get rotation matrix in opengl format (-Z forward, +Y up) + z_axis = -torch.nn.functional.normalize(targets - eyes, eps=1e-5) + x_axis = torch.nn.functional.normalize(torch.cross(up_axis_vec, z_axis, dim=1), eps=1e-5) + y_axis = torch.nn.functional.normalize(torch.cross(z_axis, x_axis, dim=1), eps=1e-5) + is_close = torch.isclose(x_axis, torch.tensor(0.0), atol=5e-3).all(dim=1, keepdim=True) + if is_close.any(): + replacement = torch.nn.functional.normalize(torch.cross(y_axis, z_axis, dim=1), eps=1e-5) + x_axis = torch.where(is_close, replacement, x_axis) + R = torch.cat((x_axis[:, None, :], y_axis[:, None, :], z_axis[:, None, :]), dim=1) + return R.transpose(1, 2)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/modifiers/modifier.html b/_modules/omni/isaac/lab/utils/modifiers/modifier.html new file mode 100644 index 0000000000..43c4f3a938 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/modifiers/modifier.html @@ -0,0 +1,818 @@ + + + + + + + + + + + omni.isaac.lab.utils.modifiers.modifier — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.modifiers.modifier 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+from .modifier_base import ModifierBase
+
+if TYPE_CHECKING:
+    from . import modifier_cfg
+
+##
+# Modifiers as functions
+##
+
+
+
[文档]def scale(data: torch.Tensor, multiplier: float) -> torch.Tensor: + """Scales input data by a multiplier. + + Args: + data: The data to apply the scale to. + multiplier: Value to scale input by. + + Returns: + Scaled data. Shape is the same as data. + """ + return data * multiplier
+ + +
[文档]def clip(data: torch.Tensor, bounds: tuple[float | None, float | None]) -> torch.Tensor: + """Clips the data to a minimum and maximum value. + + Args: + data: The data to apply the clip to. + bounds: A tuple containing the minimum and maximum values to clip data to. + If the value is None, that bound is not applied. + + Returns: + Clipped data. Shape is the same as data. + """ + return data.clip(min=bounds[0], max=bounds[1])
+ + +
[文档]def bias(data: torch.Tensor, value: float) -> torch.Tensor: + """Adds a uniform bias to the data. + + Args: + data: The data to add bias to. + value: Value of bias to add to data. + + Returns: + Biased data. Shape is the same as data. + """ + return data + value
+ + +## +# Sample of class based modifiers +## + + +
[文档]class DigitalFilter(ModifierBase): + r"""Modifier used to apply digital filtering to the input data. + + `Digital filters <https://en.wikipedia.org/wiki/Digital_filter>`_ are used to process discrete-time + signals to extract useful parts of the signal, such as smoothing, noise reduction, or frequency separation. + + The filter can be implemented as a linear difference equation in the time domain. This equation + can be used to calculate the output at each time-step based on the current and previous inputs and outputs. + + .. math:: + y_{i} = X B - Y A = \sum_{j=0}^{N} b_j x_{i-j} - \sum_{j=1}^{M} a_j y_{i-j} + + where :math:`y_{i}` is the current output of the filter. The array :math:`Y` contains previous + outputs from the filter :math:`\{y_{i-j}\}_{j=1}^M` for :math:`M` previous time-steps. The array + :math:`X` contains current :math:`x_{i}` and previous inputs to the filter + :math:`\{x_{i-j}\}_{j=1}^N` for :math:`N` previous time-steps respectively. + The filter coefficients :math:`A` and :math:`B` are used to design the filter. They are column vectors of + length :math:`M` and :math:`N + 1` respectively. + + Different types of filters can be implemented by choosing different values for :math:`A` and :math:`B`. + We provide some examples below. + + Examples + ^^^^^^^^ + + **Unit Delay Filter** + + A filter that delays the input signal by a single time-step simply outputs the previous input value. + + .. math:: y_{i} = x_{i-1} + + This can be implemented as a digital filter with the coefficients :math:`A = [0.0]` and :math:`B = [0.0, 1.0]`. + + **Moving Average Filter** + + A moving average filter is used to smooth out noise in a signal. It is similar to a low-pass filter + but has a finite impulse response (FIR) and is non-recursive. + + The filter calculates the average of the input signal over a window of time-steps. The linear difference + equation for a moving average filter is: + + .. math:: y_{i} = \frac{1}{N} \sum_{j=0}^{N} x_{i-j} + + This can be implemented as a digital filter with the coefficients :math:`A = [0.0]` and + :math:`B = [1/N, 1/N, \cdots, 1/N]`. + + **First-order recursive low-pass filter** + + A recursive low-pass filter is used to smooth out high-frequency noise in a signal. It is a first-order + infinite impulse response (IIR) filter which means it has a recursive component (previous output) in the + linear difference equation. + + A first-order low-pass IIR filter has the difference equation: + + .. math:: y_{i} = \alpha y_{i-1} + (1-\alpha)x_{i} + + where :math:`\alpha` is a smoothing parameter between 0 and 1. Typically, the value of :math:`\alpha` is + chosen based on the desired cut-off frequency of the filter. + + This filter can be implemented as a digital filter with the coefficients :math:`A = [\alpha]` and + :math:`B = [1 - \alpha]`. + """ + + def __init__(self, cfg: modifier_cfg.DigitalFilterCfg, data_dim: tuple[int, ...], device: str) -> None: + """Initializes digital filter. + + Args: + cfg: Configuration parameters. + data_dim: The dimensions of the data to be modified. First element is the batch size + which usually corresponds to number of environments in the simulation. + device: The device to run the modifier on. + + Raises: + ValueError: If filter coefficients are None. + """ + # check that filter coefficients are not None + if cfg.A is None or cfg.B is None: + raise ValueError("Digital filter coefficients A and B must not be None. Please provide valid coefficients.") + + # initialize parent class + super().__init__(cfg, data_dim, device) + + # assign filter coefficients and make sure they are column vectors + self.A = torch.tensor(self._cfg.A, device=self._device).unsqueeze(1) + self.B = torch.tensor(self._cfg.B, device=self._device).unsqueeze(1) + + # create buffer for input and output history + self.x_n = torch.zeros(self._data_dim + (self.B.shape[0],), device=self._device) + self.y_n = torch.zeros(self._data_dim + (self.A.shape[0],), device=self._device) + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + """Resets digital filter history. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + """ + if env_ids is None: + env_ids = slice(None) + # reset history buffers + self.x_n[env_ids] = 0.0 + self.y_n[env_ids] = 0.0
+ +
[文档] def __call__(self, data: torch.Tensor) -> torch.Tensor: + """Applies digital filter modification with a rolling history window inputs and outputs. + + Args: + data: The data to apply filter to. + + Returns: + Filtered data. Shape is the same as data. + """ + # move history window for input + self.x_n = torch.roll(self.x_n, shifts=1, dims=-1) + self.x_n[..., 0] = data + + # calculate current filter value: y[i] = Y*A - X*B + y_i = torch.matmul(self.x_n, self.B) - torch.matmul(self.y_n, self.A) + y_i.squeeze_(-1) + + # move history window for output and add current filter value to history + self.y_n = torch.roll(self.y_n, shifts=1, dims=-1) + self.y_n[..., 0] = y_i + + return y_i
+ + +
[文档]class Integrator(ModifierBase): + r"""Modifier that applies a numerical forward integration based on a middle Reimann sum. + + An integrator is used to calculate the integral of a signal over time. The integral of a signal + is the area under the curve of the signal. The integral can be approximated using numerical methods + such as the `Riemann sum <https://en.wikipedia.org/wiki/Riemann_sum>`_. + + The middle Riemann sum is a method to approximate the integral of a function by dividing the area + under the curve into rectangles. The height of each rectangle is the value of the function at the + midpoint of the interval. The area of each rectangle is the width of the interval multiplied by the + height of the rectangle. + + This integral method is useful for signals that are sampled at regular intervals. The integral + can be written as: + + .. math:: + \int_{t_0}^{t_n} f(t) dt & \approx \int_{t_0}^{t_{n-1}} f(t) dt + \frac{f(t_{n-1}) + f(t_n)}{2} \Delta t + + where :math:`f(t)` is the signal to integrate, :math:`t_i` is the time at the i-th sample, and + :math:`\Delta t` is the time step between samples. + """ + + def __init__(self, cfg: modifier_cfg.IntegratorCfg, data_dim: tuple[int, ...], device: str): + """Initializes the integrator configuration and state. + + Args: + cfg: Integral parameters. + data_dim: The dimensions of the data to be modified. First element is the batch size + which usually corresponds to number of environments in the simulation. + device: The device to run the modifier on. + """ + # initialize parent class + super().__init__(cfg, data_dim, device) + + # assign buffer for integral and previous value + self.integral = torch.zeros(self._data_dim, device=self._device) + self.y_prev = torch.zeros(self._data_dim, device=self._device) + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + """Resets integrator state to zero. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + """ + if env_ids is None: + env_ids = slice(None) + # reset history buffers + self.integral[env_ids] = 0.0 + self.y_prev[env_ids] = 0.0
+ +
[文档] def __call__(self, data: torch.Tensor) -> torch.Tensor: + """Applies integral modification to input data. + + Args: + data: The data to integrate. + + Returns: + Integral of input signal. Shape is the same as data. + """ + # integrate using middle Riemann sum + self.integral += (data + self.y_prev) / 2 * self._cfg.dt + # update previous value + self.y_prev[:] = data + + return self.integral
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/modifiers/modifier_base.html b/_modules/omni/isaac/lab/utils/modifiers/modifier_base.html new file mode 100644 index 0000000000..23ed721bd4 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/modifiers/modifier_base.html @@ -0,0 +1,637 @@ + + + + + + + + + + + omni.isaac.lab.utils.modifiers.modifier_base — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.modifiers.modifier_base 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from abc import ABC, abstractmethod
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from .modifier_cfg import ModifierCfg
+
+
+
[文档]class ModifierBase(ABC): + """Base class for modifiers implemented as classes. + + Modifiers implementations can be functions or classes. If a modifier is a class, it should + inherit from this class and implement the required methods. + + A class implementation of a modifier can be used to store state information between calls. + This is useful for modifiers that require stateful operations, such as rolling averages + or delays or decaying filters. + + Example pseudo-code to create and use the class: + + .. code-block:: python + + from omni.isaac.lab.utils import modifiers + + # define custom keyword arguments to pass to ModifierCfg + kwarg_dict = {"arg_1" : VAL_1, "arg_2" : VAL_2} + + # create modifier configuration object + # func is the class name of the modifier and params is the dictionary of arguments + modifier_config = modifiers.ModifierCfg(func=modifiers.ModifierBase, params=kwarg_dict) + + # define modifier instance + my_modifier = modifiers.ModifierBase(cfg=modifier_config) + + """ + + def __init__(self, cfg: ModifierCfg, data_dim: tuple[int, ...], device: str) -> None: + """Initializes the modifier class. + + Args: + cfg: Configuration parameters. + data_dim: The dimensions of the data to be modified. First element is the batch size + which usually corresponds to number of environments in the simulation. + device: The device to run the modifier on. + """ + self._cfg = cfg + self._data_dim = data_dim + self._device = device + +
[文档] @abstractmethod + def reset(self, env_ids: Sequence[int] | None = None): + """Resets the Modifier. + + Args: + env_ids: The environment ids. Defaults to None, in which case + all environments are considered. + """ + raise NotImplementedError
+ +
[文档] @abstractmethod + def __call__(self, data: torch.Tensor) -> torch.Tensor: + """Abstract method for defining the modification function. + + Args: + data: The data to be modified. Shape should match the data_dim passed during initialization. + + Returns: + Modified data. Shape is the same as the input data. + """ + raise NotImplementedError
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/modifiers/modifier_cfg.html b/_modules/omni/isaac/lab/utils/modifiers/modifier_cfg.html new file mode 100644 index 0000000000..a1f11ff74e --- /dev/null +++ b/_modules/omni/isaac/lab/utils/modifiers/modifier_cfg.html @@ -0,0 +1,637 @@ + + + + + + + + + + + omni.isaac.lab.utils.modifiers.modifier_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.modifiers.modifier_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import torch
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Any
+
+from omni.isaac.lab.utils import configclass
+
+from . import modifier
+
+
+
[文档]@configclass +class ModifierCfg: + """Configuration parameters modifiers""" + + func: Callable[..., torch.Tensor] = MISSING + """Function or callable class used by modifier. + + The function must take a torch tensor as the first argument. The remaining arguments are specified + in the :attr:`params` attribute. + + It also supports `callable classes <https://docs.python.org/3/reference/datamodel.html#object.__call__>`_, + i.e. classes that implement the ``__call__()`` method. In this case, the class should inherit from the + :class:`ModifierBase` class and implement the required methods. + """ + + params: dict[str, Any] = dict() + """The parameters to be passed to the function or callable class as keyword arguments. Defaults to + an empty dictionary."""
+ + +
[文档]@configclass +class DigitalFilterCfg(ModifierCfg): + """Configuration parameters for a digital filter modifier. + + For more information, please check the :class:`DigitalFilter` class. + """ + + func: type[modifier.DigitalFilter] = modifier.DigitalFilter + """The digital filter function to be called for applying the filter.""" + + A: list[float] = MISSING + """The coefficients corresponding the the filter's response to past outputs. + + These correspond to the weights of the past outputs of the filter. The first element is the coefficient + for the output at the previous time step, the second element is the coefficient for the output at two + time steps ago, and so on. + + It is the denominator coefficients of the transfer function of the filter. + """ + + B: list[float] = MISSING + """The coefficients corresponding the the filter's response to current and past inputs. + + These correspond to the weights of the current and past inputs of the filter. The first element is the + coefficient for the current input, the second element is the coefficient for the input at the previous + time step, and so on. + + It is the numerator coefficients of the transfer function of the filter. + """
+ + +
[文档]@configclass +class IntegratorCfg(ModifierCfg): + """Configuration parameters for an integrator modifier. + + For more information, please check the :class:`Integrator` class. + """ + + func: type[modifier.Integrator] = modifier.Integrator + """The integrator function to be called for applying the integrator.""" + + dt: float = MISSING + """The time step of the integrator."""
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/noise/noise_cfg.html b/_modules/omni/isaac/lab/utils/noise/noise_cfg.html new file mode 100644 index 0000000000..52b0365ea6 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/noise/noise_cfg.html @@ -0,0 +1,651 @@ + + + + + + + + + + + omni.isaac.lab.utils.noise.noise_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.noise.noise_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Callable
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+from . import noise_model
+
+
+
[文档]@configclass +class NoiseCfg: + """Base configuration for a noise term.""" + + func: Callable[[torch.Tensor, NoiseCfg], torch.Tensor] = MISSING + """The function to be called for applying the noise. + + Note: + The shape of the input and output tensors must be the same. + """ + operation: Literal["add", "scale", "abs"] = "add" + """The operation to apply the noise on the data. Defaults to "add"."""
+ + +
[文档]@configclass +class ConstantNoiseCfg(NoiseCfg): + """Configuration for an additive constant noise term.""" + + func = noise_model.constant_noise + + bias: torch.Tensor | float = 0.0 + """The bias to add. Defaults to 0.0."""
+ + +
[文档]@configclass +class UniformNoiseCfg(NoiseCfg): + """Configuration for a additive uniform noise term.""" + + func = noise_model.uniform_noise + + n_min: torch.Tensor | float = -1.0 + """The minimum value of the noise. Defaults to -1.0.""" + n_max: torch.Tensor | float = 1.0 + """The maximum value of the noise. Defaults to 1.0."""
+ + +
[文档]@configclass +class GaussianNoiseCfg(NoiseCfg): + """Configuration for an additive gaussian noise term.""" + + func = noise_model.gaussian_noise + + mean: torch.Tensor | float = 0.0 + """The mean of the noise. Defaults to 0.0.""" + std: torch.Tensor | float = 1.0 + """The standard deviation of the noise. Defaults to 1.0."""
+ + +## +# Noise models +## + + +
[文档]@configclass +class NoiseModelCfg: + """Configuration for a noise model.""" + + class_type: type = noise_model.NoiseModel + """The class type of the noise model.""" + + noise_cfg: NoiseCfg = MISSING + """The noise configuration to use."""
+ + +
[文档]@configclass +class NoiseModelWithAdditiveBiasCfg(NoiseModelCfg): + """Configuration for an additive gaussian noise with bias model.""" + + class_type: type = noise_model.NoiseModelWithAdditiveBias + + bias_noise_cfg: NoiseCfg = MISSING + """The noise configuration for the bias. + + Based on this configuration, the bias is sampled at every reset of the noise model. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/noise/noise_model.html b/_modules/omni/isaac/lab/utils/noise/noise_model.html new file mode 100644 index 0000000000..bca85974f5 --- /dev/null +++ b/_modules/omni/isaac/lab/utils/noise/noise_model.html @@ -0,0 +1,741 @@ + + + + + + + + + + + omni.isaac.lab.utils.noise.noise_model — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.noise.noise_model 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from . import noise_cfg
+
+##
+# Noise as functions.
+##
+
+
+
[文档]def constant_noise(data: torch.Tensor, cfg: noise_cfg.ConstantNoiseCfg) -> torch.Tensor: + """Applies a constant noise bias to a given data set. + + Args: + data: The unmodified data set to apply noise to. + cfg: The configuration parameters for constant noise. + + Returns: + The data modified by the noise parameters provided. + """ + + # fix tensor device for bias on first call and update config parameters + if isinstance(cfg.bias, torch.Tensor): + cfg.bias = cfg.bias.to(device=data.device) + + if cfg.operation == "add": + return data + cfg.bias + elif cfg.operation == "scale": + return data * cfg.bias + elif cfg.operation == "abs": + return torch.zeros_like(data) + cfg.bias + else: + raise ValueError(f"Unknown operation in noise: {cfg.operation}")
+ + +
[文档]def uniform_noise(data: torch.Tensor, cfg: noise_cfg.UniformNoiseCfg) -> torch.Tensor: + """Applies a uniform noise to a given data set. + + Args: + data: The unmodified data set to apply noise to. + cfg: The configuration parameters for uniform noise. + + Returns: + The data modified by the noise parameters provided. + """ + + # fix tensor device for n_max on first call and update config parameters + if isinstance(cfg.n_max, torch.Tensor): + cfg.n_max = cfg.n_max.to(data.device) + # fix tensor device for n_min on first call and update config parameters + if isinstance(cfg.n_min, torch.Tensor): + cfg.n_min = cfg.n_min.to(data.device) + + if cfg.operation == "add": + return data + torch.rand_like(data) * (cfg.n_max - cfg.n_min) + cfg.n_min + elif cfg.operation == "scale": + return data * (torch.rand_like(data) * (cfg.n_max - cfg.n_min) + cfg.n_min) + elif cfg.operation == "abs": + return torch.rand_like(data) * (cfg.n_max - cfg.n_min) + cfg.n_min + else: + raise ValueError(f"Unknown operation in noise: {cfg.operation}")
+ + +
[文档]def gaussian_noise(data: torch.Tensor, cfg: noise_cfg.GaussianNoiseCfg) -> torch.Tensor: + """Applies a gaussian noise to a given data set. + + Args: + data: The unmodified data set to apply noise to. + cfg: The configuration parameters for gaussian noise. + + Returns: + The data modified by the noise parameters provided. + """ + + # fix tensor device for mean on first call and update config parameters + if isinstance(cfg.mean, torch.Tensor): + cfg.mean = cfg.mean.to(data.device) + # fix tensor device for std on first call and update config parameters + if isinstance(cfg.std, torch.Tensor): + cfg.std = cfg.std.to(data.device) + + if cfg.operation == "add": + return data + cfg.mean + cfg.std * torch.randn_like(data) + elif cfg.operation == "scale": + return data * (cfg.mean + cfg.std * torch.randn_like(data)) + elif cfg.operation == "abs": + return cfg.mean + cfg.std * torch.randn_like(data) + else: + raise ValueError(f"Unknown operation in noise: {cfg.operation}")
+ + +## +# Noise models as classes +## + + +
[文档]class NoiseModel: + """Base class for noise models.""" + + def __init__(self, noise_model_cfg: noise_cfg.NoiseModelCfg, num_envs: int, device: str): + """Initialize the noise model. + + Args: + noise_model_cfg: The noise configuration to use. + num_envs: The number of environments. + device: The device to use for the noise model. + """ + self._noise_model_cfg = noise_model_cfg + self._num_envs = num_envs + self._device = device + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + """Reset the noise model. + + This method can be implemented by derived classes to reset the noise model. + This is useful when implementing temporal noise models such as random walk. + + Args: + env_ids: The environment ids to reset the noise model for. Defaults to None, + in which case all environments are considered. + """ + pass
+ +
[文档] def apply(self, data: torch.Tensor) -> torch.Tensor: + """Apply the noise to the data. + + Args: + data: The data to apply the noise to. Shape is (num_envs, ...). + + Returns: + The data with the noise applied. Shape is the same as the input data. + """ + return self._noise_model_cfg.noise_cfg.func(data, self._noise_model_cfg.noise_cfg)
+ + +
[文档]class NoiseModelWithAdditiveBias(NoiseModel): + """Noise model with an additive bias. + + The bias term is sampled from a the specified distribution on reset. + """ + + def __init__(self, noise_model_cfg: noise_cfg.NoiseModelWithAdditiveBiasCfg, num_envs: int, device: str): + # initialize parent class + super().__init__(noise_model_cfg, num_envs, device) + # store the bias noise configuration + self._bias_noise_cfg = noise_model_cfg.bias_noise_cfg + self._bias = torch.zeros((num_envs, 1), device=self._device) + +
[文档] def reset(self, env_ids: Sequence[int] | None = None): + """Reset the noise model. + + This method resets the bias term for the specified environments. + + Args: + env_ids: The environment ids to reset the noise model for. Defaults to None, + in which case all environments are considered. + """ + # resolve the environment ids + if env_ids is None: + env_ids = slice(None) + # reset the bias term + self._bias[env_ids] = self._bias_noise_cfg.func(self._bias[env_ids], self._bias_noise_cfg)
+ +
[文档] def apply(self, data: torch.Tensor) -> torch.Tensor: + """Apply bias noise to the data. + + Args: + data: The data to apply the noise to. Shape is (num_envs, ...). + + Returns: + The data with the noise applied. Shape is the same as the input data. + """ + return super().apply(data) + self._bias
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/string.html b/_modules/omni/isaac/lab/utils/string.html new file mode 100644 index 0000000000..c07414459d --- /dev/null +++ b/_modules/omni/isaac/lab/utils/string.html @@ -0,0 +1,927 @@ + + + + + + + + + + + omni.isaac.lab.utils.string — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.string 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module containing utilities for transforming strings and regular expressions."""
+
+import ast
+import importlib
+import inspect
+import re
+from collections.abc import Callable, Sequence
+from typing import Any
+
+"""
+String formatting.
+"""
+
+
+
[文档]def to_camel_case(snake_str: str, to: str = "cC") -> str: + """Converts a string from snake case to camel case. + + Args: + snake_str: A string in snake case (i.e. with '_') + to: Convention to convert string to. Defaults to "cC". + + Raises: + ValueError: Invalid input argument `to`, i.e. not "cC" or "CC". + + Returns: + A string in camel-case format. + """ + # check input is correct + if to not in ["cC", "CC"]: + msg = "to_camel_case(): Choose a valid `to` argument (CC or cC)" + raise ValueError(msg) + # convert string to lower case and split + components = snake_str.lower().split("_") + if to == "cC": + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + "".join(x.title() for x in components[1:]) + else: + # Capitalize first letter in all the components + return "".join(x.title() for x in components)
+ + +
[文档]def to_snake_case(camel_str: str) -> str: + """Converts a string from camel case to snake case. + + Args: + camel_str: A string in camel case. + + Returns: + A string in snake case (i.e. with '_') + """ + camel_str = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_str) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", camel_str).lower()
+ + +
[文档]def string_to_slice(s: str): + """Convert a string representation of a slice to a slice object. + + Args: + s: The string representation of the slice. + + Returns: + The slice object. + """ + # extract the content inside the slice() + match = re.match(r"slice\((.*),(.*),(.*)\)", s) + if not match: + raise ValueError(f"Invalid slice string format: {s}") + + # extract start, stop, and step values + start_str, stop_str, step_str = match.groups() + + # convert 'None' to None and other strings to integers + start = None if start_str == "None" else int(start_str) + stop = None if stop_str == "None" else int(stop_str) + step = None if step_str == "None" else int(step_str) + + # create and return the slice object + return slice(start, stop, step)
+ + +""" +String <-> Callable operations. +""" + + +
[文档]def is_lambda_expression(name: str) -> bool: + """Checks if the input string is a lambda expression. + + Args: + name: The input string. + + Returns: + Whether the input string is a lambda expression. + """ + try: + ast.parse(name) + return isinstance(ast.parse(name).body[0], ast.Expr) and isinstance(ast.parse(name).body[0].value, ast.Lambda) + except SyntaxError: + return False
+ + +
[文档]def callable_to_string(value: Callable) -> str: + """Converts a callable object to a string. + + Args: + value: A callable object. + + Raises: + ValueError: When the input argument is not a callable object. + + Returns: + A string representation of the callable object. + """ + # check if callable + if not callable(value): + raise ValueError(f"The input argument is not callable: {value}.") + # check if lambda function + if value.__name__ == "<lambda>": + # we resolve the lambda expression by checking the source code and extracting the line with lambda expression + # we also remove any comments from the line + lambda_line = inspect.getsourcelines(value)[0][0].strip().split("lambda")[1].strip().split(",")[0] + lambda_line = re.sub(r"#.*$", "", lambda_line).rstrip() + return f"lambda {lambda_line}" + else: + # get the module and function name + module_name = value.__module__ + function_name = value.__name__ + # return the string + return f"{module_name}:{function_name}"
+ + +
[文档]def string_to_callable(name: str) -> Callable: + """Resolves the module and function names to return the function. + + Args: + name: The function name. The format should be 'module:attribute_name' or a + lambda expression of format: 'lambda x: x'. + + Raises: + ValueError: When the resolved attribute is not a function. + ValueError: When the module cannot be found. + + Returns: + Callable: The function loaded from the module. + """ + try: + if is_lambda_expression(name): + callable_object = eval(name) + else: + mod_name, attr_name = name.split(":") + mod = importlib.import_module(mod_name) + callable_object = getattr(mod, attr_name) + # check if attribute is callable + if callable(callable_object): + return callable_object + else: + raise AttributeError(f"The imported object is not callable: '{name}'") + except (ValueError, ModuleNotFoundError) as e: + msg = ( + f"Could not resolve the input string '{name}' into callable object." + " The format of input should be 'module:attribute_name'.\n" + f"Received the error:\n {e}." + ) + raise ValueError(msg)
+ + +""" +Regex operations. +""" + + +
[文档]def resolve_matching_names( + keys: str | Sequence[str], list_of_strings: Sequence[str], preserve_order: bool = False +) -> tuple[list[int], list[str]]: + """Match a list of query regular expressions against a list of strings and return the matched indices and names. + + When a list of query regular expressions is provided, the function checks each target string against each + query regular expression and returns the indices of the matched strings and the matched strings. + + If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order + of the provided list of strings. This means that the ordering is dictated by the order of the target strings + and not the order of the query regular expressions. + + If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order + of the provided list of query regular expressions. + + For example, consider the list of strings is ['a', 'b', 'c', 'd', 'e'] and the regular expressions are ['a|c', 'b']. + If :attr:`preserve_order` is False, then the function will return the indices of the matched strings and the + strings as: ([0, 1, 2], ['a', 'b', 'c']). When :attr:`preserve_order` is True, it will return them as: + ([0, 2, 1], ['a', 'c', 'b']). + + Note: + The function does not sort the indices. It returns the indices in the order they are found. + + Args: + keys: A regular expression or a list of regular expressions to match the strings in the list. + list_of_strings: A list of strings to match. + preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False. + + Returns: + A tuple of lists containing the matched indices and names. + + Raises: + ValueError: When multiple matches are found for a string in the list. + ValueError: When not all regular expressions are matched. + """ + # resolve name keys + if isinstance(keys, str): + keys = [keys] + # find matching patterns + index_list = [] + names_list = [] + key_idx_list = [] + # book-keeping to check that we always have a one-to-one mapping + # i.e. each target string should match only one regular expression + target_strings_match_found = [None for _ in range(len(list_of_strings))] + keys_match_found = [[] for _ in range(len(keys))] + # loop over all target strings + for target_index, potential_match_string in enumerate(list_of_strings): + for key_index, re_key in enumerate(keys): + if re.fullmatch(re_key, potential_match_string): + # check if match already found + if target_strings_match_found[target_index]: + raise ValueError( + f"Multiple matches for '{potential_match_string}':" + f" '{target_strings_match_found[target_index]}' and '{re_key}'!" + ) + # add to list + target_strings_match_found[target_index] = re_key + index_list.append(target_index) + names_list.append(potential_match_string) + key_idx_list.append(key_index) + # add for regex key + keys_match_found[key_index].append(potential_match_string) + # reorder keys if they should be returned in order of the query keys + if preserve_order: + reordered_index_list = [None] * len(index_list) + global_index = 0 + for key_index in range(len(keys)): + for key_idx_position, key_idx_entry in enumerate(key_idx_list): + if key_idx_entry == key_index: + reordered_index_list[key_idx_position] = global_index + global_index += 1 + # reorder index and names list + index_list_reorder = [None] * len(index_list) + names_list_reorder = [None] * len(index_list) + for idx, reorder_idx in enumerate(reordered_index_list): + index_list_reorder[reorder_idx] = index_list[idx] + names_list_reorder[reorder_idx] = names_list[idx] + # update + index_list = index_list_reorder + names_list = names_list_reorder + # check that all regular expressions are matched + if not all(keys_match_found): + # make this print nicely aligned for debugging + msg = "\n" + for key, value in zip(keys, keys_match_found): + msg += f"\t{key}: {value}\n" + msg += f"Available strings: {list_of_strings}\n" + # raise error + raise ValueError( + f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}" + ) + # return + return index_list, names_list
+ + +
[文档]def resolve_matching_names_values( + data: dict[str, Any], list_of_strings: Sequence[str], preserve_order: bool = False +) -> tuple[list[int], list[str], list[Any]]: + """Match a list of regular expressions in a dictionary against a list of strings and return + the matched indices, names, and values. + + If the :attr:`preserve_order` is True, the ordering of the matched indices and names is the same as the order + of the provided list of strings. This means that the ordering is dictated by the order of the target strings + and not the order of the query regular expressions. + + If the :attr:`preserve_order` is False, the ordering of the matched indices and names is the same as the order + of the provided list of query regular expressions. + + For example, consider the dictionary is {"a|d|e": 1, "b|c": 2}, the list of strings is ['a', 'b', 'c', 'd', 'e']. + If :attr:`preserve_order` is False, then the function will return the indices of the matched strings, the + matched strings, and the values as: ([0, 1, 2, 3, 4], ['a', 'b', 'c', 'd', 'e'], [1, 2, 2, 1, 1]). When + :attr:`preserve_order` is True, it will return them as: ([0, 3, 4, 1, 2], ['a', 'd', 'e', 'b', 'c'], [1, 1, 1, 2, 2]). + + Args: + data: A dictionary of regular expressions and values to match the strings in the list. + list_of_strings: A list of strings to match. + preserve_order: Whether to preserve the order of the query keys in the returned values. Defaults to False. + + Returns: + A tuple of lists containing the matched indices, names, and values. + + Raises: + TypeError: When the input argument :attr:`data` is not a dictionary. + ValueError: When multiple matches are found for a string in the dictionary. + ValueError: When not all regular expressions in the data keys are matched. + """ + # check valid input + if not isinstance(data, dict): + raise TypeError(f"Input argument `data` should be a dictionary. Received: {data}") + # find matching patterns + index_list = [] + names_list = [] + values_list = [] + key_idx_list = [] + # book-keeping to check that we always have a one-to-one mapping + # i.e. each target string should match only one regular expression + target_strings_match_found = [None for _ in range(len(list_of_strings))] + keys_match_found = [[] for _ in range(len(data))] + # loop over all target strings + for target_index, potential_match_string in enumerate(list_of_strings): + for key_index, (re_key, value) in enumerate(data.items()): + if re.fullmatch(re_key, potential_match_string): + # check if match already found + if target_strings_match_found[target_index]: + raise ValueError( + f"Multiple matches for '{potential_match_string}':" + f" '{target_strings_match_found[target_index]}' and '{re_key}'!" + ) + # add to list + target_strings_match_found[target_index] = re_key + index_list.append(target_index) + names_list.append(potential_match_string) + values_list.append(value) + key_idx_list.append(key_index) + # add for regex key + keys_match_found[key_index].append(potential_match_string) + # reorder keys if they should be returned in order of the query keys + if preserve_order: + reordered_index_list = [None] * len(index_list) + global_index = 0 + for key_index in range(len(data)): + for key_idx_position, key_idx_entry in enumerate(key_idx_list): + if key_idx_entry == key_index: + reordered_index_list[key_idx_position] = global_index + global_index += 1 + # reorder index and names list + index_list_reorder = [None] * len(index_list) + names_list_reorder = [None] * len(index_list) + values_list_reorder = [None] * len(index_list) + for idx, reorder_idx in enumerate(reordered_index_list): + index_list_reorder[reorder_idx] = index_list[idx] + names_list_reorder[reorder_idx] = names_list[idx] + values_list_reorder[reorder_idx] = values_list[idx] + # update + index_list = index_list_reorder + names_list = names_list_reorder + values_list = values_list_reorder + # check that all regular expressions are matched + if not all(keys_match_found): + # make this print nicely aligned for debugging + msg = "\n" + for key, value in zip(data.keys(), keys_match_found): + msg += f"\t{key}: {value}\n" + msg += f"Available strings: {list_of_strings}\n" + # raise error + raise ValueError( + f"Not all regular expressions are matched! Please check that the regular expressions are correct: {msg}" + ) + # return + return index_list, names_list, values_list
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/timer.html b/_modules/omni/isaac/lab/utils/timer.html new file mode 100644 index 0000000000..a3c6efd3fb --- /dev/null +++ b/_modules/omni/isaac/lab/utils/timer.html @@ -0,0 +1,730 @@ + + + + + + + + + + + omni.isaac.lab.utils.timer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.timer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module for a timer class that can be used for performance measurements."""
+
+from __future__ import annotations
+
+import time
+from contextlib import ContextDecorator
+from typing import Any, ClassVar
+
+
+
[文档]class TimerError(Exception): + """A custom exception used to report errors in use of :class:`Timer` class.""" + + pass
+ + +
[文档]class Timer(ContextDecorator): + """A timer for performance measurements. + + A class to keep track of time for performance measurement. + It allows timing via context managers and decorators as well. + + It uses the `time.perf_counter` function to measure time. This function + returns the number of seconds since the epoch as a float. It has the + highest resolution available on the system. + + As a regular object: + + .. code-block:: python + + import time + + from omni.isaac.lab.utils.timer import Timer + + timer = Timer() + timer.start() + time.sleep(1) + print(1 <= timer.time_elapsed <= 2) # Output: True + + time.sleep(1) + timer.stop() + print(2 <= stopwatch.total_run_time) # Output: True + + As a context manager: + + .. code-block:: python + + import time + + from omni.isaac.lab.utils.timer import Timer + + with Timer() as timer: + time.sleep(1) + print(1 <= timer.time_elapsed <= 2) # Output: True + + Reference: https://gist.github.com/sumeet/1123871 + """ + + timing_info: ClassVar[dict[str, float]] = dict() + """Dictionary for storing the elapsed time per timer instances globally. + + This dictionary logs the timer information. The keys are the names given to the timer class + at its initialization. If no :attr:`name` is passed to the constructor, no time + is recorded in the dictionary. + """ + +
[文档] def __init__(self, msg: str | None = None, name: str | None = None): + """Initializes the timer. + + Args: + msg: The message to display when using the timer + class in a context manager. Defaults to None. + name: The name to use for logging times in a global + dictionary. Defaults to None. + """ + self._msg = msg + self._name = name + self._start_time = None + self._stop_time = None + self._elapsed_time = None
+ + def __str__(self) -> str: + """A string representation of the class object. + + Returns: + A string containing the elapsed time. + """ + return f"{self.time_elapsed:0.6f} seconds" + + """ + Properties + """ + + @property + def time_elapsed(self) -> float: + """The number of seconds that have elapsed since this timer started timing. + + Note: + This is used for checking how much time has elapsed while the timer is still running. + """ + return time.perf_counter() - self._start_time + + @property + def total_run_time(self) -> float: + """The number of seconds that elapsed from when the timer started to when it ended.""" + return self._elapsed_time + + """ + Operations + """ + +
[文档] def start(self): + """Start timing.""" + if self._start_time is not None: + raise TimerError("Timer is running. Use .stop() to stop it") + + self._start_time = time.perf_counter()
+ +
[文档] def stop(self): + """Stop timing.""" + if self._start_time is None: + raise TimerError("Timer is not running. Use .start() to start it") + + self._stop_time = time.perf_counter() + self._elapsed_time = self._stop_time - self._start_time + self._start_time = None + + if self._name: + Timer.timing_info[self._name] = self._elapsed_time
+ + """ + Context managers + """ + + def __enter__(self) -> Timer: + """Start timing and return this `Timer` instance.""" + self.start() + return self + + def __exit__(self, *exc_info: Any): + """Stop timing.""" + self.stop() + # print message + if self._msg is not None: + print(self._msg, f": {self._elapsed_time:0.6f} seconds") + + """ + Static Methods + """ + +
[文档] @staticmethod + def get_timer_info(name: str) -> float: + """Retrieves the time logged in the global dictionary + based on name. + + Args: + name: Name of the the entry to be retrieved. + + Raises: + TimerError: If name doesn't exist in the log. + + Returns: + A float containing the time logged if the name exists. + """ + if name not in Timer.timing_info: + raise TimerError(f"Timer {name} does not exist") + return Timer.timing_info.get(name)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/types.html b/_modules/omni/isaac/lab/utils/types.html new file mode 100644 index 0000000000..ff68fb366f --- /dev/null +++ b/_modules/omni/isaac/lab/utils/types.html @@ -0,0 +1,598 @@ + + + + + + + + + + + omni.isaac.lab.utils.types — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.types 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module for different data types."""
+
+from __future__ import annotations
+
+import torch
+from collections.abc import Sequence
+from dataclasses import dataclass
+
+
+
[文档]@dataclass +class ArticulationActions: + """Data container to store articulation's joints actions. + + This class is used to store the actions of the joints of an articulation. + It is used to store the joint positions, velocities, efforts, and indices. + + If the actions are not provided, the values are set to None. + """ + + joint_positions: torch.Tensor | None = None + """The joint positions of the articulation. Defaults to None.""" + + joint_velocities: torch.Tensor | None = None + """The joint velocities of the articulation. Defaults to None.""" + + joint_efforts: torch.Tensor | None = None + """The joint efforts of the articulation. Defaults to None.""" + + joint_indices: torch.Tensor | Sequence[int] | slice | None = None + """The joint indices of the articulation. Defaults to None. + + If the joint indices are a slice, this indicates that the indices are continuous and correspond + to all the joints of the articulation. We use a slice to make the indexing more efficient. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab/utils/warp/ops.html b/_modules/omni/isaac/lab/utils/warp/ops.html new file mode 100644 index 0000000000..cf41fc73db --- /dev/null +++ b/_modules/omni/isaac/lab/utils/warp/ops.html @@ -0,0 +1,704 @@ + + + + + + + + + + + omni.isaac.lab.utils.warp.ops — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab.utils.warp.ops 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Wrapping around warp kernels for compatibility with torch tensors."""
+
+# needed to import for allowing type-hinting: torch.Tensor | None
+from __future__ import annotations
+
+import numpy as np
+import torch
+
+import warp as wp
+
+# disable warp module initialization messages
+wp.config.quiet = True
+# initialize the warp module
+wp.init()
+
+from . import kernels
+
+
+
[文档]def raycast_mesh( + ray_starts: torch.Tensor, + ray_directions: torch.Tensor, + mesh: wp.Mesh, + max_dist: float = 1e6, + return_distance: bool = False, + return_normal: bool = False, + return_face_id: bool = False, +) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]: + """Performs ray-casting against a mesh. + + Note that the `ray_starts` and `ray_directions`, and `ray_hits` should have compatible shapes + and data types to ensure proper execution. Additionally, they all must be in the same frame. + + Args: + ray_starts: The starting position of the rays. Shape (N, 3). + ray_directions: The ray directions for each ray. Shape (N, 3). + mesh: The warp mesh to ray-cast against. + max_dist: The maximum distance to ray-cast. Defaults to 1e6. + return_distance: Whether to return the distance of the ray until it hits the mesh. Defaults to False. + return_normal: Whether to return the normal of the mesh face the ray hits. Defaults to False. + return_face_id: Whether to return the face id of the mesh face the ray hits. Defaults to False. + + Returns: + The ray hit position. Shape (N, 3). + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit distance. Shape (N,). + Will only return if :attr:`return_distance` is True, else returns None. + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit normal. Shape (N, 3). + Will only return if :attr:`return_normal` is True else returns None. + The returned tensor contains :obj:`float('inf')` for missed hits. + The ray hit face id. Shape (N,). + Will only return if :attr:`return_face_id` is True else returns None. + The returned tensor contains :obj:`int(-1)` for missed hits. + """ + # extract device and shape information + shape = ray_starts.shape + device = ray_starts.device + # device of the mesh + torch_device = wp.device_to_torch(mesh.device) + # reshape the tensors + ray_starts = ray_starts.to(torch_device).view(-1, 3).contiguous() + ray_directions = ray_directions.to(torch_device).view(-1, 3).contiguous() + num_rays = ray_starts.shape[0] + # create output tensor for the ray hits + ray_hits = torch.full((num_rays, 3), float("inf"), device=torch_device).contiguous() + + # map the memory to warp arrays + ray_starts_wp = wp.from_torch(ray_starts, dtype=wp.vec3) + ray_directions_wp = wp.from_torch(ray_directions, dtype=wp.vec3) + ray_hits_wp = wp.from_torch(ray_hits, dtype=wp.vec3) + + if return_distance: + ray_distance = torch.full((num_rays,), float("inf"), device=torch_device).contiguous() + ray_distance_wp = wp.from_torch(ray_distance, dtype=wp.float32) + else: + ray_distance = None + ray_distance_wp = wp.empty((1,), dtype=wp.float32, device=torch_device) + + if return_normal: + ray_normal = torch.full((num_rays, 3), float("inf"), device=torch_device).contiguous() + ray_normal_wp = wp.from_torch(ray_normal, dtype=wp.vec3) + else: + ray_normal = None + ray_normal_wp = wp.empty((1,), dtype=wp.vec3, device=torch_device) + + if return_face_id: + ray_face_id = torch.ones((num_rays,), dtype=torch.int32, device=torch_device).contiguous() * (-1) + ray_face_id_wp = wp.from_torch(ray_face_id, dtype=wp.int32) + else: + ray_face_id = None + ray_face_id_wp = wp.empty((1,), dtype=wp.int32, device=torch_device) + + # launch the warp kernel + wp.launch( + kernel=kernels.raycast_mesh_kernel, + dim=num_rays, + inputs=[ + mesh.id, + ray_starts_wp, + ray_directions_wp, + ray_hits_wp, + ray_distance_wp, + ray_normal_wp, + ray_face_id_wp, + float(max_dist), + int(return_distance), + int(return_normal), + int(return_face_id), + ], + device=mesh.device, + ) + # NOTE: Synchronize is not needed anymore, but we keep it for now. Check with @dhoeller. + wp.synchronize() + + if return_distance: + ray_distance = ray_distance.to(device).view(shape[0], shape[1]) + if return_normal: + ray_normal = ray_normal.to(device).view(shape) + if return_face_id: + ray_face_id = ray_face_id.to(device).view(shape[0], shape[1]) + + return ray_hits.to(device).view(shape), ray_distance, ray_normal, ray_face_id
+ + +
[文档]def convert_to_warp_mesh(points: np.ndarray, indices: np.ndarray, device: str) -> wp.Mesh: + """Create a warp mesh object with a mesh defined from vertices and triangles. + + Args: + points: The vertices of the mesh. Shape is (N, 3), where N is the number of vertices. + indices: The triangles of the mesh as references to vertices for each triangle. + Shape is (M, 3), where M is the number of triangles / faces. + device: The device to use for the mesh. + + Returns: + The warp mesh object. + """ + 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), + )
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.html b/_modules/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.html new file mode 100644 index 0000000000..b5936cd38f --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/data_collector/robomimic_data_collector.html @@ -0,0 +1,841 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.data_collector.robomimic_data_collector — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.data_collector.robomimic_data_collector 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Interface to collect and store data from the environment using format from `robomimic`."""
+
+# needed to import for allowing type-hinting: np.ndarray | torch.Tensor
+from __future__ import annotations
+
+import h5py
+import json
+import numpy as np
+import os
+import torch
+from collections.abc import Iterable
+
+import omni.log
+
+
+
[文档]class RobomimicDataCollector: + """Data collection interface for robomimic. + + This class implements a data collector interface for saving simulation states to disk. + The data is stored in `HDF5`_ binary data format. The class is useful for collecting + demonstrations. The collected data follows the `structure`_ from robomimic. + + All datasets in `robomimic` require the observations and next observations obtained + from before and after the environment step. These are stored as a dictionary of + observations in the keys "obs" and "next_obs" respectively. + + For certain agents in `robomimic`, the episode data should have the following + additional keys: "actions", "rewards", "dones". This behavior can be altered by changing + the dataset keys required in the training configuration for the respective learning agent. + + For reference on datasets, please check the robomimic `documentation`. + + .. _HDF5: https://www.h5py.org/ + .. _structure: https://robomimic.github.io/docs/datasets/overview.html#dataset-structure + .. _documentation: https://github.com/ARISE-Initiative/robomimic/blob/master/robomimic/config/base_config.py#L167-L173 + """ + +
[文档] def __init__( + self, + env_name: str, + directory_path: str, + filename: str = "test", + num_demos: int = 1, + flush_freq: int = 1, + env_config: dict | None = None, + ): + """Initializes the data collection wrapper. + + Args: + env_name: The name of the environment. + directory_path: The path to store collected data. + filename: The basename of the saved file. Defaults to "test". + num_demos: Number of demonstrations to record until stopping. Defaults to 1. + flush_freq: Frequency to dump data to disk. Defaults to 1. + env_config: The configuration for the environment. Defaults to None. + """ + # save input arguments + self._env_name = env_name + self._env_config = env_config + self._directory = os.path.abspath(directory_path) + self._filename = filename + self._num_demos = num_demos + self._flush_freq = flush_freq + # print info + print(self.__str__()) + + # create directory it doesn't exist + if not os.path.isdir(self._directory): + os.makedirs(self._directory) + + # placeholder for current hdf5 file object + self._h5_file_stream = None + self._h5_data_group = None + self._h5_episode_group = None + + # store count of demos within episode + self._demo_count = 0 + # flags for setting up + self._is_first_interaction = True + self._is_stop = False + # create buffers to store data + self._dataset = dict()
+ + def __del__(self): + """Destructor for data collector.""" + if not self._is_stop: + self.close() + + def __str__(self) -> str: + """Represents the data collector as a string.""" + msg = "Dataset collector <class RobomimicDataCollector> object" + msg += f"\tStoring trajectories in directory: {self._directory}\n" + msg += f"\tNumber of demos for collection : {self._num_demos}\n" + msg += f"\tFrequency for saving data to disk: {self._flush_freq}\n" + + return msg + + """ + Properties + """ + + @property + def demo_count(self) -> int: + """The number of demos collected so far.""" + return self._demo_count + + """ + Operations. + """ + +
[文档] def is_stopped(self) -> bool: + """Whether data collection is stopped or not. + + Returns: + True if data collection has stopped. + """ + return self._is_stop
+ +
[文档] def reset(self): + """Reset the internals of data logger.""" + # setup the file to store data in + if self._is_first_interaction: + self._demo_count = 0 + self._create_new_file(self._filename) + self._is_first_interaction = False + # clear out existing buffers + self._dataset = dict()
+ +
[文档] def add(self, key: str, value: np.ndarray | torch.Tensor): + """Add a key-value pair to the dataset. + + The key can be nested by using the "/" character. For example: + "obs/joint_pos". Currently only two-level nesting is supported. + + Args: + key: The key name. + value: The corresponding value + of shape (N, ...), where `N` is number of environments. + + Raises: + ValueError: When provided key has sub-keys more than 2. Example: "obs/joints/pos", instead + of "obs/joint_pos". + """ + # check if data should be recorded + if self._is_first_interaction: + omni.log.warn("Please call reset before adding new data. Calling reset...") + self.reset() + if self._is_stop: + omni.log.warn(f"Desired number of demonstrations collected: {self._demo_count} >= {self._num_demos}.") + return + # check datatype + if isinstance(value, torch.Tensor): + value = value.cpu().numpy() + else: + value = np.asarray(value) + # check if there are sub-keys + sub_keys = key.split("/") + num_sub_keys = len(sub_keys) + if len(sub_keys) > 2: + raise ValueError(f"Input key '{key}' has elements {num_sub_keys} which is more than two.") + # add key to dictionary if it doesn't exist + for i in range(value.shape[0]): + # demo index + if f"env_{i}" not in self._dataset: + self._dataset[f"env_{i}"] = dict() + # key index + if num_sub_keys == 2: + # create keys + if sub_keys[0] not in self._dataset[f"env_{i}"]: + self._dataset[f"env_{i}"][sub_keys[0]] = dict() + if sub_keys[1] not in self._dataset[f"env_{i}"][sub_keys[0]]: + self._dataset[f"env_{i}"][sub_keys[0]][sub_keys[1]] = list() + # add data to key + self._dataset[f"env_{i}"][sub_keys[0]][sub_keys[1]].append(value[i]) + else: + # create keys + if sub_keys[0] not in self._dataset[f"env_{i}"]: + self._dataset[f"env_{i}"][sub_keys[0]] = list() + # add data to key + self._dataset[f"env_{i}"][sub_keys[0]].append(value[i])
+ +
[文档] def flush(self, env_ids: Iterable[int] = (0,)): + """Flush the episode data based on environment indices. + + Args: + env_ids: Environment indices to write data for. Defaults to (0). + """ + # check that data is being recorded + if self._h5_file_stream is None or self._h5_data_group is None: + omni.log.error("No file stream has been opened. Please call reset before flushing data.") + return + + # iterate over each environment and add their data + for index in env_ids: + # data corresponding to demo + env_dataset = self._dataset[f"env_{index}"] + + # create episode group based on demo count + h5_episode_group = self._h5_data_group.create_group(f"demo_{self._demo_count}") + # store number of steps taken + h5_episode_group.attrs["num_samples"] = len(env_dataset["actions"]) + # store other data from dictionary + for key, value in env_dataset.items(): + if isinstance(value, dict): + # create group + key_group = h5_episode_group.create_group(key) + # add sub-keys values + for sub_key, sub_value in value.items(): + key_group.create_dataset(sub_key, data=np.array(sub_value)) + else: + h5_episode_group.create_dataset(key, data=np.array(value)) + # increment total step counts + self._h5_data_group.attrs["total"] += h5_episode_group.attrs["num_samples"] + + # increment total demo counts + self._demo_count += 1 + # reset buffer for environment + self._dataset[f"env_{index}"] = dict() + + # dump at desired frequency + if self._demo_count % self._flush_freq == 0: + self._h5_file_stream.flush() + print(f">>> Flushing data to disk. Collected demos: {self._demo_count} / {self._num_demos}") + + # if demos collected then stop + if self._demo_count >= self._num_demos: + print(f">>> Desired number of demonstrations collected: {self._demo_count} >= {self._num_demos}.") + self.close() + # break out of loop + break
+ +
[文档] def close(self): + """Stop recording and save the file at its current state.""" + if not self._is_stop: + print(f">>> Closing recording of data. Collected demos: {self._demo_count} / {self._num_demos}") + # close the file safely + if self._h5_file_stream is not None: + self._h5_file_stream.close() + # mark that data collection is stopped + self._is_stop = True
+ + """ + Helper functions. + """ + + def _create_new_file(self, fname: str): + """Create a new HDF5 file for writing episode info into. + + Reference: + https://robomimic.github.io/docs/datasets/overview.html + + Args: + fname: The base name of the file. + """ + if not fname.endswith(".hdf5"): + fname += ".hdf5" + # define path to file + hdf5_path = os.path.join(self._directory, fname) + # construct the stream object + self._h5_file_stream = h5py.File(hdf5_path, "w") + # create group to store data + self._h5_data_group = self._h5_file_stream.create_group("data") + # stores total number of samples accumulated across demonstrations + self._h5_data_group.attrs["total"] = 0 + # store the environment meta-info + # -- we use gym environment type + # Ref: https://github.com/ARISE-Initiative/robomimic/blob/master/robomimic/envs/env_base.py#L15 + env_type = 2 + # -- check if env config provided + if self._env_config is None: + self._env_config = dict() + # -- add info + self._h5_data_group.attrs["env_args"] = json.dumps({ + "env_name": self._env_name, + "type": env_type, + "env_kwargs": self._env_config, + })
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/importer.html b/_modules/omni/isaac/lab_tasks/utils/importer.html new file mode 100644 index 0000000000..ac6de31433 --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/importer.html @@ -0,0 +1,646 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.importer — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.importer 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module with utility for importing all modules in a package recursively."""
+
+from __future__ import annotations
+
+import importlib
+import pkgutil
+import sys
+
+
+
[文档]def import_packages(package_name: str, blacklist_pkgs: list[str] | None = None): + """Import all sub-packages in a package recursively. + + It is easier to use this function to import all sub-packages in a package recursively + than to manually import each sub-package. + + It replaces the need of the following code snippet on the top of each package's ``__init__.py`` file: + + .. code-block:: python + + import .locomotion.velocity + import .manipulation.reach + import .manipulation.lift + + Args: + package_name: The package name. + blacklist_pkgs: The list of blacklisted packages to skip. Defaults to None, + which means no packages are blacklisted. + """ + # Default blacklist + if blacklist_pkgs is None: + blacklist_pkgs = [] + # Import the package itself + package = importlib.import_module(package_name) + # Import all Python files + for _ in _walk_packages(package.__path__, package.__name__ + ".", blacklist_pkgs=blacklist_pkgs): + pass
+ + +def _walk_packages( + path: str | None = None, + prefix: str = "", + onerror: callable | None = None, + blacklist_pkgs: list[str] | None = None, +): + """Yields ModuleInfo for all modules recursively on path, or, if path is None, all accessible modules. + + Note: + This function is a modified version of the original ``pkgutil.walk_packages`` function. It adds + the `blacklist_pkgs` argument to skip blacklisted packages. Please refer to the original + ``pkgutil.walk_packages`` function for more details. + """ + if blacklist_pkgs is None: + blacklist_pkgs = [] + + def seen(p, m={}): + if p in m: + return True + m[p] = True # noqa: R503 + + for info in pkgutil.iter_modules(path, prefix): + # check blacklisted + if any([black_pkg_name in info.name for black_pkg_name in blacklist_pkgs]): + continue + + # yield the module info + yield info + + if info.ispkg: + try: + __import__(info.name) + except Exception: + if onerror is not None: + onerror(info.name) + else: + raise + else: + path = getattr(sys.modules[info.name], "__path__", None) or [] + + # don't traverse path items we've seen before + path = [p for p in path if not seen(p)] + + yield from _walk_packages(path, info.name + ".", onerror, blacklist_pkgs) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/parse_cfg.html b/_modules/omni/isaac/lab_tasks/utils/parse_cfg.html new file mode 100644 index 0000000000..c4cd74fb20 --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/parse_cfg.html @@ -0,0 +1,757 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.parse_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.parse_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Sub-module with utilities for parsing and loading configurations."""
+
+
+import gymnasium as gym
+import importlib
+import inspect
+import os
+import re
+import yaml
+
+from omni.isaac.lab.envs import DirectRLEnvCfg, ManagerBasedRLEnvCfg
+
+
+
[文档]def load_cfg_from_registry(task_name: str, entry_point_key: str) -> dict | object: + """Load default configuration given its entry point from the gym registry. + + This function loads the configuration object from the gym registry for the given task name. + It supports both YAML and Python configuration files. + + It expects the configuration to be registered in the gym registry as: + + .. code-block:: python + + gym.register( + id="My-Awesome-Task-v0", + ... + kwargs={"env_entry_point_cfg": "path.to.config:ConfigClass"}, + ) + + The parsed configuration object for above example can be obtained as: + + .. code-block:: python + + from omni.isaac.lab_tasks.utils.parse_cfg import load_cfg_from_registry + + cfg = load_cfg_from_registry("My-Awesome-Task-v0", "env_entry_point_cfg") + + Args: + task_name: The name of the environment. + entry_point_key: The entry point key to resolve the configuration file. + + Returns: + The parsed configuration object. If the entry point is a YAML file, it is parsed into a dictionary. + If the entry point is a Python class, it is instantiated and returned. + + Raises: + ValueError: If the entry point key is not available in the gym registry for the task. + """ + # obtain the configuration entry point + cfg_entry_point = gym.spec(task_name).kwargs.get(entry_point_key) + # check if entry point exists + if cfg_entry_point is None: + raise ValueError( + f"Could not find configuration for the environment: '{task_name}'." + f" Please check that the gym registry has the entry point: '{entry_point_key}'." + ) + # parse the default config file + if isinstance(cfg_entry_point, str) and cfg_entry_point.endswith(".yaml"): + if os.path.exists(cfg_entry_point): + # absolute path for the config file + config_file = cfg_entry_point + else: + # resolve path to the module location + mod_name, file_name = cfg_entry_point.split(":") + mod_path = os.path.dirname(importlib.import_module(mod_name).__file__) + # obtain the configuration file path + config_file = os.path.join(mod_path, file_name) + # load the configuration + print(f"[INFO]: Parsing configuration from: {config_file}") + with open(config_file, encoding="utf-8") as f: + cfg = yaml.full_load(f) + else: + if callable(cfg_entry_point): + # resolve path to the module location + mod_path = inspect.getfile(cfg_entry_point) + # load the configuration + cfg_cls = cfg_entry_point() + elif isinstance(cfg_entry_point, str): + # resolve path to the module location + mod_name, attr_name = cfg_entry_point.split(":") + mod = importlib.import_module(mod_name) + cfg_cls = getattr(mod, attr_name) + else: + cfg_cls = cfg_entry_point + # load the configuration + print(f"[INFO]: Parsing configuration from: {cfg_entry_point}") + if callable(cfg_cls): + cfg = cfg_cls() + else: + cfg = cfg_cls + return cfg
+ + +
[文档]def parse_env_cfg( + task_name: str, device: str = "cuda:0", num_envs: int | None = None, use_fabric: bool | None = None +) -> ManagerBasedRLEnvCfg | DirectRLEnvCfg: + """Parse configuration for an environment and override based on inputs. + + Args: + task_name: The name of the environment. + device: The device to run the simulation on. Defaults to "cuda:0". + num_envs: Number of environments to create. Defaults to None, in which case it is left unchanged. + use_fabric: Whether to enable/disable fabric interface. If false, all read/write operations go through USD. + This slows down the simulation but allows seeing the changes in the USD through the USD stage. + Defaults to None, in which case it is left unchanged. + + Returns: + The parsed configuration object. + + Raises: + RuntimeError: If the configuration for the task is not a class. We assume users always use a class for the + environment configuration. + """ + # load the default configuration + cfg = load_cfg_from_registry(task_name, "env_cfg_entry_point") + + # check that it is not a dict + # we assume users always use a class for the configuration + if isinstance(cfg, dict): + raise RuntimeError(f"Configuration for the task: '{task_name}' is not a class. Please provide a class.") + + # simulation device + cfg.sim.device = device + # disable fabric to read/write through USD + if use_fabric is not None: + cfg.sim.use_fabric = use_fabric + # number of environments + if num_envs is not None: + cfg.scene.num_envs = num_envs + + return cfg
+ + +
[文档]def get_checkpoint_path( + log_path: str, run_dir: str = ".*", checkpoint: str = ".*", other_dirs: list[str] = None, sort_alpha: bool = True +) -> str: + """Get path to the model checkpoint in input directory. + + The checkpoint file is resolved as: ``<log_path>/<run_dir>/<*other_dirs>/<checkpoint>``, 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) + run and checkpoint are selected. To disable this behavior, set the flag :attr:`sort_alpha` to False. + + 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 :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 :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. + + Returns: + The path to the model checkpoint. + + Raises: + ValueError: When no runs are found in the input directory. + ValueError: When no checkpoints are found in the input directory. + + """ + # check if runs present in directory + try: + # find all runs in the directory that math the regex expression + runs = [ + os.path.join(log_path, run) for run in os.scandir(log_path) if run.is_dir() and re.match(run_dir, run.name) + ] + # sort matched runs by alphabetical order (latest run should be last) + if sort_alpha: + runs.sort() + else: + runs = sorted(runs, key=os.path.getmtime) + # create last run file path + if other_dirs is not None: + run_path = os.path.join(runs[-1], *other_dirs) + else: + run_path = runs[-1] + except IndexError: + raise ValueError(f"No runs present in the directory: '{log_path}' match: '{run_dir}'.") + + # list all model checkpoints in the directory + model_checkpoints = [f for f in os.listdir(run_path) if re.match(checkpoint, f)] + # check if any checkpoints are present + if len(model_checkpoints) == 0: + raise ValueError(f"No checkpoints in the directory: '{run_path}' match '{checkpoint}'.") + # sort alphabetically while ensuring that *_10 comes after *_9 + model_checkpoints.sort(key=lambda m: f"{m:0>15}") + # get latest matched checkpoint file + checkpoint_file = model_checkpoints[-1] + + return os.path.join(run_path, checkpoint_file)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/wrappers/rl_games.html b/_modules/omni/isaac/lab_tasks/utils/wrappers/rl_games.html new file mode 100644 index 0000000000..49cd30f1ca --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/wrappers/rl_games.html @@ -0,0 +1,909 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.wrappers.rl_games — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.wrappers.rl_games 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Wrapper to configure a :class:`ManagerBasedRLEnv` or :class:`DirectRlEnv` instance to RL-Games vectorized environment.
+
+The following example shows how to wrap an environment for RL-Games and register the environment construction
+for RL-Games :class:`Runner` class:
+
+.. code-block:: python
+
+    from rl_games.common import env_configurations, vecenv
+
+    from omni.isaac.lab_tasks.utils.wrappers.rl_games import RlGamesGpuEnv, RlGamesVecEnvWrapper
+
+    # configuration parameters
+    rl_device = "cuda:0"
+    clip_obs = 10.0
+    clip_actions = 1.0
+
+    # wrap around environment for rl-games
+    env = RlGamesVecEnvWrapper(env, rl_device, clip_obs, clip_actions)
+
+    # register the environment to rl-games registry
+    # note: in agents configuration: environment name must be "rlgpu"
+    vecenv.register(
+        "IsaacRlgWrapper", lambda config_name, num_actors, **kwargs: RlGamesGpuEnv(config_name, num_actors, **kwargs)
+    )
+    env_configurations.register("rlgpu", {"vecenv_type": "IsaacRlgWrapper", "env_creator": lambda **kwargs: env})
+
+"""
+
+# needed to import for allowing type-hinting:gym.spaces.Box | None
+from __future__ import annotations
+
+import gym.spaces  # needed for rl-games incompatibility: https://github.com/Denys88/rl_games/issues/261
+import gymnasium
+import torch
+
+from rl_games.common import env_configurations
+from rl_games.common.vecenv import IVecEnv
+
+from omni.isaac.lab.envs import DirectRLEnv, ManagerBasedRLEnv, VecEnvObs
+
+"""
+Vectorized environment wrapper.
+"""
+
+
+
[文档]class RlGamesVecEnvWrapper(IVecEnv): + """Wraps around Isaac Lab environment for RL-Games. + + This class wraps around the Isaac Lab environment. Since RL-Games works directly on + GPU buffers, the wrapper handles moving of buffers from the simulation environment + to the same device as the learning agent. Additionally, it performs clipping of + observations and actions. + + For algorithms like asymmetric actor-critic, RL-Games expects a dictionary for + observations. This dictionary contains "obs" and "states" which typically correspond + to the actor and critic observations respectively. + + To use asymmetric actor-critic, the environment observations from :class:`ManagerBasedRLEnv` or :class:`DirectRLEnv` + must have the key or group name "critic". The observation group is used to set the + :attr:`num_states` (int) and :attr:`state_space` (:obj:`gym.spaces.Box`). These are + used by the learning agent in RL-Games to allocate buffers in the trajectory memory. + Since this is optional for some environments, the wrapper checks if these attributes exist. + If they don't then the wrapper defaults to zero as number of privileged observations. + + .. caution:: + + This class must be the last wrapper in the wrapper chain. This is because the wrapper does not follow + the :class:`gym.Wrapper` interface. Any subsequent wrappers will need to be modified to work with this + wrapper. + + + Reference: + https://github.com/Denys88/rl_games/blob/master/rl_games/common/ivecenv.py + https://github.com/NVIDIA-Omniverse/IsaacGymEnvs + """ + +
[文档] def __init__(self, env: ManagerBasedRLEnv | DirectRLEnv, rl_device: str, clip_obs: float, clip_actions: float): + """Initializes the wrapper instance. + + Args: + env: The environment to wrap around. + rl_device: The device on which agent computations are performed. + clip_obs: The clipping value for observations. + clip_actions: The clipping value for actions. + + Raises: + ValueError: The environment is not inherited from :class:`ManagerBasedRLEnv` or :class:`DirectRLEnv`. + ValueError: If specified, the privileged observations (critic) are not of type :obj:`gym.spaces.Box`. + """ + # check that input is valid + if not isinstance(env.unwrapped, ManagerBasedRLEnv) and not isinstance(env.unwrapped, DirectRLEnv): + raise ValueError( + "The environment must be inherited from ManagerBasedRLEnv or DirectRLEnv. Environment type:" + f" {type(env)}" + ) + # initialize the wrapper + self.env = env + # store provided arguments + self._rl_device = rl_device + self._clip_obs = clip_obs + self._clip_actions = clip_actions + self._sim_device = env.unwrapped.device + # information for privileged observations + if self.state_space is None: + self.rlg_num_states = 0 + else: + self.rlg_num_states = self.state_space.shape[0]
+ + def __str__(self): + """Returns the wrapper name and the :attr:`env` representation string.""" + return ( + f"<{type(self).__name__}{self.env}>" + f"\n\tObservations clipping: {self._clip_obs}" + f"\n\tActions clipping : {self._clip_actions}" + f"\n\tAgent device : {self._rl_device}" + f"\n\tAsymmetric-learning : {self.rlg_num_states != 0}" + ) + + def __repr__(self): + """Returns the string representation of the wrapper.""" + return str(self) + + """ + Properties -- Gym.Wrapper + """ + + @property + def render_mode(self) -> str | None: + """Returns the :attr:`Env` :attr:`render_mode`.""" + return self.env.render_mode + + @property + def observation_space(self) -> gym.spaces.Box: + """Returns the :attr:`Env` :attr:`observation_space`.""" + # note: rl-games only wants single observation space + policy_obs_space = self.unwrapped.single_observation_space["policy"] + if not isinstance(policy_obs_space, gymnasium.spaces.Box): + raise NotImplementedError( + f"The RL-Games wrapper does not currently support observation space: '{type(policy_obs_space)}'." + f" If you need to support this, please modify the wrapper: {self.__class__.__name__}," + " and if you are nice, please send a merge-request." + ) + # note: maybe should check if we are a sub-set of the actual space. don't do it right now since + # in ManagerBasedRLEnv we are setting action space as (-inf, inf). + return gym.spaces.Box(-self._clip_obs, self._clip_obs, policy_obs_space.shape) + + @property + def action_space(self) -> gym.Space: + """Returns the :attr:`Env` :attr:`action_space`.""" + # note: rl-games only wants single action space + action_space = self.unwrapped.single_action_space + if not isinstance(action_space, gymnasium.spaces.Box): + raise NotImplementedError( + f"The RL-Games wrapper does not currently support action space: '{type(action_space)}'." + f" If you need to support this, please modify the wrapper: {self.__class__.__name__}," + " and if you are nice, please send a merge-request." + ) + # return casted space in gym.spaces.Box (OpenAI Gym) + # note: maybe should check if we are a sub-set of the actual space. don't do it right now since + # in ManagerBasedRLEnv we are setting action space as (-inf, inf). + return gym.spaces.Box(-self._clip_actions, self._clip_actions, action_space.shape) + +
[文档] @classmethod + def class_name(cls) -> str: + """Returns the class name of the wrapper.""" + return cls.__name__
+ + @property + def unwrapped(self) -> ManagerBasedRLEnv | DirectRLEnv: + """Returns the base environment of the wrapper. + + This will be the bare :class:`gymnasium.Env` environment, underneath all layers of wrappers. + """ + return self.env.unwrapped + + """ + Properties + """ + + @property + def num_envs(self) -> int: + """Returns the number of sub-environment instances.""" + return self.unwrapped.num_envs + + @property + def device(self) -> str: + """Returns the base environment simulation device.""" + return self.unwrapped.device + + @property + def state_space(self) -> gym.spaces.Box | None: + """Returns the :attr:`Env` :attr:`observation_space`.""" + # note: rl-games only wants single observation space + critic_obs_space = self.unwrapped.single_observation_space.get("critic") + # check if we even have a critic obs + if critic_obs_space is None: + return None + elif not isinstance(critic_obs_space, gymnasium.spaces.Box): + raise NotImplementedError( + f"The RL-Games wrapper does not currently support state space: '{type(critic_obs_space)}'." + f" If you need to support this, please modify the wrapper: {self.__class__.__name__}," + " and if you are nice, please send a merge-request." + ) + # return casted space in gym.spaces.Box (OpenAI Gym) + # note: maybe should check if we are a sub-set of the actual space. don't do it right now since + # in ManagerBasedRLEnv we are setting action space as (-inf, inf). + return gym.spaces.Box(-self._clip_obs, self._clip_obs, critic_obs_space.shape) + +
[文档] def get_number_of_agents(self) -> int: + """Returns number of actors in the environment.""" + return getattr(self, "num_agents", 1)
+ +
[文档] def get_env_info(self) -> dict: + """Returns the Gym spaces for the environment.""" + return { + "observation_space": self.observation_space, + "action_space": self.action_space, + "state_space": self.state_space, + }
+ + """ + Operations - MDP + """ + + def seed(self, seed: int = -1) -> int: # noqa: D102 + return self.unwrapped.seed(seed) + + def reset(self): # noqa: D102 + obs_dict, _ = self.env.reset() + # process observations and states + return self._process_obs(obs_dict) + + def step(self, actions): # noqa: D102 + # move actions to sim-device + actions = actions.detach().clone().to(device=self._sim_device) + # clip the actions + actions = torch.clamp(actions, -self._clip_actions, self._clip_actions) + # perform environment step + obs_dict, rew, terminated, truncated, extras = self.env.step(actions) + + # move time out information to the extras dict + # this is only needed for infinite horizon tasks + # note: only useful when `value_bootstrap` is True in the agent configuration + if not self.unwrapped.cfg.is_finite_horizon: + extras["time_outs"] = truncated.to(device=self._rl_device) + # process observations and states + obs_and_states = self._process_obs(obs_dict) + # move buffers to rl-device + # note: we perform clone to prevent issues when rl-device and sim-device are the same. + rew = rew.to(device=self._rl_device) + dones = (terminated | truncated).to(device=self._rl_device) + extras = { + k: v.to(device=self._rl_device, non_blocking=True) if hasattr(v, "to") else v for k, v in extras.items() + } + # remap extras from "log" to "episode" + if "log" in extras: + extras["episode"] = extras.pop("log") + + return obs_and_states, rew, dones, extras + + def close(self): # noqa: D102 + return self.env.close() + + """ + Helper functions + """ + + def _process_obs(self, obs_dict: VecEnvObs) -> torch.Tensor | dict[str, torch.Tensor]: + """Processing of the observations and states from the environment. + + Note: + States typically refers to privileged observations for the critic function. It is typically used in + asymmetric actor-critic algorithms. + + Args: + obs_dict: The current observations from environment. + + Returns: + If environment provides states, then a dictionary containing the observations and states is returned. + Otherwise just the observations tensor is returned. + """ + # process policy obs + obs = obs_dict["policy"] + # clip the observations + obs = torch.clamp(obs, -self._clip_obs, self._clip_obs) + # move the buffer to rl-device + obs = obs.to(device=self._rl_device).clone() + + # check if asymmetric actor-critic or not + if self.rlg_num_states > 0: + # acquire states from the environment if it exists + try: + states = obs_dict["critic"] + except AttributeError: + raise NotImplementedError("Environment does not define key 'critic' for privileged observations.") + # clip the states + states = torch.clamp(states, -self._clip_obs, self._clip_obs) + # move buffers to rl-device + states = states.to(self._rl_device).clone() + # convert to dictionary + return {"obs": obs, "states": states} + else: + return obs
+ + +""" +Environment Handler. +""" + + +
[文档]class RlGamesGpuEnv(IVecEnv): + """Thin wrapper to create instance of the environment to fit RL-Games runner.""" + + # TODO: Adding this for now but do we really need this? + +
[文档] def __init__(self, config_name: str, num_actors: int, **kwargs): + """Initialize the environment. + + Args: + config_name: The name of the environment configuration. + num_actors: The number of actors in the environment. This is not used in this wrapper. + """ + self.env: RlGamesVecEnvWrapper = env_configurations.configurations[config_name]["env_creator"](**kwargs)
+ + def step(self, action): # noqa: D102 + return self.env.step(action) + + def reset(self): # noqa: D102 + return self.env.reset() + +
[文档] def get_number_of_agents(self) -> int: + """Get number of agents in the environment. + + Returns: + The number of agents in the environment. + """ + return self.env.get_number_of_agents()
+ +
[文档] def get_env_info(self) -> dict: + """Get the Gym spaces for the environment. + + Returns: + The Gym spaces for the environment. + """ + return self.env.get_env_info()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/exporter.html b/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/exporter.html new file mode 100644 index 0000000000..bf090fc9ab --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/exporter.html @@ -0,0 +1,710 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.wrappers.rsl_rl.exporter — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.wrappers.rsl_rl.exporter 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+import copy
+import os
+import torch
+
+
+
[文档]def export_policy_as_jit(actor_critic: object, normalizer: object | None, path: str, filename="policy.pt"): + """Export policy into a Torch JIT file. + + Args: + actor_critic: The actor-critic torch module. + normalizer: The empirical normalizer module. If None, Identity is used. + path: The path to the saving directory. + filename: The name of exported JIT file. Defaults to "policy.pt". + """ + policy_exporter = _TorchPolicyExporter(actor_critic, normalizer) + policy_exporter.export(path, filename)
+ + +
[文档]def export_policy_as_onnx( + actor_critic: object, path: str, normalizer: object | None = None, filename="policy.onnx", verbose=False +): + """Export policy into a Torch ONNX file. + + Args: + actor_critic: The actor-critic torch module. + normalizer: The empirical normalizer module. If None, Identity is used. + path: The path to the saving directory. + filename: The name of exported ONNX file. Defaults to "policy.onnx". + verbose: Whether to print the model summary. Defaults to False. + """ + if not os.path.exists(path): + os.makedirs(path, exist_ok=True) + policy_exporter = _OnnxPolicyExporter(actor_critic, normalizer, verbose) + policy_exporter.export(path, filename)
+ + +""" +Helper Classes - Private. +""" + + +class _TorchPolicyExporter(torch.nn.Module): + """Exporter of actor-critic into JIT file.""" + + def __init__(self, actor_critic, normalizer=None): + super().__init__() + self.actor = copy.deepcopy(actor_critic.actor) + self.is_recurrent = actor_critic.is_recurrent + if self.is_recurrent: + self.rnn = copy.deepcopy(actor_critic.memory_a.rnn) + self.rnn.cpu() + self.register_buffer("hidden_state", torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size)) + self.register_buffer("cell_state", torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size)) + self.forward = self.forward_lstm + self.reset = self.reset_memory + # copy normalizer if exists + if normalizer: + self.normalizer = copy.deepcopy(normalizer) + else: + self.normalizer = torch.nn.Identity() + + def forward_lstm(self, x): + x = self.normalizer(x) + x, (h, c) = self.rnn(x.unsqueeze(0), (self.hidden_state, self.cell_state)) + self.hidden_state[:] = h + self.cell_state[:] = c + x = x.squeeze(0) + return self.actor(x) + + def forward(self, x): + return self.actor(self.normalizer(x)) + + @torch.jit.export + def reset(self): + pass + + def reset_memory(self): + self.hidden_state[:] = 0.0 + self.cell_state[:] = 0.0 + + def export(self, path, filename): + os.makedirs(path, exist_ok=True) + path = os.path.join(path, filename) + self.to("cpu") + traced_script_module = torch.jit.script(self) + traced_script_module.save(path) + + +class _OnnxPolicyExporter(torch.nn.Module): + """Exporter of actor-critic into ONNX file.""" + + def __init__(self, actor_critic, normalizer=None, verbose=False): + super().__init__() + self.verbose = verbose + self.actor = copy.deepcopy(actor_critic.actor) + self.is_recurrent = actor_critic.is_recurrent + if self.is_recurrent: + self.rnn = copy.deepcopy(actor_critic.memory_a.rnn) + self.rnn.cpu() + self.forward = self.forward_lstm + # copy normalizer if exists + if normalizer: + self.normalizer = copy.deepcopy(normalizer) + else: + self.normalizer = torch.nn.Identity() + + def forward_lstm(self, x_in, h_in, c_in): + x_in = self.normalizer(x_in) + x, (h, c) = self.rnn(x_in.unsqueeze(0), (h_in, c_in)) + x = x.squeeze(0) + return self.actor(x), h, c + + def forward(self, x): + return self.actor(self.normalizer(x)) + + def export(self, path, filename): + self.to("cpu") + if self.is_recurrent: + obs = torch.zeros(1, self.rnn.input_size) + h_in = torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size) + c_in = torch.zeros(self.rnn.num_layers, 1, self.rnn.hidden_size) + actions, h_out, c_out = self(obs, h_in, c_in) + torch.onnx.export( + self, + (obs, h_in, c_in), + os.path.join(path, filename), + export_params=True, + opset_version=11, + verbose=self.verbose, + input_names=["obs", "h_in", "c_in"], + output_names=["actions", "h_out", "c_out"], + dynamic_axes={}, + ) + else: + obs = torch.zeros(1, self.actor[0].in_features) + torch.onnx.export( + self, + obs, + os.path.join(path, filename), + export_params=True, + opset_version=11, + verbose=self.verbose, + input_names=["obs"], + output_names=["actions"], + dynamic_axes={}, + ) +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/rl_cfg.html b/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/rl_cfg.html new file mode 100644 index 0000000000..38876ac256 --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/rl_cfg.html @@ -0,0 +1,708 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.wrappers.rsl_rl.rl_cfg — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.wrappers.rsl_rl.rl_cfg 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+from dataclasses import MISSING
+from typing import Literal
+
+from omni.isaac.lab.utils import configclass
+
+
+
[文档]@configclass +class RslRlPpoActorCriticCfg: + """Configuration for the PPO actor-critic networks.""" + + class_name: str = "ActorCritic" + """The policy class name. Default is ActorCritic.""" + + init_noise_std: float = MISSING + """The initial noise standard deviation for the policy.""" + + actor_hidden_dims: list[int] = MISSING + """The hidden dimensions of the actor network.""" + + critic_hidden_dims: list[int] = MISSING + """The hidden dimensions of the critic network.""" + + activation: str = MISSING + """The activation function for the actor and critic networks."""
+ + +
[文档]@configclass +class RslRlPpoAlgorithmCfg: + """Configuration for the PPO algorithm.""" + + class_name: str = "PPO" + """The algorithm class name. Default is PPO.""" + + value_loss_coef: float = MISSING + """The coefficient for the value loss.""" + + use_clipped_value_loss: bool = MISSING + """Whether to use clipped value loss.""" + + clip_param: float = MISSING + """The clipping parameter for the policy.""" + + entropy_coef: float = MISSING + """The coefficient for the entropy loss.""" + + num_learning_epochs: int = MISSING + """The number of learning epochs per update.""" + + num_mini_batches: int = MISSING + """The number of mini-batches per update.""" + + learning_rate: float = MISSING + """The learning rate for the policy.""" + + schedule: str = MISSING + """The learning rate schedule.""" + + gamma: float = MISSING + """The discount factor.""" + + lam: float = MISSING + """The lambda parameter for Generalized Advantage Estimation (GAE).""" + + desired_kl: float = MISSING + """The desired KL divergence.""" + + max_grad_norm: float = MISSING + """The maximum gradient norm."""
+ + +
[文档]@configclass +class RslRlOnPolicyRunnerCfg: + """Configuration of the runner for on-policy algorithms.""" + + seed: int = 42 + """The seed for the experiment. Default is 42.""" + + device: str = "cuda:0" + """The device for the rl-agent. Default is cuda:0.""" + + num_steps_per_env: int = MISSING + """The number of steps per environment per update.""" + + max_iterations: int = MISSING + """The maximum number of iterations.""" + + empirical_normalization: bool = MISSING + """Whether to use empirical normalization.""" + + policy: RslRlPpoActorCriticCfg = MISSING + """The policy configuration.""" + + algorithm: RslRlPpoAlgorithmCfg = MISSING + """The algorithm configuration.""" + + ## + # Checkpointing parameters + ## + + save_interval: int = MISSING + """The number of iterations between saves.""" + + experiment_name: str = MISSING + """The experiment name.""" + + run_name: str = "" + """The run name. Default is empty string. + + The name of the run directory is typically the time-stamp at execution. If the run name is not empty, + then it is appended to the run directory's name, i.e. the logging directory's name will become + ``{time-stamp}_{run_name}``. + """ + + ## + # Logging parameters + ## + + logger: Literal["tensorboard", "neptune", "wandb"] = "tensorboard" + """The logger to use. Default is tensorboard.""" + + neptune_project: str = "isaaclab" + """The neptune project name. Default is "isaaclab".""" + + wandb_project: str = "isaaclab" + """The wandb project name. Default is "isaaclab".""" + + ## + # Loading parameters + ## + + resume: bool = False + """Whether to resume. Default is False.""" + + load_run: str = ".*" + """The run directory to load. Default is ".*" (all). + + If regex expression, the latest (alphabetical order) matching run will be loaded. + """ + + load_checkpoint: str = "model_.*.pt" + """The checkpoint file to load. Default is ``"model_.*.pt"`` (all). + + If regex expression, the latest (alphabetical order) matching file will be loaded. + """
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.html b/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.html new file mode 100644 index 0000000000..3daddb9252 --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/wrappers/rsl_rl/vecenv_wrapper.html @@ -0,0 +1,751 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.wrappers.rsl_rl.vecenv_wrapper — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.wrappers.rsl_rl.vecenv_wrapper 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Wrapper to configure a :class:`ManagerBasedRLEnv` or :class:`DirectRlEnv` instance to RSL-RL vectorized environment.
+
+The following example shows how to wrap an environment for RSL-RL:
+
+.. code-block:: python
+
+    from omni.isaac.lab_tasks.utils.wrappers.rsl_rl import RslRlVecEnvWrapper
+
+    env = RslRlVecEnvWrapper(env)
+
+"""
+
+
+import gymnasium as gym
+import torch
+
+from rsl_rl.env import VecEnv
+
+from omni.isaac.lab.envs import DirectRLEnv, ManagerBasedRLEnv
+
+
+
[文档]class RslRlVecEnvWrapper(VecEnv): + """Wraps around Isaac Lab environment for RSL-RL library + + To use asymmetric actor-critic, the environment instance must have the attributes :attr:`num_privileged_obs` (int). + This is used by the learning agent to allocate buffers in the trajectory memory. Additionally, the returned + observations should have the key "critic" which corresponds to the privileged observations. Since this is + optional for some environments, the wrapper checks if these attributes exist. If they don't then the wrapper + defaults to zero as number of privileged observations. + + .. caution:: + + This class must be the last wrapper in the wrapper chain. This is because the wrapper does not follow + the :class:`gym.Wrapper` interface. Any subsequent wrappers will need to be modified to work with this + wrapper. + + Reference: + https://github.com/leggedrobotics/rsl_rl/blob/master/rsl_rl/env/vec_env.py + """ + +
[文档] def __init__(self, env: ManagerBasedRLEnv | DirectRLEnv): + """Initializes the wrapper. + + Note: + The wrapper calls :meth:`reset` at the start since the RSL-RL runner does not call reset. + + Args: + env: The environment to wrap around. + + Raises: + ValueError: When the environment is not an instance of :class:`ManagerBasedRLEnv` or :class:`DirectRLEnv`. + """ + # check that input is valid + if not isinstance(env.unwrapped, ManagerBasedRLEnv) and not isinstance(env.unwrapped, DirectRLEnv): + raise ValueError( + "The environment must be inherited from ManagerBasedRLEnv or DirectRLEnv. Environment type:" + f" {type(env)}" + ) + # initialize the wrapper + self.env = env + # store information required by wrapper + self.num_envs = self.unwrapped.num_envs + self.device = self.unwrapped.device + self.max_episode_length = self.unwrapped.max_episode_length + if hasattr(self.unwrapped, "action_manager"): + self.num_actions = self.unwrapped.action_manager.total_action_dim + else: + self.num_actions = gym.spaces.flatdim(self.unwrapped.single_action_space) + if hasattr(self.unwrapped, "observation_manager"): + self.num_obs = self.unwrapped.observation_manager.group_obs_dim["policy"][0] + else: + self.num_obs = gym.spaces.flatdim(self.unwrapped.single_observation_space["policy"]) + # -- privileged observations + if ( + hasattr(self.unwrapped, "observation_manager") + and "critic" in self.unwrapped.observation_manager.group_obs_dim + ): + self.num_privileged_obs = self.unwrapped.observation_manager.group_obs_dim["critic"][0] + elif hasattr(self.unwrapped, "num_states") and "critic" in self.unwrapped.single_observation_space: + self.num_privileged_obs = gym.spaces.flatdim(self.unwrapped.single_observation_space["critic"]) + else: + self.num_privileged_obs = 0 + # reset at the start since the RSL-RL runner does not call reset + self.env.reset()
+ + def __str__(self): + """Returns the wrapper name and the :attr:`env` representation string.""" + return f"<{type(self).__name__}{self.env}>" + + def __repr__(self): + """Returns the string representation of the wrapper.""" + return str(self) + + """ + Properties -- Gym.Wrapper + """ + + @property + def cfg(self) -> object: + """Returns the configuration class instance of the environment.""" + return self.unwrapped.cfg + + @property + def render_mode(self) -> str | None: + """Returns the :attr:`Env` :attr:`render_mode`.""" + return self.env.render_mode + + @property + def observation_space(self) -> gym.Space: + """Returns the :attr:`Env` :attr:`observation_space`.""" + return self.env.observation_space + + @property + def action_space(self) -> gym.Space: + """Returns the :attr:`Env` :attr:`action_space`.""" + return self.env.action_space + +
[文档] @classmethod + def class_name(cls) -> str: + """Returns the class name of the wrapper.""" + return cls.__name__
+ + @property + def unwrapped(self) -> ManagerBasedRLEnv | DirectRLEnv: + """Returns the base environment of the wrapper. + + This will be the bare :class:`gymnasium.Env` environment, underneath all layers of wrappers. + """ + return self.env.unwrapped + + """ + Properties + """ + +
[文档] def get_observations(self) -> tuple[torch.Tensor, dict]: + """Returns the current observations of the environment.""" + if hasattr(self.unwrapped, "observation_manager"): + obs_dict = self.unwrapped.observation_manager.compute() + else: + obs_dict = self.unwrapped._get_observations() + return obs_dict["policy"], {"observations": obs_dict}
+ + @property + def episode_length_buf(self) -> torch.Tensor: + """The episode length buffer.""" + return self.unwrapped.episode_length_buf + + @episode_length_buf.setter + 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. + """ + self.unwrapped.episode_length_buf = value + + """ + Operations - MDP + """ + + def seed(self, seed: int = -1) -> int: # noqa: D102 + return self.unwrapped.seed(seed) + + def reset(self) -> tuple[torch.Tensor, dict]: # noqa: D102 + # reset the environment + obs_dict, _ = self.env.reset() + # return observations + return obs_dict["policy"], {"observations": obs_dict} + + def step(self, actions: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, dict]: + # record step information + obs_dict, rew, terminated, truncated, extras = self.env.step(actions) + # compute dones for compatibility with RSL-RL + dones = (terminated | truncated).to(dtype=torch.long) + # move extra observations to the extras dict + obs = obs_dict["policy"] + extras["observations"] = obs_dict + # move time out information to the extras dict + # this is only needed for infinite horizon tasks + if not self.unwrapped.cfg.is_finite_horizon: + extras["time_outs"] = truncated + + # return the step information + return obs, rew, dones, extras + + def close(self): # noqa: D102 + return self.env.close()
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/wrappers/sb3.html b/_modules/omni/isaac/lab_tasks/utils/wrappers/sb3.html new file mode 100644 index 0000000000..6487858751 --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/wrappers/sb3.html @@ -0,0 +1,907 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.wrappers.sb3 — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.wrappers.sb3 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Wrapper to configure a :class:`ManagerBasedRLEnv` or :class:`DirectRLEnv` instance to Stable-Baselines3 vectorized environment.
+
+The following example shows how to wrap an environment for Stable-Baselines3:
+
+.. code-block:: python
+
+    from omni.isaac.lab_tasks.utils.wrappers.sb3 import Sb3VecEnvWrapper
+
+    env = Sb3VecEnvWrapper(env)
+
+"""
+
+# needed to import for allowing type-hinting: torch.Tensor | dict[str, torch.Tensor]
+from __future__ import annotations
+
+import gymnasium as gym
+import numpy as np
+import torch
+import torch.nn as nn  # noqa: F401
+from typing import Any
+
+from stable_baselines3.common.utils import constant_fn
+from stable_baselines3.common.vec_env.base_vec_env import VecEnv, VecEnvObs, VecEnvStepReturn
+
+from omni.isaac.lab.envs import DirectRLEnv, ManagerBasedRLEnv
+
+"""
+Configuration Parser.
+"""
+
+
+
[文档]def process_sb3_cfg(cfg: dict) -> dict: + """Convert simple YAML types to Stable-Baselines classes/components. + + Args: + cfg: A configuration dictionary. + + Returns: + A dictionary containing the converted configuration. + + Reference: + https://github.com/DLR-RM/rl-baselines3-zoo/blob/0e5eb145faefa33e7d79c7f8c179788574b20da5/utils/exp_manager.py#L358 + """ + + def update_dict(hyperparams: dict[str, Any]) -> dict[str, Any]: + for key, value in hyperparams.items(): + if isinstance(value, dict): + update_dict(value) + else: + if key in ["policy_kwargs", "replay_buffer_class", "replay_buffer_kwargs"]: + hyperparams[key] = eval(value) + elif key in ["learning_rate", "clip_range", "clip_range_vf", "delta_std"]: + if isinstance(value, str): + _, initial_value = value.split("_") + initial_value = float(initial_value) + hyperparams[key] = lambda progress_remaining: progress_remaining * initial_value + elif isinstance(value, (float, int)): + # Negative value: ignore (ex: for clipping) + if value < 0: + continue + hyperparams[key] = constant_fn(float(value)) + else: + raise ValueError(f"Invalid value for {key}: {hyperparams[key]}") + + return hyperparams + + # parse agent configuration and convert to classes + return update_dict(cfg)
+ + +""" +Vectorized environment wrapper. +""" + + +
[文档]class Sb3VecEnvWrapper(VecEnv): + """Wraps around Isaac Lab environment for Stable Baselines3. + + Isaac Sim internally implements a vectorized environment. However, since it is + still considered a single environment instance, Stable Baselines tries to wrap + around it using the :class:`DummyVecEnv`. This is only done if the environment + is not inheriting from their :class:`VecEnv`. Thus, this class thinly wraps + over the environment from :class:`ManagerBasedRLEnv` or :class:`DirectRLEnv`. + + Note: + While Stable-Baselines3 supports Gym 0.26+ API, their vectorized environment + still uses the old API (i.e. it is closer to Gym 0.21). Thus, we implement + the old API for the vectorized environment. + + We also add monitoring functionality that computes the un-discounted episode + return and length. This information is added to the info dicts under key `episode`. + + In contrast to the Isaac Lab environment, stable-baselines expect the following: + + 1. numpy datatype for MDP signals + 2. a list of info dicts for each sub-environment (instead of a dict) + 3. when environment has terminated, the observations from the environment should correspond + to the one after reset. The "real" final observation is passed using the info dicts + under the key ``terminal_observation``. + + .. warning:: + + By the nature of physics stepping in Isaac Sim, it is not possible to forward the + simulation buffers without performing a physics step. Thus, reset is performed + inside the :meth:`step()` function after the actual physics step is taken. + Thus, the returned observations for terminated environments is the one after the reset. + + .. caution:: + + This class must be the last wrapper in the wrapper chain. This is because the wrapper does not follow + the :class:`gym.Wrapper` interface. Any subsequent wrappers will need to be modified to work with this + wrapper. + + Reference: + + 1. https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html + 2. https://stable-baselines3.readthedocs.io/en/master/common/monitor.html + + """ + +
[文档] def __init__(self, env: ManagerBasedRLEnv | DirectRLEnv): + """Initialize the wrapper. + + Args: + env: The environment to wrap around. + + Raises: + ValueError: When the environment is not an instance of :class:`ManagerBasedRLEnv` or :class:`DirectRLEnv`. + """ + # check that input is valid + if not isinstance(env.unwrapped, ManagerBasedRLEnv) and not isinstance(env.unwrapped, DirectRLEnv): + raise ValueError( + "The environment must be inherited from ManagerBasedRLEnv or DirectRLEnv. Environment type:" + f" {type(env)}" + ) + # initialize the wrapper + self.env = env + # collect common information + self.num_envs = self.unwrapped.num_envs + self.sim_device = self.unwrapped.device + self.render_mode = self.unwrapped.render_mode + + # obtain gym spaces + # note: stable-baselines3 does not like when we have unbounded action space so + # we set it to some high value here. Maybe this is not general but something to think about. + observation_space = self.unwrapped.single_observation_space["policy"] + action_space = self.unwrapped.single_action_space + if isinstance(action_space, gym.spaces.Box) and not action_space.is_bounded("both"): + action_space = gym.spaces.Box(low=-100, high=100, shape=action_space.shape) + + # initialize vec-env + VecEnv.__init__(self, self.num_envs, observation_space, action_space) + # add buffer for logging episodic information + self._ep_rew_buf = torch.zeros(self.num_envs, device=self.sim_device) + self._ep_len_buf = torch.zeros(self.num_envs, device=self.sim_device)
+ + def __str__(self): + """Returns the wrapper name and the :attr:`env` representation string.""" + return f"<{type(self).__name__}{self.env}>" + + def __repr__(self): + """Returns the string representation of the wrapper.""" + return str(self) + + """ + Properties -- Gym.Wrapper + """ + +
[文档] @classmethod + def class_name(cls) -> str: + """Returns the class name of the wrapper.""" + return cls.__name__
+ + @property + def unwrapped(self) -> ManagerBasedRLEnv | DirectRLEnv: + """Returns the base environment of the wrapper. + + This will be the bare :class:`gymnasium.Env` environment, underneath all layers of wrappers. + """ + return self.env.unwrapped + + """ + Properties + """ + +
[文档] def get_episode_rewards(self) -> list[float]: + """Returns the rewards of all the episodes.""" + return self._ep_rew_buf.cpu().tolist()
+ +
[文档] def get_episode_lengths(self) -> list[int]: + """Returns the number of time-steps of all the episodes.""" + return self._ep_len_buf.cpu().tolist()
+ + """ + Operations - MDP + """ + + def seed(self, seed: int | None = None) -> list[int | None]: # noqa: D102 + return [self.unwrapped.seed(seed)] * self.unwrapped.num_envs + + def reset(self) -> VecEnvObs: # noqa: D102 + obs_dict, _ = self.env.reset() + # reset episodic information buffers + self._ep_rew_buf.zero_() + self._ep_len_buf.zero_() + # convert data types to numpy depending on backend + return self._process_obs(obs_dict) + + def step_async(self, actions): # noqa: D102 + # convert input to numpy array + if not isinstance(actions, torch.Tensor): + actions = np.asarray(actions) + actions = torch.from_numpy(actions).to(device=self.sim_device, dtype=torch.float32) + else: + actions = actions.to(device=self.sim_device, dtype=torch.float32) + # convert to tensor + self._async_actions = actions + + def step_wait(self) -> VecEnvStepReturn: # noqa: D102 + # record step information + obs_dict, rew, terminated, truncated, extras = self.env.step(self._async_actions) + # update episode un-discounted return and length + self._ep_rew_buf += rew + self._ep_len_buf += 1 + # compute reset ids + dones = terminated | truncated + reset_ids = (dones > 0).nonzero(as_tuple=False) + + # convert data types to numpy depending on backend + # note: ManagerBasedRLEnv uses torch backend (by default). + obs = self._process_obs(obs_dict) + rew = rew.detach().cpu().numpy() + terminated = terminated.detach().cpu().numpy() + truncated = truncated.detach().cpu().numpy() + dones = dones.detach().cpu().numpy() + # convert extra information to list of dicts + infos = self._process_extras(obs, terminated, truncated, extras, reset_ids) + + # reset info for terminated environments + self._ep_rew_buf[reset_ids] = 0 + self._ep_len_buf[reset_ids] = 0 + + return obs, rew, dones, infos + + def close(self): # noqa: D102 + self.env.close() + + def get_attr(self, attr_name, indices=None): # noqa: D102 + # resolve indices + if indices is None: + indices = slice(None) + num_indices = self.num_envs + else: + num_indices = len(indices) + # obtain attribute value + attr_val = getattr(self.env, attr_name) + # return the value + if not isinstance(attr_val, torch.Tensor): + return [attr_val] * num_indices + else: + return attr_val[indices].detach().cpu().numpy() + + def set_attr(self, attr_name, value, indices=None): # noqa: D102 + raise NotImplementedError("Setting attributes is not supported.") + + def env_method(self, method_name: str, *method_args, indices=None, **method_kwargs): # noqa: D102 + if method_name == "render": + # gymnasium does not support changing render mode at runtime + return self.env.render() + else: + # this isn't properly implemented but it is not necessary. + # mostly done for completeness. + env_method = getattr(self.env, method_name) + return env_method(*method_args, indices=indices, **method_kwargs) + + def env_is_wrapped(self, wrapper_class, indices=None): # noqa: D102 + raise NotImplementedError("Checking if environment is wrapped is not supported.") + + def get_images(self): # noqa: D102 + raise NotImplementedError("Getting images is not supported.") + + """ + Helper functions. + """ + + def _process_obs(self, obs_dict: torch.Tensor | dict[str, torch.Tensor]) -> np.ndarray | dict[str, np.ndarray]: + """Convert observations into NumPy data type.""" + # Sb3 doesn't support asymmetric observation spaces, so we only use "policy" + obs = obs_dict["policy"] + # note: ManagerBasedRLEnv uses torch backend (by default). + if isinstance(obs, dict): + for key, value in obs.items(): + obs[key] = value.detach().cpu().numpy() + elif isinstance(obs, torch.Tensor): + obs = obs.detach().cpu().numpy() + else: + raise NotImplementedError(f"Unsupported data type: {type(obs)}") + return obs + + def _process_extras( + self, obs: np.ndarray, terminated: np.ndarray, truncated: np.ndarray, extras: dict, reset_ids: np.ndarray + ) -> list[dict[str, Any]]: + """Convert miscellaneous information into dictionary for each sub-environment.""" + # 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. + for idx in range(self.num_envs): + # fill-in episode monitoring info + if idx in reset_ids: + infos[idx]["episode"] = dict() + infos[idx]["episode"]["r"] = float(self._ep_rew_buf[idx]) + infos[idx]["episode"]["l"] = float(self._ep_len_buf[idx]) + else: + infos[idx]["episode"] = None + # fill-in bootstrap information + infos[idx]["TimeLimit.truncated"] = truncated[idx] and not terminated[idx] + # fill-in information from extras + for key, value in extras.items(): + # 1. remap extra episodes information safely + # 2. for others just store their values + if key == "log": + # only log this data for episodes that are terminated + if infos[idx]["episode"] is not None: + for sub_key, sub_value in value.items(): + infos[idx]["episode"][sub_key] = sub_value + else: + infos[idx][key] = value[idx] + # add information about terminal observation separately + if idx in reset_ids: + # extract terminal observations + if isinstance(obs, dict): + terminal_obs = dict.fromkeys(obs.keys()) + for key, value in obs.items(): + terminal_obs[key] = value[idx] + else: + terminal_obs = obs[idx] + # add info to dict + infos[idx]["terminal_observation"] = terminal_obs + else: + infos[idx]["terminal_observation"] = None + # return list of dictionaries + return infos
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/omni/isaac/lab_tasks/utils/wrappers/skrl.html b/_modules/omni/isaac/lab_tasks/utils/wrappers/skrl.html new file mode 100644 index 0000000000..2034f1e669 --- /dev/null +++ b/_modules/omni/isaac/lab_tasks/utils/wrappers/skrl.html @@ -0,0 +1,645 @@ + + + + + + + + + + + omni.isaac.lab_tasks.utils.wrappers.skrl — Isaac Lab Documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
+ +
+ + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

omni.isaac.lab_tasks.utils.wrappers.skrl 源代码

+# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
+# All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""Wrapper to configure an Isaac Lab environment instance to skrl environment.
+
+The following example shows how to wrap an environment for skrl:
+
+.. code-block:: python
+
+    from omni.isaac.lab_tasks.utils.wrappers.skrl import SkrlVecEnvWrapper
+
+    env = SkrlVecEnvWrapper(env, ml_framework="torch")  # or ml_framework="jax"
+
+Or, equivalently, by directly calling the skrl library API as follows:
+
+.. code-block:: python
+
+    from skrl.envs.torch.wrappers import wrap_env  # for PyTorch, or...
+    from skrl.envs.jax.wrappers import wrap_env    # for JAX
+
+    env = wrap_env(env, wrapper="isaaclab")
+
+"""
+
+# needed to import for type hinting: Agent | list[Agent]
+from __future__ import annotations
+
+from typing import Literal
+
+from omni.isaac.lab.envs import DirectMARLEnv, DirectRLEnv, ManagerBasedRLEnv
+
+"""
+Vectorized environment wrapper.
+"""
+
+
+
[文档]def SkrlVecEnvWrapper( + env: ManagerBasedRLEnv | DirectRLEnv | DirectMARLEnv, + ml_framework: Literal["torch", "jax", "jax-numpy"] = "torch", + wrapper: Literal["auto", "isaaclab", "isaaclab-single-agent", "isaaclab-multi-agent"] = "isaaclab", +): + """Wraps around Isaac Lab environment for skrl. + + This function wraps around the Isaac Lab environment. Since the wrapping + functionality is defined within the skrl library itself, this implementation + is maintained for compatibility with the structure of the extension that contains it. + Internally it calls the :func:`wrap_env` from the skrl library API. + + Args: + env: The environment to wrap around. + ml_framework: The ML framework to use for the wrapper. Defaults to "torch". + wrapper: The wrapper to use. Defaults to "isaaclab": leave it to skrl to determine if the environment + will be wrapped as single-agent or multi-agent. + + Raises: + ValueError: When the environment is not an instance of any Isaac Lab environment interface. + ValueError: If the specified ML framework is not valid. + + Reference: + https://skrl.readthedocs.io/en/latest/api/envs/wrapping.html + """ + # check that input is valid + if ( + not isinstance(env.unwrapped, ManagerBasedRLEnv) + and not isinstance(env.unwrapped, DirectRLEnv) + and not isinstance(env.unwrapped, DirectMARLEnv) + ): + raise ValueError( + "The environment must be inherited from ManagerBasedRLEnv, DirectRLEnv or DirectMARLEnv. Environment type:" + f" {type(env)}" + ) + + # import statements according to the ML framework + if ml_framework.startswith("torch"): + from skrl.envs.wrappers.torch import wrap_env + elif ml_framework.startswith("jax"): + from skrl.envs.wrappers.jax import wrap_env + else: + ValueError( + f"Invalid ML framework for skrl: {ml_framework}. Available options are: 'torch', 'jax' or 'jax-numpy'" + ) + + # wrap and return the environment + return wrap_env(env, wrapper)
+
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_sources/index.rst b/_sources/index.rst new file mode 100644 index 0000000000..c027c5c37a --- /dev/null +++ b/_sources/index.rst @@ -0,0 +1,165 @@ +Overview +======== + +.. figure:: source/_static/isaaclab.jpg + :width: 100% + :alt: H1 Humanoid example using Isaac Lab + + +.. attention:: + + 本翻译项目不属于 NVIDIA 或 IsaacLab 官方文档,由 `范子琦 `__ 提供中文翻译,仅供学习交流使用,禁止转载或用于商业用途。详情请查看 `关于翻译 `_ 章节。 + + 官方文档引入了版本系统,可以查看历史版本的文档。译者精力有限,故不提供历史版本翻译,本站只同步更新main分支的文档。 + + +**Isaac Lab** is a unified and modular framework for robot learning that aims to simplify common workflows +in robotics research (such as reinforcement learning, 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. + +Key features available in Isaac Lab include fast and accurate physics simulation provided by PhysX, +tiled rendering APIs for vectorized rendering, domain randomization for improving robustness and adaptability, +and support for running in the cloud. + +Additionally, Isaac Lab provides over 26 environments, and we are actively working on adding more environments +to the list. These include classic control tasks, fixed-arm and dexterous manipulation tasks, legged locomotion tasks, +and navigation tasks. A complete list is available in the `environments `_ section. + +The framework also includes over 16 robots. If you are looking to add a new robot, please refer to the +:ref:`how-to` section. The current list of robots includes: + +- **Classic** Cartpole, Humanoid, Ant +- **Fixed-Arm and Hands**: UR10, Franka, Allegro, Shadow Hand +- **Quadrupeds**: Anybotics Anymal-B, Anymal-C, Anymal-D, Unitree A1, Unitree Go1, Unitree Go2, Boston Dynamics Spot +- **Humanoids**: Unitree H1, Unitree G1 +- **Quadcopter**: Crazyflie + +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/setup/faq` section. + +.. figure:: source/_static/tasks.jpg + :width: 100% + :alt: Example tasks created using Isaac Lab + + +License +======= + +The Isaac Lab framework is open-sourced under the BSD-3-Clause license. +Please refer to :ref:`license` for more details. + +Acknowledgement +=============== +Isaac Lab development initiated from the `Orbit `_ framework. We would appreciate if you would cite it in academic publications as well: + +.. code:: 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} + } + + +Table of Contents +================= + +.. toctree:: + :maxdepth: 2 + :caption: Getting Started + + source/setup/wechat + source/setup/translation + source/setup/installation/index + source/setup/installation/cloud_installation + source/setup/faq + +.. toctree:: + :maxdepth: 3 + :caption: Overview + :titlesonly: + + source/overview/developer-guide/index + source/overview/core-concepts/index + source/overview/environments + source/overview/reinforcement-learning/index + source/overview/teleop_imitation + source/overview/showroom + source/overview/simple_agents + +.. toctree:: + :maxdepth: 2 + :caption: Features + + source/features/hydra + source/features/multi_gpu + source/features/tiled_rendering + source/features/reproducibility + +.. toctree:: + :maxdepth: 1 + :caption: Resources + :titlesonly: + + source/tutorials/index + source/how-to/index + source/deployment/index + +.. toctree:: + :maxdepth: 1 + :caption: Migration Guides + :titlesonly: + + source/migration/migrating_from_isaacgymenvs + source/migration/migrating_from_omniisaacgymenvs + source/migration/migrating_from_orbit + +.. toctree:: + :maxdepth: 1 + :caption: Source API + + source/api/index + +.. toctree:: + :maxdepth: 1 + :caption: References + + source/refs/reference_architecture/index + source/refs/additional_resources + source/refs/contributing + source/refs/troubleshooting + source/refs/issues + source/refs/changelog + source/refs/license + source/refs/bibliography + +.. toctree:: + :hidden: + :caption: Project Links + + GitHub + NVIDIA Isaac Sim + NVIDIA PhysX + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + +.. _NVIDIA Isaac Sim: https://docs.omniverse.nvidia.com/isaacsim/latest/index.html diff --git a/_sources/source/api/index.rst b/_sources/source/api/index.rst new file mode 100644 index 0000000000..4de3c159a5 --- /dev/null +++ b/_sources/source/api/index.rst @@ -0,0 +1,57 @@ +API Reference +============= + +This page gives an overview of all the modules and classes in the Isaac Lab extensions. + +omni.isaac.lab extension +------------------------ + +The following modules are available in the ``omni.isaac.lab`` extension: + +.. currentmodule:: omni.isaac.lab + +.. autosummary:: + :toctree: lab + + app + actuators + assets + controllers + devices + envs + managers + markers + scene + sensors + sim + terrains + utils + +.. toctree:: + :hidden: + + lab/omni.isaac.lab.envs.mdp + lab/omni.isaac.lab.envs.ui + lab/omni.isaac.lab.sensors.patterns + lab/omni.isaac.lab.sim.converters + lab/omni.isaac.lab.sim.schemas + lab/omni.isaac.lab.sim.spawners + +omni.isaac.lab_tasks extension +-------------------------------- + +The following modules are available in the ``omni.isaac.lab_tasks`` extension: + +.. currentmodule:: omni.isaac.lab_tasks + +.. autosummary:: + :toctree: lab_tasks + + utils + + +.. toctree:: + :hidden: + + lab_tasks/omni.isaac.lab_tasks.utils.wrappers + lab_tasks/omni.isaac.lab_tasks.utils.data_collector diff --git a/_sources/source/api/lab/omni.isaac.lab.actuators.rst b/_sources/source/api/lab/omni.isaac.lab.actuators.rst new file mode 100644 index 0000000000..312747f889 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.actuators.rst @@ -0,0 +1,135 @@ +omni.isaac.lab.actuators +======================== + +.. automodule:: omni.isaac.lab.actuators + + .. rubric:: Classes + + .. autosummary:: + + ActuatorBase + ActuatorBaseCfg + ImplicitActuator + ImplicitActuatorCfg + IdealPDActuator + IdealPDActuatorCfg + DCMotor + DCMotorCfg + DelayedPDActuator + DelayedPDActuatorCfg + RemotizedPDActuator + RemotizedPDActuatorCfg + 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 + +Delayed PD Actuator +------------------- + +.. autoclass:: DelayedPDActuator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DelayedPDActuatorCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Remotized PD Actuator +--------------------- + +.. autoclass:: RemotizedPDActuator + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RemotizedPDActuatorCfg + :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/_sources/source/api/lab/omni.isaac.lab.app.rst b/_sources/source/api/lab/omni.isaac.lab.app.rst new file mode 100644 index 0000000000..5616fa74ec --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.app.rst @@ -0,0 +1,115 @@ +omni.isaac.lab.app +================== + +.. automodule:: omni.isaac.lab.app + + .. rubric:: Classes + + .. autosummary:: + + AppLauncher + + +Environment variables +--------------------- + +The following details the behavior of the class based on the environment variables: + +* **Headless mode**: If the environment variable ``HEADLESS=1``, then SimulationApp will be started in headless mode. + If ``LIVESTREAM={1,2}``, then it will supersede the ``HEADLESS`` envvar and force headlessness. + + * ``HEADLESS=1`` causes the app to run in headless mode. + +* **Livestreaming**: If the environment variable ``LIVESTREAM={1,2}`` , then `livestream`_ is enabled. Any + of the livestream modes being true forces the app to run in headless mode. + + * ``LIVESTREAM=1`` enables streaming via the Isaac `Native Livestream`_ extension. This allows users to + connect through the Omniverse Streaming Client. + * ``LIVESTREAM=2`` enables streaming via the `WebRTC Livestream`_ extension. This allows users to + connect in a browser using the WebRTC protocol. + + .. note:: + + Each Isaac Sim instance can only connect to one streaming client. + Connecting to an Isaac Sim instance that is currently serving a streaming client + results in an error for the second user. + +* **Enable cameras**: If the environment variable ``ENABLE_CAMERAS`` is set to 1, then the + cameras are enabled. This is useful for running the simulator without a GUI but still rendering the + viewport and camera images. + + * ``ENABLE_CAMERAS=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.lab.sim.SimulationContext` class. This is because the off-screen rendering + pipeline enables flags that are internally used by the SimulationContext class. + + +To set the environment variables, one can use the following command in the terminal: + +.. code:: bash + + export REMOTE_DEPLOYMENT=3 + export ENABLE_CAMERAS=1 + # run the python script + ./isaaclab.sh -p source/standalone/demo/play_quadrupeds.py + +Alternatively, one can set the environment variables to the python script directly: + +.. code:: bash + + REMOTE_DEPLOYMENT=3 ENABLE_CAMERAS=1 ./isaaclab.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.lab.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 + + +Simulation App Launcher +----------------------- + +.. autoclass:: AppLauncher + :members: + + +.. _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 +.. _`Websocket Livestream`: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/manual_livestream_clients.html#isaac-sim-setup-livestream-webrtc +.. _`WebRTC Livestream`: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/manual_livestream_clients.html#isaac-sim-setup-livestream-websocket diff --git a/_sources/source/api/lab/omni.isaac.lab.assets.rst b/_sources/source/api/lab/omni.isaac.lab.assets.rst new file mode 100644 index 0000000000..ba139a2395 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.assets.rst @@ -0,0 +1,115 @@ +omni.isaac.lab.assets +===================== + +.. automodule:: omni.isaac.lab.assets + + .. rubric:: Classes + + .. autosummary:: + + AssetBase + AssetBaseCfg + RigidObject + RigidObjectData + RigidObjectCfg + RigidObjectCollection + RigidObjectCollectionData + RigidObjectCollectionCfg + Articulation + ArticulationData + ArticulationCfg + DeformableObject + DeformableObjectData + DeformableObjectCfg + +.. currentmodule:: omni.isaac.lab.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 + +Rigid Object Collection +----------------------- + +.. autoclass:: RigidObjectCollection + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: RigidObjectCollectionData + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: RigidObjectCollectionCfg + :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 + +Deformable Object +----------------- + +.. autoclass:: DeformableObject + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DeformableObjectData + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: DeformableObjectCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/_sources/source/api/lab/omni.isaac.lab.controllers.rst b/_sources/source/api/lab/omni.isaac.lab.controllers.rst new file mode 100644 index 0000000000..d56efdd05a --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.controllers.rst @@ -0,0 +1,25 @@ +omni.isaac.lab.controllers +========================== + +.. automodule:: omni.isaac.lab.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/_sources/source/api/lab/omni.isaac.lab.devices.rst b/_sources/source/api/lab/omni.isaac.lab.devices.rst new file mode 100644 index 0000000000..876787ca36 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.devices.rst @@ -0,0 +1,61 @@ +omni.isaac.lab.devices +====================== + +.. automodule:: omni.isaac.lab.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/_sources/source/api/lab/omni.isaac.lab.envs.mdp.rst b/_sources/source/api/lab/omni.isaac.lab.envs.mdp.rst new file mode 100644 index 0000000000..0a24a84c85 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.envs.mdp.rst @@ -0,0 +1,54 @@ +omni.isaac.lab.envs.mdp +======================= + +.. automodule:: omni.isaac.lab.envs.mdp + +Observations +------------ + +.. automodule:: omni.isaac.lab.envs.mdp.observations + :members: + +Actions +------- + +.. automodule:: omni.isaac.lab.envs.mdp.actions + +.. automodule:: omni.isaac.lab.envs.mdp.actions.actions_cfg + :members: + :show-inheritance: + :exclude-members: __init__, class_type + +Events +------ + +.. automodule:: omni.isaac.lab.envs.mdp.events + :members: + +Commands +-------- + +.. automodule:: omni.isaac.lab.envs.mdp.commands + +.. automodule:: omni.isaac.lab.envs.mdp.commands.commands_cfg + :members: + :show-inheritance: + :exclude-members: __init__, class_type + +Rewards +------- + +.. automodule:: omni.isaac.lab.envs.mdp.rewards + :members: + +Terminations +------------ + +.. automodule:: omni.isaac.lab.envs.mdp.terminations + :members: + +Curriculum +---------- + +.. automodule:: omni.isaac.lab.envs.mdp.curriculums + :members: diff --git a/_sources/source/api/lab/omni.isaac.lab.envs.rst b/_sources/source/api/lab/omni.isaac.lab.envs.rst new file mode 100644 index 0000000000..da8e6e5c0e --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.envs.rst @@ -0,0 +1,84 @@ +omni.isaac.lab.envs +=================== + +.. automodule:: omni.isaac.lab.envs + + .. rubric:: Submodules + + .. autosummary:: + + mdp + ui + + .. rubric:: Classes + + .. autosummary:: + + ManagerBasedEnv + ManagerBasedEnvCfg + ManagerBasedRLEnv + ManagerBasedRLEnvCfg + DirectRLEnv + DirectRLEnvCfg + DirectMARLEnv + DirectMARLEnvCfg + ViewerCfg + +Manager Based Environment +------------------------- + +.. autoclass:: ManagerBasedEnv + :members: + +.. autoclass:: ManagerBasedEnvCfg + :members: + :exclude-members: __init__, class_type + +Manager Based RL Environment +---------------------------- + +.. autoclass:: ManagerBasedRLEnv + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ManagerBasedRLEnvCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Direct RL Environment +--------------------- + +.. autoclass:: DirectRLEnv + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DirectRLEnvCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Direct Multi-Agent RL Environment +--------------------------------- + +.. autoclass:: DirectMARLEnv + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: DirectMARLEnvCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type + +Common +------ + +.. autoclass:: ViewerCfg + :members: + :exclude-members: __init__ diff --git a/_sources/source/api/lab/omni.isaac.lab.envs.ui.rst b/_sources/source/api/lab/omni.isaac.lab.envs.ui.rst new file mode 100644 index 0000000000..7818b780da --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.envs.ui.rst @@ -0,0 +1,31 @@ +omni.isaac.lab.envs.ui +====================== + +.. automodule:: omni.isaac.lab.envs.ui + + .. rubric:: Classes + + .. autosummary:: + + BaseEnvWindow + ManagerBasedRLEnvWindow + ViewportCameraController + +Base Environment UI +------------------- + +.. autoclass:: BaseEnvWindow + :members: + +Config Based RL Environment UI +------------------------------ + +.. autoclass:: ManagerBasedRLEnvWindow + :members: + :show-inheritance: + +Viewport Camera Controller +-------------------------- + +.. autoclass:: ViewportCameraController + :members: diff --git a/_sources/source/api/lab/omni.isaac.lab.managers.rst b/_sources/source/api/lab/omni.isaac.lab.managers.rst new file mode 100644 index 0000000000..cf6e14a7e4 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.managers.rst @@ -0,0 +1,146 @@ +omni.isaac.lab.managers +======================= + +.. automodule:: omni.isaac.lab.managers + + .. rubric:: Classes + + .. autosummary:: + + SceneEntityCfg + ManagerBase + ManagerTermBase + ManagerTermBaseCfg + ObservationManager + ObservationGroupCfg + ObservationTermCfg + ActionManager + ActionTerm + ActionTermCfg + EventManager + EventTermCfg + CommandManager + CommandTerm + CommandTermCfg + 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__ + +Event Manager +------------- + +.. autoclass:: EventManager + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: EventTermCfg + :members: + :exclude-members: __init__ + + +Command Manager +--------------- + +.. autoclass:: CommandManager + :members: + +.. autoclass:: CommandTerm + :members: + :exclude-members: __init__, class_type + +.. autoclass:: CommandTermCfg + :members: + :exclude-members: __init__, class_type + + +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/_sources/source/api/lab/omni.isaac.lab.markers.rst b/_sources/source/api/lab/omni.isaac.lab.markers.rst new file mode 100644 index 0000000000..f6a23e773d --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.markers.rst @@ -0,0 +1,23 @@ +omni.isaac.lab.markers +====================== + +.. automodule:: omni.isaac.lab.markers + + .. rubric:: Classes + + .. autosummary:: + + VisualizationMarkers + VisualizationMarkersCfg + +Visualization Markers +--------------------- + +.. autoclass:: VisualizationMarkers + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: VisualizationMarkersCfg + :members: + :exclude-members: __init__ diff --git a/_sources/source/api/lab/omni.isaac.lab.scene.rst b/_sources/source/api/lab/omni.isaac.lab.scene.rst new file mode 100644 index 0000000000..5a9bba3a33 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.scene.rst @@ -0,0 +1,23 @@ +omni.isaac.lab.scene +==================== + +.. automodule:: omni.isaac.lab.scene + + .. rubric:: Classes + + .. autosummary:: + + InteractiveScene + InteractiveSceneCfg + +interactive Scene +----------------- + +.. autoclass:: InteractiveScene + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: InteractiveSceneCfg + :members: + :exclude-members: __init__ diff --git a/_sources/source/api/lab/omni.isaac.lab.sensors.patterns.rst b/_sources/source/api/lab/omni.isaac.lab.sensors.patterns.rst new file mode 100644 index 0000000000..41d6b727a3 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.sensors.patterns.rst @@ -0,0 +1,61 @@ +omni.isaac.lab.sensors.patterns +=============================== + +.. automodule:: omni.isaac.lab.sensors.patterns + + .. rubric:: Classes + + .. autosummary:: + + PatternBaseCfg + GridPatternCfg + PinholeCameraPatternCfg + BpearlPatternCfg + +Pattern Base +------------ + +.. autoclass:: PatternBaseCfg + :members: + :inherited-members: + :exclude-members: __init__ + +Grid Pattern +------------ + +.. autofunction:: omni.isaac.lab.sensors.patterns.grid_pattern + +.. autoclass:: GridPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func + +Pinhole Camera Pattern +---------------------- + +.. autofunction:: omni.isaac.lab.sensors.patterns.pinhole_camera_pattern + +.. autoclass:: PinholeCameraPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func + +RS-Bpearl Pattern +----------------- + +.. autofunction:: omni.isaac.lab.sensors.patterns.bpearl_pattern + +.. autoclass:: BpearlPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func + +LiDAR Pattern +------------- + +.. autofunction:: omni.isaac.lab.sensors.patterns.lidar_pattern + +.. autoclass:: LidarPatternCfg + :members: + :inherited-members: + :exclude-members: __init__, func diff --git a/_sources/source/api/lab/omni.isaac.lab.sensors.rst b/_sources/source/api/lab/omni.isaac.lab.sensors.rst new file mode 100644 index 0000000000..3a82a42904 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.sensors.rst @@ -0,0 +1,168 @@ +omni.isaac.lab.sensors +====================== + +.. automodule:: omni.isaac.lab.sensors + + .. rubric:: Submodules + + .. autosummary:: + + patterns + + .. rubric:: Classes + + .. autosummary:: + + SensorBase + SensorBaseCfg + Camera + CameraData + CameraCfg + TiledCamera + TiledCameraCfg + ContactSensor + ContactSensorData + ContactSensorCfg + FrameTransformer + FrameTransformerData + FrameTransformerCfg + RayCaster + RayCasterData + RayCasterCfg + RayCasterCamera + RayCasterCameraCfg + Imu + ImuCfg + +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 + +Tile-Rendered USD Camera +------------------------ + +.. autoclass:: TiledCamera + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: TiledCameraCfg + :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 + +Inertia Measurement Unit +------------------------ + +.. autoclass:: Imu + :members: + :inherited-members: + :show-inheritance: + +.. autoclass:: ImuCfg + :members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, class_type diff --git a/_sources/source/api/lab/omni.isaac.lab.sim.converters.rst b/_sources/source/api/lab/omni.isaac.lab.sim.converters.rst new file mode 100644 index 0000000000..1b5ee45c08 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.sim.converters.rst @@ -0,0 +1,54 @@ +omni.isaac.lab.sim.converters +============================= + +.. automodule:: omni.isaac.lab.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/_sources/source/api/lab/omni.isaac.lab.sim.rst b/_sources/source/api/lab/omni.isaac.lab.sim.rst new file mode 100644 index 0000000000..b2b582c68b --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.sim.rst @@ -0,0 +1,65 @@ +omni.isaac.lab.sim +================== + +.. automodule:: omni.isaac.lab.sim + + .. rubric:: Submodules + + .. autosummary:: + + converters + schemas + spawners + utils + + .. rubric:: Classes + + .. autosummary:: + + SimulationContext + SimulationCfg + PhysxCfg + RenderCfg + + .. rubric:: Functions + + .. autosummary:: + + simulation_context.build_simulation_context + +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__ + +.. autoclass:: RenderCfg + :members: + :show-inheritance: + :exclude-members: __init__ + +Simulation Context Builder +-------------------------- + +.. automethod:: simulation_context.build_simulation_context + +Utilities +--------- + +.. automodule:: omni.isaac.lab.sim.utils + :members: + :show-inheritance: diff --git a/_sources/source/api/lab/omni.isaac.lab.sim.schemas.rst b/_sources/source/api/lab/omni.isaac.lab.sim.schemas.rst new file mode 100644 index 0000000000..7b1b8cfffd --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.sim.schemas.rst @@ -0,0 +1,103 @@ +omni.isaac.lab.sim.schemas +========================== + +.. automodule:: omni.isaac.lab.sim.schemas + + .. rubric:: Classes + + .. autosummary:: + + ArticulationRootPropertiesCfg + RigidBodyPropertiesCfg + CollisionPropertiesCfg + MassPropertiesCfg + JointDrivePropertiesCfg + FixedTendonPropertiesCfg + DeformableBodyPropertiesCfg + + .. 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 + modify_joint_drive_properties + modify_fixed_tendon_properties + define_deformable_body_properties + modify_deformable_body_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 + +Joint Drive +----------- + +.. autoclass:: JointDrivePropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: modify_joint_drive_properties + +Fixed Tendon +------------ + +.. autoclass:: FixedTendonPropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: modify_fixed_tendon_properties + +Deformable Body +--------------- + +.. autoclass:: DeformableBodyPropertiesCfg + :members: + :exclude-members: __init__ + +.. autofunction:: define_deformable_body_properties +.. autofunction:: modify_deformable_body_properties diff --git a/_sources/source/api/lab/omni.isaac.lab.sim.spawners.rst b/_sources/source/api/lab/omni.isaac.lab.sim.spawners.rst new file mode 100644 index 0000000000..a1c073d4c2 --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.sim.spawners.rst @@ -0,0 +1,329 @@ +omni.isaac.lab.sim.spawners +=========================== + +.. automodule:: omni.isaac.lab.sim.spawners + + .. rubric:: Submodules + + .. autosummary:: + + shapes + meshes + lights + sensors + from_files + materials + wrappers + + .. rubric:: Classes + + .. autosummary:: + + SpawnerCfg + RigidObjectSpawnerCfg + DeformableObjectSpawnerCfg + +Spawners +-------- + +.. autoclass:: SpawnerCfg + :members: + :exclude-members: __init__ + +.. autoclass:: RigidObjectSpawnerCfg + :members: + :show-inheritance: + :exclude-members: __init__ + +.. autoclass:: DeformableObjectSpawnerCfg + :members: + :show-inheritance: + :exclude-members: __init__ + +Shapes +------ + +.. automodule:: omni.isaac.lab.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 + +Meshes +------ + +.. automodule:: omni.isaac.lab.sim.spawners.meshes + + .. rubric:: Classes + + .. autosummary:: + + MeshCfg + MeshCapsuleCfg + MeshConeCfg + MeshCuboidCfg + MeshCylinderCfg + MeshSphereCfg + +.. autoclass:: MeshCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_mesh_capsule + +.. autoclass:: MeshCapsuleCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_mesh_cone + +.. autoclass:: MeshConeCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_mesh_cuboid + +.. autoclass:: MeshCuboidCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_mesh_cylinder + +.. autoclass:: MeshCylinderCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +.. autofunction:: spawn_mesh_sphere + +.. autoclass:: MeshSphereCfg + :members: + :show-inheritance: + :exclude-members: __init__, func + +Lights +------ + +.. automodule:: omni.isaac.lab.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.lab.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.lab.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.lab.sim.spawners.materials + + .. rubric:: Classes + + .. autosummary:: + + VisualMaterialCfg + PreviewSurfaceCfg + MdlFileCfg + GlassMdlCfg + PhysicsMaterialCfg + RigidBodyMaterialCfg + DeformableBodyMaterialCfg + +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 + +.. autofunction:: spawn_deformable_body_material + +.. autoclass:: DeformableBodyMaterialCfg + :members: + :exclude-members: __init__, func + +Wrappers +-------- + +.. automodule:: omni.isaac.lab.sim.spawners.wrappers + + .. rubric:: Classes + + .. autosummary:: + + MultiAssetSpawnerCfg + MultiUsdFileCfg + +.. autofunction:: spawn_multi_asset + +.. autoclass:: MultiAssetSpawnerCfg + :members: + :exclude-members: __init__, func + +.. autofunction:: spawn_multi_usd_file + +.. autoclass:: MultiUsdFileCfg + :members: + :exclude-members: __init__, func diff --git a/_sources/source/api/lab/omni.isaac.lab.terrains.rst b/_sources/source/api/lab/omni.isaac.lab.terrains.rst new file mode 100644 index 0000000000..3d11fa1f1b --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.terrains.rst @@ -0,0 +1,261 @@ +omni.isaac.lab.terrains +======================= + +.. automodule:: omni.isaac.lab.terrains + + .. rubric:: Classes + + .. autosummary:: + + TerrainImporter + TerrainImporterCfg + TerrainGenerator + TerrainGeneratorCfg + SubTerrainBaseCfg + + +Terrain importer +---------------- + +.. autoclass:: TerrainImporter + :members: + :show-inheritance: + +.. autoclass:: TerrainImporterCfg + :members: + :exclude-members: __init__, class_type + +Terrain generator +----------------- + +.. autoclass:: TerrainGenerator + :members: + +.. autoclass:: TerrainGeneratorCfg + :members: + :exclude-members: __init__ + +.. autoclass:: SubTerrainBaseCfg + :members: + :exclude-members: __init__ + +Height fields +------------- + +.. automodule:: omni.isaac.lab.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.lab.terrains.height_field.hf_terrains_cfg.HfTerrainBaseCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Random Uniform Terrain +^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.height_field.hf_terrains.random_uniform_terrain + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfRandomUniformTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Pyramid Sloped Terrain +^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.height_field.hf_terrains.pyramid_sloped_terrain + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfPyramidSlopedTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfInvertedPyramidSlopedTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Pyramid Stairs Terrain +^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.height_field.hf_terrains.pyramid_stairs_terrain + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfPyramidStairsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfInvertedPyramidStairsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Discrete Obstacles Terrain +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.height_field.hf_terrains.discrete_obstacles_terrain + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfDiscreteObstaclesTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Wave Terrain +^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.height_field.hf_terrains.wave_terrain + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfWaveTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Stepping Stones Terrain +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.height_field.hf_terrains.stepping_stones_terrain + +.. autoclass:: omni.isaac.lab.terrains.height_field.hf_terrains_cfg.HfSteppingStonesTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Trimesh terrains +---------------- + +.. automodule:: omni.isaac.lab.terrains.trimesh + + +Flat terrain +^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.flat_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshPlaneTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Pyramid terrain +^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.pyramid_stairs_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshPyramidStairsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Inverted pyramid terrain +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.inverted_pyramid_stairs_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshInvertedPyramidStairsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Random grid terrain +^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.random_grid_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshRandomGridTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Rails terrain +^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.rails_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshRailsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Pit terrain +^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.pit_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshPitTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Box terrain +^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.box_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshBoxTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Gap terrain +^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.gap_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshGapTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Floating ring terrain +^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.floating_ring_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshFloatingRingTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Star terrain +^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.star_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshStarTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Repeated Objects Terrain +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: omni.isaac.lab.terrains.trimesh.mesh_terrains.repeated_objects_terrain + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedObjectsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedPyramidsTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedBoxesTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +.. autoclass:: omni.isaac.lab.terrains.trimesh.mesh_terrains_cfg.MeshRepeatedCylindersTerrainCfg + :members: + :show-inheritance: + :exclude-members: __init__, function + +Utilities +--------- + +.. automodule:: omni.isaac.lab.terrains.utils + :members: + :undoc-members: diff --git a/_sources/source/api/lab/omni.isaac.lab.utils.rst b/_sources/source/api/lab/omni.isaac.lab.utils.rst new file mode 100644 index 0000000000..c14ae19b6c --- /dev/null +++ b/_sources/source/api/lab/omni.isaac.lab.utils.rst @@ -0,0 +1,140 @@ +omni.isaac.lab.utils +==================== + +.. automodule:: omni.isaac.lab.utils + + .. Rubric:: Submodules + + .. autosummary:: + + io + array + assets + buffers + dict + interpolation + math + modifiers + noise + string + timer + types + warp + + .. Rubric:: Functions + + .. autosummary:: + + configclass + +Configuration class +~~~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.configclass + :members: + :show-inheritance: + +IO operations +~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.io + :members: + :imported-members: + :show-inheritance: + +Array operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.array + :members: + :show-inheritance: + +Asset operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.assets + :members: + :show-inheritance: + +Buffer operations +~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.buffers + :members: + :imported-members: + :inherited-members: + :show-inheritance: + +Dictionary operations +~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.dict + :members: + :show-inheritance: + +Interpolation operations +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.interpolation + :members: + :imported-members: + :inherited-members: + :show-inheritance: + +Math operations +~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.math + :members: + :inherited-members: + :show-inheritance: + +Modifier operations +~~~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.modifiers + :members: + :imported-members: + :special-members: __call__ + :inherited-members: + :show-inheritance: + :exclude-members: __init__, func + +Noise operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.noise + :members: + :imported-members: + :inherited-members: + :show-inheritance: + :exclude-members: __init__, func + +String operations +~~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.string + :members: + :show-inheritance: + +Timer operations +~~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.timer + :members: + :show-inheritance: + +Type operations +~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.types + :members: + :show-inheritance: + +Warp operations +~~~~~~~~~~~~~~~ + +.. automodule:: omni.isaac.lab.utils.warp + :members: + :imported-members: + :show-inheritance: diff --git a/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.data_collector.rst b/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.data_collector.rst new file mode 100644 index 0000000000..b29b1446b0 --- /dev/null +++ b/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.data_collector.rst @@ -0,0 +1,17 @@ +omni.isaac.lab_tasks.utils.data_collector +========================================= + +.. automodule:: omni.isaac.lab_tasks.utils.data_collector + + .. Rubric:: Classes + + .. autosummary:: + + RobomimicDataCollector + +Robomimic Data Collector +------------------------ + +.. autoclass:: RobomimicDataCollector + :members: + :show-inheritance: diff --git a/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.rst b/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.rst new file mode 100644 index 0000000000..644bcaa439 --- /dev/null +++ b/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.rst @@ -0,0 +1,13 @@ +omni.isaac.lab_tasks.utils +========================== + +.. automodule:: omni.isaac.lab_tasks.utils + :members: + :imported-members: + + .. rubric:: Submodules + + .. autosummary:: + + data_collector + wrappers diff --git a/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.wrappers.rst b/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.wrappers.rst new file mode 100644 index 0000000000..e70975375d --- /dev/null +++ b/_sources/source/api/lab_tasks/omni.isaac.lab_tasks.utils.wrappers.rst @@ -0,0 +1,33 @@ +omni.isaac.lab_tasks.utils.wrappers +=================================== + +.. automodule:: omni.isaac.lab_tasks.utils.wrappers + +RL-Games Wrapper +---------------- + +.. automodule:: omni.isaac.lab_tasks.utils.wrappers.rl_games + :members: + :show-inheritance: + +RSL-RL Wrapper +-------------- + +.. automodule:: omni.isaac.lab_tasks.utils.wrappers.rsl_rl + :members: + :imported-members: + :show-inheritance: + +SKRL Wrapper +------------ + +.. automodule:: omni.isaac.lab_tasks.utils.wrappers.skrl + :members: + :show-inheritance: + +Stable-Baselines3 Wrapper +------------------------- + +.. automodule:: omni.isaac.lab_tasks.utils.wrappers.sb3 + :members: + :show-inheritance: diff --git a/_sources/source/deployment/cluster.rst b/_sources/source/deployment/cluster.rst new file mode 100644 index 0000000000..467fda90f4 --- /dev/null +++ b/_sources/source/deployment/cluster.rst @@ -0,0 +1,211 @@ +.. _deployment-cluster: + + +Cluster Guide +============= + +Clusters are a great way to speed up training and evaluation of learning algorithms. +While the Isaac Lab Docker image can be used to run jobs on a cluster, many clusters only +support singularity images. This is because `singularity`_ is designed for +ease-of-use on shared multi-user systems and high performance computing (HPC) environments. +It does not require root privileges to run containers and can be used to run user-defined +containers. + +Singularity is compatible with all Docker images. In this section, we describe how to +convert the Isaac Lab Docker image into a singularity image and use it to submit jobs to a cluster. + +.. attention:: + + Cluster setup varies across different institutions. The following instructions have been + tested on the `ETH Zurich Euler`_ cluster (which uses the SLURM workload manager), and the + IIT Genoa Franklin cluster (which uses PBS workload manager). + + The instructions may need to be adapted for other clusters. If you have successfully + adapted the instructions for another cluster, please consider contributing to the + documentation. + + +Setup Instructions +------------------ + +In order to export the Docker Image to a singularity image, `apptainer`_ is required. +A detailed overview of the installation procedure for ``apptainer`` can be found in its +`documentation`_. For convenience, we summarize the steps here for a local installation: + +.. code:: bash + + sudo apt update + sudo apt install -y software-properties-common + sudo add-apt-repository -y ppa:apptainer/ppa + sudo apt update + sudo apt install -y apptainer + +For simplicity, we recommend that an SSH connection is set up between the local +development machine and the cluster. Such a connection will simplify the file transfer and prevent +the user cluster password from being requested multiple times. + +.. attention:: + The workflow has been tested with: + + - ``apptainer version 1.2.5-1.el7`` and ``docker version 24.0.7`` + - ``apptainer version 1.3.4`` and ``docker version 27.3.1`` + + In the case of issues, please try to switch to those versions. + + +Configuring the cluster parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you need to configure the cluster-specific parameters in ``docker/cluster/.env.cluster`` file. +The following describes the parameters that need to be configured: + +.. list-table:: + :header-rows: 1 + :widths: 20 80 + + * - Parameter + - Description + * - CLUSTER_JOB_SCHEDULER + - The job scheduler/workload manager used by your cluster. Currently, we support 'SLURM' and + 'PBS' workload managers. + * - CLUSTER_ISAAC_SIM_CACHE_DIR + - The directory on the cluster where the Isaac Sim cache is stored. This directory + has to end on ``docker-isaac-sim``. It will be copied to the compute node + and mounted into the singularity container. This should increase the speed of starting + the simulation. + * - CLUSTER_ISAACLAB_DIR + - The directory on the cluster where the Isaac Lab logs are stored. This directory has to + end on ``isaaclab``. It will be copied to the compute node and mounted into + the singularity container. When a job is submitted, the latest local changes will + be copied to the cluster to a new directory in the format ``${CLUSTER_ISAACLAB_DIR}_${datetime}`` + with the date and time of the job submission. This allows to run multiple jobs with different code versions at + the same time. + * - CLUSTER_LOGIN + - The login to the cluster. Typically, this is the user and cluster names, + e.g., ``your_user@euler.ethz.ch``. + * - CLUSTER_SIF_PATH + - The path on the cluster where the singularity image will be stored. The image will be + copied to the compute node but not uploaded again to the cluster when a job is submitted. + * - REMOVE_CODE_COPY_AFTER_JOB + - Whether the copied code should be removed after the job is finished or not. The logs from the job will not be deleted + as these are saved under the permanent ``CLUSTER_ISAACLAB_DIR``. This feature is useful + to save disk space on the cluster. If set to ``true``, the code copy will be removed. + * - CLUSTER_PYTHON_EXECUTABLE + - The path within Isaac Lab to the Python executable that should be executed in the submitted job. + +When a ``job`` is submitted, it will also use variables defined in ``docker/.env.base``, though these +should be correct by default. + +Exporting to singularity image +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Next, we need to export the Docker image to a singularity image and upload +it to the cluster. This step is only required once when the first job is submitted +or when the Docker image is updated. For instance, due to an upgrade of the Isaac Sim +version, or additional requirements for your project. + +To export to a singularity image, execute the following command: + +.. code:: bash + + ./docker/cluster/cluster_interface.sh push [profile] + +This command will create a singularity image under ``docker/exports`` directory and +upload it to the defined location on the cluster. It requires that you have previously +built the image with the ``container.py`` interface. Be aware that creating the singularity +image can take a while. +``[profile]`` is an optional argument that specifies the container profile to be used. If no profile is +specified, the default profile ``base`` will be used. + +.. note:: + By default, the singularity image is created without root access by providing the ``--fakeroot`` flag to + the ``apptainer build`` command. In case the image creation fails, you can try to create it with root + access by removing the flag in ``docker/cluster/cluster_interface.sh``. + + +Defining the job parameters +--------------------------- + +The job parameters need to be defined based on the job scheduler used by your cluster. +You only need to update the appropriate script for the scheduler available to you. + +- For SLURM, update the parameters in ``docker/cluster/submit_job_slurm.sh``. +- For PBS, update the parameters in ``docker/cluster/submit_job_pbs.sh``. + +For SLURM +~~~~~~~~~ + +The job parameters are defined inside the ``docker/cluster/submit_job_slurm.sh``. +A typical SLURM operation requires specifying the number of CPUs and GPUs, the memory, and +the time limit. For more information, please check the `SLURM documentation`_. + +The default configuration is as follows: + +.. literalinclude:: ../../../docker/cluster/submit_job_slurm.sh + :language: bash + :lines: 12-19 + :linenos: + :lineno-start: 12 + +An essential requirement for the cluster is that the compute node has access to the internet at all times. +This is required to load assets from the Nucleus server. For some cluster architectures, extra modules +must be loaded to allow internet access. + +For instance, on ETH Zurich Euler cluster, the ``eth_proxy`` module needs to be loaded. This can be done +by adding the following line to the ``submit_job_slurm.sh`` script: + +.. literalinclude:: ../../../docker/cluster/submit_job_slurm.sh + :language: bash + :lines: 3-5 + :linenos: + :lineno-start: 3 + +For PBS +~~~~~~~ + +The job parameters are defined inside the ``docker/cluster/submit_job_pbs.sh``. +A typical PBS operation requires specifying the number of CPUs and GPUs, and the time limit. For more +information, please check the `PBS Official Site`_. + +The default configuration is as follows: + +.. literalinclude:: ../../../docker/cluster/submit_job_pbs.sh + :language: bash + :lines: 11-17 + :linenos: + :lineno-start: 11 + + +Submitting a job +---------------- + +To submit a job on the cluster, the following command can be used: + +.. code:: bash + + ./docker/cluster/cluster_interface.sh job [profile] "argument1" "argument2" ... + +This command will copy the latest changes in your code to the cluster and submit a job. Please ensure that +your Python executable's output is stored under ``isaaclab/logs`` as this directory is synced between the compute +node and ``CLUSTER_ISAACLAB_DIR``. + +``[profile]`` is an optional argument that specifies which singularity image corresponding to the container profile +will be used. If no profile is specified, the default profile ``base`` will be used. The profile has be defined +directlty after the ``job`` command. All other arguments are passed to the Python executable. If no profile is +defined, all arguments are passed to the Python executable. + +The training arguments are passed to the Python executable. As an example, the standard +ANYmal rough terrain locomotion training can be executed with the following command: + +.. code:: bash + + ./docker/cluster/cluster_interface.sh job --task Isaac-Velocity-Rough-Anymal-C-v0 --headless --video --enable_cameras + +The above will, in addition, also render videos of the training progress and store them under ``isaaclab/logs`` directory. + +.. _Singularity: https://docs.sylabs.io/guides/2.6/user-guide/index.html +.. _ETH Zurich Euler: https://scicomp.ethz.ch/wiki/Euler +.. _PBS Official Site: https://openpbs.org/ +.. _apptainer: https://apptainer.org/ +.. _documentation: https://www.apptainer.org/docs/admin/main/installation.html#install-ubuntu-packages +.. _SLURM documentation: https://www.slurm.schedmd.com/sbatch.html diff --git a/_sources/source/deployment/docker.rst b/_sources/source/deployment/docker.rst new file mode 100644 index 0000000000..dc5ce052c6 --- /dev/null +++ b/_sources/source/deployment/docker.rst @@ -0,0 +1,314 @@ +.. _deployment-docker: + + +Docker Guide +============ + +.. caution:: + + Due to the dependency on Isaac Sim docker image, by running this container you are implicitly + agreeing to the `NVIDIA Omniverse EULA`_. If you do not agree to the EULA, do not run this container. + +Setup Instructions +------------------ + +.. note:: + + The following steps are taken from the NVIDIA Omniverse Isaac Sim documentation on `container installation`_. + They have been added here for the sake of completeness. + + +Docker and Docker Compose +~~~~~~~~~~~~~~~~~~~~~~~~~ + +We have tested the container using Docker Engine version 26.0.0 and Docker Compose version 2.25.0 +We recommend using these versions or newer. + +* To install Docker, please follow the instructions for your operating system on the `Docker website`_. +* To install Docker Compose, please follow the instructions for your operating system on the `docker compose`_ page. +* Follow the post-installation steps for Docker on the `post-installation steps`_ page. These steps allow you to run + Docker without using ``sudo``. +* To build and run GPU-accelerated containers, you also need install the `NVIDIA Container Toolkit`_. + Please follow the instructions on the `Container Toolkit website`_ for installation steps. + +.. note:: + + Due to limitations with `snap `_, please make sure + the Isaac Lab directory is placed under the ``/home`` directory tree when using docker. + + +Obtaining the Isaac Sim Container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Get access to the `Isaac Sim container`_ by joining the NVIDIA Developer Program credentials. +* Generate your `NGC API key`_ to access locked container images from NVIDIA GPU Cloud (NGC). + + * This step requires you to create an NGC account if you do not already have one. + * You would also need to install the NGC CLI to perform operations from the command line. + * Once you have your generated API key and have installed the NGC CLI, you need to log in to NGC + from the terminal. + + .. code:: bash + + ngc config set + +* Use the command line to pull the Isaac Sim container image from NGC. + + .. code:: bash + + docker login nvcr.io + + * For the username, enter ``$oauthtoken`` exactly as shown. It is a special username that is used to + authenticate with NGC. + + .. code:: text + + Username: $oauthtoken + Password: + + +Directory Organization +---------------------- + +The root of the Isaac Lab repository contains the ``docker`` directory that has various files and scripts +needed to run Isaac Lab inside a Docker container. A subset of these are summarized below: + +* **Dockerfile.base**: Defines the base Isaac Lab image by overlaying its dependencies onto the Isaac Sim Docker image. + Dockerfiles which end with something else, (i.e. ``Dockerfile.ros2``) build an `image extension <#isaac-lab-image-extensions>`_. +* **docker-compose.yaml**: Creates mounts to allow direct editing of Isaac Lab code from the host machine that runs + the container. It also creates several named volumes such as ``isaac-cache-kit`` to + store frequently re-used resources compiled by Isaac Sim, such as shaders, and to retain logs, data, and documents. +* **.env.base**: Stores environment variables required for the ``base`` build process and the container itself. ``.env`` + files which end with something else (i.e. ``.env.ros2``) define these for `image extension <#isaac-lab-image-extensions>`_. +* **container.py**: A utility script that interfaces with tools in ``utils`` to configure and build the image, + and run and interact with the container. + +Running the Container +--------------------- + +.. note:: + + The docker container copies all the files from the repository into the container at the + location ``/workspace/isaaclab`` at build time. This means that any changes made to the files in the container would not + normally be reflected in the repository after the image has been built, i.e. after ``./container.py start`` is run. + + For a faster development cycle, we mount the following directories in the Isaac Lab repository into the container + so that you can edit their files from the host machine: + + * **IsaacLab/source**: This is the directory that contains the Isaac Lab source code. + * **IsaacLab/docs**: This is the directory that contains the source code for Isaac Lab documentation. This is overlaid except + for the ``_build`` subdirectory where build artifacts are stored. + + +The script ``container.py`` parallels basic ``docker compose`` commands. Each can accept an `image extension argument <#isaac-lab-image-extensions>`_, +or else they will default to the ``base`` image extension. These commands are: + +* **start**: This builds the image and brings up the container in detached mode (i.e. in the background). +* **enter**: This begins a new bash process in an existing Isaac Lab container, and which can be exited + without bringing down the container. +* **config**: This outputs the compose.yaml which would be result from the inputs given to ``container.py start``. This command is useful + for debugging a compose configuration. +* **copy**: This copies the ``logs``, ``data_storage`` and ``docs/_build`` artifacts, from the ``isaac-lab-logs``, ``isaac-lab-data`` and ``isaac-lab-docs`` + volumes respectively, to the ``docker/artifacts`` directory. These artifacts persist between docker container instances and are shared between image extensions. +* **stop**: This brings down the container and removes it. + +The following shows how to launch the container in a detached state and enter it: + +.. code:: bash + + # Launch the container in detached mode + # We don't pass an image extension arg, so it defaults to 'base' + ./docker/container.py start + + # If we want to add .env or .yaml files to customize our compose config, + # we can simply specify them in the same manner as the compose cli + # ./docker/container.py start --file my-compose.yaml --env-file .env.my-vars + + # Enter the container + # We pass 'base' explicitly, but if we hadn't it would default to 'base' + ./docker/container.py enter base + +To copy files from the base container to the host machine, you can use the following command: + +.. code:: bash + + # Copy the file /workspace/isaaclab/logs to the current directory + docker cp isaac-lab-base:/workspace/isaaclab/logs . + +The script ``container.py`` provides a wrapper around this command to copy the ``logs`` , ``data_storage`` and ``docs/_build`` +directories to the ``docker/artifacts`` directory. This is useful for copying the logs, data and documentation: + +.. code:: bash + + # stop the container + ./docker/container.py stop + + +X11 forwarding +~~~~~~~~~~~~~~ + +The container supports X11 forwarding, which allows the user to run GUI applications from the container +and display them on the host machine. + +The first time a container is started with ``./docker/container.py start``, the script prompts +the user whether to activate X11 forwarding. This will create a file at ``docker/.container.cfg`` +to store the user's choice for future runs. + +If you want to change the choice, you can set the parameter ``X11_FORWARDING_ENABLED`` to '0' or '1' +in the ``docker/.container.cfg`` file to disable or enable X11 forwarding, respectively. After that, you need to +re-build the container by running ``./docker/container.py start``. The rebuilding process ensures that the changes +are applied to the container. Otherwise, the changes will not take effect. + +After the container is started, you can enter the container and run GUI applications from it with X11 forwarding enabled. +The display will be forwarded to the host machine. + + +Python Interpreter +~~~~~~~~~~~~~~~~~~ + +The container uses the Python interpreter provided by Isaac Sim. This interpreter is located at +``/isaac-sim/python.sh``. We set aliases inside the container to make it easier to run the Python +interpreter. You can use the following commands to run the Python interpreter: + +.. code:: bash + + # Run the Python interpreter -> points to /isaac-sim/python.sh + python + + +Understanding the mounted volumes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``docker-compose.yaml`` file creates several named volumes that are mounted to the container. +These are summarized below: + +.. list-table:: + :header-rows: 1 + :widths: 23 45 32 + + * - Volume Name + - Description + - Container Path + * - isaac-cache-kit + - Stores cached Kit resources + - /isaac-sim/kit/cache + * - isaac-cache-ov + - Stores cached OV resources + - /root/.cache/ov + * - isaac-cache-pip + - Stores cached pip resources + - /root/.cache/pip + * - isaac-cache-gl + - Stores cached GLCache resources + - /root/.cache/nvidia/GLCache + * - isaac-cache-compute + - Stores cached compute resources + - /root/.nv/ComputeCache + * - isaac-logs + - Stores logs generated by Omniverse + - /root/.nvidia-omniverse/logs + * - isaac-carb-logs + - Stores logs generated by carb + - /isaac-sim/kit/logs/Kit/Isaac-Sim + * - isaac-data + - Stores data generated by Omniverse + - /root/.local/share/ov/data + * - isaac-docs + - Stores documents generated by Omniverse + - /root/Documents + * - isaac-lab-docs + - Stores documentation of Isaac Lab when built inside the container + - /workspace/isaaclab/docs/_build + * - isaac-lab-logs + - Stores logs generated by Isaac Lab workflows when run inside the container + - /workspace/isaaclab/logs + * - isaac-lab-data + - Stores whatever data users may want to preserve between container runs + - /workspace/isaaclab/data_storage + +To view the contents of these volumes, you can use the following command: + +.. code:: bash + + # list all volumes + docker volume ls + # inspect a specific volume, e.g. isaac-cache-kit + docker volume inspect isaac-cache-kit + + + +Isaac Lab Image Extensions +-------------------------- + +The produced image depends upon the arguments passed to ``container.py start`` and ``container.py stop``. These +commands accept an image extension parameter as an additional argument. If no argument is passed, then this +parameter defaults to ``base``. Currently, the only valid values are (``base``, ``ros2``). +Only one image extension can be passed at a time. The produced container will be named ``isaac-lab-${profile}``, +where ``${profile}`` is the image extension name. + +.. code:: bash + + # start base by default + ./docker/container.py start + # stop base explicitly + ./docker/container.py stop base + # start ros2 container + ./docker/container.py start ros2 + # stop ros2 container + ./docker/container.py stop ros2 + +The passed image extension argument will build the image defined in ``Dockerfile.${image_extension}``, +with the corresponding `profile`_ in the ``docker-compose.yaml`` and the envars from ``.env.${image_extension}`` +in addition to the ``.env.base``, if any. + +ROS2 Image Extension +~~~~~~~~~~~~~~~~~~~~ + +In ``Dockerfile.ros2``, the container installs ROS2 Humble via an `apt package`_, and it is sourced in the ``.bashrc``. +The exact version is specified by the variable ``ROS_APT_PACKAGE`` in the ``.env.ros2`` file, +defaulting to ``ros-base``. Other relevant ROS2 variables are also specified in the ``.env.ros2`` file, +including variables defining the `various middleware`_ options. + +The container defaults to ``FastRTPS``, but ``CylconeDDS`` is also supported. Each of these middlewares can be +`tuned`_ using their corresponding ``.xml`` files under ``docker/.ros``. + + +.. dropdown:: Parameters for ROS2 Image Extension + :icon: code + + .. literalinclude:: ../../../docker/.env.ros2 + :language: bash + + +Known Issues +------------ + +WebRTC Streaming +~~~~~~~~~~~~~~~~ + +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 the Native Streaming Client or using the +Mozilla Firefox browser on which WebRTC works. + +Streaming is the only supported method for visualizing the Isaac GUI from within the container. The Omniverse Streaming Client +is freely available from the Omniverse app, and is easy to use. The other streaming methods similarly require only a web browser. +If users want to use X11 forwarding in order to have the apps behave as local GUI windows, they can uncomment the relevant portions +in docker-compose.yaml. + + +.. _`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 +.. _`Container Toolkit website`: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html +.. _`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 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 +.. _`profile`: https://docs.docker.com/compose/compose-file/15-profiles/ +.. _`apt package`: https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html#install-ros-2-packages +.. _`various middleware`: https://docs.ros.org/en/humble/How-To-Guides/Working-with-multiple-RMW-implementations.html +.. _`tuned`: https://docs.ros.org/en/foxy/How-To-Guides/DDS-tuning.html diff --git a/_sources/source/deployment/index.rst b/_sources/source/deployment/index.rst new file mode 100644 index 0000000000..c8e07ef9e2 --- /dev/null +++ b/_sources/source/deployment/index.rst @@ -0,0 +1,22 @@ +Container Deployment +==================== + +Docker is a tool that allows for the creation of containers, which are isolated environments that can +be used to run applications. They are useful for ensuring that an application can run on any machine +that has Docker installed, regardless of the host machine's operating system or installed libraries. + +We include a Dockerfile and docker-compose.yaml file that can be used to build a Docker image that +contains Isaac Lab and all of its dependencies. This image can then be used to run Isaac Lab in a container. +The Dockerfile is based on the Isaac Sim image provided by NVIDIA, which includes the Omniverse +application launcher and the Isaac Sim application. The Dockerfile installs Isaac Lab and its dependencies +on top of this image. + +The following guides provide instructions for building the Docker image and running Isaac Lab in a +container. + +.. toctree:: + :maxdepth: 1 + + docker + cluster + run_docker_example diff --git a/_sources/source/deployment/run_docker_example.rst b/_sources/source/deployment/run_docker_example.rst new file mode 100644 index 0000000000..9134369f2b --- /dev/null +++ b/_sources/source/deployment/run_docker_example.rst @@ -0,0 +1,141 @@ +Running an example with Docker +============================== + +From the root of the Isaac Lab repository, the ``docker`` directory contains all the Docker relevant files. These include the three files +(**Dockerfile**, **docker-compose.yaml**, **.env**) which are used by Docker, and an additional script that we use to interface with them, +**container.py**. + +In this tutorial, we will learn how to use the Isaac Lab Docker container for development. For a detailed description of the Docker setup, +including installation and obtaining access to an Isaac Sim image, please reference the :ref:`deployment-docker`. For a description +of Docker in general, please refer to `their official documentation `_. + + +Building the Container +~~~~~~~~~~~~~~~~~~~~~~ + +To build the Isaac Lab container from the root of the Isaac Lab repository, we will run the following: + + +.. code-block:: console + + python docker/container.py start + + +The terminal will first pull the base IsaacSim image, build the Isaac Lab image's additional layers on top of it, and run the Isaac Lab container. +This should take several minutes upon the first build but will be shorter in subsequent runs as Docker's caching prevents repeated work. +If we run the command ``docker container ls`` on the terminal, the output will list the containers that are running on the system. If +everything has been set up correctly, a container with the ``NAME`` **isaac-lab-base** should appear, similar to below: + + +.. code-block:: console + + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + 483d1d5e2def isaac-lab-base "bash" 30 seconds ago Up 30 seconds isaac-lab-base + + +Once the container is up and running, we can enter it from our terminal. + +.. code-block:: console + + python docker/container.py enter + + +On entering the Isaac Lab container, we are in the terminal as the superuser, ``root``. This environment contains a copy of the +Isaac Lab repository, but also has access to the directories and libraries of Isaac Sim. We can run experiments from this environment +using a few convenient aliases that have been put into the ``root`` **.bashrc**. For instance, we have made the **isaaclab.sh** script +usable from anywhere by typing its alias ``isaaclab``. + +Additionally in the container, we have `bind mounted`_ the ``IsaacLab/source`` directory from the +host machine. This means that if we modify files under this directory from an editor on the host machine, the changes are +reflected immediately within the container without requiring us to rebuild the Docker image. + +We will now run a sample script from within the container to demonstrate how to extract artifacts +from the Isaac Lab Docker container. + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``log_time.py`` script in the ``IsaacLab/source/standalone/tutorials/00_sim`` directory. + +.. dropdown:: Code for log_time.py + :icon: code + + .. literalinclude:: ../../../source/standalone/tutorials/00_sim/log_time.py + :language: python + :emphasize-lines: 46-55, 72-79 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +The Isaac Lab Docker container has several `volumes`_ to facilitate persistent storage between the host computer and the +container. One such volume is the ``/workspace/isaaclab/logs`` directory. +The ``log_time.py`` script designates this directory as the location to which a ``log.txt`` should be written: + +.. literalinclude:: ../../../source/standalone/tutorials/00_sim/log_time.py + :language: python + :start-at: # Specify that the logs must be in logs/docker_tutorial + :end-at: print(f"[INFO] Logging experiment to directory: {log_dir_path}") + + +As the comments note, :func:`os.path.abspath()` will prepend ``/workspace/isaaclab`` because in +the Docker container all python execution is done through ``/workspace/isaaclab/isaaclab.sh``. +The output will be a file, ``log.txt``, with the ``sim_time`` written on a newline at every simulation step: + +.. literalinclude:: ../../../source/standalone/tutorials/00_sim/log_time.py + :language: python + :start-at: # Prepare to count sim_time + :end-at: sim_time += sim_dt + + +Executing the Script +~~~~~~~~~~~~~~~~~~~~ + +We will execute the script to produce a log, adding a ``--headless`` flag to our execution to prevent a GUI: + +.. code-block:: bash + + isaaclab -p source/standalone/tutorials/00_sim/log_time.py --headless + + +Now ``log.txt`` will have been produced at ``/workspace/isaaclab/logs/docker_tutorial``. If we exit the container +by typing ``exit``, we will return to ``IsaacLab/docker`` in our host terminal environment. We can then enter +the following command to retrieve our logs from the Docker container and put them on our host machine: + +.. code-block:: console + + ./container.py copy + + +We will see a terminal readout reporting the artifacts we have retrieved from the container. If we navigate to +``/isaaclab/docker/artifacts/logs/docker_tutorial``, we will see a copy of the ``log.txt`` file which was produced +by the script above. + +Each of the directories under ``artifacts`` corresponds to Docker `volumes`_ mapped to directories +within the container and the ``container.py copy`` command copies them from those `volumes`_ to these directories. + +We could return to the Isaac Lab Docker terminal environment by running ``container.py enter`` again, +but we have retrieved our logs and wish to go inspect them. We can stop the Isaac Lab Docker container with the following command: + +.. code-block:: console + + ./container.py stop + + +This will bring down the Docker Isaac Lab container. The image will persist and remain available for further use, as will +the contents of any `volumes`_. If we wish to free up the disk space taken by the image, (~20.1GB), and do not mind repeating +the build process when we next run ``./container.py start``, we may enter the following command to delete the **isaac-lab-base** image: + +.. code-block:: console + + docker image rm isaac-lab-base + +A subsequent run of ``docker image ls`` will show that the image tagged **isaac-lab-base** is now gone. We can repeat the process for the +underlying NVIDIA container if we wish to free up more space. If a more powerful method of freeing resources from Docker is desired, +please consult the documentation for the `docker prune`_ commands. + + +.. _volumes: https://docs.docker.com/storage/volumes/ +.. _bind mounted: https://docs.docker.com/storage/bind-mounts/ +.. _docker prune: https://docs.docker.com/config/pruning/ diff --git a/_sources/source/features/hydra.rst b/_sources/source/features/hydra.rst new file mode 100644 index 0000000000..577eb7a6ec --- /dev/null +++ b/_sources/source/features/hydra.rst @@ -0,0 +1,129 @@ +Hydra Configuration System +========================== + +.. currentmodule:: omni.isaac.lab + +Isaac Lab supports the `Hydra `_ configuration system to modify the task's +configuration using command line arguments, which can be useful to automate experiments and perform hyperparameter tuning. + +Any parameter of the environment can be modified by adding one or multiple elements of the form ``env.a.b.param1=value`` +to the command line input, where ``a.b.param1`` reflects the parameter's hierarchy, for example ``env.actions.joint_effort.scale=10.0``. +Similarly, the agent's parameters can be modified by using the ``agent`` prefix, for example ``agent.seed=2024``. + +The way these command line arguments are set follow the exact structure of the configuration files. Since the different +RL frameworks use different conventions, there might be differences in the way the parameters are set. For example, +with *rl_games* the seed will be set with ``agent.params.seed``, while with *rsl_rl*, *skrl* and *sb3* it will be set with +``agent.seed``. + +As a result, training with hydra arguments can be run with the following syntax: + +.. tab-set:: + :sync-group: rl-train + + .. tab-item:: rsl_rl + :sync: rsl_rl + + .. code-block:: shell + + python source/standalone/workflows/rsl_rl/train.py --task=Isaac-Cartpole-v0 --headless env.actions.joint_effort.scale=10.0 agent.seed=2024 + + .. tab-item:: rl_games + :sync: rl_games + + .. code-block:: shell + + python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 --headless env.actions.joint_effort.scale=10.0 agent.params.seed=2024 + + .. tab-item:: skrl + :sync: skrl + + .. code-block:: shell + + python source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless env.actions.joint_effort.scale=10.0 agent.seed=2024 + + .. tab-item:: sb3 + :sync: sb3 + + .. code-block:: shell + + python source/standalone/workflows/sb3/train.py --task=Isaac-Cartpole-v0 --headless env.actions.joint_effort.scale=10.0 agent.seed=2024 + +The above command will run the training script with the task ``Isaac-Cartpole-v0`` in headless mode, and set the +``env.actions.joint_effort.scale`` parameter to 10.0 and the ``agent.seed`` parameter to 2024. + +.. note:: + + To keep backwards compatibility, and to provide a more user-friendly experience, we have kept the old cli arguments + of the form ``--param``, for example ``--num_envs``, ``--seed``, ``--max_iterations``. These arguments have precedence + over the hydra arguments, and will overwrite the values set by the hydra arguments. + + +Modifying advanced parameters +----------------------------- + +Callables +^^^^^^^^^ + +It is possible to modify functions and classes in the configuration files by using the syntax ``module:attribute_name``. +For example, in the Cartpole environment: + +.. literalinclude:: ../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :start-at: class ObservationsCfg + :end-at: policy: PolicyCfg = PolicyCfg() + :emphasize-lines: 9 + +we could modify ``joint_pos_rel`` to compute absolute positions instead of relative positions with +``env.observations.policy.joint_pos_rel.func=omni.isaac.lab.envs.mdp:joint_pos``. + +Setting parameters to None +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To set parameters to None, use the ``null`` keyword, which is a special keyword in Hydra that is automatically converted to None. +In the above example, we could also disable the ``joint_pos_rel`` observation by setting it to None with +``env.observations.policy.joint_pos_rel=null``. + +Dictionaries +^^^^^^^^^^^^ +Elements in dictionaries are handled as a parameters in the hierarchy. For example, in the Cartpole environment: + +.. literalinclude:: ../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :lines: 90-114 + :emphasize-lines: 11 + +the ``position_range`` parameter can be modified with ``env.events.reset_cart_position.params.position_range="[-2.0, 2.0]"``. +This example shows two noteworthy points: + +- The parameter we set has a space, so it must be enclosed in quotes. +- The parameter is a list while it is a tuple in the config. This is due to the fact that Hydra does not support tuples. + + +Modifying inter-dependent parameters +------------------------------------ + +Particular care should be taken when modifying the parameters using command line arguments. Some of the configurations +perform intermediate computations based on other parameters. These computations will not be updated when the parameters +are modified. + +For example, for the configuration of the Cartpole camera depth environment: + +.. literalinclude:: ../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_camera_env.py + :language: python + :start-at: class CartpoleDepthCameraEnvCfg + :end-at: tiled_camera.width + :emphasize-lines: 10, 15 + +If the user were to modify the width of the camera, i.e. ``env.tiled_camera.width=128``, then the parameter +``env.observation_space=[80,128,1]`` must be updated and given as input as well. + +Similarly, the ``__post_init__`` method is not updated with the command line inputs. In the ``LocomotionVelocityRoughEnvCfg``, for example, +the post init update is as follows: + +.. literalinclude:: ../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/locomotion/velocity/velocity_env_cfg.py + :language: python + :start-at: class LocomotionVelocityRoughEnvCfg + :emphasize-lines: 23, 29, 31 + +Here, when modifying ``env.decimation`` or ``env.sim.dt``, the user needs to give the updated ``env.sim.render_interval``, +``env.scene.height_scanner.update_period``, and ``env.scene.contact_forces.update_period`` as input as well. diff --git a/_sources/source/features/multi_gpu.rst b/_sources/source/features/multi_gpu.rst new file mode 100644 index 0000000000..1611651a6a --- /dev/null +++ b/_sources/source/features/multi_gpu.rst @@ -0,0 +1,160 @@ +Multi-GPU and Multi-Node Training +================================= + +.. currentmodule:: omni.isaac.lab + +Isaac Lab supports multi-GPU and multi-node reinforcement learning. Currently, this feature is only +available for RL-Games and skrl libraries workflows. We are working on extending this feature to +other workflows. + +.. attention:: + + Multi-GPU and multi-node training is only supported on Linux. Windows support is not available at this time. + This is due to limitations of the NCCL library on Windows. + + +Multi-GPU Training +------------------ + +For complex reinforcement learning environments, it may be desirable to scale up training across multiple GPUs. +This is possible in Isaac Lab through the use of the +`PyTorch distributed `_ framework or the +`JAX distributed `_ module respectively. + +In PyTorch, the :meth:`torch.distributed` API is used to launch multiple processes of training, where the number of +processes must be equal to or less than the number of GPUs available. Each process runs on +a dedicated GPU and launches its own instance of Isaac Sim and the Isaac Lab environment. +Each process collects its own rollouts during the training process and has its own copy of the policy +network. During training, gradients are aggregated across the processes and broadcasted back to the process +at the end of the epoch. + +In JAX, since the ML framework doesn't automatically start multiple processes from a single program invocation, +the skrl library provides a module to start them. + +.. image:: ../_static/multi-gpu-rl/a3c-light.svg + :class: only-light + :align: center + :alt: Multi-GPU training paradigm + :width: 80% + +.. image:: ../_static/multi-gpu-rl/a3c-dark.svg + :class: only-dark + :align: center + :width: 80% + :alt: Multi-GPU training paradigm + +| + +To train with multiple GPUs, use the following command, where ``--proc_per_node`` represents the number of available GPUs: + +.. tab-set:: + :sync-group: rl-train + + .. tab-item:: rl_games + :sync: rl_games + + .. code-block:: shell + + python -m torch.distributed.run --nnodes=1 --nproc_per_node=2 source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed + + .. tab-item:: skrl + :sync: skrl + + .. tab-set:: + + .. tab-item:: PyTorch + :sync: torch + + .. code-block:: shell + + python -m torch.distributed.run --nnodes=1 --nproc_per_node=2 source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + + .. tab-item:: JAX + :sync: jax + + .. code-block:: shell + + python -m skrl.utils.distributed.jax --nnodes=1 --nproc_per_node=2 source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --ml_framework jax + +Multi-Node Training +------------------- + +To scale up training beyond multiple GPUs on a single machine, it is also possible to train across multiple nodes. +To train across multiple nodes/machines, it is required to launch an individual process on each node. + +For the master node, use the following command, where ``--nproc_per_node`` represents the number of available GPUs, and +``--nnodes`` represents the number of nodes: + +.. tab-set:: + :sync-group: rl-train + + .. tab-item:: rl_games + :sync: rl_games + + .. code-block:: shell + + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed + + .. tab-item:: skrl + :sync: skrl + + .. tab-set:: + + .. tab-item:: PyTorch + :sync: torch + + .. code-block:: shell + + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=0 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=localhost:5555 source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + + .. tab-item:: JAX + :sync: jax + + .. code-block:: shell + + python -m skrl.utils.distributed.jax --nproc_per_node=2 --nnodes=2 --node_rank=0 --coordinator_address=ip_of_master_machine:5555 source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --ml_framework jax + +Note that the port (``5555``) can be replaced with any other available port. + +For non-master nodes, use the following command, replacing ``--node_rank`` with the index of each machine: + +.. tab-set:: + :sync-group: rl-train + + .. tab-item:: rl_games + :sync: rl_games + + .. code-block:: shell + + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=ip_of_master_machine:5555 source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --distributed + + .. tab-item:: skrl + :sync: skrl + + .. tab-set:: + + .. tab-item:: PyTorch + :sync: torch + + .. code-block:: shell + + python -m torch.distributed.run --nproc_per_node=2 --nnodes=2 --node_rank=1 --rdzv_id=123 --rdzv_backend=c10d --rdzv_endpoint=ip_of_master_machine:5555 source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed + + .. tab-item:: JAX + :sync: jax + + .. code-block:: shell + + python -m skrl.utils.distributed.jax --nproc_per_node=2 --nnodes=2 --node_rank=1 --coordinator_address=ip_of_master_machine:5555 source/standalone/workflows/skrl/train.py --task=Isaac-Cartpole-v0 --headless --distributed --ml_framework jax + +For more details on multi-node training with PyTorch, please visit the +`PyTorch documentation `_. +For more details on multi-node training with JAX, please visit the +`skrl documentation `_ and the +`JAX documentation `_. + +.. note:: + + As mentioned in the PyTorch documentation, "multi-node training is bottlenecked by inter-node communication + latencies". When this latency is high, it is possible multi-node training will perform worse than running on + a single node instance. diff --git a/_sources/source/features/reproducibility.rst b/_sources/source/features/reproducibility.rst new file mode 100644 index 0000000000..5f77630d81 --- /dev/null +++ b/_sources/source/features/reproducibility.rst @@ -0,0 +1,42 @@ +Reproducibility and Determinism +------------------------------- + +Given the same hardware and Isaac Sim (and consequently PhysX) version, the simulation produces +identical results for scenes with rigid bodies and articulations. However, the simulation results can +vary across different hardware configurations due to floating point precision and rounding errors. +At present, PhysX does not guarantee determinism for any scene with non-rigid bodies, such as cloth +or soft bodies. For more information, please refer to the `PhysX Determinism documentation`_. + +Based on above, Isaac Lab provides a deterministic simulation that ensures consistent simulation +results across different runs. This is achieved by using the same random seed for the +simulation environment and the physics engine. At construction of the environment, the random seed +is set to a fixed value using the :meth:`~omni.isaac.core.utils.torch.set_seed` method. This method sets the +random seed for both the CPU and GPU globally across different libraries, including PyTorch and +NumPy. + +In the included workflow scripts, the seed specified in the learning agent's configuration file or the +command line argument is used to set the random seed for the environment. This ensures that the +simulation results are reproducible across different runs. The seed is set into the environment +parameters :attr:`omni.isaac.lab.envs.ManagerBasedEnvCfg.seed` or :attr:`omni.isaac.lab.envs.DirectRLEnvCfg.seed` +depending on the manager-based or direct environment implementation respectively. + +For results on our determinacy testing for RL training, please check the GitHub Pull Request `#940`_. + +.. tip:: + + Due to GPU work scheduling, there's a possibility that runtime changes to simulation parameters + may alter the order in which operations take place. This occurs because environment updates can + happen while the GPU is occupied with other tasks. Due to the inherent nature of floating-point + numeric storage, any modification to the execution ordering can result in minor changes in the + least significant bits of output data. These changes may lead to divergent execution over the + course of simulating thousands of environments and simulation frames. + + An illustrative example of this issue is observed with the runtime domain randomization of object's + physics materials. This process can introduce both determinacy and simulation issues when executed + on the GPU due to the way these parameters are passed from the CPU to the GPU in the lower-level APIs. + Consequently, it is strongly advised to perform this operation only at setup time, before the + environment stepping commences. + + +.. _PhysX Determinism documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/API.html#determinism +.. _#940: https://github.com/isaac-sim/IsaacLab/pull/940 diff --git a/_sources/source/features/tiled_rendering.rst b/_sources/source/features/tiled_rendering.rst new file mode 100644 index 0000000000..d8ee1d1118 --- /dev/null +++ b/_sources/source/features/tiled_rendering.rst @@ -0,0 +1,167 @@ +Tiled-Camera Rendering +====================== + +.. currentmodule:: omni.isaac.lab + +.. note:: + + This feature is only available from Isaac Sim version 4.2.0 onwards. + + Tiled rendering in combination with image processing networks require heavy memory resources, especially + at larger resolutions. We recommend running 512 cameras in the scene on RTX 4090 GPUs or similar. + + +Tiled rendering APIs provide a vectorized interface for collecting data from camera sensors. +This is useful for reinforcement learning environments requiring vision in the loop. +Tiled rendering works by concatenating camera outputs from multiple cameras and rendering +one single large image instead of multiple smaller images that would have been produced +by each individual camera. This reduces the amount of time required for rendering and +provides a more efficient API for working with vision data. + +Isaac Lab provides tiled rendering APIs for RGB, depth, along with other annotators through the :class:`~sensors.TiledCamera` +class. Configurations for the tiled rendering APIs can be defined through the :class:`~sensors.TiledCameraCfg` +class, specifying parameters such as the regex expression for all camera paths, the transform +for the cameras, the desired data type, the type of cameras to add to the scene, and the camera +resolution. + +.. code-block:: python + + tiled_camera: TiledCameraCfg = TiledCameraCfg( + prim_path="/World/envs/env_.*/Camera", + offset=TiledCameraCfg.OffsetCfg(pos=(-7.0, 0.0, 3.0), rot=(0.9945, 0.0, 0.1045, 0.0), convention="world"), + data_types=["rgb"], + spawn=sim_utils.PinholeCameraCfg( + focal_length=24.0, focus_distance=400.0, horizontal_aperture=20.955, clipping_range=(0.1, 20.0) + ), + width=80, + height=80, + ) + +To access the tiled rendering interface, a :class:`~sensors.TiledCamera` object can be created and used +to retrieve data from the cameras. + +.. code-block:: python + + tiled_camera = TiledCamera(cfg.tiled_camera) + data_type = "rgb" + data = tiled_camera.data.output[data_type] + +The returned data will be transformed into the shape (num_cameras, height, width, num_channels), which +can be used directly as observation for reinforcement learning. + +When working with rendering, make sure to add the ``--enable_cameras`` argument when launching the +environment. For example: + +.. code-block:: shell + + python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-RGB-Camera-Direct-v0 --headless --enable_cameras + + +Annotators and Data Types +------------------------- + +Both :class:`~sensors.TiledCamera` and :class:`~sensors.Camera` classes provide APIs for retrieving various types annotator data from replicator: + +* ``"rgb"``: A 3-channel rendered color image. +* ``"rgba"``: A 4-channel rendered color image with alpha channel. +* ``"distance_to_camera"``: An image containing the distance to camera optical center. +* ``"distance_to_image_plane"``: An image containing distances of 3D points from camera plane along camera's z-axis. +* ``"depth"``: The same as ``"distance_to_image_plane"``. +* ``"normals"``: An image containing the local surface normal vectors at each pixel. +* ``"motion_vectors"``: An image containing the motion vector data at each pixel. +* ``"semantic_segmentation"``: The semantic segmentation data. +* ``"instance_segmentation_fast"``: The instance segmentation data. +* ``"instance_id_segmentation_fast"``: The instance id segmentation data. + +RGB and RGBA +~~~~~~~~~~~~ + +``rgb`` data type returns a 3-channel RGB colored image of type ``torch.uint8``, with dimension (B, H, W, 3). + +``rgba`` data type returns a 4-channel RGBA colored image of type ``torch.uint8``, with dimension (B, H, W, 4). + +To convert the ``torch.uint8`` data to ``torch.float32``, divide the buffer by 255.0 to obtain a ``torch.float32`` buffer containing data from 0 to 1. + +Depth and Distances +~~~~~~~~~~~~~~~~~~~ + +``distance_to_camera`` returns a single-channel depth image with distance to the camera optical center. The dimension for this annotator is (B, H, W, 1) and has type ``torch.float32``. + +``distance_to_image_plane`` returns a single-channel depth image with distances of 3D points from the camera plane along the camera's Z-axis. The dimension for this annotator is (B, H, W, 1) and has type ``torch.float32``. + +``depth`` is provided as an alias for ``distance_to_image_plane`` and will return the same data as the ``distance_to_image_plane`` annotator, with dimension (B, H, W, 1) and type ``torch.float32``. + +Normals +~~~~~~~ + +``normals`` returns an image containing the local surface normal vectors at each pixel. The buffer has dimension (B, H, W, 3), containing the (x, y, z) information for each vector, and has data type ``torch.float32``. + +Motion Vectors +~~~~~~~~~~~~~~ + +``motion_vectors`` returns the per-pixel motion vectors in image space, with a 2D array of motion vectors representing the relative motion of a pixel in the camera’s viewport between frames. The buffer has dimension (B, H, W, 2), representing x - the motion distance in the horizontal axis (image width) with movement to the left of the image being positive and movement to the right being negative and y - motion distance in the vertical axis (image height) with movement towards the top of the image being positive and movement to the bottom being negative. The data type is ``torch.float32``. + +Semantic Segmentation +~~~~~~~~~~~~~~~~~~~~~ + +``semantic_segmentation`` outputs semantic segmentation of each entity in the camera’s viewport that has semantic labels. In addition to the image buffer, an ``info`` dictionary can be retrieved with ``tiled_camera.data.info['semantic_segmentation']`` containing ID to labels information. + +- If ``colorize_semantic_segmentation=True`` in the camera config, a 4-channel RGBA image will be returned with dimension (B, H, W, 4) and type ``torch.uint8``. The info ``idToLabels`` dictionary will be the mapping from color to semantic labels. + +- If ``colorize_semantic_segmentation=False``, a buffer of dimension (B, H, W, 1) of type ``torch.int32`` will be returned, containing the semantic ID of each pixel. The info ``idToLabels`` dictionary will be the mapping from semantic ID to semantic labels. + +Instance ID Segmentation +~~~~~~~~~~~~~~~~~~~~~~~~ + +``instance_id_segmentation_fast`` outputs instance ID segmentation of each entity in the camera’s viewport. The instance ID is unique for each prim in the scene with different paths. In addition to the image buffer, an ``info`` dictionary can be retrieved with ``tiled_camera.data.info['instance_id_segmentation_fast']`` containing ID to labels information. + +The main difference between ``instance_id_segmentation_fast`` and ``instance_segmentation_fast`` are that instance segmentation annotator goes down the hierarchy to the lowest level prim which has semantic labels, where instance ID segmentation always goes down to the leaf prim. + +- If ``colorize_instance_id_segmentation=True`` in the camera config, a 4-channel RGBA image will be returned with dimension (B, H, W, 4) and type ``torch.uint8``. The info ``idToLabels`` dictionary will be the mapping from color to USD prim path of that entity. + +- If ``colorize_instance_id_segmentation=False``, a buffer of dimension (B, H, W, 1) of type ``torch.int32`` will be returned, containing the instance ID of each pixel. The info ``idToLabels`` dictionary will be the mapping from instance ID to USD prim path of that entity. + +Instance Segmentation +""""""""""""""""""""" + +``instance_segmentation_fast`` outputs instance segmentation of each entity in the camera’s viewport. In addition to the image buffer, an ``info`` dictionary can be retrieved with ``tiled_camera.data.info['instance_segmentation_fast']`` containing ID to labels and ID to semantic information. + +- If ``colorize_instance_segmentation=True`` in the camera config, a 4-channel RGBA image will be returned with dimension (B, H, W, 4) and type ``torch.uint8``. The info ``idToLabels`` dictionary will be the mapping from color to USD prim path of that semantic entity. The info ``idToSemantics`` dictionary will be the mapping from color to semantic labels of that semantic entity. + +- If ``colorize_instance_segmentation=False``, a buffer of dimension (B, H, W, 1) of type ``torch.int32`` will be returned, containing the instance ID of each pixel. The info ``idToLabels`` dictionary will be the mapping from instance ID to USD prim path of that semantic entity. The info ``idToSemantics`` dictionary will be the mapping from instance ID to semantic labels of that semantic entity. + + +Current Limitations +------------------- + +Due to current limitations in the renderer, we can have only **one** :class:`~sensors.TiledCamera` instance in the scene. +For use cases that require a setup with more than one camera, we can imitate the multi-camera behavior by moving the location +of the camera in between render calls in a step. + +For example, in a stereo vision setup, the below snippet can be implemented: + +.. code-block:: python + + # render image from "first" camera + camera_data_1 = self._tiled_camera.data.output["rgb"].clone() / 255.0 + # update camera transform to the "second" camera location + self._tiled_camera.set_world_poses( + positions=pos, + orientations=rot, + convention="world" + ) + # step the renderer + self.sim.render() + self._tiled_camera.update(0, force_recompute=True) + # render image from "second" camera + camera_data_2 = self._tiled_camera.data.output["rgb"].clone() / 255.0 + +Note that this approach still limits the rendering resolution to be identical for all cameras. Currently, there is no workaround +to achieve different resolution images using :class:`~sensors.TiledCamera`. The best approach is to use the largest resolution out of all of the +desired resolutions and add additional scaling or cropping operations to the rendered output as a post-processing step. + +In addition, there may be visible quality differences when comparing render outputs of different numbers of environments. +Currently, any combined resolution that has a width less than 265 pixels or height less than 265 will automatically switch +to the DLAA anti-aliasing mode, which does not perform up-sampling during anti-aliasing. For resolutions larger than 265 in both +width and height dimensions, we default to using the "performance" DLSS mode for anti-aliasing for performance benefits. +Anti-aliasing modes and other rendering parameters can be specified in the :class:`~sim.RenderCfg`. diff --git a/_sources/source/how-to/add_own_library.rst b/_sources/source/how-to/add_own_library.rst new file mode 100644 index 0000000000..32e2d5b63e --- /dev/null +++ b/_sources/source/how-to/add_own_library.rst @@ -0,0 +1,91 @@ +Adding your own learning library +================================ + +Isaac Lab comes pre-integrated with a number of libraries (such as RSL-RL, RL-Games, SKRL, Stable Baselines, etc.). +However, you may want to integrate your own library with Isaac Lab or use a different version of the libraries than +the one installed by Isaac Lab. This is possible as long as the library is available as Python package that supports +the Python version used by the underlying simulator. For instance, if you are using Isaac Sim 4.0.0 onwards, you need +to ensure that the library is available for Python 3.10. + +Using a different version of a library +-------------------------------------- + +If you want to use a different version of a library than the one installed by Isaac Lab, you can install the library +by building it from source or using a different version of the library available on PyPI. + +For instance, if you want to use your own modified version of the `rsl-rl`_ library, you can follow these steps: + +1. Follow the instructions for installing Isaac Lab. This will install the default version of the ``rsl-rl`` library. +2. Clone the ``rsl-rl`` library from the GitHub repository: + + .. code-block:: bash + + git clone git@github.com:leggedrobotics/rsl_rl.git + + +3. Install the library in your Python environment: + + .. code-block:: bash + + # Assuming you are in the root directory of the Isaac Lab repository + cd IsaacLab + + # Note: If you are using a virtual environment, make sure to activate it before running the following command + ./isaaclab.sh -p -m pip install -e /path/to/rsl_rl + +In this case, the ``rsl-rl`` library will be installed in the Python environment used by Isaac Lab. You can now use the +``rsl-rl`` library in your experiments. To check the library version and other details, you can use the following +command: + +.. code-block:: bash + + ./isaaclab.sh -p -m pip show rsl-rl + +This should now show the location of the ``rsl-rl`` library as the directory where you cloned the library. +For instance, if you cloned the library to ``/home/user/git/rsl_rl``, the output of the above command should be: + +.. code-block:: bash + + Name: rsl_rl + Version: 2.0.2 + Summary: Fast and simple RL algorithms implemented in pytorch + Home-page: https://github.com/leggedrobotics/rsl_rl + Author: ETH Zurich, NVIDIA CORPORATION + Author-email: + License: BSD-3 + Location: /home/user/git/rsl_rl + Requires: torch, torchvision, numpy, GitPython, onnx + Required-by: + + +Integrating a new library +------------------------- + +Adding a new library to Isaac Lab is similar to using a different version of a library. You can install the library +in your Python environment and use it in your experiments. However, if you want to integrate the library with +Isaac Lab, you can will first need to make a wrapper for the library, as explained in +:ref:`how-to-env-wrappers`. + +The following steps can be followed to integrate a new library with Isaac Lab: + +1. Add your library as an extra-dependency in the ``setup.py`` for the extension ``omni.isaac.lab_tasks``. + This will ensure that the library is installed when you install Isaac Lab or it will complain if the library is not + installed or available. +2. Install your library in the Python environment used by Isaac Lab. You can do this by following the steps mentioned + in the previous section. +3. Create a wrapper for the library. You can check the module :mod:`omni.isaac.lab_tasks.utils.wrappers` + for examples of wrappers for different libraries. You can create a new wrapper for your library and add it to the + module. You can also create a new module for the wrapper if you prefer. +4. Create workflow scripts for your library to train and evaluate agents. You can check the existing workflow scripts + in the ``source/standalone/workflows`` directory for examples. You can create new workflow + scripts for your library and add them to the directory. + +Optionally, you can also add some tests and documentation for the wrapper. This will help ensure that the wrapper +works as expected and can guide users on how to use the wrapper. + +* Add some tests to ensure that the wrapper works as expected and remains compatible with the library. + These tests can be added to the ``source/extensions/omni.isaac.lab_tasks/test/wrappers`` directory. +* Add some documentation for the wrapper. You can add the API documentation to the + ``docs/source/api/lab_tasks/omni.isaac.lab_tasks.utils.wrappers.rst`` file. + +.. _rsl-rl: https://github.com/leggedrobotics/rsl_rl diff --git a/_sources/source/how-to/draw_markers.rst b/_sources/source/how-to/draw_markers.rst new file mode 100644 index 0000000000..80f872d253 --- /dev/null +++ b/_sources/source/how-to/draw_markers.rst @@ -0,0 +1,75 @@ +Creating Visualization Markers +============================== + +.. currentmodule:: omni.isaac.lab + +Visualization markers are useful to debug the state of the environment. They can be used to visualize +the frames, commands, and other information in the simulation. + +While Isaac Sim provides its own :mod:`omni.isaac.debug_draw` extension, it is limited to rendering only +points, lines and splines. For cases, where you need to render more complex shapes, you can use the +:class:`markers.VisualizationMarkers` class. + +This guide is accompanied by a sample script ``markers.py`` in the ``IsaacLab/source/standalone/demos`` directory. + +.. dropdown:: Code for markers.py + :icon: code + + .. literalinclude:: ../../../source/standalone/demos/markers.py + :language: python + :emphasize-lines: 45-90, 106-107, 136-142 + :linenos: + + + +Configuring the markers +----------------------- + +The :class:`~markers.VisualizationMarkersCfg` class provides a simple interface to configure +different types of markers. It takes in the following parameters: + +- :attr:`~markers.VisualizationMarkersCfg.prim_path`: The corresponding prim path for the marker class. +- :attr:`~markers.VisualizationMarkersCfg.markers`: A dictionary specifying the different marker prototypes + handled by the class. The key is the name of the marker prototype and the value is its spawn configuration. + +.. note:: + + In case the marker prototype specifies a configuration with physics properties, these are removed. + This is because the markers are not meant to be simulated. + +Here we show all the different types of markers that can be configured. These range from simple shapes like +cones and spheres to more complex geometries like a frame or arrows. The marker prototypes can also be +configured from USD files. + +.. literalinclude:: ../../../source/standalone/demos/markers.py + :language: python + :lines: 45-90 + :dedent: + + +Drawing the markers +------------------- + +To draw the markers, we call the :class:`~markers.VisualizationMarkers.visualize` method. This method takes in +as arguments the pose of the markers and the corresponding marker prototypes to draw. + +.. literalinclude:: ../../../source/standalone/demos/markers.py + :language: python + :lines: 136-142 + :dedent: + + +Executing the Script +-------------------- + +To run the accompanying script, execute the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/demos/markers.py + +The simulation should start, and you can observe the different types of markers arranged in a grid pattern. +The markers will rotating around their respective axes. Additionally every few rotations, they will +roll forward on the grid. + +To stop the simulation, close the window, or use ``Ctrl+C`` in the terminal. diff --git a/_sources/source/how-to/estimate_how_many_cameras_can_run.rst b/_sources/source/how-to/estimate_how_many_cameras_can_run.rst new file mode 100644 index 0000000000..41238dcbe6 --- /dev/null +++ b/_sources/source/how-to/estimate_how_many_cameras_can_run.rst @@ -0,0 +1,121 @@ +.. _how-to-estimate-how-cameras-can-run: + + +Find How Many/What Cameras You Should Train With +================================================ + +.. currentmodule:: omni.isaac.lab + +Currently in Isaac Lab, there are several camera types; USD Cameras (standard), Tiled Cameras, +and Ray Caster cameras. These camera types differ in functionality and performance. The ``benchmark_cameras.py`` +script can be used to understand the difference in cameras types, as well to characterize their relative performance +at different parameters such as camera quantity, image dimensions, and data types. + +This utility is provided so that one easily can find the camera type/parameters that are the most performant +while meeting the requirements of the user's scenario. This utility also helps estimate +the maximum number of cameras one can realistically run, assuming that one wants to maximize the number +of environments while minimizing step time. + +This utility can inject cameras into an existing task from the gym registry, +which can be useful for benchmarking cameras in a specific scenario. Also, +if you install ``pynvml``, you can let this utility automatically find the maximum +numbers of cameras that can run in your task environment up to a +certain specified system resource utilization threshold (without training; taking zero actions +at each timestep). + +This guide accompanies the ``benchmark_cameras.py`` script in the ``source/standalone/benchmarks`` +directory. + +.. dropdown:: Code for benchmark_cameras.py + :icon: code + + .. literalinclude:: ../../../source/standalone/benchmarks/benchmark_cameras.py + :language: python + :linenos: + + +Possible Parameters +------------------- + +First, run + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/benchmarks/benchmark_cameras.py -h + +to see all possible parameters you can vary with this utility. + + +See the command line parameters related to ``autotune`` for more information about +automatically determining maximum camera count. + + +Compare Performance in Task Environments and Automatically Determine Task Max Camera Count +------------------------------------------------------------------------------------------ + +Currently, tiled cameras are the most performant camera that can handle multiple dynamic objects. + +For example, to see how your system could handle 100 tiled cameras in +the cartpole environment, with 2 cameras per environment (so 50 environments total) +only in RGB mode, run + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/benchmarks/benchmark_cameras.py \ + --task Isaac-Cartpole-v0 --num_tiled_cameras 100 \ + --task_num_cameras_per_env 2 \ + --tiled_camera_data_types rgb + +If you have pynvml installed, (``./isaaclab.sh -p -m pip install pynvml``), you can also +find the maximum number of cameras that you could run in the specified environment up to +a certain performance threshold (specified by max CPU utilization percent, max RAM utilization percent, +max GPU compute percent, and max GPU memory percent). For example, to find the maximum number of cameras +you can run with cartpole, you could run: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/benchmarks/benchmark_cameras.py \ + --task Isaac-Cartpole-v0 --num_tiled_cameras 100 \ + --task_num_cameras_per_env 2 \ + --tiled_camera_data_types rgb --autotune \ + --autotune_max_percentage_util 100 80 50 50 + +Autotune may lead to the program crashing, which means that it tried to run too many cameras at once. +However, the max percentage utilization parameter is meant to prevent this from happening. + +The output of the benchmark doesn't include the overhead of training the network, so consider +decreasing the maximum utilization percentages to account for this overhead. The final output camera +count is for all cameras, so to get the total number of environments, divide the output camera count +by the number of cameras per environment. + + +Compare Camera Type and Performance (Without a Specified Task) +-------------------------------------------------------------- + +This tool can also asses performance without a task environment. +For example, to view 100 random objects with 2 standard cameras, one could run + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/benchmarks/benchmark_cameras.py \ + --height 100 --width 100 --num_standard_cameras 2 \ + --standard_camera_data_types instance_segmentation_fast normals --num_objects 100 \ + --experiment_length 100 + +If your system cannot handle this due to performance reasons, then the process will be killed. +It's recommended to monitor CPU/RAM utilization and GPU utilization while running this script, to get +an idea of how many resources rendering the desired camera requires. In Ubuntu, you can use tools like ``htop`` and ``nvtop`` +to live monitor resources while running this script, and in Windows, you can use the Task Manager. + +If your system has a hard time handling the desired cameras, you can try the following + + - Switch to headless mode (supply ``--headless``) + - Ensure you are using the GPU pipeline not CPU! + - If you aren't using Tiled Cameras, switch to Tiled Cameras + - Decrease camera resolution + - Decrease how many data_types there are for each camera. + - Decrease the number of cameras + - Decrease the number of objects in the scene + +If your system is able to handle the amount of cameras, then the time statistics will be printed to the terminal. +After the simulations stops it can be closed with CTRL+C. diff --git a/_sources/source/how-to/import_new_asset.rst b/_sources/source/how-to/import_new_asset.rst new file mode 100644 index 0000000000..14a76a64c8 --- /dev/null +++ b/_sources/source/how-to/import_new_asset.rst @@ -0,0 +1,241 @@ +Importing a New Asset +===================== + +.. currentmodule:: omni.isaac.lab + +NVIDIA Omniverse relies on the Universal Scene Description (USD) file format to +import and export assets. USD is an open source file format developed by Pixar +Animation Studios. It is a scene description format optimized for large-scale, +complex data sets. While this format is widely used in the film and animation +industry, it is less common in the robotics community. + +To this end, NVIDIA has developed various importers that allow you to import +assets from other file formats into USD. These importers are available as +extensions to Omniverse Kit: + +* **URDF Importer** - Import assets from URDF files. +* **MJCF Importer** - Import assets from MJCF files. +* **Asset Importer** - Import assets from various file formats, including + OBJ, FBX, STL, and glTF. + +The recommended workflow from NVIDIA is to use the above importers to convert +the asset into its USD representation. Once the asset is in USD format, you can +use the Omniverse Kit to edit the asset and export it to other file formats. Isaac Sim includes these importers by default. They can also be enabled manually in Omniverse Kit. + + +An important note to use assets for large-scale simulation is to ensure that they +are in `instanceable`_ format. This allows the asset to be efficiently loaded +into memory and used multiple times in a scene. Otherwise, the asset will be +loaded into memory multiple times, which can cause performance issues. +For more details on instanceable assets, please check the Isaac Sim `documentation`_. + + +Using URDF Importer +------------------- + +For using the URDF importer in the GUI, please check the documentation at `URDF importer`_. For using the URDF importer from Python scripts, we include a utility tool called ``convert_urdf.py``. This script creates an instance of :class:`~sim.converters.UrdfConverterCfg` which +is then passed to the :class:`~sim.converters.UrdfConverter` class. + +The URDF importer has various configuration parameters that can be set to control the behavior of the importer. +The default values for the importer's configuration parameters are specified are in the :class:`~sim.converters.UrdfConverterCfg` class, and they are listed below. We made a few commonly modified settings to be available as command-line arguments when calling the ``convert_urdf.py``, and they are marked with ``*``` in the list. For a comprehensive list of the configuration parameters, please check the the documentation at `URDF importer`_. + +* :attr:`~sim.converters.UrdfConverterCfg.fix_base*` - Whether to fix the base of the robot. + This depends on whether you have a floating-base or fixed-base robot. The command-line flag is + ``--fix-base`` where when set, the importer will fix the base of the robot, otherwise it will default to floating-base. +* :attr:`~sim.converters.UrdfConverterCfg.make_instanceable*` - Whether to create instanceable assets. + Usually, this should be set to ``True``. The command-line flag is ``--make-instanceable`` where + when set, the importer will create instanceable assets, otherwise it will default to non-instanceable. +* :attr:`~sim.converters.UrdfConverterCfg.merge_fixed_joints*` - Whether to merge the fixed joints. + Usually, this should be set to ``True`` to reduce the asset complexity. The command-line flag is + ``--merge-joints`` where when set, the importer will merge the fixed joints, otherwise it will default to not merging the fixed joints. +* :attr:`~sim.converters.UrdfConverterCfg.default_drive_type` - The drive-type for the joints. + We recommend this to always be ``"none"``. This allows changing the drive configuration using the + actuator models. +* :attr:`~sim.converters.UrdfConverterCfg.default_drive_stiffness` - The drive stiffness for the joints. + We recommend this to always be ``0.0``. This allows changing the drive configuration using the + actuator models. +* :attr:`~sim.converters.UrdfConverterCfg.default_drive_damping` - The drive damping for the joints. + Similar to the stiffness, we recommend this to always be ``0.0``. + + +Note that when instanceable option is selected, the +importer will create two USD files: one for all the mesh data and one for +all the non-mesh data (e.g. joints, rigid bodies, etc.). The prims in the mesh data file are +referenced in the non-mesh data file. This allows the mesh data (which is often bulky) to be +loaded into memory only once and used multiple times in a scene. + +Example Usage +~~~~~~~~~~~~~ + +In this example, we use the pre-processed URDF file of the ANYmal-D robot. To check the +pre-process URDF, please check the file the `anymal.urdf`_. The main difference between the +pre-processed URDF and the original URDF are: + +* We removed the ```` tag from the URDF. This tag is not supported by the URDF importer. +* We removed the ```` tag from the URDF. This tag is not supported by the URDF importer. +* We removed various collision bodies from the URDF to reduce the complexity of the asset. +* We changed all the joint's damping and friction parameters to ``0.0``. This ensures that we can perform + effort-control on the joints without PhysX adding additional damping. +* We added the ```` tag to fixed joints. This ensures that the importer does + not merge these fixed joints. + +The following shows the steps to clone the repository and run the converter: + +.. code-block:: bash + + # create a directory to clone + mkdir ~/git && cd ~/git + # clone a repository with URDF files + git clone git@github.com:isaac-orbit/anymal_d_simple_description.git + + # go to top of the Isaac Lab repository + cd IsaacLab + # run the converter + ./isaaclab.sh -p source/standalone/tools/convert_urdf.py \ + ~/git/anymal_d_simple_description/urdf/anymal.urdf \ + source/extensions/omni.isaac.lab_assets/data/Robots/ANYbotics/anymal_d.usd \ + --merge-joints \ + --make-instanceable + + +Executing the above script will create two USD files inside the +``source/extensions/omni.isaac.lab_assets/data/Robots/ANYbotics/`` directory: + +* ``anymal_d.usd`` - This is the main asset file. It contains all the non-mesh data. +* ``Props/instanceable_assets.usd`` - This is the mesh data file. + +.. note:: + + Since Isaac Sim 2023.1.1, the URDF importer behavior has changed and it stores the mesh data inside the + main asset file even if the ``--make-instanceable`` flag is set. This means that the + ``Props/instanceable_assets.usd`` file is created but not used anymore. + +To run the script headless, you can add the ``--headless`` flag. This will not open the GUI and +exit the script after the conversion is complete. + +You can press play on the opened window to see the asset in the scene. The asset should fall under gravity. If it blows up, then it might be that you have self-collisions present in the URDF. + + +.. figure:: ../_static/tutorials/tutorial_convert_urdf.jpg + :align: center + :figwidth: 100% + :alt: result of convert_urdf.py + + + +Using MJCF Importer +------------------- + +Similar to the URDF Importer, the MJCF Importer also has a GUI interface. Please check the documentation at `MJCF importer`_ for more details. For using the MJCF importer from Python scripts, we include a utility tool called ``convert_mjcf.py``. This script creates an instance of :class:`~sim.converters.MjcfConverterCfg` which is then passed to the :class:`~sim.converters.MjcfConverter` class. + +The default values for the importer's configuration parameters are specified in the :class:`~sim.converters.MjcfConverterCfg` class. The configuration parameters are listed below. We made a few commonly modified settings to be available as command-line arguments when calling the ``convert_mjcf.py``, and they are marked with ``*`` in the list. For a comprehensive list of the configuration parameters, please check the the documentation at `MJCF importer`_. + + +* :attr:`~sim.converters.MjcfConverterCfg.fix_base*` - Whether to fix the base of the robot. + This depends on whether you have a floating-base or fixed-base robot. The command-line flag is + ``--fix-base`` where when set, the importer will fix the base of the robot, otherwise it will default to floating-base. +* :attr:`~sim.converters.MjcfConverterCfg.make_instanceable*` - Whether to create instanceable assets. + Usually, this should be set to ``True``. The command-line flag is ``--make-instanceable`` where + when set, the importer will create instanceable assets, otherwise it will default to non-instanceable. +* :attr:`~sim.converters.MjcfConverterCfg.import_sites*` - Whether to parse the tag in the MJCF. + Usually, this should be set to ``True``. The command-line flag is ``--import-sites`` where when set, the importer will parse the tag, otherwise it will default to not parsing the tag. + + +Example Usage +~~~~~~~~~~~~~ + +In this example, we use the MuJoCo model of the Unitree's H1 humanoid robot in the `mujoco_menagerie`_. + +The following shows the steps to clone the repository and run the converter: + +.. code-block:: bash + + # create a directory to clone + mkdir ~/git && cd ~/git + # clone a repository with URDF files + git clone git@github.com:git@github.com:google-deepmind/mujoco_menagerie.git + + # go to top of the Isaac Lab repository + cd IsaacLab + # run the converter + ./isaaclab.sh -p source/standalone/tools/convert_mjcf.py \ + ~/git/mujoco_menagerie/unitree_h1/h1.xml \ + source/extensions/omni.isaac.lab_assets/data/Robots/Unitree/h1.usd \ + --import-sites \ + --make-instanceable + +.. figure:: ../_static/tutorials/tutorial_convert_mjcf.jpg + :align: center + :figwidth: 100% + :alt: result of convert_mjcf.py + + + + +Using Mesh Importer +------------------- + +Omniverse Kit includes the mesh converter tool that uses the ASSIMP library to import assets +from various mesh formats (e.g. OBJ, FBX, STL, glTF, etc.). The asset converter tool is available +as an extension to Omniverse Kit. Please check the `asset converter`_ documentation for more details. +However, unlike Isaac Sim's URDF and MJCF importers, the asset converter tool does not support +creating instanceable assets. This means that the asset will be loaded into memory multiple times +if it is used multiple times in a scene. + +Thus, we include a utility tool called ``convert_mesh.py`` that uses the asset converter tool to +import the asset and then converts it into an instanceable asset. Internally, this script creates +an instance of :class:`~sim.converters.MeshConverterCfg` which is then passed to the +:class:`~sim.converters.MeshConverter` class. Since the mesh file does not contain any physics +information, the configuration class accepts different physics properties (such as mass, collision +shape, etc.) as input. Please check the documentation for :class:`~sim.converters.MeshConverterCfg` +for more details. + +Example Usage +~~~~~~~~~~~~~ + +We use an OBJ file of a cube to demonstrate the usage of the mesh converter. The following shows +the steps to clone the repository and run the converter: + +.. code-block:: bash + + # create a directory to clone + mkdir ~/git && cd ~/git + # clone a repository with URDF files + git clone git@github.com:NVIDIA-Omniverse/IsaacGymEnvs.git + + # go to top of the Isaac Lab repository + cd IsaacLab + # run the converter + ./isaaclab.sh -p source/standalone/tools/convert_mesh.py \ + ~/git/IsaacGymEnvs/assets/trifinger/objects/meshes/cube_multicolor.obj \ + source/extensions/omni.isaac.lab_assets/data/Props/CubeMultiColor/cube_multicolor.usd \ + --make-instanceable \ + --collision-approximation convexDecomposition \ + --mass 1.0 + +You may need to press 'F' to zoom in on the asset after import. + +Similar to the URDF and MJCF converter, executing the above script will create two USD files inside the +``source/extensions/omni.isaac.lab_assets/data/Props/CubeMultiColor/`` directory. Additionally, +if you press play on the opened window, you should see the asset fall down under the influence +of gravity. + +* If you do not set the ``--mass`` flag, then no rigid body properties will be added to the asset. + It will be imported as a static asset. +* If you also do not set the ``--collision-approximation`` flag, then the asset will not have any collider + properties as well and will be imported as a visual asset. + + +.. figure:: ../_static/tutorials/tutorial_convert_mesh.jpg + :align: center + :figwidth: 100% + :alt: result of convert_mesh.py + + +.. _instanceable: https://openusd.org/dev/api/_usd__page__scenegraph_instancing.html +.. _documentation: https://docs.omniverse.nvidia.com/isaacsim/latest/isaac_lab_tutorials/tutorial_instanceable_assets.html +.. _MJCF importer: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_mjcf.html +.. _URDF importer: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_urdf.html +.. _anymal.urdf: https://github.com/isaac-orbit/anymal_d_simple_description/blob/master/urdf/anymal.urdf +.. _asset converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_asset-converter.html +.. _mujoco_menagerie: https://github.com/google-deepmind/mujoco_menagerie/tree/main/unitree_h1 diff --git a/_sources/source/how-to/index.rst b/_sources/source/how-to/index.rst new file mode 100644 index 0000000000..4b5c426d82 --- /dev/null +++ b/_sources/source/how-to/index.rst @@ -0,0 +1,115 @@ +.. _how-to: + +How-to Guides +============= + +This section includes guides that help you use Isaac Lab. These are intended for users who +have already worked through the tutorials and are looking for more information on how to +use Isaac Lab. If you are new to Isaac Lab, we recommend you start with the tutorials. + +.. note:: + + This section is a work in progress. If you have a question that is not answered here, + please open an issue on our `GitHub page `_. + +Importing a New Asset +--------------------- + +Importing an asset into Isaac Lab is a common task. It contains two steps: importing the asset into +a USD format and then setting up the configuration object for the asset. The following guide explains +how to import a new asset into Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + import_new_asset + write_articulation_cfg + +Creating a Fixed Asset +---------------------- + +Often you may want to create a fixed asset in your scene. For instance, making a floating base robot +a fixed base robot. This guide goes over the various considerations and steps to create a fixed asset. + +.. toctree:: + :maxdepth: 1 + + make_fixed_prim + +Spawning Multiple Assets +------------------------ + +This guide explains how to import and configure different assets in each environment. This is +useful when you want to create diverse environments with different objects. + +.. toctree:: + :maxdepth: 1 + + multi_asset_spawning + +Saving Camera Output +-------------------- + +This guide explains how to save the camera output in Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + save_camera_output + +Estimate How Many Cameras Can Run On Your Machine +------------------------------------------------- + +This guide demonstrates how to estimate the number of cameras one can run on their machine under the desired parameters. + +.. toctree:: + :maxdepth: 1 + + estimate_how_many_cameras_can_run + + +Drawing Markers +--------------- + +This guide explains how to use the :class:`~omni.isaac.lab.markers.VisualizationMarkers` class to draw markers in +Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + draw_markers + + +Interfacing with Environments +----------------------------- + +These guides explain how to interface with reinforcement learning environments in Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + wrap_rl_env + add_own_library + + +Recording an Animation and Video +-------------------------------- + +This guide explains how to record an animation and video in Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + record_animation + record_video + +Mastering Omniverse +------------------- + +Omniverse is a powerful platform that provides a wide range of features. This guide links to +additional resources that help you use Omniverse features in Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + master_omniverse diff --git a/_sources/source/how-to/make_fixed_prim.rst b/_sources/source/how-to/make_fixed_prim.rst new file mode 100644 index 0000000000..1847c4805b --- /dev/null +++ b/_sources/source/how-to/make_fixed_prim.rst @@ -0,0 +1,179 @@ +Making a physics prim fixed in the simulation +============================================= + +.. currentmodule:: omni.isaac.lab + +When a USD prim has physics schemas applied on it, it is affected by physics simulation. +This means that the prim can move, rotate, and collide with other prims in the simulation world. +However, there are cases where it is desirable to make certain prims static in the simulation world, +i.e. the prim should still participate in collisions but its position and orientation should not change. + +The following sections describe how to spawn a prim with physics schemas and make it static in the simulation world. + +Static colliders +---------------- + +Static colliders are prims that are not affected by physics but can collide with other prims in the simulation world. +These don't have any rigid body properties applied on them. However, this also means that they can't be accessed +using the physics tensor API (i.e., through the :class:`assets.RigidObject` class). + +For instance, to spawn a cone static in the simulation world, the following code can be used: + +.. code-block:: python + + import omni.isaac.lab.sim as sim_utils + + cone_spawn_cfg = sim_utils.ConeCfg( + radius=0.15, + height=0.5, + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + ) + cone_spawn_cfg.func( + "/World/Cone", cone_spawn_cfg, translation=(0.0, 0.0, 2.0), orientation=(0.5, 0.0, 0.5, 0.0) + ) + + +Rigid object +------------ + +Rigid objects (i.e. object only has a single body) can be made static by setting the parameter +:attr:`sim.schemas.RigidBodyPropertiesCfg.kinematic_enabled` as True. This will make the object +kinematic and it will not be affected by physics. + +For instance, to spawn a cone static in the simulation world but with rigid body schema on it, +the following code can be used: + +.. code-block:: python + + import omni.isaac.lab.sim as sim_utils + + cone_spawn_cfg = sim_utils.ConeCfg( + radius=0.15, + height=0.5, + rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), + mass_props=sim_utils.MassPropertiesCfg(mass=1.0), + collision_props=sim_utils.CollisionPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.0, 1.0, 0.0)), + ) + cone_spawn_cfg.func( + "/World/Cone", cone_spawn_cfg, translation=(0.0, 0.0, 2.0), orientation=(0.5, 0.0, 0.5, 0.0) + ) + + +Articulation +------------ + +Fixing the root of an articulation requires having a fixed joint to the root rigid body link of the articulation. +This can be achieved by setting the parameter :attr:`sim.schemas.ArticulationRootPropertiesCfg.fix_root_link` +as True. Based on the value of this parameter, the following cases are possible: + +* If set to :obj:`None`, the root link is not modified. +* If the articulation already has a fixed root link, this flag will enable or disable the fixed joint. +* If the articulation does not have a fixed root link, this flag will create a fixed joint between the world + frame and the root link. The joint is created with the name "FixedJoint" under the root link. + +For instance, to spawn an ANYmal robot and make it static in the simulation world, the following code can be used: + +.. code-block:: python + + import omni.isaac.lab.sim as sim_utils + from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR + + anymal_spawn_cfg = sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/ANYbotics/ANYmal-C/anymal_c.usd", + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + retain_accelerations=False, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=True, + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + fix_root_link=True, + ), + ) + anymal_spawn_cfg.func( + "/World/ANYmal", anymal_spawn_cfg, translation=(0.0, 0.0, 0.8), orientation=(1.0, 0.0, 0.0, 0.0) + ) + + +This will create a fixed joint between the world frame and the root link of the ANYmal robot +at the prim path ``"/World/ANYmal/base/FixedJoint"`` since the root link is at the path ``"/World/ANYmal/base"``. + + +Further notes +------------- + +Given the flexibility of USD asset designing the following possible scenarios are usually encountered: + +1. **Articulation root schema on the rigid body prim without a fixed joint**: + + This is the most common and recommended scenario for floating-base articulations. The root prim + has both the rigid body and the articulation root properties. In this case, the articulation root + is parsed as a floating-base with the root prim of the articulation ``Link0Xform``. + + .. code-block:: text + + ArticulationXform + └── Link0Xform (RigidBody and ArticulationRoot schema) + +2. **Articulation root schema on the parent prim with a fixed joint**: + + This is the expected arrangement for fixed-base articulations. The root prim has only the rigid body + properties and the articulation root properties are applied to its parent prim. In this case, the + articulation root is parsed as a fixed-base with the root prim of the articulation ``Link0Xform``. + + .. code-block:: text + + ArticulationXform (ArticulationRoot schema) + └── Link0Xform (RigidBody schema) + └── FixedJoint (connecting the world frame and Link0Xform) + +3. **Articulation root schema on the parent prim without a fixed joint**: + + This is a scenario where the root prim has only the rigid body properties and the articulation root properties + are applied to its parent prim. However, the fixed joint is not created between the world frame and the root link. + In this case, the articulation is parsed as a floating-base system. However, the PhysX parser uses its own + heuristic (such as alphabetical order) to determine the root prim of the articulation. It may select the root prim + at ``Link0Xform`` or choose another prim as the root prim. + + .. code-block:: text + + ArticulationXform (ArticulationRoot schema) + └── Link0Xform (RigidBody schema) + +4. **Articulation root schema on the rigid body prim with a fixed joint**: + + While this is a valid scenario, it is not recommended as it may lead to unexpected behavior. In this case, + the articulation is still parsed as a floating-base system. However, the fixed joint, created between the + world frame and the root link, is considered as a part of the maximal coordinate tree. This is different from + PhysX considering the articulation as a fixed-base system. Hence, the simulation may not behave as expected. + + .. code-block:: text + + ArticulationXform + └── Link0Xform (RigidBody and ArticulationRoot schema) + └── FixedJoint (connecting the world frame and Link0Xform) + +For floating base articulations, the root prim usually has both the rigid body and the articulation +root properties. However, directly connecting this prim to the world frame will cause the simulation +to consider the fixed joint as a part of the maximal coordinate tree. This is different from PhysX +considering the articulation as a fixed-base system. + +Internally, when the parameter :attr:`sim.schemas.ArticulationRootPropertiesCfg.fix_root_link` is set to True +and the articulation is detected as a floating-base system, the fixed joint is created between the world frame +the root rigid body link of the articulation. However, to make the PhysX parser consider the articulation as a +fixed-base system, the articulation root properties are removed from the root rigid body prim and applied to +its parent prim instead. + +.. note:: + + In future release of Isaac Sim, an explicit flag will be added to the articulation root schema from PhysX + to toggle between fixed-base and floating-base systems. This will resolve the need of the above workaround. diff --git a/_sources/source/how-to/master_omniverse.rst b/_sources/source/how-to/master_omniverse.rst new file mode 100644 index 0000000000..80385f1b89 --- /dev/null +++ b/_sources/source/how-to/master_omniverse.rst @@ -0,0 +1,124 @@ +Mastering Omniverse for Robotics +================================ + +NVIDIA Omniverse offers a large suite of tools for 3D content workflows. +There are three main components (relevant to robotics) in Omniverse: + +- **USD Composer**: This is based on a novel file format (Universal Scene + Description) from the animation (originally Pixar) community that is + used in Omniverse +- **PhysX SDK**: This is the main physics engine behind Omniverse that + leverages GPU-based parallelization for massive scenes +- **RTX-enabled Renderer**: This uses ray-tracing kernels in NVIDIA RTX + GPUs for real-time physically-based rendering + +Of these, the first two require a deeper understanding to start working +with Omniverse and its constituent applications (Isaac Sim and Isaac Lab). + +The main things to learn: + +- How to use the Composer GUI efficiently? +- What are USD prims and schemas? +- How do you compose a USD scene? +- What is the difference between references and payloads in USD? +- What is meant by scene-graph instancing? +- How to apply PhysX schemas on prims? What all schemas are possible? +- How to write basic operations in USD for creating prims and modifying + their attributes? + + +Part 1: Using USD Composer +-------------------------- + +While several `video +tutorials `__ and +`documentation `__ exist +out there on NVIDIA Omniverse, going through all of them would take an +extensive amount of time and effort. Thus, we have curated these +resources to guide you through using Omniverse, specifically for +robotics. + +Introduction to Omniverse and USD + +- `What is NVIDIA Omniverse? `__ +- `What is the USD File Type? \| Getting Started in NVIDIA Omniverse `__ +- `What Makes USD Unique in NVIDIA Omniverse `__ + +Using Omniverse USD Composer + +- `Introduction to Omniverse USD Composer `__ +- `Navigation Basics in Omniverse USD Composer `__ +- `Lighting Basics in NVIDIA Omniverse USD Composer `__ +- `Rendering Overview in NVIDIA Omniverse USD Composer `__ + +Materials and MDL + +- `Five Things to Know About Materials in NVIDIA Omniverse `__ +- `How to apply materials? `__ + +Omniverse Physics and PhysX SDK + +- `Basics - Setting Up Physics and Toolbar Overview `__ +- `Basics - Demos Overview `__ +- `Rigid Bodies - Mass Editing `__ +- `Materials - Friction Restitution and Defaults `__ +- `Overview of Simulation Ready Assets Physics in Omniverse `__ + +Importing assets + +- `Omniverse Create - Importing FBX Files \| NVIDIA Omniverse Tutorials `__ +- `Omniverse Asset Importer `__ +- `Isaac Sim URDF impoter `__ + + +Part 2: Scripting in Omniverse +------------------------------ + +The above links mainly introduced how to use the USD Composer and its +functionalities through UI operations. However, often developers +need to write scripts to perform operations. This is especially true +when you want to automate certain tasks or create custom applications +that use Omniverse as a backend. This section will introduce you to +scripting in Omniverse. + +USD is the main file format Omniverse operates with. So naturally, the +APIs (from OpenUSD) for modifying USD are at the core of Omniverse. +Most of the APIs are in C++ and Python bindings are provided for them. +Thus, to script in Omniverse, you need to understand the USD APIs. + +.. note:: + + While Isaac Sim and Isaac Lab try to "relieve" users from understanding + the core USD concepts and APIs, understanding these basics still + help a lot once you start diving inside the codebase and modifying + it for your own application. + +Before diving into USD scripting, it is good to get acquainted with the +terminologies used in USD. We recommend the following `introduction to +USD basics `__ by +Houdini, which is a 3D animation software. +Make sure to go through the following sections: + +- `Quick example `__ +- `Attributes and primvars `__ +- `Composition `__ +- `Schemas `__ +- `Instances `__ + and `Scene-graph Instancing `__ + +As a test of understanding, make sure you can answer the following: + +- What are prims? What is meant by a prim path in a stage? +- How are attributes related to prims? +- How are schemas related to prims? +- What is the difference between attributes and schemas? +- What is asset instancing? + +Part 3: More Resources +---------------------- + +- `Omniverse Glossary of Terms `__ +- `Omniverse Code Samples `__ +- `PhysX Collider Compatibility `__ +- `PhysX Limitations `__ +- `PhysX Documentation `__. diff --git a/_sources/source/how-to/multi_asset_spawning.rst b/_sources/source/how-to/multi_asset_spawning.rst new file mode 100644 index 0000000000..fe141981fc --- /dev/null +++ b/_sources/source/how-to/multi_asset_spawning.rst @@ -0,0 +1,129 @@ + +Spawning Multiple Assets +======================== + +.. currentmodule:: omni.isaac.lab + +Typical spawning configurations (introduced in the :ref:`tutorial-spawn-prims` tutorial) copy the same +asset (or USD primitive) across the different resolved prim paths from the expressions. +For instance, if the user specifies to spawn the asset at "/World/Table\_.*/Object", the same +asset is created at the paths "/World/Table_0/Object", "/World/Table_1/Object" and so on. + +However, we also support multi-asset spawning with two mechanisms: + +1. Rigid object collections. This allows the user to spawn multiple rigid objects in each environment and access/modify + them with a unified API, improving performance. + +2. Spawning different assets under the same prim path. This allows the user to create diverse simulations, where each + environment has a different asset. + +This guide describes how to use these two mechanisms. + +The sample script ``multi_asset.py`` is used as a reference, located in the +``IsaacLab/source/standalone/demos`` directory. + +.. dropdown:: Code for multi_asset.py + :icon: code + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :emphasize-lines: 109-131, 135-179, 184-203 + :linenos: + +This script creates multiple environments, where each environment has: + +* a rigid object collection containing a cone, a cube, and a sphere +* a rigid object that is either a cone, a cube, or a sphere, chosen at random +* an articulation that is either the ANYmal-C or ANYmal-D robot, chosen at random + +.. image:: ../_static/demos/multi_asset.jpg + :width: 100% + :alt: result of multi_asset.py + + +Rigid Object Collections +------------------------ + +Multiple rigid objects can be spawned in each environment and accessed/modified with a unified ``(env_ids, obj_ids)`` API. +While the user could also create multiple rigid objects by spawning them individually, the API is more user-friendly and +more efficient since it uses a single physics view under the hood to handle all the objects. + +.. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 135-179 + :dedent: + +The configuration :class:`~assets.RigidObjectCollectionCfg` is used to create the collection. It's attribute :attr:`~assets.RigidObjectCollectionCfg.rigid_objects` +is a dictionary containing :class:`~assets.RigidObjectCfg` objects. The keys serve as unique identifiers for each +rigid object in the collection. + + +Spawning different assets under the same prim path +-------------------------------------------------- + +It is possible to spawn different assets and USDs under the same prim path in each environment using the spawners +:class:`~sim.spawners.wrappers.MultiAssetSpawnerCfg` and :class:`~sim.spawners.wrappers.MultiUsdFileCfg`: + +* We set the spawn configuration in :class:`~assets.RigidObjectCfg` to be + :class:`~sim.spawners.wrappers.MultiAssetSpawnerCfg`: + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 107-133 + :dedent: + + This function allows you to define a list of different assets that can be spawned as rigid objects. + When :attr:`~sim.spawners.wrappers.MultiAssetSpawnerCfg.random_choice` is set to True, one asset from the list + is randomly selected and spawned at the specified prim path. + +* Similarly, we set the spawn configuration in :class:`~assets.ArticulationCfg` to be + :class:`~sim.spawners.wrappers.MultiUsdFileCfg`: + + .. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 182-215 + :dedent: + + Similar to before, this configuration allows the selection of different USD files representing articulated assets. + + +Things to Note +~~~~~~~~~~~~~~ + +Similar asset structuring +~~~~~~~~~~~~~~~~~~~~~~~~~ + +While spawning and handling multiple assets using the same physics interface (the rigid object or articulation classes), +it is essential to have the assets at all the prim locations follow a similar structure. In case of an articulation, +this means that they all must have the same number of links and joints, the same number of collision bodies and +the same names for them. If that is not the case, the physics parsing of the prims can get affected and fail. + +The main purpose of this functionality is to enable the user to create randomized versions of the same asset, +for example robots with different link lengths, or rigid objects with different collider shapes. + +Disabling physics replication in interactive scene +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, the flag :attr:`scene.InteractiveScene.replicate_physics` is set to True. This flag informs the physics +engine that the simulation environments are copies of one another so it just needs to parse the first environment +to understand the entire simulation scene. This helps speed up the simulation scene parsing. + +However, in the case of spawning different assets in different environments, this assumption does not hold +anymore. Hence the flag :attr:`scene.InteractiveScene.replicate_physics` must be disabled. + +.. literalinclude:: ../../../source/standalone/demos/multi_asset.py + :language: python + :lines: 280-283 + :dedent: + +The Code Execution +------------------ + +To execute the script with multiple environments and randomized assets, use the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/demos/multi_asset.py --num_envs 2048 + +This command runs the simulation with 2048 environments, each with randomly selected assets. +To stop the simulation, you can close the window, or press ``Ctrl+C`` in the terminal. diff --git a/_sources/source/how-to/record_animation.rst b/_sources/source/how-to/record_animation.rst new file mode 100644 index 0000000000..035a09e943 --- /dev/null +++ b/_sources/source/how-to/record_animation.rst @@ -0,0 +1,78 @@ +Recording Animations of Simulations +=================================== + +.. currentmodule:: omni.isaac.lab + +Omniverse includes tools to record animations of physics simulations. The `Stage Recorder`_ extension +listens to all the motion and USD property changes within a USD stage and records them to a USD file. +This file contains the time samples of the changes, which can be played back to render the animation. + +The timeSampled USD file only contains the changes to the stage. It uses the same hierarchy as the original +stage at the time of recording. This allows adding the animation to the original stage, or to a different +stage with the same hierarchy. The timeSampled file can be directly added as a sublayer to the original stage +to play back the animation. + +.. note:: + + Omniverse only supports playing animation or playing physics on a USD prim at the same time. If you want to + play back the animation of a USD prim, you need to disable the physics simulation on the prim. + + +In Isaac Lab, we directly use the `Stage Recorder`_ extension to record the animation of the physics simulation. +This is available as a feature in the :class:`~omni.isaac.lab.envs.ui.BaseEnvWindow` class. +However, to record the animation of a simulation, you need to disable `Fabric`_ to allow reading and writing +all the changes (such as motion and USD properties) to the USD stage. + + +Stage Recorder Settings +~~~~~~~~~~~~~~~~~~~~~~~ + +Isaac Lab integration of the `Stage Recorder`_ extension assumes certain default settings. If you want to change the +settings, you can directly use the `Stage Recorder`_ extension in the Omniverse Create application. + +.. dropdown:: Settings used in base_env_window.py + :icon: code + + .. literalinclude:: ../../../source/extensions/omni.isaac.lab/omni/isaac/lab/envs/ui/base_env_window.py + :language: python + :linenos: + :pyobject: BaseEnvWindow._toggle_recording_animation_fn + + +Example Usage +~~~~~~~~~~~~~ + +In all environment standalone scripts, Fabric can be disabled by passing the ``--disable_fabric`` flag to the script. +Here we run the state-machine example and record the animation of the simulation. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/environments/state_machine/lift_cube_sm.py --num_envs 8 --device cpu --disable_fabric + + +On running the script, the Isaac Lab UI window opens with the button "Record Animation" in the toolbar. +Clicking this button starts recording the animation of the simulation. On clicking the button again, the +recording stops. The recorded animation and the original stage (with all physics disabled) are saved +to the ``recordings`` folder in the current working directory. The files are stored in the ``usd`` format: + +- ``Stage.usd``: The original stage with all physics disabled +- ``TimeSample_tk001.usd``: The timeSampled file containing the recorded animation + +You can open Omniverse Isaac Sim application to play back the animation. There are many ways to launch +the application (such as from terminal or `Omniverse Launcher`_). Here we use the terminal to open the +application and play the animation. + +.. code-block:: bash + + ./isaaclab.sh -s # Opens Isaac Sim application through _isaac_sim/isaac-sim.sh + +On a new stage, add the ``Stage.usd`` as a sublayer and then add the ``TimeSample_tk001.usd`` as a sublayer. +You can do this by dragging and dropping the files from the file explorer to the stage. Please check out +the `tutorial on layering in Omniverse`_ for more details. + +You can then play the animation by pressing the play button. + +.. _Stage Recorder: https://docs.omniverse.nvidia.com/extensions/latest/ext_animation_stage-recorder.html +.. _Fabric: https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/usd_fabric_usdrt.html +.. _Omniverse Launcher: https://docs.omniverse.nvidia.com/launcher/latest/index.html +.. _tutorial on layering in Omniverse: https://www.youtube.com/watch?v=LTwmNkSDh-c&ab_channel=NVIDIAOmniverse diff --git a/_sources/source/how-to/record_video.rst b/_sources/source/how-to/record_video.rst new file mode 100644 index 0000000000..7b8276c067 --- /dev/null +++ b/_sources/source/how-to/record_video.rst @@ -0,0 +1,25 @@ +Recording video clips during training +===================================== + +Isaac Lab supports recording video clips during training using the +`gymnasium.wrappers.RecordVideo `_ class. + +This feature can be enabled by installing ``ffmpeg`` and using the following command line arguments with the training +script: + +* ``--video``: enables video recording during training +* ``--video_length``: length of each recorded video (in steps) +* ``--video_interval``: interval between each video recording (in steps) + +Make sure to also add the ``--enable_cameras`` argument when running headless. +Note that enabling recording is equivalent to enabling rendering during training, which will slow down both startup and runtime performance. + +Example usage: + +.. code-block:: shell + + python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 --headless --video --video_length 100 --video_interval 500 + + +The recorded videos will be saved in the same directory as the training checkpoints, under +``IsaacLab/logs////videos/train``. diff --git a/_sources/source/how-to/save_camera_output.rst b/_sources/source/how-to/save_camera_output.rst new file mode 100644 index 0000000000..bbc7327b22 --- /dev/null +++ b/_sources/source/how-to/save_camera_output.rst @@ -0,0 +1,102 @@ +.. _how-to-save-images-and-3d-reprojection: + + +Saving rendered images and 3D re-projection +=========================================== + +.. currentmodule:: omni.isaac.lab + +This guide accompanied with the ``run_usd_camera.py`` script in the ``IsaacLab/source/standalone/tutorials/04_sensors`` +directory. + +.. dropdown:: Code for run_usd_camera.py + :icon: code + + .. literalinclude:: ../../../source/standalone/tutorials/04_sensors/run_usd_camera.py + :language: python + :emphasize-lines: 171-179, 229-247, 251-264 + :linenos: + + +Saving using Replicator Basic Writer +------------------------------------ + +To save camera outputs, we use the basic write class from Omniverse Replicator. This class allows us to save the +images in a numpy format. For more information on the basic writer, please check the +`documentation `_. + +.. literalinclude:: ../../../source/standalone/tutorials/04_sensors/run_usd_camera.py + :language: python + :start-at: rep_writer = rep.BasicWriter( + :end-before: # Camera positions, targets, orientations + +While stepping the simulator, the images can be saved to the defined folder. Since the BasicWriter only supports +saving data using NumPy format, we first need to convert the PyTorch sensors to NumPy arrays before packing +them in a dictionary. + +.. literalinclude:: ../../../source/standalone/tutorials/04_sensors/run_usd_camera.py + :language: python + :start-at: # Save images from camera at camera_index + :end-at: single_cam_info = camera.data.info[camera_index] + +After this step, we can save the images using the BasicWriter. + +.. literalinclude:: ../../../source/standalone/tutorials/04_sensors/run_usd_camera.py + :language: python + :start-at: # Pack data back into replicator format to save them using its writer + :end-at: rep_writer.write(rep_output) + + +Projection into 3D Space +------------------------ + +We include utilities to project the depth image into 3D Space. The re-projection operations are done using +PyTorch operations which allows faster computation. + +.. code-block:: python + + from omni.isaac.lab.utils.math import transform_points, unproject_depth + + # Pointcloud in world frame + points_3d_cam = unproject_depth( + camera.data.output["distance_to_image_plane"], camera.data.intrinsic_matrices + ) + + points_3d_world = transform_points(points_3d_cam, camera.data.pos_w, camera.data.quat_w_ros) + +Alternately, we can use the :meth:`omni.isaac.lab.sensors.camera.utils.create_pointcloud_from_depth` function +to create a point cloud from the depth image and transform it to the world frame. + +.. literalinclude:: ../../../source/standalone/tutorials/04_sensors/run_usd_camera.py + :language: python + :start-at: # Derive pointcloud from camera at camera_index + :end-before: # In the first few steps, things are still being instanced and Camera.data + +The resulting point cloud can be visualized using the :mod:`omni.isaac.debug_draw` extension from Isaac Sim. +This makes it easy to visualize the point cloud in the 3D space. + +.. literalinclude:: ../../../source/standalone/tutorials/04_sensors/run_usd_camera.py + :language: python + :start-at: # In the first few steps, things are still being instanced and Camera.data + :end-at: pc_markers.visualize(translations=pointcloud) + + +Executing the script +-------------------- + +To run the accompanying script, execute the following command: + +.. code-block:: bash + + # Usage with saving and drawing + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_usd_camera.py --save --draw --enable_cameras + + # Usage with saving only in headless mode + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_usd_camera.py --save --headless --enable_cameras + + +The simulation should start, and you can observe different objects falling down. An output folder will be created +in the ``IsaacLab/source/standalone/tutorials/04_sensors`` directory, where the images will be saved. Additionally, +you should see the point cloud in the 3D space drawn on the viewport. + +To stop the simulation, close the window, or use ``Ctrl+C`` in the terminal. diff --git a/_sources/source/how-to/wrap_rl_env.rst b/_sources/source/how-to/wrap_rl_env.rst new file mode 100644 index 0000000000..c6c2594b28 --- /dev/null +++ b/_sources/source/how-to/wrap_rl_env.rst @@ -0,0 +1,167 @@ +.. _how-to-env-wrappers: + + +Wrapping environments +===================== + +.. currentmodule:: omni.isaac.lab + +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 :class:`gymnasium.Wrapper` class. + +At present, all RL environments inheriting from the :class:`~envs.ManagerBasedRLEnv` or :class:`~envs.DirectRLEnv` classes +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. + +For example, here is how you would wrap an environment to enforce that reset is called before step or render: + +.. code-block:: python + + """Launch Isaac Sim Simulator first.""" + + + from omni.isaac.lab.app import AppLauncher + + # launch omniverse app in headless mode + app_launcher = AppLauncher(headless=True) + simulation_app = app_launcher.app + + """Rest everything follows.""" + + import gymnasium as gym + + import omni.isaac.lab_tasks # noqa: F401 + from omni.isaac.lab_tasks.utils import load_cfg_from_registry + + # create base environment + cfg = load_cfg_from_registry("Isaac-Reach-Franka-v0", "env_cfg_entry_point") + env = gym.make("Isaac-Reach-Franka-v0", cfg=cfg) + # wrap environment to enforce that reset is called before step + env = gym.wrappers.OrderEnforcing(env) + + +Wrapper for recording videos +---------------------------- + +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. + +To use the wrapper, you need to first install ``ffmpeg``. On Ubuntu, you can install it by running: + +.. code-block:: bash + + sudo apt-get install ffmpeg + +.. attention:: + + By default, when running an environment in headless mode, the Omniverse viewport is disabled. This is done to + improve performance by avoiding unnecessary rendering. + + We notice the following performance in different rendering modes with the ``Isaac-Reach-Franka-v0`` environment + using an RTX 3090 GPU: + + * No GUI execution without off-screen rendering enabled: ~65,000 FPS + * No GUI execution with off-screen enabled: ~57,000 FPS + * GUI execution with full rendering: ~13,000 FPS + + +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:`~envs.ViewerCfg` class. + + +.. dropdown:: Default parameters of the ViewerCfg class: + :icon: code + + .. literalinclude:: ../../../source/extensions/omni.isaac.lab/omni/isaac/lab/envs/common.py + :language: python + :pyobject: ViewerCfg + + +After adjusting the parameters, you can record videos by wrapping the environment with the +: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 + + """Launch Isaac Sim Simulator first.""" + + + from omni.isaac.lab.app import AppLauncher + + # launch omniverse app in headless mode with off-screen rendering + app_launcher = AppLauncher(headless=True, enable_cameras=True) + simulation_app = app_launcher.app + + """Rest everything follows.""" + + import gymnasium as gym + + # adjust camera resolution and pose + env_cfg.viewer.resolution = (640, 480) + env_cfg.viewer.eye = (1.0, 1.0, 1.0) + env_cfg.viewer.lookat = (0.0, 0.0, 0.0) + # create isaac-env instance + # 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/train", + "step_trigger": lambda step: step % 1500 == 0, + "video_length": 200, + } + env = gym.wrappers.RecordVideo(env, **video_kwargs) + + +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`_, `RSL-RL`_ or `SKRL`_ +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:`~envs.ManagerBasedRLEnv` and :class:`~envs.DirectRLEnv` classes 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 RL task environment with Stable-Baselines3: + +.. code:: python + + from omni.isaac.lab_tasks.utils.wrappers.sb3 import Sb3VecEnvWrapper + + # create isaac-env instance + env = gym.make(task_name, cfg=env_cfg) + # wrap around environment for stable baselines + env = Sb3VecEnvWrapper(env) + + +.. caution:: + + 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 :class:`gymnasium.Env`. + + +Adding new wrappers +------------------- + +All new wrappers should be added to the :mod:`omni.isaac.lab_tasks.utils.wrappers` module. +They should check that the underlying environment is an instance of :class:`omni.isaac.lab.envs.ManagerBasedRLEnv` +or :class:`~envs.DirectRLEnv` +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. + +.. _Stable-Baselines3: https://stable-baselines3.readthedocs.io/en/master/ +.. _SKRL: https://skrl.readthedocs.io +.. _RL-Games: https://github.com/Denys88/rl_games +.. _RSL-RL: https://github.com/leggedrobotics/rsl_rl diff --git a/_sources/source/how-to/write_articulation_cfg.rst b/_sources/source/how-to/write_articulation_cfg.rst new file mode 100644 index 0000000000..30d88dde34 --- /dev/null +++ b/_sources/source/how-to/write_articulation_cfg.rst @@ -0,0 +1,116 @@ +.. _how-to-write-articulation-config: + + +Writing an Asset Configuration +============================== + +.. currentmodule:: omni.isaac.lab + +This guide walks through the process of creating an :class:`~assets.ArticulationCfg`. +The :class:`~assets.ArticulationCfg` is a configuration object that defines the +properties of an :class:`~assets.Articulation` in Isaac Lab. + +.. note:: + + While we only cover the creation of an :class:`~assets.ArticulationCfg` in this guide, + the process is similar for creating any other asset configuration object. + +We will use the Cartpole example to demonstrate how to create an :class:`~assets.ArticulationCfg`. +The Cartpole is a simple robot that consists of a cart with a pole attached to it. The cart +is free to move along a rail, and the pole is free to rotate about the cart. + +.. dropdown:: Code for Cartpole configuration + :icon: code + + .. literalinclude:: ../../../source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/cartpole.py + :language: python + :linenos: + + +Defining the spawn configuration +-------------------------------- + +As explained in :ref:`tutorial-spawn-prims` tutorials, the spawn configuration defines +the properties of the assets to be spawned. This spawning may happen procedurally, or +through an existing asset file (e.g. USD or URDF). In this example, we will spawn the +Cartpole from a USD file. + +When spawning an asset from a USD file, we define its :class:`~sim.spawners.from_files.UsdFileCfg`. +This configuration object takes in the following parameters: + +* :class:`~sim.spawners.from_files.UsdFileCfg.usd_path`: The USD file path to spawn from +* :class:`~sim.spawners.from_files.UsdFileCfg.rigid_props`: The properties of the articulation's root +* :class:`~sim.spawners.from_files.UsdFileCfg.articulation_props`: The properties of all the articulation's links + +The last two parameters are optional. If not specified, they are kept at their default values in the USD file. + +.. literalinclude:: ../../../source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/cartpole.py + :language: python + :lines: 19-35 + :dedent: + +To import articulation from a URDF file instead of a USD file, you can replace the +:class:`~sim.spawners.from_files.UsdFileCfg` with a :class:`~sim.spawners.from_files.UrdfFileCfg`. +For more details, please check the API documentation. + + +Defining the initial state +-------------------------- + +Every asset requires defining their initial or *default* state in the simulation through its configuration. +This configuration is stored into the asset's default state buffers that can be accessed when the asset's +state needs to be reset. + +.. note:: + The initial state of an asset is defined w.r.t. its local environment frame. This then needs to + be transformed into the global simulation frame when resetting the asset's state. For more + details, please check the :ref:`tutorial-interact-articulation` tutorial. + + +For an articulation, the :class:`~assets.ArticulationCfg.InitialStateCfg` object defines the +initial state of the root of the articulation and the initial state of all its joints. In this +example, we will spawn the Cartpole at the origin of the XY plane at a Z height of 2.0 meters. +Meanwhile, the joint positions and velocities are set to 0.0. + +.. literalinclude:: ../../../source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/cartpole.py + :language: python + :lines: 36-38 + :dedent: + +Defining the actuator configuration +----------------------------------- + +Actuators are a crucial component of an articulation. Through this configuration, it is possible +to define the type of actuator model to use. We can use the internal actuator model provided by +the physics engine (i.e. the implicit actuator model), or use a custom actuator model which is +governed by a user-defined system of equations (i.e. the explicit actuator model). +For more details on actuators, see :ref:`overview-actuators`. + +The cartpole's articulation has two actuators, one corresponding to its each joint: +``cart_to_pole`` and ``slider_to_cart``. We use two different actuator models for these actuators as +an example. However, since they are both using the same actuator model, it is possible +to combine them into a single actuator model. + +.. dropdown:: Actuator model configuration with separate actuator models + :icon: code + + .. literalinclude:: ../../../source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets/cartpole.py + :language: python + :lines: 39-49 + :dedent: + + +.. dropdown:: Actuator model configuration with a single actuator model + :icon: code + + .. code-block:: python + + actuators={ + "all_joints": ImplicitActuatorCfg( + joint_names_expr=[".*"], + effort_limit=400.0, + velocity_limit=100.0, + stiffness={"slider_to_cart": 0.0, "cart_to_pole": 0.0}, + damping={"slider_to_cart": 10.0, "cart_to_pole": 0.0}, + ), + }, diff --git a/_sources/source/migration/migrating_from_isaacgymenvs.rst b/_sources/source/migration/migrating_from_isaacgymenvs.rst new file mode 100644 index 0000000000..2632321917 --- /dev/null +++ b/_sources/source/migration/migrating_from_isaacgymenvs.rst @@ -0,0 +1,929 @@ +.. _migrating-from-isaacgymenvs: + +From IsaacGymEnvs +================= + +.. currentmodule:: omni.isaac.lab + + +`IsaacGymEnvs`_ was a reinforcement learning framework designed for the `Isaac Gym Preview Release`_. +As both IsaacGymEnvs and the Isaac Gym Preview Release are now deprecated, the following guide walks through +the key differences between IsaacGymEnvs and Isaac Lab, as well as differences in APIs between Isaac Gym Preview +Release and Isaac Sim. + +.. note:: + + The following changes are with respect to Isaac Lab 1.0 release. Please refer to the `release notes`_ for any changes + in the future releases. + + +Task Config Setup +~~~~~~~~~~~~~~~~~ + +In IsaacGymEnvs, task config files were defined in ``.yaml`` format. With Isaac Lab, configs are now specified using +a specialized Python class :class:`~omni.isaac.lab.utils.configclass`. The :class:`~omni.isaac.lab.utils.configclass` +module provides a wrapper on top of Python's ``dataclasses`` module. Each environment should specify its own config +class annotated by ``@configclass`` that inherits from :class:`~envs.DirectRLEnvCfg`, which can include simulation +parameters, environment scene parameters, robot parameters, and task-specific parameters. + +Below is an example skeleton of a task config class: + +.. code-block:: python + + from omni.isaac.lab.envs import DirectRLEnvCfg + from omni.isaac.lab.scene import InteractiveSceneCfg + from omni.isaac.lab.sim import SimulationCfg + + @configclass + class MyEnvCfg(DirectRLEnvCfg): + # simulation + sim: SimulationCfg = SimulationCfg() + # robot + robot_cfg: ArticulationCfg = ArticulationCfg() + # scene + scene: InteractiveSceneCfg = InteractiveSceneCfg() + # env + decimation = 2 + episode_length_s = 5.0 + action_space = 1 + observation_space = 4 + state_space = 0 + # task-specific parameters + ... + +Simulation Config +----------------- + +Simulation related parameters are defined as part of the :class:`~omni.isaac.lab.sim.SimulationCfg` class, +which is a :class:`~omni.isaac.lab.utils.configclass` module that holds simulation parameters such as ``dt``, +``device``, and ``gravity``. Each task config must have a variable named ``sim`` defined that holds the type +:class:`~omni.isaac.lab.sim.SimulationCfg`. + +In Isaac Lab, the use of ``substeps`` has been replaced +by a combination of the simulation ``dt`` and the ``decimation`` parameters. For example, in IsaacGymEnvs, having +``dt=1/60`` and ``substeps=2`` is equivalent to taking 2 simulation steps with ``dt=1/120``, but running the task step +at ``1/60`` seconds. The ``decimation`` parameter is a task parameter that controls the number of simulation steps to +take for each task (or RL) step, replacing the ``controlFrequencyInv`` parameter in IsaacGymEnvs. +Thus, the same setup in Isaac Lab will become ``dt=1/120`` and ``decimation=2``. + +In Isaac Sim, physx simulation parameters such as ``num_position_iterations``, ``num_velocity_iterations``, +``contact_offset``, ``rest_offset``, ``bounce_threshold_velocity``, ``max_depenetration_velocity`` can all +be specified on a per-actor basis. These parameters have been moved from the physx simulation config +to each individual articulation and rigid body config. + +When running simulation on the GPU, buffers in PhysX require pre-allocation for computing and storing +information such as contacts, collisions and aggregate pairs. These buffers may need to be adjusted +depending on the complexity of the environment, the number of expected contacts and collisions, +and the number of actors in the environment. The :class:`~omni.isaac.lab.sim.PhysxCfg` class provides access for +setting the GPU buffer dimensions. + ++--------------------------------------------------------------+-------------------------------------------------------------------+ +| | | +|.. code-block:: yaml |.. code-block:: python | +| | | +| # IsaacGymEnvs | # IsaacLab | +| sim: | sim: SimulationCfg = SimulationCfg( | +| | device = "cuda:0" # can be "cpu", "cuda", "cuda:" | +| dt: 0.0166 # 1/60 s | dt=1 / 120, | +| substeps: 2 | # decimation will be set in the task config | +| up_axis: "z" | # up axis will always be Z in isaac sim | +| use_gpu_pipeline: ${eq:${...pipeline},"gpu"} | # use_gpu_pipeline is deduced from the device | +| gravity: [0.0, 0.0, -9.81] | gravity=(0.0, 0.0, -9.81), | +| physx: | physx: PhysxCfg = PhysxCfg( | +| num_threads: ${....num_threads} | # num_threads is no longer needed | +| solver_type: ${....solver_type} | solver_type=1, | +| use_gpu: ${contains:"cuda",${....sim_device}} | # use_gpu is deduced from the device | +| num_position_iterations: 4 | max_position_iteration_count=4, | +| num_velocity_iterations: 0 | max_velocity_iteration_count=0, | +| contact_offset: 0.02 | # moved to actor config | +| rest_offset: 0.001 | # moved to actor config | +| bounce_threshold_velocity: 0.2 | bounce_threshold_velocity=0.2, | +| max_depenetration_velocity: 100.0 | # moved to actor config | +| default_buffer_size_multiplier: 2.0 | # default_buffer_size_multiplier is no longer needed | +| max_gpu_contact_pairs: 1048576 # 1024*1024 | gpu_max_rigid_contact_count=2**23 | +| num_subscenes: ${....num_subscenes} | # num_subscenes is no longer needed | +| contact_collection: 0 | # contact_collection is no longer needed | +| | )) | ++--------------------------------------------------------------+-------------------------------------------------------------------+ + +Scene Config +------------ + +The :class:`~omni.isaac.lab.scene.InteractiveSceneCfg` class can be used to specify parameters related to the scene, +such as the number of environments and the spacing between environments. Each task config must have a variable named +``scene`` defined that holds the type :class:`~omni.isaac.lab.scene.InteractiveSceneCfg`. + ++--------------------------------------------------------------+-------------------------------------------------------------------+ +| | | +|.. code-block:: yaml |.. code-block:: python | +| | | +| # IsaacGymEnvs | # IsaacLab | +| env: | scene: InteractiveSceneCfg = InteractiveSceneCfg( | +| numEnvs: ${resolve_default:512,${...num_envs}} | num_envs=512, | +| envSpacing: 4.0 | env_spacing=4.0) | ++--------------------------------------------------------------+-------------------------------------------------------------------+ + +Task Config +----------- + +Each environment should specify its own config class that holds task specific parameters, such as the dimensions of the +observation and action buffers. Reward term scaling parameters can also be specified in the config class. + +The following parameters must be set for each environment config: + +.. code-block:: python + + decimation = 2 + episode_length_s = 5.0 + action_space = 1 + observation_space = 4 + state_space = 0 + +Note that the maximum episode length parameter (now ``episode_length_s``) is in seconds instead of steps as it was +in IsaacGymEnvs. To convert between step count to seconds, use the equation: +``episode_length_s = dt * decimation * num_steps`` + + +RL Config Setup +~~~~~~~~~~~~~~~ + +RL config files for the rl_games library can continue to be defined in ``.yaml`` files in Isaac Lab. +Most of the content of the config file can be copied directly from IsaacGymEnvs. +Note that in Isaac Lab, we do not use hydra to resolve relative paths in config files. +Please replace any relative paths such as ``${....device}`` with the actual values of the parameters. + +Additionally, the observation and action clip ranges have been moved to the RL config file. +For any ``clipObservations`` and ``clipActions`` parameters that were defined in the IsaacGymEnvs task config file, +they should be moved to the RL config file in Isaac Lab. + ++--------------------------+----------------------------+ +| | | +| IsaacGymEnvs Task Config | Isaac Lab RL Config | ++--------------------------+----------------------------+ +|.. code-block:: yaml |.. code-block:: yaml | +| | | +| # IsaacGymEnvs | # IsaacLab | +| env: | params: | +| clipObservations: 5.0 | env: | +| clipActions: 1.0 | clip_observations: 5.0 | +| | clip_actions: 1.0 | ++--------------------------+----------------------------+ + +Environment Creation +~~~~~~~~~~~~~~~~~~~~ + +In IsaacGymEnvs, environment creation generally included four components: creating the sim object with ``create_sim()``, +creating the ground plane, importing the assets from MJCF or URDF files, and finally creating the environments +by looping through each environment and adding actors into the environments. + +Isaac Lab no longer requires calling the ``create_sim()`` method to retrieve the sim object. Instead, the simulation +context is retrieved automatically by the framework. It is also no longer required to use the ``sim`` as an +argument for the simulation APIs. + +In replacement of ``create_sim()``, tasks can implement the ``_setup_scene()`` method in Isaac Lab. +This method can be used for adding actors into the scene, adding ground plane, cloning the actors, and +adding any other optional objects into the scene, such as lights. + ++------------------------------------------------------------------------------+------------------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------------------+------------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def create_sim(self): | def _setup_scene(self): | +| # set the up axis to be z-up | self.cartpole = Articulation(self.cfg.robot_cfg) | +| self.up_axis = self.cfg["sim"]["up_axis"] | # add ground plane | +| | spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg() | +| self.sim = super().create_sim(self.device_id, self.graphics_device_id, | # clone, filter, and replicate | +| self.physics_engine, self.sim_params) | self.scene.clone_environments(copy_from_source=False) | +| self._create_ground_plane() | self.scene.filter_collisions(global_prim_paths=[]) | +| self._create_envs(self.num_envs, self.cfg["env"]['envSpacing'], | # add articulation to scene | +| int(np.sqrt(self.num_envs))) | self.scene.articulations["cartpole"] = self.cartpole | +| | # add lights | +| | light_cfg = sim_utils.DomeLightCfg(intensity=2000.0) | +| | light_cfg.func("/World/Light", light_cfg) | ++------------------------------------------------------------------------------+------------------------------------------------------------------------+ + + +Ground Plane +------------ + +In Isaac Lab, most of the environment creation process has been simplified into configs with the :class:`~omni.isaac.lab.utils.configclass` module. + +The ground plane can be defined using the :class:`~terrains.TerrainImporterCfg` class. + +.. code-block:: python + + from omni.isaac.lab.terrains import TerrainImporterCfg + + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + ) + +The terrain can then be added to the scene in ``_setup_scene(self)`` by referencing the ``TerrainImporterCfg`` object: + +.. code-block::python + + def _setup_scene(self): + ... + self.cfg.terrain.num_envs = self.scene.cfg.num_envs + self.cfg.terrain.env_spacing = self.scene.cfg.env_spacing + self._terrain = self.cfg.terrain.class_type(self.cfg.terrain) + + +Actors +------ + +Isaac Lab and Isaac Sim both use the `USD (Universal Scene Description) `_ +library for describing the scene. Assets defined in MJCF and URDF formats can be imported to USD using importer +tools described in the `Importing a New Asset <../how-to/import_new_asset.html>`_ tutorial. + +Each Articulation and Rigid Body actor can also have its own config class. The +:class:`~omni.isaac.lab.assets.ArticulationCfg` class can be used to define parameters for articulation actors, +including file path, simulation parameters, actuator properties, and initial states. + +.. code-block::python + + from omni.isaac.lab.actuators import ImplicitActuatorCfg + from omni.isaac.lab.assets import ArticulationCfg + + CARTPOLE_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/Classic/Cartpole/cartpole.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + rigid_body_enabled=True, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=100.0, + enable_gyroscopic_forces=True, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + sleep_threshold=0.005, + stabilization_threshold=0.001, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 2.0), joint_pos={"slider_to_cart": 0.0, "cart_to_pole": 0.0} + ), + actuators={ + "cart_actuator": ImplicitActuatorCfg( + joint_names_expr=["slider_to_cart"], + effort_limit=400.0, + velocity_limit=100.0, + stiffness=0.0, + damping=10.0, + ), + "pole_actuator": ImplicitActuatorCfg( + joint_names_expr=["cart_to_pole"], effort_limit=400.0, velocity_limit=100.0, stiffness=0.0, damping=0.0 + ), + }, + ) + +Within the :class:`~assets.ArticulationCfg`, the ``spawn`` attribute can be used to add the robot to the scene by +specifying the path to the robot file. In addition, :class:`~omni.isaac.lab.sim.schemas.RigidBodyPropertiesCfg` can +be used to specify simulation properties for the rigid bodies in the articulation. +Similarly, the :class:`~omni.isaac.lab.sim.schemas.ArticulationRootPropertiesCfg` class can be used to specify +simulation properties for the articulation. Joint properties are now specified as part of the ``actuators`` +dictionary using :class:`~actuators.ImplicitActuatorCfg`. Joints with the same properties can be grouped into +regex expressions or provided as a list of names or expressions. + +Actors are added to the scene by simply calling ``self.cartpole = Articulation(self.cfg.robot_cfg)``, +where ``self.cfg.robot_cfg`` is an :class:`~assets.ArticulationCfg` object. Once initialized, they should also +be added to the :class:`~scene.InteractiveScene` by calling ``self.scene.articulations["cartpole"] = self.cartpole`` +so that the :class:`~scene.InteractiveScene` can traverse through actors in the scene for writing values to the +simulation and resetting. + +Simulation Parameters for Actors +"""""""""""""""""""""""""""""""" + +Some simulation parameters related to Rigid Bodies and Articulations may have different +default values between Isaac Gym Preview Release and Isaac Sim. +It may be helpful to double check the USD assets to ensure that the default values are +applicable for the asset. + +For instance, the following parameters in the ``RigidBodyAPI`` could be different +between Isaac Gym Preview Release and Isaac Sim: + +.. list-table:: + :widths: 50 50 50 + :header-rows: 1 + + * - RigidBodyAPI Parameter + - Default Value in Isaac Sim + - Default Value in Isaac Gym Preview Release + * - Linear Damping + - 0.00 + - 0.00 + * - Angular Damping + - 0.05 + - 0.0 + * - Max Linear Velocity + - inf + - 1000 + * - Max Angular Velocity + - 5729.58008 (degree/s) + - 64.0 (rad/s) + * - Max Contact Impulse + - inf + - 1e32 + +Articulation parameters for the ``JointAPI`` and ``DriveAPI`` could be altered as well. Note +that the Isaac Sim UI assumes the unit of angle to be degrees. It is particularly +worth noting that the ``Damping`` and ``Stiffness`` parameters in the ``DriveAPI`` have the unit +of ``1/deg`` in the Isaac Sim UI but ``1/rad`` in Isaac Gym Preview Release. + +.. list-table:: + :widths: 50 50 50 + :header-rows: 1 + + * - Joint Parameter + - Default Value in Isaac Sim + - Default Value in Isaac Gym Preview Releases + * - Maximum Joint Velocity + - 1000000.0 (deg) + - 100.0 (rad) + + +Cloner +------ + +Isaac Sim introduced a concept of ``Cloner``, which is a class designed for replication during the scene creation process. +In IsaacGymEnvs, scenes had to be created by looping through the number of environments. +Within each iteration, actors were added to each environment and their handles had to be cached. +Isaac Lab eliminates the need for looping through the environments by using the ``Cloner`` APIs. +The scene creation process is as follow: + +#. Construct a single environment (what the scene would look like if number of environments = 1) +#. Call ``clone_environments()`` to replicate the single environment +#. Call ``filter_collisions()`` to filter out collision between environments (if required) + + +.. code-block:: python + + # construct a single environment with the Cartpole robot + self.cartpole = Articulation(self.cfg.robot_cfg) + # clone the environment + self.scene.clone_environments(copy_from_source=False) + # filter collisions + self.scene.filter_collisions(global_prim_paths=[self.cfg.terrain.prim_path]) + + +Accessing States from Simulation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +APIs for accessing physics states in Isaac Lab require the creation of an :class:`~assets.Articulation` or :class:`~assets.RigidObject` +object. Multiple objects can be initialized for different articulations or rigid bodies in the scene by defining +corresponding :class:`~assets.ArticulationCfg` or :class:`~assets.RigidObjectCfg` config as outlined in the section above. +This approach eliminates the need of retrieving body handles to slice states for specific bodies in the scene. + + +.. code-block:: python + + self._robot = Articulation(self.cfg.robot) + self._cabinet = Articulation(self.cfg.cabinet) + self._object = RigidObject(self.cfg.object_cfg) + + +We have also removed ``acquire`` and ``refresh`` APIs in Isaac Lab. Physics states can be directly applied or retrieved +using APIs defined for the articulations and rigid objects. + +APIs provided in Isaac Lab no longer require explicit wrapping and un-wrapping of underlying buffers. +APIs can now work with tensors directly for reading and writing data. + ++------------------------------------------------------------------+-----------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------+-----------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| dof_state_tensor = self.gym.acquire_dof_state_tensor(self.sim) | self.joint_pos = self._robot.data.joint_pos | +| self.dof_state = gymtorch.wrap_tensor(dof_state_tensor) | self.joint_vel = self._robot.data.joint_vel | +| self.gym.refresh_dof_state_tensor(self.sim) | | ++------------------------------------------------------------------+-----------------------------------------------------------------+ + +Note some naming differences between APIs in Isaac Gym Preview Release and Isaac Lab. Most ``dof`` related APIs have been +named to ``joint`` in Isaac Lab. +APIs in Isaac Lab also no longer follow the explicit ``_tensors`` or ``_tensor_indexed`` suffixes in naming. +Indexed versions of APIs now happen implicitly through the optional ``indices`` parameter. + +Most APIs in Isaac Lab also provide +the option to specify an ``indices`` parameter, which can be used when reading or writing data for a subset +of environments. Note that when setting states with the ``indices`` parameter, the shape of the states buffer +should match with the dimension of the ``indices`` list. + ++---------------------------------------------------------------------------+---------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++---------------------------------------------------------------------------+---------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| env_ids_int32 = env_ids.to(dtype=torch.int32) | self._robot.write_joint_state_to_sim(joint_pos, joint_vel, | +| self.gym.set_dof_state_tensor_indexed(self.sim, | joint_ids, env_ids) | +| gymtorch.unwrap_tensor(self.dof_state), | | +| gymtorch.unwrap_tensor(env_ids_int32), len(env_ids_int32)) | | ++---------------------------------------------------------------------------+---------------------------------------------------------------+ + +Quaternion Convention +--------------------- + +Isaac Lab and Isaac Sim both adopt ``wxyz`` as the quaternion convention. However, the quaternion +convention used in Isaac Gym Preview Release was ``xyzw``. +Remember to switch all quaternions to use the ``xyzw`` convention when working indexing rotation data. +Similarly, please ensure all quaternions are in ``wxyz`` before passing them to Isaac Lab APIs. + + +Articulation Joint Order +------------------------ + +Physics simulation in Isaac Sim and Isaac Lab assumes a breadth-first +ordering for the joints in a given kinematic tree. +However, Isaac Gym Preview Release assumed a depth-first ordering for joints in the kinematic tree. +This means that indexing joints based on their ordering may be different in IsaacGymEnvs and Isaac Lab. + +In Isaac Lab, the list of joint names can be retrieved with ``Articulation.data.joint_names``, which will +also correspond to the ordering of the joints in the Articulation. + + +Creating a New Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each environment in Isaac Lab should be in its own directory following this structure: + +.. code-block:: none + + my_environment/ + - agents/ + - __init__.py + - rl_games_ppo_cfg.py + - __init__.py + my_env.py + +* ``my_environment`` is the root directory of the task. +* ``my_environment/agents`` is the directory containing all RL config files for the task. Isaac Lab supports multiple RL libraries that can each have its own individual config file. +* ``my_environment/__init__.py`` is the main file that registers the environment with the Gymnasium interface. This allows the training and inferencing scripts to find the task by its name. The content of this file should be as follow: + +.. code-block:: python + + import gymnasium as gym + + from . import agents + from .cartpole_env import CartpoleEnv, CartpoleEnvCfg + + ## + # Register Gym environments. + ## + + gym.register( + id="Isaac-Cartpole-Direct-v0", + entry_point="omni.isaac.lab_tasks.direct_workflow.cartpole:CartpoleEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": CartpoleEnvCfg, + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml" + }, + ) + +* ``my_environment/my_env.py`` is the main python script that implements the task logic and task config class for the environment. + + +Task Logic +~~~~~~~~~~ + +In Isaac Lab, the ``post_physics_step`` function has been moved to the framework in the base class. +Tasks are not required to implement this method, but can choose to override it if a different workflow is desired. + +By default, Isaac Lab follows the following flow in logic: + ++----------------------------------+----------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++----------------------------------+----------------------------------+ +|.. code-block:: none |.. code-block:: none | +| | | +| pre_physics_step | pre_physics_step | +| |-- apply_action | |-- _pre_physics_step(action)| +| | |-- _apply_action() | +| | | +| post_physics_step | post_physics_step | +| |-- reset_idx() | |-- _get_dones() | +| |-- compute_observation() | |-- _get_rewards() | +| |-- compute_reward() | |-- _reset_idx() | +| | |-- _get_observations() | ++----------------------------------+----------------------------------+ + +In Isaac Lab, we also separate the ``pre_physics_step`` API for processing actions from the policy with +the ``apply_action`` API, which sets the actions into the simulation. This provides more flexibility in controlling +when actions should be written to simulation when ``decimation`` is used. +``pre_physics_step`` will be called once per step before stepping simulation. +``apply_actions`` will be called ``decimation`` number of times for each RL step, once before each simulation step call. + +With this approach, resets are performed based on actions from the current step instead of the previous step. +Observations will also be computed with the correct states after resets. + +We have also performed some renamings of APIs: + +* ``create_sim(self)`` --> ``_setup_scene(self)`` +* ``pre_physics_step(self, actions)`` --> ``_pre_physics_step(self, actions)`` and ``_apply_action(self)`` +* ``reset_idx(self, env_ids)`` --> ``_reset_idx(self, env_ids)`` +* ``compute_observations(self)`` --> ``_get_observations(self)`` - ``_get_observations()`` should now return a dictionary ``{"policy": obs}`` +* ``compute_reward(self)`` --> ``_get_rewards(self)`` - ``_get_rewards()`` should now return the reward buffer +* ``post_physics_step(self)`` --> moved to the base class +* In addition, Isaac Lab requires the implementation of ``_is_done(self)``, which should return two buffers: the ``reset`` buffer and the ``time_out`` buffer. + + +Putting It All Together +~~~~~~~~~~~~~~~~~~~~~~~ + +The Cartpole environment is shown here in completion to fully show the comparison between the IsaacGymEnvs implementation and the Isaac Lab implementation. + +Task Config +----------- + ++--------------------------------------------------------+---------------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++--------------------------------------------------------+---------------------------------------------------------------------+ +|.. code-block:: yaml |.. code-block:: python | +| | | +| # used to create the object | @configclass | +| name: Cartpole | class CartpoleEnvCfg(DirectRLEnvCfg): | +| | | +| physics_engine: ${..physics_engine} | # simulation | +| | sim: SimulationCfg = SimulationCfg(dt=1 / 120) | +| # if given, will override the device setting in gym. | # robot | +| env: | robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace( | +| numEnvs: ${resolve_default:512,${...num_envs}} | prim_path="/World/envs/env_.*/Robot") | +| envSpacing: 4.0 | cart_dof_name = "slider_to_cart" | +| resetDist: 3.0 | pole_dof_name = "cart_to_pole" | +| maxEffort: 400.0 | # scene | +| | scene: InteractiveSceneCfg = InteractiveSceneCfg( | +| clipObservations: 5.0 | num_envs=4096, env_spacing=4.0, replicate_physics=True) | +| clipActions: 1.0 | # env | +| | decimation = 2 | +| asset: | episode_length_s = 5.0 | +| assetRoot: "../../assets" | action_scale = 100.0 # [N] | +| assetFileName: "urdf/cartpole.urdf" | action_space = 1 | +| | observation_space = 4 | +| enableCameraSensors: False | state_space = 0 | +| | # reset | +| sim: | max_cart_pos = 3.0 | +| dt: 0.0166 # 1/60 s | initial_pole_angle_range = [-0.25, 0.25] | +| substeps: 2 | # reward scales | +| up_axis: "z" | rew_scale_alive = 1.0 | +| use_gpu_pipeline: ${eq:${...pipeline},"gpu"} | rew_scale_terminated = -2.0 | +| gravity: [0.0, 0.0, -9.81] | rew_scale_pole_pos = -1.0 | +| physx: | rew_scale_cart_vel = -0.01 | +| num_threads: ${....num_threads} | rew_scale_pole_vel = -0.005 | +| solver_type: ${....solver_type} | | +| use_gpu: ${contains:"cuda",${....sim_device}} | | +| num_position_iterations: 4 | | +| num_velocity_iterations: 0 | | +| contact_offset: 0.02 | | +| rest_offset: 0.001 | | +| bounce_threshold_velocity: 0.2 | | +| max_depenetration_velocity: 100.0 | | +| default_buffer_size_multiplier: 2.0 | | +| max_gpu_contact_pairs: 1048576 # 1024*1024 | | +| num_subscenes: ${....num_subscenes} | | +| contact_collection: 0 | | ++--------------------------------------------------------+---------------------------------------------------------------------+ + + + +Task Setup +---------- + +Isaac Lab no longer requires pre-initialization of buffers through the ``acquire_*`` APIs that were used in IsaacGymEnvs. +It is also no longer necessary to ``wrap`` and ``unwrap`` tensors. + ++-------------------------------------------------------------------------+-------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++-------------------------------------------------------------------------+-------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| class Cartpole(VecTask): | class CartpoleEnv(DirectRLEnv): | +| | cfg: CartpoleEnvCfg | +| def __init__(self, cfg, rl_device, sim_device, graphics_device_id, | def __init__(self, cfg: CartpoleEnvCfg, | +| headless, virtual_screen_capture, force_render): | render_mode: str | None = None, **kwargs): | +| self.cfg = cfg | | +| | super().__init__(cfg, render_mode, **kwargs) | +| self.reset_dist = self.cfg["env"]["resetDist"] | | +| | self._cart_dof_idx, _ = self.cartpole.find_joints( | +| self.max_push_effort = self.cfg["env"]["maxEffort"] | self.cfg.cart_dof_name) | +| self.max_episode_length = 500 | self._pole_dof_idx, _ = self.cartpole.find_joints( | +| | self.cfg.pole_dof_name) | +| self.cfg["env"]["numObservations"] = 4 | self.action_scale = self.cfg.action_scale | +| self.cfg["env"]["numActions"] = 1 | | +| | self.joint_pos = self.cartpole.data.joint_pos | +| super().__init__(config=self.cfg, | self.joint_vel = self.cartpole.data.joint_vel | +| rl_device=rl_device, sim_device=sim_device, | | +| graphics_device_id=graphics_device_id, headless=headless, | | +| virtual_screen_capture=virtual_screen_capture, | | +| force_render=force_render) | | +| | | +| dof_state_tensor = self.gym.acquire_dof_state_tensor(self.sim) | | +| self.dof_state = gymtorch.wrap_tensor(dof_state_tensor) | | +| self.dof_pos = self.dof_state.view( | | +| self.num_envs, self.num_dof, 2)[..., 0] | | +| self.dof_vel = self.dof_state.view( | | +| self.num_envs, self.num_dof, 2)[..., 1] | | ++-------------------------------------------------------------------------+-------------------------------------------------------------+ + + + +Scene Setup +----------- + +Scene setup is now done through the ``Cloner`` API and by specifying actor attributes in config objects. +This eliminates the need to loop through the number of environments to set up the environments and avoids +the need to set simulation parameters for actors in the task implementation. + ++------------------------------------------------------------------------+---------------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------------+---------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def create_sim(self): | def _setup_scene(self): | +| # set the up axis to be z-up given that assets are y-up by default | self.cartpole = Articulation(self.cfg.robot_cfg) | +| self.up_axis = self.cfg["sim"]["up_axis"] | # add ground plane | +| | spawn_ground_plane(prim_path="/World/ground", | +| self.sim = super().create_sim(self.device_id, | cfg=GroundPlaneCfg()) | +| self.graphics_device_id, self.physics_engine, | # clone, filter, and replicate | +| self.sim_params) | self.scene.clone_environments( | +| self._create_ground_plane() | copy_from_source=False) | +| self._create_envs(self.num_envs, | self.scene.filter_collisions( | +| self.cfg["env"]['envSpacing'], | global_prim_paths=[]) | +| int(np.sqrt(self.num_envs))) | # add articulation to scene | +| | self.scene.articulations["cartpole"] = self.cartpole | +| def _create_ground_plane(self): | # add lights | +| plane_params = gymapi.PlaneParams() | light_cfg = sim_utils.DomeLightCfg( | +| # set the normal force to be z dimension | intensity=2000.0, color=(0.75, 0.75, 0.75)) | +| plane_params.normal = (gymapi.Vec3(0.0, 0.0, 1.0) | light_cfg.func("/World/Light", light_cfg) | +| if self.up_axis == 'z' | | +| else gymapi.Vec3(0.0, 1.0, 0.0)) | CARTPOLE_CFG = ArticulationCfg( | +| self.gym.add_ground(self.sim, plane_params) | spawn=sim_utils.UsdFileCfg( | +| | usd_path=f"{ISAACLAB_NUCLEUS_DIR}/.../cartpole.usd", | +| def _create_envs(self, num_envs, spacing, num_per_row): | rigid_props=sim_utils.RigidBodyPropertiesCfg( | +| # define plane on which environments are initialized | rigid_body_enabled=True, | +| lower = (gymapi.Vec3(0.5 * -spacing, -spacing, 0.0) | max_linear_velocity=1000.0, | +| if self.up_axis == 'z' | max_angular_velocity=1000.0, | +| else gymapi.Vec3(0.5 * -spacing, 0.0, -spacing)) | max_depenetration_velocity=100.0, | +| upper = gymapi.Vec3(0.5 * spacing, spacing, spacing) | enable_gyroscopic_forces=True, | +| | ), | +| asset_root = os.path.join(os.path.dirname( | articulation_props=sim_utils.ArticulationRootPropertiesCfg( | +| os.path.abspath(__file__)), "../../assets") | enabled_self_collisions=False, | +| asset_file = "urdf/cartpole.urdf" | solver_position_iteration_count=4, | +| | solver_velocity_iteration_count=0, | +| if "asset" in self.cfg["env"]: | sleep_threshold=0.005, | +| asset_root = os.path.join(os.path.dirname( | stabilization_threshold=0.001, | +| os.path.abspath(__file__)), | ), | +| self.cfg["env"]["asset"].get("assetRoot", asset_root)) | ), | +| asset_file = self.cfg["env"]["asset"].get( | init_state=ArticulationCfg.InitialStateCfg( | +| "assetFileName", asset_file) | pos=(0.0, 0.0, 2.0), | +| | joint_pos={"slider_to_cart": 0.0, "cart_to_pole": 0.0} | +| asset_path = os.path.join(asset_root, asset_file) | ), | +| asset_root = os.path.dirname(asset_path) | actuators={ | +| asset_file = os.path.basename(asset_path) | "cart_actuator": ImplicitActuatorCfg( | +| | joint_names_expr=["slider_to_cart"], | +| asset_options = gymapi.AssetOptions() | effort_limit=400.0, | +| asset_options.fix_base_link = True | velocity_limit=100.0, | +| cartpole_asset = self.gym.load_asset(self.sim, | stiffness=0.0, | +| asset_root, asset_file, asset_options) | damping=10.0, | +| self.num_dof = self.gym.get_asset_dof_count( | ), | +| cartpole_asset) | "pole_actuator": ImplicitActuatorCfg( | +| | joint_names_expr=["cart_to_pole"], effort_limit=400.0, | +| pose = gymapi.Transform() | velocity_limit=100.0, stiffness=0.0, damping=0.0 | +| if self.up_axis == 'z': | ), | +| pose.p.z = 2.0 | }, | +| pose.r = gymapi.Quat(0.0, 0.0, 0.0, 1.0) | ) | +| else: | | +| pose.p.y = 2.0 | | +| pose.r = gymapi.Quat( | | +| -np.sqrt(2)/2, 0.0, 0.0, np.sqrt(2)/2) | | +| | | +| self.cartpole_handles = [] | | +| self.envs = [] | | +| for i in range(self.num_envs): | | +| # create env instance | | +| env_ptr = self.gym.create_env( | | +| self.sim, lower, upper, num_per_row | | +| ) | | +| cartpole_handle = self.gym.create_actor( | | +| env_ptr, cartpole_asset, pose, | | +| "cartpole", i, 1, 0) | | +| | | +| dof_props = self.gym.get_actor_dof_properties( | | +| env_ptr, cartpole_handle) | | +| dof_props['driveMode'][0] = gymapi.DOF_MODE_EFFORT | | +| dof_props['driveMode'][1] = gymapi.DOF_MODE_NONE | | +| dof_props['stiffness'][:] = 0.0 | | +| dof_props['damping'][:] = 0.0 | | +| self.gym.set_actor_dof_properties(env_ptr, c | | +| artpole_handle, dof_props) | | +| | | +| self.envs.append(env_ptr) | | +| self.cartpole_handles.append(cartpole_handle) | | ++------------------------------------------------------------------------+---------------------------------------------------------------------+ + + +Pre and Post Physics Step +------------------------- + +In IsaacGymEnvs, due to limitations of the GPU APIs, observations had stale data when environments had to perform resets. +This restriction has been eliminated in Isaac Lab, and thus, tasks follow the correct workflow of applying actions, stepping simulation, +collecting states, computing dones, calculating rewards, performing resets, and finally computing observations. +This workflow is done automatically by the framework such that a ``post_physics_step`` API is not required in the task. +However, individual tasks can override the ``step()`` API to control the workflow. + ++------------------------------------------------------------------+-------------------------------------------------------------+ +| IsaacGymEnvs | IsaacLab | ++------------------------------------------------------------------+-------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def pre_physics_step(self, actions): | def _pre_physics_step(self, actions: torch.Tensor) -> None: | +| actions_tensor = torch.zeros( | self.actions = self.action_scale * actions | +| self.num_envs * self.num_dof, | | +| device=self.device, dtype=torch.float) | def _apply_action(self) -> None: | +| actions_tensor[::self.num_dof] = actions.to( | self.cartpole.set_joint_effort_target( | +| self.device).squeeze() * self.max_push_effort | self.actions, joint_ids=self._cart_dof_idx) | +| forces = gymtorch.unwrap_tensor(actions_tensor) | | +| self.gym.set_dof_actuation_force_tensor( | | +| self.sim, forces) | | +| | | +| def post_physics_step(self): | | +| self.progress_buf += 1 | | +| | | +| env_ids = self.reset_buf.nonzero( | | +| as_tuple=False).squeeze(-1) | | +| if len(env_ids) > 0: | | +| self.reset_idx(env_ids) | | +| | | +| self.compute_observations() | | +| self.compute_reward() | | ++------------------------------------------------------------------+-------------------------------------------------------------+ + + +Dones and Resets +---------------- + +In Isaac Lab, ``dones`` are computed in the ``_get_dones()`` method and should return two variables: ``resets`` and ``time_out``. +Tracking of the ``progress_buf`` has been moved to the base class and is now automatically incremented and reset by the framework. +The ``progress_buf`` variable has also been renamed to ``episode_length_buf``. + ++-----------------------------------------------------------------------+---------------------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++-----------------------------------------------------------------------+---------------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def reset_idx(self, env_ids): | def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: | +| positions = 0.2 * (torch.rand((len(env_ids), self.num_dof), | self.joint_pos = self.cartpole.data.joint_pos | +| device=self.device) - 0.5) | self.joint_vel = self.cartpole.data.joint_vel | +| velocities = 0.5 * (torch.rand((len(env_ids), self.num_dof), | | +| device=self.device) - 0.5) | time_out = self.episode_length_buf >= self.max_episode_length - 1 | +| | out_of_bounds = torch.any(torch.abs( | +| self.dof_pos[env_ids, :] = positions[:] | self.joint_pos[:, self._cart_dof_idx]) > self.cfg.max_cart_pos, | +| self.dof_vel[env_ids, :] = velocities[:] | dim=1) | +| | out_of_bounds = out_of_bounds | torch.any( | +| env_ids_int32 = env_ids.to(dtype=torch.int32) | torch.abs(self.joint_pos[:, self._pole_dof_idx]) > math.pi / 2, | +| self.gym.set_dof_state_tensor_indexed(self.sim, | dim=1) | +| gymtorch.unwrap_tensor(self.dof_state), | return out_of_bounds, time_out | +| gymtorch.unwrap_tensor(env_ids_int32), len(env_ids_int32)) | | +| self.reset_buf[env_ids] = 0 | def _reset_idx(self, env_ids: Sequence[int] | None): | +| self.progress_buf[env_ids] = 0 | if env_ids is None: | +| | env_ids = self.cartpole._ALL_INDICES | +| | super()._reset_idx(env_ids) | +| | | +| | joint_pos = self.cartpole.data.default_joint_pos[env_ids] | +| | joint_pos[:, self._pole_dof_idx] += sample_uniform( | +| | self.cfg.initial_pole_angle_range[0] * math.pi, | +| | self.cfg.initial_pole_angle_range[1] * math.pi, | +| | joint_pos[:, self._pole_dof_idx].shape, | +| | joint_pos.device, | +| | ) | +| | joint_vel = self.cartpole.data.default_joint_vel[env_ids] | +| | | +| | default_root_state = self.cartpole.data.default_root_state[env_ids] | +| | default_root_state[:, :3] += self.scene.env_origins[env_ids] | +| | | +| | self.joint_pos[env_ids] = joint_pos | +| | | +| | self.cartpole.write_root_pose_to_sim( | +| | default_root_state[:, :7], env_ids) | +| | self.cartpole.write_root_velocity_to_sim( | +| | default_root_state[:, 7:], env_ids) | +| | self.cartpole.write_joint_state_to_sim( | +| | joint_pos, joint_vel, None, env_ids) | ++-----------------------------------------------------------------------+---------------------------------------------------------------------------+ + + +Observations +------------ + +In Isaac Lab, the ``_get_observations()`` API should now return a dictionary containing the ``policy`` key with the observation +buffer as the value. +For asymmetric policies, the dictionary should also include a ``critic`` key that holds the state buffer. + ++--------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++--------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def compute_observations(self, env_ids=None): | def _get_observations(self) -> dict: | +| if env_ids is None: | obs = torch.cat( | +| env_ids = np.arange(self.num_envs) | ( | +| | self.joint_pos[:, self._pole_dof_idx[0]], | +| self.gym.refresh_dof_state_tensor(self.sim) | self.joint_vel[:, self._pole_dof_idx[0]], | +| | self.joint_pos[:, self._cart_dof_idx[0]], | +| self.obs_buf[env_ids, 0] = self.dof_pos[env_ids, 0] | self.joint_vel[:, self._cart_dof_idx[0]], | +| self.obs_buf[env_ids, 1] = self.dof_vel[env_ids, 0] | ), | +| self.obs_buf[env_ids, 2] = self.dof_pos[env_ids, 1] | dim=-1, | +| self.obs_buf[env_ids, 3] = self.dof_vel[env_ids, 1] | ) | +| | observations = {"policy": obs} | +| return self.obs_buf | return observations | ++--------------------------------------------------------------------------+---------------------------------------------------------------------------------------+ + + +Rewards +------- + +In Isaac Lab, the reward method ``_get_rewards`` should return the reward buffer as a return value. +Similar to IsaacGymEnvs, computations in the reward function can also be performed using pytorch jit +by adding the ``@torch.jit.script`` annotation. + ++--------------------------------------------------------------------------+----------------------------------------------------------------------------------------+ +| IsaacGymEnvs | Isaac Lab | ++--------------------------------------------------------------------------+----------------------------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def compute_reward(self): | def _get_rewards(self) -> torch.Tensor: | +| # retrieve environment observations from buffer | total_reward = compute_rewards( | +| pole_angle = self.obs_buf[:, 2] | self.cfg.rew_scale_alive, | +| pole_vel = self.obs_buf[:, 3] | self.cfg.rew_scale_terminated, | +| cart_vel = self.obs_buf[:, 1] | self.cfg.rew_scale_pole_pos, | +| cart_pos = self.obs_buf[:, 0] | self.cfg.rew_scale_cart_vel, | +| | self.cfg.rew_scale_pole_vel, | +| self.rew_buf[:], self.reset_buf[:] = compute_cartpole_reward( | self.joint_pos[:, self._pole_dof_idx[0]], | +| pole_angle, pole_vel, cart_vel, cart_pos, | self.joint_vel[:, self._pole_dof_idx[0]], | +| self.reset_dist, self.reset_buf, | self.joint_pos[:, self._cart_dof_idx[0]], | +| self.progress_buf, self.max_episode_length | self.joint_vel[:, self._cart_dof_idx[0]], | +| ) | self.reset_terminated, | +| | ) | +| @torch.jit.script | return total_reward | +| def compute_cartpole_reward(pole_angle, pole_vel, | | +| cart_vel, cart_pos, | @torch.jit.script | +| reset_dist, reset_buf, | def compute_rewards( | +| progress_buf, max_episode_length): | rew_scale_alive: float, | +| | rew_scale_terminated: float, | +| reward = (1.0 - pole_angle * pole_angle - | rew_scale_pole_pos: float, | +| 0.01 * torch.abs(cart_vel) - | rew_scale_cart_vel: float, | +| 0.005 * torch.abs(pole_vel)) | rew_scale_pole_vel: float, | +| | pole_pos: torch.Tensor, | +| # adjust reward for reset agents | pole_vel: torch.Tensor, | +| reward = torch.where(torch.abs(cart_pos) > reset_dist, | cart_pos: torch.Tensor, | +| torch.ones_like(reward) * -2.0, reward) | cart_vel: torch.Tensor, | +| reward = torch.where(torch.abs(pole_angle) > np.pi / 2, | reset_terminated: torch.Tensor, | +| torch.ones_like(reward) * -2.0, reward) | ): | +| | rew_alive = rew_scale_alive * (1.0 - reset_terminated.float()) | +| reset = torch.where(torch.abs(cart_pos) > reset_dist, | rew_termination = rew_scale_terminated * reset_terminated.float() | +| torch.ones_like(reset_buf), reset_buf) | rew_pole_pos = rew_scale_pole_pos * torch.sum( | +| reset = torch.where(torch.abs(pole_angle) > np.pi / 2, | torch.square(pole_pos), dim=-1) | +| torch.ones_like(reset_buf), reset_buf) | rew_cart_vel = rew_scale_cart_vel * torch.sum( | +| reset = torch.where(progress_buf >= max_episode_length - 1, | torch.abs(cart_vel), dim=-1) | +| torch.ones_like(reset_buf), reset) | rew_pole_vel = rew_scale_pole_vel * torch.sum( | +| | torch.abs(pole_vel), dim=-1) | +| | total_reward = (rew_alive + rew_termination | +| | + rew_pole_pos + rew_cart_vel + rew_pole_vel) | +| | return total_reward | ++--------------------------------------------------------------------------+----------------------------------------------------------------------------------------+ + + + +Launching Training +~~~~~~~~~~~~~~~~~~ + +To launch a training in Isaac Lab, use the command: + +.. code-block:: bash + + python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-Direct-v0 --headless + +Launching Inferencing +~~~~~~~~~~~~~~~~~~~~~ + +To launch inferencing in Isaac Lab, use the command: + +.. code-block:: bash + + python source/standalone/workflows/rl_games/play.py --task=Isaac-Cartpole-Direct-v0 --num_envs=25 --checkpoint= + + +.. _IsaacGymEnvs: https://github.com/isaac-sim/IsaacGymEnvs +.. _Isaac Gym Preview Release: https://developer.nvidia.com/isaac-gym +.. _release notes: https://github.com/isaac-sim/IsaacLab/releases diff --git a/_sources/source/migration/migrating_from_omniisaacgymenvs.rst b/_sources/source/migration/migrating_from_omniisaacgymenvs.rst new file mode 100644 index 0000000000..7945ccb0b1 --- /dev/null +++ b/_sources/source/migration/migrating_from_omniisaacgymenvs.rst @@ -0,0 +1,1000 @@ +.. _migrating-from-omniisaacgymenvs: + +From OmniIsaacGymEnvs +===================== + +.. currentmodule:: omni.isaac.lab + + +`OmniIsaacGymEnvs`_ was a reinforcement learning framework using the Isaac Sim platform. +Features from OmniIsaacGymEnvs have been integrated into the Isaac Lab framework. +We have updated OmniIsaacGymEnvs to Isaac Sim version 4.0.0 to support the migration process +to Isaac Lab. Moving forward, OmniIsaacGymEnvs will be deprecated and future development +will continue in Isaac Lab. + +.. note:: + + The following changes are with respect to Isaac Lab 1.0 release. Please refer to the `release notes`_ for any changes + in the future releases. + +Task Config Setup +~~~~~~~~~~~~~~~~~ + +In OmniIsaacGymEnvs, task config files were defined in ``.yaml`` format. With Isaac Lab, configs are now specified +using a specialized Python class :class:`~omni.isaac.lab.utils.configclass`. The +:class:`~omni.isaac.lab.utils.configclass` module provides a wrapper on top of Python's ``dataclasses`` module. +Each environment should specify its own config class annotated by ``@configclass`` that inherits from the +:class:`~envs.DirectRLEnvCfg` class, which can include simulation parameters, environment scene parameters, +robot parameters, and task-specific parameters. + +Below is an example skeleton of a task config class: + +.. code-block:: python + + from omni.isaac.lab.envs import DirectRLEnvCfg + from omni.isaac.lab.scene import InteractiveSceneCfg + from omni.isaac.lab.sim import SimulationCfg + + @configclass + class MyEnvCfg(DirectRLEnvCfg): + # simulation + sim: SimulationCfg = SimulationCfg() + # robot + robot_cfg: ArticulationCfg = ArticulationCfg() + # scene + scene: InteractiveSceneCfg = InteractiveSceneCfg() + # env + decimation = 2 + episode_length_s = 5.0 + action_space = 1 + observation_space = 4 + state_space = 0 + # task-specific parameters + ... + +Simulation Config +----------------- + +Simulation related parameters are defined as part of the :class:`~omni.isaac.lab.sim.SimulationCfg` class, +which is a :class:`~omni.isaac.lab.utils.configclass` module that holds simulation parameters such as ``dt``, +``device``, and ``gravity``. Each task config must have a variable named ``sim`` defined that holds the type +:class:`~omni.isaac.lab.sim.SimulationCfg`. + +Simulation parameters for articulations and rigid bodies such as ``num_position_iterations``, ``num_velocity_iterations``, +``contact_offset``, ``rest_offset``, ``bounce_threshold_velocity``, ``max_depenetration_velocity`` can all +be specified on a per-actor basis in the config class for each individual articulation and rigid body. + +When running simulation on the GPU, buffers in PhysX require pre-allocation for computing and storing +information such as contacts, collisions and aggregate pairs. These buffers may need to be adjusted +depending on the complexity of the environment, the number of expected contacts and collisions, +and the number of actors in the environment. The :class:`~omni.isaac.lab.sim.PhysxCfg` class provides access +for setting the GPU buffer dimensions. + ++--------------------------------------------------------------+-------------------------------------------------------------------+ +| | | +|.. code-block:: yaml |.. code-block:: python | +| | | +| # OmniIsaacGymEnvs | # IsaacLab | +| sim: | sim: SimulationCfg = SimulationCfg( | +| | device = "cuda:0" # can be "cpu", "cuda", "cuda:" | +| dt: 0.0083 # 1/120 s | dt=1 / 120, | +| use_gpu_pipeline: ${eq:${...pipeline},"gpu"} | # use_gpu_pipeline is deduced from the device | +| use_fabric: True | use_fabric=True, | +| enable_scene_query_support: False | enable_scene_query_support=False, | +| disable_contact_processing: False | disable_contact_processing=False, | +| gravity: [0.0, 0.0, -9.81] | gravity=(0.0, 0.0, -9.81), | +| | | +| default_physics_material: | physics_material=RigidBodyMaterialCfg( | +| static_friction: 1.0 | static_friction=1.0, | +| dynamic_friction: 1.0 | dynamic_friction=1.0, | +| restitution: 0.0 | restitution=0.0 | +| | ) | +| physx: | physx: PhysxCfg = PhysxCfg( | +| worker_thread_count: ${....num_threads} | # worker_thread_count is no longer needed | +| solver_type: ${....solver_type} | solver_type=1, | +| use_gpu: ${contains:"cuda",${....sim_device}} | # use_gpu is deduced from the device | +| solver_position_iteration_count: 4 | max_position_iteration_count=4, | +| solver_velocity_iteration_count: 0 | max_velocity_iteration_count=0, | +| contact_offset: 0.02 | # moved to actor config | +| rest_offset: 0.001 | # moved to actor config | +| bounce_threshold_velocity: 0.2 | bounce_threshold_velocity=0.2, | +| friction_offset_threshold: 0.04 | friction_offset_threshold=0.04, | +| friction_correlation_distance: 0.025 | friction_correlation_distance=0.025, | +| enable_sleeping: True | # enable_sleeping is no longer needed | +| enable_stabilization: True | enable_stabilization=True, | +| max_depenetration_velocity: 100.0 | # moved to RigidBodyPropertiesCfg | +| | | +| gpu_max_rigid_contact_count: 524288 | gpu_max_rigid_contact_count=2**23, | +| gpu_max_rigid_patch_count: 81920 | gpu_max_rigid_patch_count=5 * 2**15, | +| gpu_found_lost_pairs_capacity: 1024 | gpu_found_lost_pairs_capacity=2**21, | +| gpu_found_lost_aggregate_pairs_capacity: 262144 | gpu_found_lost_aggregate_pairs_capacity=2**25, | +| gpu_total_aggregate_pairs_capacity: 1024 | gpu_total_aggregate_pairs_capacity=2**21, | +| gpu_heap_capacity: 67108864 | gpu_heap_capacity=2**26, | +| gpu_temp_buffer_capacity: 16777216 | gpu_temp_buffer_capacity=2**24, | +| gpu_max_num_partitions: 8 | gpu_max_num_partitions=8, | +| gpu_max_soft_body_contacts: 1048576 | gpu_max_soft_body_contacts=2**20, | +| gpu_max_particle_contacts: 1048576 | gpu_max_particle_contacts=2**20, | +| | ) | +| | ) | ++--------------------------------------------------------------+-------------------------------------------------------------------+ + +Parameters such as ``add_ground_plane`` and ``add_distant_light`` are now part of the task logic when creating the scene. +``enable_cameras`` is now a command line argument ``--enable_cameras`` that can be passed directly to the training script. + + +Scene Config +------------ + +The :class:`~omni.isaac.lab.scene.InteractiveSceneCfg` class can be used to specify parameters related to the scene, +such as the number of environments and the spacing between environments. Each task config must have a variable named +``scene`` defined that holds the type :class:`~omni.isaac.lab.scene.InteractiveSceneCfg`. + ++--------------------------------------------------------------+-------------------------------------------------------------------+ +| | | +|.. code-block:: yaml |.. code-block:: python | +| | | +| # OmniIsaacGymEnvs | # IsaacLab | +| env: | scene: InteractiveSceneCfg = InteractiveSceneCfg( | +| numEnvs: ${resolve_default:512,${...num_envs}} | num_envs=512, | +| envSpacing: 4.0 | env_spacing=4.0) | ++--------------------------------------------------------------+-------------------------------------------------------------------+ + +Task Config +----------- + +Each environment should specify its own config class that holds task specific parameters, such as the dimensions of the +observation and action buffers. Reward term scaling parameters can also be specified in the config class. + +In Isaac Lab, the ``controlFrequencyInv`` parameter has been renamed to ``decimation``, +which must be specified as a parameter in the config class. + +In addition, the maximum episode length parameter (now ``episode_length_s``) is in seconds instead of steps as it was +in OmniIsaacGymEnvs. To convert between step count to seconds, use the equation: +``episode_length_s = dt * decimation * num_steps``. + +The following parameters must be set for each environment config: + +.. code-block:: python + + decimation = 2 + episode_length_s = 5.0 + action_space = 1 + observation_space = 4 + state_space = 0 + + +RL Config Setup +~~~~~~~~~~~~~~~ + +RL config files for the rl_games library can continue to be defined in ``.yaml`` files in Isaac Lab. +Most of the content of the config file can be copied directly from OmniIsaacGymEnvs. +Note that in Isaac Lab, we do not use hydra to resolve relative paths in config files. +Please replace any relative paths such as ``${....device}`` with the actual values of the parameters. + +Additionally, the observation and action clip ranges have been moved to the RL config file. +For any ``clipObservations`` and ``clipActions`` parameters that were defined in the IsaacGymEnvs task config file, +they should be moved to the RL config file in Isaac Lab. + ++--------------------------+----------------------------+ +| | | +| IsaacGymEnvs Task Config | Isaac Lab RL Config | ++--------------------------+----------------------------+ +|.. code-block:: yaml |.. code-block:: yaml | +| | | +| # OmniIsaacGymEnvs | # IsaacLab | +| env: | params: | +| clipObservations: 5.0 | env: | +| clipActions: 1.0 | clip_observations: 5.0 | +| | clip_actions: 1.0 | ++--------------------------+----------------------------+ + +Environment Creation +~~~~~~~~~~~~~~~~~~~~ + +In OmniIsaacGymEnvs, environment creation generally happened in the ``set_up_scene()`` API, +which involved creating the initial environment, cloning the environment, filtering collisions, +adding the ground plane and lights, and creating the ``View`` classes for the actors. + +Similar functionality is performed in Isaac Lab in the ``_setup_scene()`` API. +The main difference is that the base class ``_setup_scene()`` no longer performs operations for +cloning the environment and adding ground plane and lights. Instead, these operations +should now be implemented in individual tasks' ``_setup_scene`` implementations to provide more +flexibility around the scene setup process. + +Also note that by defining an ``Articulation`` or ``RigidObject`` object, the actors will be +added to the scene by parsing the ``spawn`` parameter in the actor config and a ``View`` class +will automatically be created for the actor. This avoids the need to separately define an +``ArticulationView`` or ``RigidPrimView`` object for the actors. + + ++------------------------------------------------------------------------------+------------------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------------------+------------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def set_up_scene(self, scene) -> None: | def _setup_scene(self): | +| self.get_cartpole() | self.cartpole = Articulation(self.cfg.robot_cfg) | +| super().set_up_scene(scene) | # add ground plane | +| | spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg() | +| self._cartpoles = ArticulationView( | # clone, filter, and replicate | +| prim_paths_expr="/World/envs/.*/Cartpole", | self.scene.clone_environments(copy_from_source=False) | +| name="cartpole_view", reset_xform_properties=False | self.scene.filter_collisions(global_prim_paths=[]) | +| ) | # add articulation to scene | +| scene.add(self._cartpoles) | self.scene.articulations["cartpole"] = self.cartpole | +| | # add lights | +| | light_cfg = sim_utils.DomeLightCfg(intensity=2000.0) | +| | light_cfg.func("/World/Light", light_cfg) | ++------------------------------------------------------------------------------+------------------------------------------------------------------------+ + + +Ground Plane +------------ + +In addition to the above example, more sophisticated ground planes can be defined using the :class:`~terrains.TerrainImporterCfg` class. + +.. code-block:: python + + from omni.isaac.lab.terrains import TerrainImporterCfg + + terrain = TerrainImporterCfg( + prim_path="/World/ground", + terrain_type="plane", + collision_group=-1, + physics_material=sim_utils.RigidBodyMaterialCfg( + friction_combine_mode="multiply", + restitution_combine_mode="multiply", + static_friction=1.0, + dynamic_friction=1.0, + restitution=0.0, + ), + ) + +The terrain can then be added to the scene in ``_setup_scene(self)`` by referencing the ``TerrainImporterCfg`` object: + +.. code-block::python + + def _setup_scene(self): + ... + self.cfg.terrain.num_envs = self.scene.cfg.num_envs + self.cfg.terrain.env_spacing = self.scene.cfg.env_spacing + self._terrain = self.cfg.terrain.class_type(self.cfg.terrain) + + +Actors +------ + +In Isaac Lab, each Articulation and Rigid Body actor can have its own config class. The +:class:`~omni.isaac.lab.assets.ArticulationCfg` class can be used to define parameters for articulation actors, +including file path, simulation parameters, actuator properties, and initial states. + +.. code-block::python + + from omni.isaac.lab.actuators import ImplicitActuatorCfg + from omni.isaac.lab.assets import ArticulationCfg + + CARTPOLE_CFG = ArticulationCfg( + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Robots/Classic/Cartpole/cartpole.usd", + rigid_props=sim_utils.RigidBodyPropertiesCfg( + rigid_body_enabled=True, + max_linear_velocity=1000.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=100.0, + enable_gyroscopic_forces=True, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=4, + solver_velocity_iteration_count=0, + sleep_threshold=0.005, + stabilization_threshold=0.001, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.0, 2.0), joint_pos={"slider_to_cart": 0.0, "cart_to_pole": 0.0} + ), + actuators={ + "cart_actuator": ImplicitActuatorCfg( + joint_names_expr=["slider_to_cart"], + effort_limit=400.0, + velocity_limit=100.0, + stiffness=0.0, + damping=10.0, + ), + "pole_actuator": ImplicitActuatorCfg( + joint_names_expr=["cart_to_pole"], effort_limit=400.0, velocity_limit=100.0, stiffness=0.0, damping=0.0 + ), + }, + ) + +Within the :class:`~assets.ArticulationCfg`, the ``spawn`` attribute can be used to add the robot to the scene +by specifying the path to the robot file. In addition, the :class:`~omni.isaac.lab.sim.schemas.RigidBodyPropertiesCfg` +class can be used to specify simulation properties for the rigid bodies in the articulation. Similarly, the +:class:`~omni.isaac.lab.sim.schemas.ArticulationRootPropertiesCfg` class can be used to specify simulation properties +for the articulation. The joint properties are now specified as part of the ``actuators`` dictionary using +:class:`~actuators.ImplicitActuatorCfg`. Joints with the same properties can be grouped into regex expressions or +provided as a list of names or expressions. + +Actors are added to the scene by simply calling ``self.cartpole = Articulation(self.cfg.robot_cfg)``, where +``self.cfg.robot_cfg`` is an :class:`~assets.ArticulationCfg` object. Once initialized, they should also be added +to the :class:`~scene.InteractiveScene` by calling ``self.scene.articulations["cartpole"] = self.cartpole`` so that +the :class:`~scene.InteractiveScene` can traverse through actors in the scene for writing values to the simulation +and resetting. + + +Accessing States from Simulation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +APIs for accessing physics states in Isaac Lab require the creation of an :class:`~assets.Articulation` or +:class:`~assets.RigidObject` object. Multiple objects can be initialized for different articulations or rigid bodies +in the scene by defining corresponding :class:`~assets.ArticulationCfg` or :class:`~assets.RigidObjectCfg` config, +as outlined in the section above. This replaces the previously used :class:`~omni.isaac.core.articulations.ArticulationView` +and :class:`omni.isaac.core.prims.RigidPrimView` classes used in OmniIsaacGymEnvs. + +However, functionality between the classes are similar: + ++------------------------------------------------------------------+-----------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------+-----------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| dof_pos = self._cartpoles.get_joint_positions(clone=False) | self.joint_pos = self._robot.data.joint_pos | +| dof_vel = self._cartpoles.get_joint_velocities(clone=False) | self.joint_vel = self._robot.data.joint_vel | ++------------------------------------------------------------------+-----------------------------------------------------------------+ + +In Isaac Lab, :class:`~assets.Articulation` and :class:`~assets.RigidObject` classes both have a ``data`` class. +The data classes (:class:`~assets.ArticulationData` and :class:`~assets.RigidObjectData`) contain +buffers that hold the states for the articulation and rigid objects and provide +a more performant way of retrieving states from the actors. + +Apart from some renamings of APIs, setting states for actors can also be performed similarly between OmniIsaacGymEnvs and Isaac Lab. + ++---------------------------------------------------------------------------+---------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++---------------------------------------------------------------------------+---------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| indices = env_ids.to(dtype=torch.int32) | self._robot.write_joint_state_to_sim(joint_pos, joint_vel, | +| self._cartpoles.set_joint_positions(dof_pos, indices=indices) | joint_ids, env_ids) | +| self._cartpoles.set_joint_velocities(dof_vel, indices=indices) | | ++---------------------------------------------------------------------------+---------------------------------------------------------------+ + +In Isaac Lab, ``root_pose`` and ``root_velocity`` have been combined into single buffers and no longer split between +``root_position``, ``root_orientation``, ``root_linear_velocity`` and ``root_angular_velocity``. + +.. code-block::python + + self.cartpole.write_root_pose_to_sim(default_root_state[:, :7], env_ids) + self.cartpole.write_root_velocity_to_sim(default_root_state[:, 7:], env_ids) + + +Creating a New Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each environment in Isaac Lab should be in its own directory following this structure: + +.. code-block:: none + + my_environment/ + - agents/ + - __init__.py + - rl_games_ppo_cfg.py + - __init__.py + my_env.py + +* ``my_environment`` is the root directory of the task. +* ``my_environment/agents`` is the directory containing all RL config files for the task. Isaac Lab supports multiple + RL libraries that can each have its own individual config file. +* ``my_environment/__init__.py`` is the main file that registers the environment with the Gymnasium interface. + This allows the training and inferencing scripts to find the task by its name. + The content of this file should be as follow: + + .. code-block:: python + + import gymnasium as gym + + from . import agents + from .cartpole_env import CartpoleEnv, CartpoleEnvCfg + + ## + # Register Gym environments. + ## + + gym.register( + id="Isaac-Cartpole-Direct-v0", + entry_point="omni.isaac.lab_tasks.direct_workflow.cartpole:CartpoleEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": CartpoleEnvCfg, + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml" + }, + ) + +* ``my_environment/my_env.py`` is the main python script that implements the task logic and task config class for + the environment. + + +Task Logic +~~~~~~~~~~ + +The ``post_reset`` API in OmniIsaacGymEnvs is no longer required in Isaac Lab. Everything that was previously +done in ``post_reset`` can be done in the ``__init__`` method after executing the base class's +``__init__``. At this point, simulation has already started. + +In OmniIsaacGymEnvs, due to limitations of the GPU APIs, resets could not be performed based on states of the current +step. Instead, resets have to be performed at the beginning of the next time step. +This restriction has been eliminated in Isaac Lab, and thus, tasks follow the correct workflow of applying actions, +stepping simulation, collecting states, computing dones, calculating rewards, performing resets, and finally computing +observations. This workflow is done automatically by the framework such that a ``post_physics_step`` API is not +required in the task. However, individual tasks can override the ``step()`` API to control the workflow. + +In Isaac Lab, we also separate the ``pre_physics_step`` API for processing actions from the policy with +the ``apply_action`` API, which sets the actions into the simulation. This provides more flexibility in controlling +when actions should be written to simulation when ``decimation`` is used. +The ``pre_physics_step`` method will be called once per step before stepping simulation. +The ``apply_actions`` method will be called ``decimation`` number of times for each RL step, +once before each simulation step call. + +The ordering of the calls are as follow: + ++----------------------------------+----------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++----------------------------------+----------------------------------+ +|.. code-block:: none |.. code-block:: none | +| | | +| pre_physics_step | pre_physics_step | +| |-- reset_idx() | |-- _pre_physics_step(action)| +| |-- apply_action | |-- _apply_action() | +| | | +| post_physics_step | post_physics_step | +| |-- get_observations() | |-- _get_dones() | +| |-- calculate_metrics() | |-- _get_rewards() | +| |-- is_done() | |-- _reset_idx() | +| | |-- _get_observations() | ++----------------------------------+----------------------------------+ + +With this approach, resets are performed based on actions from the current step instead of the previous step. +Observations will also be computed with the correct states after resets. + +We have also performed some renamings of APIs: + +* ``set_up_scene(self, scene)`` --> ``_setup_scene(self)`` +* ``post_reset(self)`` --> ``__init__(...)`` +* ``pre_physics_step(self, actions)`` --> ``_pre_physics_step(self, actions)`` and ``_apply_action(self)`` +* ``reset_idx(self, env_ids)`` --> ``_reset_idx(self, env_ids)`` +* ``get_observations(self)`` --> ``_get_observations(self)`` - ``_get_observations()`` should now return a dictionary ``{"policy": obs}`` +* ``calculate_metrics(self)`` --> ``_get_rewards(self)`` - ``_get_rewards()`` should now return the reward buffer +* ``is_done(self)`` --> ``_get_dones(self)`` - ``_get_dones()`` should now return 2 buffers: ``reset`` and ``time_out`` buffers + + + +Putting It All Together +~~~~~~~~~~~~~~~~~~~~~~~ + +The Cartpole environment is shown here in completion to fully show the comparison between the OmniIsaacGymEnvs +implementation and the Isaac Lab implementation. + +Task Config +----------- + +Task config in Isaac Lab can be split into the main task configuration class and individual config objects for the actors. + ++-----------------------------------------------------------------+-----------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++-----------------------------------------------------------------+-----------------------------------------------------------------+ +|.. code-block:: yaml |.. code-block:: python | +| | | +| # used to create the object | @configclass | +| | class CartpoleEnvCfg(DirectRLEnvCfg): | +| name: Cartpole | | +| | # simulation | +| physics_engine: ${..physics_engine} | sim: SimulationCfg = SimulationCfg(dt=1 / 120) | +| | # robot | +| # if given, will override the device setting in gym. | robot_cfg: ArticulationCfg = CARTPOLE_CFG.replace( | +| env: | prim_path="/World/envs/env_.*/Robot") | +| | cart_dof_name = "slider_to_cart" | +| numEnvs: ${resolve_default:512,${...num_envs}} | pole_dof_name = "cart_to_pole" | +| envSpacing: 4.0 | # scene | +| resetDist: 3.0 | scene: InteractiveSceneCfg = InteractiveSceneCfg( | +| maxEffort: 400.0 | num_envs=4096, env_spacing=4.0, replicate_physics=True) | +| | # env | +| clipObservations: 5.0 | decimation = 2 | +| clipActions: 1.0 | episode_length_s = 5.0 | +| controlFrequencyInv: 2 # 60 Hz | action_scale = 100.0 # [N] | +| | action_space = 1 | +| sim: | observation_space = 4 | +| | state_space = 0 | +| dt: 0.0083 # 1/120 s | # reset | +| use_gpu_pipeline: ${eq:${...pipeline},"gpu"} | max_cart_pos = 3.0 | +| gravity: [0.0, 0.0, -9.81] | initial_pole_angle_range = [-0.25, 0.25] | +| add_ground_plane: True | # reward scales | +| add_distant_light: False | rew_scale_alive = 1.0 | +| use_fabric: True | rew_scale_terminated = -2.0 | +| enable_scene_query_support: False | rew_scale_pole_pos = -1.0 | +| disable_contact_processing: False | rew_scale_cart_vel = -0.01 | +| | rew_scale_pole_vel = -0.005 | +| enable_cameras: False | | +| | | +| default_physics_material: | CARTPOLE_CFG = ArticulationCfg( | +| static_friction: 1.0 | spawn=sim_utils.UsdFileCfg( | +| dynamic_friction: 1.0 | usd_path=f"{ISAACLAB_NUCLEUS_DIR}/.../cartpole.usd", | +| restitution: 0.0 | rigid_props=sim_utils.RigidBodyPropertiesCfg( | +| | rigid_body_enabled=True, | +| physx: | max_linear_velocity=1000.0, | +| worker_thread_count: ${....num_threads} | max_angular_velocity=1000.0, | +| solver_type: ${....solver_type} | max_depenetration_velocity=100.0, | +| use_gpu: ${eq:${....sim_device},"gpu"} # set to False to... | enable_gyroscopic_forces=True, | +| solver_position_iteration_count: 4 | ), | +| solver_velocity_iteration_count: 0 | articulation_props=sim_utils.ArticulationRootPropertiesCfg( | +| contact_offset: 0.02 | enabled_self_collisions=False, | +| rest_offset: 0.001 | solver_position_iteration_count=4, | +| bounce_threshold_velocity: 0.2 | solver_velocity_iteration_count=0, | +| friction_offset_threshold: 0.04 | sleep_threshold=0.005, | +| friction_correlation_distance: 0.025 | stabilization_threshold=0.001, | +| enable_sleeping: True | ), | +| enable_stabilization: True | ), | +| max_depenetration_velocity: 100.0 | init_state=ArticulationCfg.InitialStateCfg( | +| | pos=(0.0, 0.0, 2.0), | +| # GPU buffers | joint_pos={"slider_to_cart": 0.0, "cart_to_pole": 0.0} | +| gpu_max_rigid_contact_count: 524288 | ), | +| gpu_max_rigid_patch_count: 81920 | actuators={ | +| gpu_found_lost_pairs_capacity: 1024 | "cart_actuator": ImplicitActuatorCfg( | +| gpu_found_lost_aggregate_pairs_capacity: 262144 | joint_names_expr=["slider_to_cart"], | +| gpu_total_aggregate_pairs_capacity: 1024 | effort_limit=400.0, | +| gpu_max_soft_body_contacts: 1048576 | velocity_limit=100.0, | +| gpu_max_particle_contacts: 1048576 | stiffness=0.0, | +| gpu_heap_capacity: 67108864 | damping=10.0, | +| gpu_temp_buffer_capacity: 16777216 | ), | +| gpu_max_num_partitions: 8 | "pole_actuator": ImplicitActuatorCfg( | +| | joint_names_expr=["cart_to_pole"], effort_limit=400.0, | +| Cartpole: | velocity_limit=100.0, stiffness=0.0, damping=0.0 | +| override_usd_defaults: False | ), | +| enable_self_collisions: False | }, | +| enable_gyroscopic_forces: True | ) | +| solver_position_iteration_count: 4 | | +| solver_velocity_iteration_count: 0 | | +| sleep_threshold: 0.005 | | +| stabilization_threshold: 0.001 | | +| density: -1 | | +| max_depenetration_velocity: 100.0 | | +| contact_offset: 0.02 | | +| rest_offset: 0.001 | | ++-----------------------------------------------------------------+-----------------------------------------------------------------+ + + + +Task Setup +---------- + +The ``post_reset`` API in OmniIsaacGymEnvs is no longer required in Isaac Lab. +Everything that was previously done in ``post_reset`` can be done in the ``__init__`` method after +executing the base class's ``__init__``. At this point, simulation has already started. + ++-------------------------------------------------------------------------+-------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++-------------------------------------------------------------------------+-------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| class CartpoleTask(RLTask): | class CartpoleEnv(DirectRLEnv): | +| | cfg: CartpoleEnvCfg | +| def __init__(self, name, sim_config, env, offset=None) -> None: | def __init__(self, cfg: CartpoleEnvCfg, | +| | render_mode: str | None = None, **kwargs): | +| self.update_config(sim_config) | super().__init__(cfg, render_mode, **kwargs) | +| self._max_episode_length = 500 | | +| | | +| self._num_observations = 4 | self._cart_dof_idx, _ = self.cartpole.find_joints( | +| self._num_actions = 1 | self.cfg.cart_dof_name) | +| | self._pole_dof_idx, _ = self.cartpole.find_joints( | +| RLTask.__init__(self, name, env) | self.cfg.pole_dof_name) | +| | self.action_scale=self.cfg.action_scale | +| def update_config(self, sim_config): | | +| self._sim_config = sim_config | self.joint_pos = self.cartpole.data.joint_pos | +| self._cfg = sim_config.config | self.joint_vel = self.cartpole.data.joint_vel | +| self._task_cfg = sim_config. | | +| task_config | | +| | | +| self._num_envs = self._task_cfg["env"]["numEnvs"] | | +| self._env_spacing = self._task_cfg["env"]["envSpacing"] | | +| self._cartpole_positions = torch.tensor([0.0, 0.0, 2.0]) | | +| | | +| self._reset_dist = self._task_cfg["env"]["resetDist"] | | +| self._max_push_effort = self._task_cfg["env"]["maxEffort"] | | +| | | +| | | +| def post_reset(self): | | +| self._cart_dof_idx = self._cartpoles.get_dof_index( | | +| "cartJoint") | | +| self._pole_dof_idx = self._cartpoles.get_dof_index( | | +| "poleJoint") | | +| # randomize all envs | | +| indices = torch.arange( | | +| self._cartpoles.count, dtype=torch.int64, | | +| device=self._device) | | +| self.reset_idx(indices) | | ++-------------------------------------------------------------------------+-------------------------------------------------------------+ + + + +Scene Setup +----------- + +The ``set_up_scene`` method in OmniIsaacGymEnvs has been replaced by the ``_setup_scene`` API in the task class in +Isaac Lab. Additionally, scene cloning and collision filtering have been provided as APIs for the task class to +call when necessary. Similarly, adding ground plane and lights should also be taken care of in the task class. +Adding actors to the scene has been replaced by ``self.scene.articulations["cartpole"] = self.cartpole``. + ++-----------------------------------------------------------+----------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++-----------------------------------------------------------+----------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def set_up_scene(self, scene) -> None: | def _setup_scene(self): | +| | self.cartpole = Articulation(self.cfg.robot_cfg) | +| self.get_cartpole() | # add ground plane | +| super().set_up_scene(scene) | spawn_ground_plane(prim_path="/World/ground", | +| self._cartpoles = ArticulationView( | cfg=GroundPlaneCfg()) | +| prim_paths_expr="/World/envs/.*/Cartpole", | # clone, filter, and replicate | +| name="cartpole_view", | self.scene.clone_environments( | +| reset_xform_properties=False | copy_from_source=False) | +| ) | self.scene.filter_collisions( | +| scene.add(self._cartpoles) | global_prim_paths=[]) | +| return | # add articulation to scene | +| | self.scene.articulations["cartpole"] = self.cartpole | +| def get_cartpole(self): | | +| cartpole = Cartpole( | # add lights | +| prim_path=self.default_zero_env_path+"/Cartpole", | light_cfg = sim_utils.DomeLightCfg( | +| name="Cartpole", | intensity=2000.0, color=(0.75, 0.75, 0.75)) | +| translation=self._cartpole_positions | light_cfg.func("/World/Light", light_cfg) | +| ) | | +| # applies articulation settings from the | | +| # task configuration yaml file | | +| self._sim_config.apply_articulation_settings( | | +| "Cartpole", get_prim_at_path(cartpole.prim_path), | | +| self._sim_config.parse_actor_config("Cartpole") | | +| ) | | ++-----------------------------------------------------------+----------------------------------------------------------+ + + +Pre-Physics Step +---------------- + +Note that resets are no longer performed in the ``pre_physics_step`` API. In addition, the separation of the +``_pre_physics_step`` and ``_apply_action`` methods allow for more flexibility in processing the action buffer +and setting actions into simulation. + ++------------------------------------------------------------------+-------------------------------------------------------------+ +| OmniIsaacGymEnvs | IsaacLab | ++------------------------------------------------------------------+-------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def pre_physics_step(self, actions) -> None: | def _pre_physics_step(self, | +| if not self.world.is_playing(): | actions: torch.Tensor) -> None: | +| return | self.actions = self.action_scale * actions | +| | | +| reset_env_ids = self.reset_buf.nonzero( | def _apply_action(self) -> None: | +| as_tuple=False).squeeze(-1) | self.cartpole.set_joint_effort_target( | +| if len(reset_env_ids) > 0: | self.actions, joint_ids=self._cart_dof_idx) | +| self.reset_idx(reset_env_ids) | | +| | | +| actions = actions.to(self._device) | | +| | | +| forces = torch.zeros((self._cartpoles.count, | | +| self._cartpoles.num_dof), | | +| dtype=torch.float32, device=self._device) | | +| forces[:, self._cart_dof_idx] = | | +| self._max_push_effort * actions[:, 0] | | +| | | +| indices = torch.arange(self._cartpoles.count, | | +| dtype=torch.int32, device=self._device) | | +| self._cartpoles.set_joint_efforts( | | +| forces, indices=indices) | | ++------------------------------------------------------------------+-------------------------------------------------------------+ + + +Dones and Resets +---------------- + +In Isaac Lab, the ``dones`` are computed in the ``_get_dones()`` method and should return two variables: ``resets`` and +``time_out``. The ``_reset_idx()`` method is also called after stepping simulation instead of before, as it was done in +OmniIsaacGymEnvs. The ``progress_buf`` tensor has been renamed to ``episode_length_buf`` in Isaac Lab and the +bookkeeping is now done automatically by the framework. Task implementations no longer need to increment or +reset the ``episode_length_buf`` buffer. + ++------------------------------------------------------------------+--------------------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------+--------------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def is_done(self) -> None: | def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]: | +| resets = torch.where( | self.joint_pos = self.cartpole.data.joint_pos | +| torch.abs(self.cart_pos) > self._reset_dist, 1, 0) | self.joint_vel = self.cartpole.data.joint_vel | +| resets = torch.where( | | +| torch.abs(self.pole_pos) > math.pi / 2, 1, resets) | time_out = self.episode_length_buf >= self.max_episode_length - 1 | +| resets = torch.where( | out_of_bounds = torch.any(torch.abs( | +| self.progress_buf >= self._max_episode_length, 1, resets) | self.joint_pos[:, self._cart_dof_idx]) > self.cfg.max_cart_pos, | +| self.reset_buf[:] = resets | dim=1) | +| | out_of_bounds = out_of_bounds | torch.any( | +| | torch.abs(self.joint_pos[:, self._pole_dof_idx]) > math.pi / 2, | +| | dim=1) | +| | return out_of_bounds, time_out | +| | | +| def reset_idx(self, env_ids): | def _reset_idx(self, env_ids: Sequence[int] | None): | +| num_resets = len(env_ids) | if env_ids is None: | +| | env_ids = self.cartpole._ALL_INDICES | +| # randomize DOF positions | super()._reset_idx(env_ids) | +| dof_pos = torch.zeros((num_resets, self._cartpoles.num_dof), | | +| device=self._device) | joint_pos = self.cartpole.data.default_joint_pos[env_ids] | +| dof_pos[:, self._cart_dof_idx] = 1.0 * ( | joint_pos[:, self._pole_dof_idx] += sample_uniform( | +| 1.0 - 2.0 * torch.rand(num_resets, device=self._device)) | self.cfg.initial_pole_angle_range[0] * math.pi, | +| dof_pos[:, self._pole_dof_idx] = 0.125 * math.pi * ( | self.cfg.initial_pole_angle_range[1] * math.pi, | +| 1.0 - 2.0 * torch.rand(num_resets, device=self._device)) | joint_pos[:, self._pole_dof_idx].shape, | +| | joint_pos.device, | +| # randomize DOF velocities | ) | +| dof_vel = torch.zeros((num_resets, self._cartpoles.num_dof), | joint_vel = self.cartpole.data.default_joint_vel[env_ids] | +| device=self._device) | | +| dof_vel[:, self._cart_dof_idx] = 0.5 * ( | default_root_state = self.cartpole.data.default_root_state[env_ids] | +| 1.0 - 2.0 * torch.rand(num_resets, device=self._device)) | default_root_state[:, :3] += self.scene.env_origins[env_ids] | +| dof_vel[:, self._pole_dof_idx] = 0.25 * math.pi * ( | | +| 1.0 - 2.0 * torch.rand(num_resets, device=self._device)) | self.joint_pos[env_ids] = joint_pos | +| | self.joint_vel[env_ids] = joint_vel | +| # apply resets | | +| indices = env_ids.to(dtype=torch.int32) | self.cartpole.write_root_pose_to_sim( | +| self._cartpoles.set_joint_positions(dof_pos, indices=indices) | default_root_state[:, :7], env_ids) | +| self._cartpoles.set_joint_velocities(dof_vel, indices=indices) | self.cartpole.write_root_velocity_to_sim( | +| | default_root_state[:, 7:], env_ids) | +| # bookkeeping | self.cartpole.write_joint_state_to_sim( | +| self.reset_buf[env_ids] = 0 | joint_pos, joint_vel, None, env_ids) | +| self.progress_buf[env_ids] = 0 | | +| | | +| | | ++------------------------------------------------------------------+--------------------------------------------------------------------------+ + + +Rewards +------- + +In Isaac Lab, rewards are implemented in the ``_get_rewards`` API and should return the reward buffer instead of assigning +it directly to ``self.rew_buf``. Computation in the reward function can also be performed using pytorch jit +through defining functions with the ``@torch.jit.script`` annotation. + ++-------------------------------------------------------+-----------------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++-------------------------------------------------------+-----------------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: python | +| | | +| def calculate_metrics(self) -> None: | def _get_rewards(self) -> torch.Tensor: | +| reward = (1.0 - self.pole_pos * self.pole_pos | total_reward = compute_rewards( | +| - 0.01 * torch.abs(self.cart_vel) - 0.005 | self.cfg.rew_scale_alive, | +| * torch.abs(self.pole_vel)) | self.cfg.rew_scale_terminated, | +| reward = torch.where( | self.cfg.rew_scale_pole_pos, | +| torch.abs(self.cart_pos) > self._reset_dist, | self.cfg.rew_scale_cart_vel, | +| torch.ones_like(reward) * -2.0, reward) | self.cfg.rew_scale_pole_vel, | +| reward = torch.where( | self.joint_pos[:, self._pole_dof_idx[0]], | +| torch.abs(self.pole_pos) > np.pi / 2, | self.joint_vel[:, self._pole_dof_idx[0]], | +| torch.ones_like(reward) * -2.0, reward) | self.joint_pos[:, self._cart_dof_idx[0]], | +| | self.joint_vel[:, self._cart_dof_idx[0]], | +| self.rew_buf[:] = reward | self.reset_terminated, | +| | ) | +| | return total_reward | +| | | +| | @torch.jit.script | +| | def compute_rewards( | +| | rew_scale_alive: float, | +| | rew_scale_terminated: float, | +| | rew_scale_pole_pos: float, | +| | rew_scale_cart_vel: float, | +| | rew_scale_pole_vel: float, | +| | pole_pos: torch.Tensor, | +| | pole_vel: torch.Tensor, | +| | cart_pos: torch.Tensor, | +| | cart_vel: torch.Tensor, | +| | reset_terminated: torch.Tensor, | +| | ): | +| | rew_alive = rew_scale_alive * (1.0 - reset_terminated.float()) | +| | rew_termination = rew_scale_terminated * reset_terminated.float() | +| | rew_pole_pos = rew_scale_pole_pos * torch.sum( | +| | torch.square(pole_pos), dim=-1) | +| | rew_cart_vel = rew_scale_cart_vel * torch.sum( | +| | torch.abs(cart_vel), dim=-1) | +| | rew_pole_vel = rew_scale_pole_vel * torch.sum( | +| | torch.abs(pole_vel), dim=-1) | +| | total_reward = (rew_alive + rew_termination | +| | + rew_pole_pos + rew_cart_vel + rew_pole_vel) | +| | return total_reward | ++-------------------------------------------------------+-----------------------------------------------------------------------+ + + +Observations +------------ + +In Isaac Lab, the ``_get_observations()`` API must return a dictionary with the key ``policy`` that has the observation buffer as the value. +When working with asymmetric actor-critic states, the states for the critic should have the key ``critic`` and be returned +with the observation buffer in the same dictionary. + ++------------------------------------------------------------------+-------------------------------------------------------------+ +| OmniIsaacGymEnvs | Isaac Lab | ++------------------------------------------------------------------+-------------------------------------------------------------+ +|.. code-block:: python |.. code-block:: | +| | | +| def get_observations(self) -> dict: | def _get_observations(self) -> dict: | +| dof_pos = self._cartpoles.get_joint_positions(clone=False) | obs = torch.cat( | +| dof_vel = self._cartpoles.get_joint_velocities(clone=False) | ( | +| | self.joint_pos[:, self._pole_dof_idx[0]], | +| self.cart_pos = dof_pos[:, self._cart_dof_idx] | self.joint_vel[:, self._pole_dof_idx[0]], | +| self.cart_vel = dof_vel[:, self._cart_dof_idx] | self.joint_pos[:, self._cart_dof_idx[0]], | +| self.pole_pos = dof_pos[:, self._pole_dof_idx] | self.joint_vel[:, self._cart_dof_idx[0]], | +| self.pole_vel = dof_vel[:, self._pole_dof_idx] | ), | +| self.obs_buf[:, 0] = self.cart_pos | dim=-1, | +| self.obs_buf[:, 1] = self.cart_vel | ) | +| self.obs_buf[:, 2] = self.pole_pos | observations = {"policy": obs} | +| self.obs_buf[:, 3] = self.pole_vel | return observations | +| | | +| observations = {self._cartpoles.name: | | +| {"obs_buf": self.obs_buf}} | | +| return observations | | ++------------------------------------------------------------------+-------------------------------------------------------------+ + + +Domain Randomization +~~~~~~~~~~~~~~~~~~~~ + +In OmniIsaacGymEnvs, domain randomization was specified through the task ``.yaml`` config file. +In Isaac Lab, the domain randomization configuration uses the :class:`~omni.isaac.lab.utils.configclass` module +to specify a configuration class consisting of :class:`~managers.EventTermCfg` variables. + +Below is an example of a configuration class for domain randomization: + +.. code-block:: python + + @configclass + class EventCfg: + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.7, 1.3), + "dynamic_friction_range": (1.0, 1.0), + "restitution_range": (1.0, 1.0), + "num_buckets": 250, + }, + ) + robot_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "stiffness_distribution_params": (0.75, 1.5), + "damping_distribution_params": (0.3, 3.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + reset_gravity = EventTerm( + func=mdp.randomize_physics_scene_gravity, + mode="interval", + is_global_time=True, + interval_range_s=(36.0, 36.0), # time_s = num_steps * (decimation * dt) + params={ + "gravity_distribution_params": ([0.0, 0.0, 0.0], [0.0, 0.0, 0.4]), + "operation": "add", + "distribution": "gaussian", + }, + ) + +Each ``EventTerm`` object is of the :class:`~managers.EventTermCfg` class and takes in a ``func`` parameter +for specifying the function to call during randomization, a ``mode`` parameter, which can be ``startup``, +``reset`` or ``interval``. THe ``params`` dictionary should provide the necessary arguments to the +function that is specified in the ``func`` parameter. +Functions specified as ``func`` for the ``EventTerm`` can be found in the :class:`~envs.mdp.events` module. + +Note that as part of the ``"asset_cfg": SceneEntityCfg("robot", body_names=".*")`` parameter, the name of +the actor ``"robot"`` is provided, along with the body or joint names specified as a regex expression, +which will be the actors and bodies/joints that will have randomization applied. + +One difference with OmniIsaacGymEnvs is that ``interval`` randomization is now specified as seconds instead of +steps. When ``mode="interval"``, the ``interval_range_s`` parameter must also be provided, which specifies +the range of seconds for which randomization should be applied. This range will then be randomized to +determine a specific time in seconds when the next randomization will occur for the term. +To convert between steps to seconds, use the equation ``time_s = num_steps * (decimation * dt)``. + +Similar to OmniIsaacGymEnvs, randomization APIs are available for randomizing articulation properties, +such as joint stiffness and damping, joint limits, rigid body materials, fixed tendon properties, +as well as rigid body properties, such as mass and rigid body materials. Randomization of the +physics scene gravity is also supported. Note that randomization of scale is current not supported +in Isaac Lab. To randomize scale, please set up the scene in a way where each environment holds the actor +at a different scale. + +Once the ``configclass`` for the randomization terms have been set up, the class must be added +to the base config class for the task and be assigned to the variable ``events``. + +.. code-block:: python + + @configclass + class MyTaskConfig: + events: EventCfg = EventCfg() + + +Action and Observation Noise +---------------------------- + +Actions and observation noise can also be added using the :class:`~utils.configclass` module. +Action and observation noise configs must be added to the main task config using the +``action_noise_model`` and ``observation_noise_model`` variables: + +.. code-block:: python + + @configclass + class MyTaskConfig: + # at every time-step add gaussian noise + bias. The bias is a gaussian sampled at reset + action_noise_model: NoiseModelWithAdditiveBiasCfg = NoiseModelWithAdditiveBiasCfg( + noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.05, operation="add"), + bias_noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.015, operation="abs"), + ) + # at every time-step add gaussian noise + bias. The bias is a gaussian sampled at reset + observation_noise_model: NoiseModelWithAdditiveBiasCfg = NoiseModelWithAdditiveBiasCfg( + noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.002, operation="add"), + bias_noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.0001, operation="abs"), + ) + + +:class:`~.utils.noise.NoiseModelWithAdditiveBiasCfg` can be used to sample both uncorrelated noise +per step as well as correlated noise that is re-sampled at reset time. +The ``noise_cfg`` term specifies the Gaussian distribution that will be sampled at each +step for all environments. This noise will be added to the corresponding actions and +observations buffers at every step. +The ``bias_noise_cfg`` term specifies the Gaussian distribution for the correlated noise +that will be sampled at reset time for the environments being reset. The same noise +will be applied each step for the remaining of the episode for the environments and +resampled at the next reset. + +This replaces the following setup in OmniIsaacGymEnvs: + +.. code-block:: yaml + + domain_randomization: + randomize: True + randomization_params: + observations: + on_reset: + operation: "additive" + distribution: "gaussian" + distribution_parameters: [0, .0001] + on_interval: + frequency_interval: 1 + operation: "additive" + distribution: "gaussian" + distribution_parameters: [0, .002] + actions: + on_reset: + operation: "additive" + distribution: "gaussian" + distribution_parameters: [0, 0.015] + on_interval: + frequency_interval: 1 + operation: "additive" + distribution: "gaussian" + distribution_parameters: [0., 0.05] + + +Launching Training +~~~~~~~~~~~~~~~~~~ + +To launch a training in Isaac Lab, use the command: + +.. code-block:: bash + + python source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-Direct-v0 --headless + +Launching Inferencing +~~~~~~~~~~~~~~~~~~~~~ + +To launch inferencing in Isaac Lab, use the command: + +.. code-block:: bash + + python source/standalone/workflows/rl_games/play.py --task=Isaac-Cartpole-Direct-v0 --num_envs=25 --checkpoint= + + +.. _`OmniIsaacGymEnvs`: https://github.com/isaac-sim/OmniIsaacGymEnvs +.. _release notes: https://github.com/isaac-sim/IsaacLab/releases diff --git a/_sources/source/migration/migrating_from_orbit.rst b/_sources/source/migration/migrating_from_orbit.rst new file mode 100644 index 0000000000..3b60cb5dcf --- /dev/null +++ b/_sources/source/migration/migrating_from_orbit.rst @@ -0,0 +1,150 @@ +.. _migrating-from-orbit: + +From Orbit +========== + +.. currentmodule:: omni.isaac.lab + +Since `Orbit`_ was used as basis for Isaac Lab, migrating from Orbit to Isaac Lab is straightforward. +The following sections describe the changes that need to be made to your code to migrate from Orbit to Isaac Lab. + +.. note:: + + The following changes are with respect to Isaac Lab 1.0 release. Please refer to the `release notes`_ for any changes + in the future releases. + + +Renaming of the launch script +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The script ``orbit.sh`` has been renamed to ``isaaclab.sh``. + + +Updates to extensions +~~~~~~~~~~~~~~~~~~~~~ + +The extensions ``omni.isaac.orbit``, ``omni.isaac.orbit_tasks``, and ``omni.isaac.orbit_assets`` have been renamed +to ``omni.isaac.lab``, ``omni.isaac.lab_tasks``, and ``omni.isaac.lab_assets``, respectively. Thus, +the new folder structure looks like this: + +- ``source/extensions/omni.isaac.lab/omni/isaac/lab`` +- ``source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks`` +- ``source/extensions/omni.isaac.lab_assets/omni/isaac/lab_assets`` + +The high level imports have to be updated as well: + ++-------------------------------------+-----------------------------------+ +| Orbit | Isaac Lab | ++=====================================+===================================+ +| ``from omni.isaac.orbit...`` | ``from omni.isaac.lab...`` | ++-------------------------------------+-----------------------------------+ +| ``from omni.isaac.orbit_tasks...`` | ``from omni.isaac.lab_tasks...`` | ++-------------------------------------+-----------------------------------+ +| ``from omni.isaac.orbit_assets...`` | ``from omni.isaac.lab_assets...`` | ++-------------------------------------+-----------------------------------+ + + +Updates to class names +~~~~~~~~~~~~~~~~~~~~~~ + +In Isaac Lab, we introduced the concept of task design workflows (see :ref:`feature-workflows`). The Orbit code is using +the manager-based workflow and the environment specific class names have been updated to reflect this change: + ++------------------------+---------------------------------------------------------+ +| Orbit | Isaac Lab | ++========================+=========================================================+ +| ``BaseEnv`` | :class:`omni.isaac.lab.envs.ManagerBasedEnv` | ++------------------------+---------------------------------------------------------+ +| ``BaseEnvCfg`` | :class:`omni.isaac.lab.envs.ManagerBasedEnvCfg` | ++------------------------+---------------------------------------------------------+ +| ``RLTaskEnv`` | :class:`omni.isaac.lab.envs.ManagerBasedRLEnv` | ++------------------------+---------------------------------------------------------+ +| ``RLTaskEnvCfg`` | :class:`omni.isaac.lab.envs.ManagerBasedRLEnvCfg` | ++------------------------+---------------------------------------------------------+ +| ``RLTaskEnvWindow`` | :class:`omni.isaac.lab.envs.ui.ManagerBasedRLEnvWindow` | ++------------------------+---------------------------------------------------------+ + + +Updates to the tasks folder structure +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To support the manager-based and direct workflows, we have added two folders in the tasks extension: + +- ``source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based`` +- ``source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct`` + +The tasks from Orbit can now be found under the ``manager_based`` folder. +This change must also be reflected in the imports for your tasks. For example, + +.. code-block:: python + + from omni.isaac.orbit_tasks.locomotion.velocity.velocity_env_cfg ... + +should now be: + +.. code-block:: python + + from omni.isaac.lab_tasks.manager_based.locomotion.velocity.velocity_env_cfg ... + + +Other Breaking changes +~~~~~~~~~~~~~~~~~~~~~~ + +Setting the device +------------------ + +The argument ``--cpu`` has been removed in favor of ``--device device_name``. Valid options for ``device_name`` are: + +- ``cpu``: Use CPU. +- ``cuda``: Use GPU with device ID ``0``. +- ``cuda:N``: Use GPU, where N is the device ID. For example, ``cuda:0``. + +The default value is ``cuda:0``. + + +Offscreen rendering +------------------- + +The input argument ``--offscreen_render`` given to :class:`omni.isaac.lab.app.AppLauncher` and the environment variable +``OFFSCREEN_RENDER`` have been renamed to ``--enable_cameras`` and ``ENABLE_CAMERAS`` respectively. + + +Event term distribution configuration +------------------------------------- + +Some of the event functions in `events.py `_ +accepted a ``distribution`` parameter and a ``range`` to sample from. In an effort to support arbitrary distributions, +we have renamed the input argument ``AAA_range`` to ``AAA_distribution_params`` for these functions. +Therefore, event term configurations whose functions have a ``distribution`` argument should be updated. For example, + +.. code-block:: python + :emphasize-lines: 6 + + add_base_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names="base"), + "mass_range": (-5.0, 5.0), + "operation": "add", + }, + ) + +should now be: + +.. code-block:: python + :emphasize-lines: 6 + + add_base_mass = EventTerm( + func=mdp.randomize_rigid_body_mass, + mode="startup", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names="base"), + "mass_distribution_params": (-5.0, 5.0), + "operation": "add", + }, + ) + + +.. _Orbit: https://isaac-orbit.github.io/ +.. _release notes: https://github.com/isaac-sim/IsaacLab/releases diff --git a/_sources/source/overview/core-concepts/actuators.rst b/_sources/source/overview/core-concepts/actuators.rst new file mode 100644 index 0000000000..15bde0cdb8 --- /dev/null +++ b/_sources/source/overview/core-concepts/actuators.rst @@ -0,0 +1,77 @@ +.. _overview-actuators: + + +Actuators +========= + +An articulated system comprises of actuated joints, also called the degrees of freedom (DOF). +In a physical system, the actuation typically happens either through active components, such as +electric or hydraulic motors, or passive components, such as springs. These components can introduce +certain non-linear characteristics which includes delays or maximum producible velocity or torque. + +In simulation, the joints are either position, velocity, or torque-controlled. For position and velocity +control, the physics engine internally implements a spring-damp (PD) controller which computes the torques +applied on the actuated joints. In torque-control, the commands are set directly as the joint efforts. +While this mimics an ideal behavior of the joint mechanism, it does not truly model how the drives work +in the physical world. Thus, we provide a mechanism to inject external models to compute the +joint commands that would represent the physical robot's behavior. + +Actuator models +--------------- + +We name two different types of actuator models: + +1. **implicit**: corresponds to the ideal simulation mechanism (provided by physics engine). +2. **explicit**: corresponds to external drive models (implemented by user). + +The explicit actuator model performs two steps: 1) it computes the desired joint torques for tracking +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. + +As an example of an ideal explicit actuator model, we provide the :class:`omni.isaac.lab.actuators.IdealPDActuator` +class, which implements a PD controller with feed-forward effort, and simple clipping based on the configured +maximum effort: + +.. math:: + + \tau_{j, computed} & = k_p * (q - q_{des}) + k_d * (\dot{q} - \dot{q}_{des}) + \tau_{ff} \\ + \tau_{j, applied} & = clip(\tau_{computed}, -\tau_{j, max}, \tau_{j, max}) + + +where, :math:`k_p` and :math:`k_d` are joint stiffness and damping gains, :math:`q` and :math:`\dot{q}` +are the current joint positions and velocities, :math:`q_{des}`, :math:`\dot{q}_{des}` and :math:`\tau_{ff}` +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. + +Actuator groups +--------------- + +The actuator models by themselves are computational blocks that take as inputs the desired joint commands +and output 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 :class:`omni.isaac.lab.assets.Articulation` +class, which wraps around the physics engine's articulation class. + +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. Having an arm with a different +actuator model, such as a DC motor, would require configuring a different actuator group. + +The following figure shows the actuator groups for a legged mobile manipulator: + +.. image:: ../../_static/actuator-group/actuator-light.svg + :class: only-light + :align: center + :alt: Actuator models for a legged mobile manipulator + :width: 80% + +.. image:: ../../_static/actuator-group/actuator-dark.svg + :class: only-dark + :align: center + :width: 80% + :alt: Actuator models for a legged mobile manipulator + +.. seealso:: + + We provide implementations for various explicit actuator models. These are detailed in + `omni.isaac.lab.actuators <../../api/lab/omni.isaac.lab.actuators.html>`_ sub-package. diff --git a/_sources/source/overview/core-concepts/index.rst b/_sources/source/overview/core-concepts/index.rst new file mode 100644 index 0000000000..488b5ee221 --- /dev/null +++ b/_sources/source/overview/core-concepts/index.rst @@ -0,0 +1,11 @@ +Core Concepts +============= + +This section we introduce core concepts in Isaac Lab. + +.. toctree:: + :maxdepth: 1 + + + task_workflows + actuators diff --git a/_sources/source/overview/core-concepts/motion_generators.rst b/_sources/source/overview/core-concepts/motion_generators.rst new file mode 100644 index 0000000000..e4e09f2f17 --- /dev/null +++ b/_sources/source/overview/core-concepts/motion_generators.rst @@ -0,0 +1,229 @@ +Motion Generators +================= + +Robotic tasks are typically defined in task-space in terms of desired +end-effector trajectory, while control actions are executed in the +joint-space. This naturally leads to *joint-space* and *task-space* +(operational-space) control methods. However, successful execution of +interaction tasks using motion control often requires an accurate model +of both the robot manipulator as well as its environment. While a +sufficiently precise manipulator's model might be known, detailed +description of environment is hard to obtain :cite:p:`siciliano2009force`. +Planning errors caused by this mismatch can be overcome by introducing a +*compliant* behavior during interaction. + +While compliance is achievable passively through robot's structure (such +as elastic actuators, soft robot arms), we are more interested in +controller designs that focus on active interaction control. These are +broadly categorized into: + +1. **impedance control:** indirect control method where motion deviations + caused during interaction relates to contact force as a mass-spring-damper + system with adjustable parameters (stiffness and damping). A specialized case + of this is *stiffness* control where only the static relationship between + position error and contact force is considered. + +2. **hybrid force/motion control:** active control method which controls motion + and force along unconstrained and constrained task directions respectively. + Among the various schemes for hybrid motion control, the provided implementation + is based on inverse dynamics control in the operational space :cite:p:`khatib1987osc`. + +.. note:: + + To provide an even broader set of motion generators, we welcome contributions from the + community. If you are interested, please open an issue to start a discussion! + + +Joint-space controllers +----------------------- + +Torque control +~~~~~~~~~~~~~~ + +Action dimensions: ``"n"`` (number of joints) + +In torque control mode, the input actions are directly set as feed-forward +joint torque commands, i.e. at every time-step, + +.. math:: + + \tau = \tau_{des} + +Thus, this control mode is achievable by setting the command type for the actuator group, via +the :class:`ActuatorControlCfg` class, to ``"t_abs"``. + + +Velocity control +~~~~~~~~~~~~~~~~ + +Action dimensions: ``"n"`` (number of joints) + +In velocity control mode, a proportional control law is required to reduce the error between the +current and desired joint velocities. Based on input actions, the joint torques commands are computed as: + +.. math:: + + \tau = k_d (\dot{q}_{des} - \dot{q}) + +where :math:`k_d` are the gains parsed from configuration. + +This control mode is achievable by setting the command type for the actuator group, via +the :class:`ActuatorControlCfg` class, to ``"v_abs"`` or ``"v_rel"``. + +.. attention:: + + While performing velocity control, in many cases, gravity compensation is required to ensure better + tracking of the command. In this case, we suggest disabling gravity for the links in the articulation + in simulation. + +Position control with fixed impedance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Action dimensions: ``"n"`` (number of joints) + +In position control mode, a proportional-damping (PD) control law is employed to track the desired joint +positions and ensuring the articulation remains still at the desired location (i.e., desired joint velocities +are zero). Based on the input actions, the joint torque commands are computed as: + +.. math:: + + \tau = k_p (q_{des} - q) - k_d \dot{q} + +where :math:`k_p` and :math:`k_d` are the gains parsed from configuration. + +In its simplest above form, the control mode is achievable by setting the command type for the actuator group, +via the :class:`ActuatorControlCfg` class, to ``"p_abs"`` or ``"p_rel"``. + +However, a more complete formulation which considers the dynamics of the articulation would be: + +.. math:: + + \tau = M \left( k_p (q_{des} - q) - k_d \dot{q} \right) + g + +where :math:`M` is the joint-space inertia matrix of size :math:`n \times n`, and :math:`g` is the joint-space +gravity vector. This implementation is available through the :class:`JointImpedanceController` class by setting the +impedance mode to ``"fixed"``. The gains :math:`k_p` are parsed from the input configuration and :math:`k_d` +are computed while considering the system as a decoupled point-mass oscillator, i.e., + +.. math:: + + k_d = 2 \sqrt{k_p} \times D + +where :math:`D` is the damping ratio of the system. Critical damping is achieved for :math:`D = 1`, overcritical +damping for :math:`D > 1` and undercritical damping for :math:`D < 1`. + +Additionally, it is possible to disable the inertial or gravity compensation in the controller by setting the +flags :attr:`inertial_compensation` and :attr:`gravity_compensation` in the configuration to :obj:`False`, +respectively. + +Position control with variable stiffness +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Action dimensions: ``"2n"`` (number of joints) + +In stiffness control, the same formulation as above is employed, however, the gains :math:`k_p` are part of +the input commands. This implementation is available through the :class:`JointImpedanceController` class by +setting the impedance mode to ``"variable_kp"``. + +Position control with variable impedance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Action dimensions: ``"3n"`` (number of joints) + +In impedance control, the same formulation as above is employed, however, both :math:`k_p` and :math:`k_d` +are part of the input commands. This implementation is available through the :class:`JointImpedanceController` +class by setting the impedance mode to ``"variable"``. + +Task-space controllers +---------------------- + +Differential inverse kinematics (IK) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Action dimensions: ``"3"`` (relative/absolute position), ``"6"`` (relative pose), or ``"7"`` (absolute pose) + +Inverse kinematics converts the task-space tracking error to joint-space error. In its most typical implementation, +the pose error in the task-sace, :math:`\Delta \chi_e = (\Delta p_e, \Delta \phi_e)`, is computed as the cartesian +distance between the desired and current task-space positions, and the shortest distance in :math:`\mathbb{SO}(3)` +between the desired and current task-space orientations. + +Using the geometric Jacobian :math:`J_{eO} \in \mathbb{R}^{6 \times n}`, that relates task-space velocity to joint-space velocities, +we design the control law to obtain the desired joint positions as: + +.. math:: + + q_{des} = q + \eta J_{eO}^{-} \Delta \chi_e + +where :math:`\eta` is a scaling parameter and :math:`J_{eO}^{-}` is the pseudo-inverse of the Jacobian. + +It is possible to compute the pseudo-inverse of the Jacobian using different formulations: + +* Moore-Penrose pseduo-inverse: :math:`A^{-} = A^T(AA^T)^{-1}`. +* Levenberg-Marquardt pseduo-inverse (damped least-squares): :math:`A^{-} = A^T (AA^T + \lambda \mathbb{I})^{-1}`. +* Tanspose pseudo-inverse: :math:`A^{-} = A^T`. +* Adaptive singular-vale decomposition (SVD) pseduo-inverse from :cite:t:`buss2004ik`. + +These implementations are available through the :class:`DifferentialInverseKinematics` class. + +Impedance controller +~~~~~~~~~~~~~~~~~~~~ + + +It uses task-space pose error and Jacobian to compute join torques through mass-spring-damper system +with a) fixed stiffness, b) variable stiffness (stiffness control), +and c) variable stiffness and damping (impedance control). + +Operational-space controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to task-space impedance +control but uses the Equation of Motion (EoM) for computing the +task-space force + +Closed-loop proportional force controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It uses a proportional term +to track the desired wrench command with respect to current wrench at +the end-effector. + +Hybrid force-motion controller +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It combines closed-loop force control +and operational-space motion control to compute the desired wrench at +the end-effector. It uses selection matrices that define the +unconstrainted and constrained task directions. + + +Reactive planners +----------------- + +Typical task-space controllers do not account for motion constraints +such as joint limits, self-collision and environment collision. Instead +they rely on high-level planners (such as RRT) to handle these +non-Euclidean constraints and give joint/task-space way-points to the +controller. However, these methods are often conservative and have +undesirable deceleration when close to an object. More recently, +different approaches combine the constraints directly into an +optimization problem, thereby providing a holistic solution for motion +generation and control. + +We currently support the following planners: + +- **RMPFlow (lula):** An acceleration-based policy that composes various Reimannian Motion Policies (RMPs) to + solve a hierarchy of tasks :cite:p:`cheng2021rmpflow`. It is capable of performing dynamic collision + avoidance while navigating the end-effector to a target. + +- **MPC (OCS2):** A receding horizon control policy based on sequential linear-quadratic (SLQ) programming. + It formulates various constraints into a single optimization problem via soft-penalties and uses automatic + differentiation to compute derivatives of the system dynamics, constraints and costs. Currently, we support + the MPC formulation for end-effector trajectory tracking in fixed-arm and mobile manipulators. The formulation + considers a kinematic system model with joint limits and self-collision avoidance :cite:p:`mittal2021articulated`. + + +.. warning:: + + We wrap around the python bindings for these reactive planners to perform a batched computing of + robot actions. However, their current implementations are CPU-based which may cause certain + slowdown for learning. diff --git a/_sources/source/overview/core-concepts/task_workflows.rst b/_sources/source/overview/core-concepts/task_workflows.rst new file mode 100644 index 0000000000..7aeb78e8dc --- /dev/null +++ b/_sources/source/overview/core-concepts/task_workflows.rst @@ -0,0 +1,155 @@ +.. _feature-workflows: + + +Task Design Workflows +===================== + +.. currentmodule:: omni.isaac.lab + +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. In a Markov Decision Process +(MDP) formulation, the environment can also provide additional information such as the current reward, done flag, and +information about the current episode. + +While the environment interface is simple to understand, its implementation can vary significantly depending on the +complexity of the task. In the context of reinforcement learning (RL), the environment implementation can be broken down +into several components, such as the reward function, observation function, termination function, and reset function. +Each of these components can be implemented in different ways depending on the complexity of the task and the desired +level of modularity. + +We provide two different workflows for designing environments with the framework: + +* **Manager-based**: The environment is decomposed into individual components (or managers) that handle different + aspects of the environment (such as computing observations, applying actions, and applying randomization). The + user defines configuration classes for each component and the environment is responsible for coordinating the + managers and calling their functions. +* **Direct**: The user defines a single class that implements the entire environment directly without the need for + separate managers. This class is responsible for computing observations, applying actions, and computing rewards. + +Both workflows have their own advantages and disadvantages. The manager-based workflow is more modular and allows +different components of the environment to be swapped out easily. This is useful when prototyping the environment +and experimenting with different configurations. On the other hand, the direct workflow is more efficient and allows +for more fine-grained control over the environment logic. This is useful when optimizing the environment for performance +or when implementing complex logic that is difficult to decompose into separate components. + + +Manager-Based Environments +-------------------------- + +A majority of environment implementations follow a similar structure. The environment processes the input actions, +steps through the simulation, computes observations and reward signals, applies randomization, and resets the terminated +environments. Motivated by this, the environment can be decomposed into individual components that handle each of these tasks. +For example, the observation manager is responsible for computing the observations, the reward manager is responsible for +computing the rewards, and the termination manager is responsible for computing the termination signal. This approach +is known as the manager-based environment design in the framework. + +.. image:: ../../_static/task-workflows/manager-based-light.svg + :class: only-light + :align: center + :alt: Manager-based Task Workflow + +.. image:: ../../_static/task-workflows/manager-based-dark.svg + :class: only-dark + :align: center + :alt: Manager-based Task Workflow + +Manager-based environments promote modular implementations of tasks by decomposing the task into individual +components that are managed by separate classes. Each component of the task, such as rewards, observations, +termination can all be specified as individual configuration classes that are then passed to the corresponding +manager classes. The manager is then responsible for parsing the configurations and processing the contents specified +in its configuration. + +The coordination between the different managers is orchestrated by the class :class:`envs.ManagerBasedRLEnv`. +It takes in a task configuration class instance (:class:`envs.ManagerBasedRLEnvCfg`) that contains the configurations +for each of the components of the task. Based on the configurations, the scene is set up and the task is initialized. +Afterwards, while stepping through the environment, all the managers are called sequentially to perform the necessary +operations. + +For their own tasks, we expect the user to mainly define the task configuration class and use the existing +:class:`envs.ManagerBasedRLEnv` class for the task implementation. The task configuration class should inherit from +the base class :class:`envs.ManagerBasedRLEnvCfg` and contain variables assigned to various configuration classes +for each component (such as the ``ObservationCfg`` and ``RewardCfg``). + +.. dropdown:: Example for defining the reward function for the Cartpole task using the manager-style + :icon: plus + + The following class is a part of the Cartpole environment configuration class. The :class:`RewardsCfg` class + defines individual terms that compose the reward function. Each reward term is defined by its function + implementation, weight and additional parameters to be passed to the function. Users can define multiple + reward terms and their weights to be used in the reward function. + + .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :pyobject: RewardsCfg + + +Through this approach, it is possible to easily vary the implementations of the task by switching some components +while leaving the remaining of the code intact. This flexibility is desirable when prototyping the environment and +experimenting with different configurations. It also allows for easy collaborating with others on implementing an +environment, since contributors may choose to use different combinations of configurations for their own task +specifications. + +.. seealso:: + + We provide a more detailed tutorial for setting up an environment using the manager-based workflow at + :ref:`tutorial-create-manager-rl-env`. + + +Direct Environments +------------------- + +The direct-style environment aligns more closely with traditional implementations of environments, +where a single script directly implements the reward function, observation function, resets, and all the other components +of the environment. This approach does not require the manager classes. Instead, users are provided the complete freedom +to implement their task through the APIs from the base classes :class:`envs.DirectRLEnv` or :class:`envs.DirectMARLEnv`. +For users migrating from the `IsaacGymEnvs`_ and `OmniIsaacGymEnvs`_ framework, this workflow may be more familiar. + +.. image:: ../../_static/task-workflows/direct-based-light.svg + :class: only-light + :align: center + :alt: Direct-based Task Workflow + +.. image:: ../../_static/task-workflows/direct-based-dark.svg + :class: only-dark + :align: center + :alt: Direct-based Task Workflow + +When defining an environment with the direct-style implementation, we expect the user define a single class that +implements the entire environment. The task class should inherit from the base classes :class:`envs.DirectRLEnv` or +:class:`envs.DirectMARLEnv` and should have its corresponding configuration class that inherits from +:class:`envs.DirectRLEnvCfg` or :class:`envs.DirectMARLEnvCfg` respectively. The task class is responsible +for setting up the scene, processing the actions, computing the rewards, observations, resets, and termination signals. + +.. dropdown:: Example for defining the reward function for the Cartpole task using the direct-style + :icon: plus + + The following function is a part of the Cartpole environment class and is responsible for computing the rewards. + + .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._get_rewards + :dedent: 4 + + It calls the :meth:`compute_rewards` function which is Torch JIT compiled for performance benefits. + + .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: compute_rewards + +This approach provides more transparency in the implementations of the environments, as logic is defined within the task +class instead of abstracted with the use of managers. This may be beneficial when implementing complex logic that is +difficult to decompose into separate components. Additionally, the direct-style implementation may bring more performance +benefits for the environment, as it allows implementing large chunks of logic with optimized frameworks such as +`PyTorch JIT`_ or `Warp`_. This may be valuable when scaling up training tremendously which requires optimizing individual +operations in the environment. + +.. seealso:: + + We provide a more detailed tutorial for setting up a RL environment using the direct workflow at + :ref:`tutorial-create-direct-rl-env`. + + +.. _IsaacGymEnvs: https://github.com/isaac-sim/IsaacGymEnvs +.. _OmniIsaacGymEnvs: https://github.com/isaac-sim/OmniIsaacGymEnvs +.. _Pytorch JIT: https://pytorch.org/docs/stable/jit.html +.. _Warp: https://github.com/NVIDIA/warp diff --git a/_sources/source/overview/developer-guide/development.rst b/_sources/source/overview/developer-guide/development.rst new file mode 100644 index 0000000000..d2280918dc --- /dev/null +++ b/_sources/source/overview/developer-guide/development.rst @@ -0,0 +1,176 @@ +Application Development +======================= + +Extensions +~~~~~~~~~~ + +Extensions are the recommended way to develop applications in Isaac Sim. They are +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 in the ``config`` directory. More information on extensions can be found in the +`Omniverse documentation `__. + +Each extension in Isaac Lab is written as a python package and follows the following structure: + +.. code:: bash + + + ├── config + │   └── extension.toml + ├── docs + │   ├── CHANGELOG.md + │   └── README.md + ├── + │ ├── __init__.py + │ ├── .... + │ └── scripts + ├── setup.py + └── tests + +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 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 `__. + +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 +:class:`omni.ext.IExt` class are executed. + +.. code:: python + + import omni.ext + + class MyExt(omni.ext.IExt): + """My extension application.""" + + def on_startup(self, ext_id): + """Called when the extension is loaded.""" + pass + + def on_shutdown(self): + """Called when the extension is unloaded. + + It releases all references to the extension and cleans up any resources. + """ + pass + +While loading extensions into Omniverse happens automatically, using the python package +in standalone applications requires additional steps. To simplify the build process and +avoiding the need to understand the `premake `__ +build system used by Omniverse, we directly use the `setuptools `__ +python package to build the python module provided by the extensions. This is done by the +``setup.py`` file in the extension directory. + +.. note:: + + The ``setup.py`` file is not required for extensions that are only loaded into Omniverse + using the `Extension Manager `__. + +Lastly, the ``tests`` directory contains the unit tests for the extension. These are written +using the `unittest `__ framework. It is +important to note that Omniverse also provides a similar +`testing framework `__. +However, it requires going through the build process and does not support testing of the python module in +standalone applications. + +Custom Extension Dependency Management +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Certain extensions may have dependencies which require installation of additional packages before the extension +can be used. While Python dependencies are handled by the `setuptools `__ +package and specified in the ``setup.py`` file, non-Python dependencies such as `ROS `__ +packages or `apt `__ packages are not handled by setuptools. +To handle these dependencies, we have created an additional setup procedure described in the next section. + +There are two types of dependencies that can be specified in the ``extension.toml`` file +under the ``isaac_lab_settings`` section: + +1. **apt_deps**: A list of apt packages that need to be installed. These are installed using the + `apt `__ package manager. +2. **ros_ws**: The path to the ROS workspace that contains the ROS packages. These are installed using + the `rosdep `__ dependency manager. + +As an example, the following ``extension.toml`` file specifies the dependencies for the extension: + +.. code-block:: toml + + [isaac_lab_settings] + # apt dependencies + apt_deps = ["libboost-all-dev"] + + # ROS workspace + # note: if this path is relative, it is relative to the extension directory's root + ros_ws = "/home/user/catkin_ws" + +These dependencies are installed using the ``install_deps.py`` script provided in the ``tools`` directory. +To install all dependencies for all extensions, run the following command: + +.. code-block:: bash + + # execute from the root of the repository + # the script expects the type of dependencies to install and the path to the extensions directory + # available types are: 'apt', 'rosdep' and 'all' + python tools/install_deps.py all ${ISAACLAB_PATH}/source/extensions + +.. note:: + Currently, this script is automatically executed during the build process of the ``Dockerfile.base`` + and ``Dockerfile.ros2``. This ensures that all the 'apt' and 'rosdep' dependencies are installed + before building the extensions respectively. + + +Standalone applications +~~~~~~~~~~~~~~~~~~~~~~~ + +In a typical Omniverse workflow, the simulator is launched first, after which the extensions are +enabled that load the python module and run the python application. While this is a recommended +workflow, it is not always possible to use this workflow. + +For example, for robot learning, it is essential to have complete control over simulation stepping +and all the other functionalities instead of asynchronously waiting for the simulator to step. In +such cases, it is necessary to write a standalone application that launches the simulator using +:class:`~omni.isaac.lab.app.AppLauncher` and allows complete control over the simulation through +the :class:`~omni.isaac.lab.sim.SimulationContext` class. + +The following snippet shows how to write a standalone application: + +.. code:: python + + """Launch Isaac Sim Simulator first.""" + + from omni.isaac.lab.app import AppLauncher + + # launch omniverse app + app_launcher = AppLauncher(headless=False) + simulation_app = app_launcher.app + + + """Rest everything follows.""" + + from omni.isaac.lab.sim import SimulationContext + + if __name__ == "__main__": + # get simulation context + simulation_context = SimulationContext() + # reset and play simulation + simulation_context.reset() + # step simulation + simulation_context.step() + # stop simulation + simulation_context.stop() + + # close the simulation + simulation_app.close() + + +It is necessary to launch the simulator before running any other code because extensions are hot-loaded +when the simulator starts. Many Omniverse modules become available only after the simulator is launched. +To do this, use the :class:~omni.isaac.lab.app.AppLauncher class to start the simulator. After that, +the :class:~omni.isaac.lab.sim.SimulationContext class can be used to control the simulation. For further +details, we recommend exploring the Isaac Lab tutorials. diff --git a/_sources/source/overview/developer-guide/index.rst b/_sources/source/overview/developer-guide/index.rst new file mode 100644 index 0000000000..f45cf9cd73 --- /dev/null +++ b/_sources/source/overview/developer-guide/index.rst @@ -0,0 +1,16 @@ +Developer's Guide +================= + +For development, we suggest using `Microsoft Visual Studio Code +(VSCode) `__. This is also suggested by +NVIDIA Omniverse and there exists tutorials on how to `debug Omniverse +extensions `__ +using VSCode. + +.. toctree:: + :maxdepth: 1 + + vs_code + repo_structure + development + template diff --git a/_sources/source/overview/developer-guide/repo_structure.rst b/_sources/source/overview/developer-guide/repo_structure.rst new file mode 100644 index 0000000000..033e5dfe68 --- /dev/null +++ b/_sources/source/overview/developer-guide/repo_structure.rst @@ -0,0 +1,70 @@ +Repository organization +----------------------- + +The Isaac Lab repository is structured as follows: + +.. code-block:: bash + + IsaacLab + ├── .vscode + ├── .flake8 + ├── CONTRIBUTING.md + ├── CONTRIBUTORS.md + ├── LICENSE + ├── isaaclab.bat + ├── isaaclab.sh + ├── pyproject.toml + ├── README.md + ├── docs + ├── docker + ├── source + │   ├── extensions + │   │   ├── omni.isaac.lab + │   │   ├── omni.isaac.lab_assets + │   │   └── omni.isaac.lab_tasks + │   ├── standalone + │   │   ├── demos + │   │   ├── environments + │   │   ├── tools + │   │   ├── tutorials + │   │   └── workflows + ├── tools + └── VERSION + +The ``source`` directory contains the source code for all Isaac Lab *extensions* +and *standalone applications*. The two are the different development workflows +supported in `Isaac Sim `__. + + +Extensions +~~~~~~~~~~ + +Extensions are modularized packages that formulate the Omniverse ecosystem. In Isaac Lab. these are written +into the ``source/extensions`` directory. To simplify the build process, Isaac Lab directly use the +`setuptools `__ python package to build the python module +provided by the extensions. This is done by the ``setup.py`` file in the extension directory. + +The extensions are organized as follows: + +* **omni.isaac.lab**: Contains the core interface extension for Isaac Lab. This provides the main modules for actuators, + objects, robots and sensors. +* **omni.isaac.lab_assets**: Contains the extension with pre-configured assets for Isaac Lab. +* **omni.isaac.lab_tasks**: Contains the extension with pre-configured environments for Isaac Lab. It also includes + wrappers for using these environments with different agents. + + +Standalone +~~~~~~~~~~ + +The ``source/standalone`` directory contains various standalone applications written in python. +They are structured as follows: + +* **benchmarks**: Contains scripts for benchmarking different framework components. +* **demos**: Contains various demo applications that showcase the core framework :mod:`omni.isaac.lab`. +* **environments**: Contains applications for running environments defined in :mod:`omni.isaac.lab_tasks` with + different agents. These include a random policy, zero-action policy, teleoperation or scripted state machines. +* **tools**: Contains applications for using the tools provided by the framework. These include converting assets, + generating datasets, etc. +* **tutorials**: Contains step-by-step tutorials for using the APIs provided by the framework. +* **workflows**: Contains applications for using environments with various learning-based frameworks. These include different + reinforcement learning or imitation learning libraries. diff --git a/_sources/source/overview/developer-guide/template.rst b/_sources/source/overview/developer-guide/template.rst new file mode 100644 index 0000000000..e6c55b9b61 --- /dev/null +++ b/_sources/source/overview/developer-guide/template.rst @@ -0,0 +1,57 @@ +Building your Own Project +========================= + +Traditionally, building new projects that utilize Isaac Lab's features required creating your own +extensions within the Isaac Lab repository. However, this approach can obscure project visibility and +complicate updates from one version of Isaac Lab to another. To circumvent these challenges, we now +provide a pre-configured and customizable `extension template `_ +for creating projects in an isolated environment. + +This template serves three distinct use cases: + +* **Project Template**: Provides essential access to Isaac Sim and Isaac Lab's features, making it ideal for projects + that require a standalone environment. +* **Python Package**: Facilitates integration with Isaac Sim's native or virtual Python environment, allowing for + the creation of Python packages that can be shared and reused across multiple projects. +* **Omniverse Extension**: Supports direct integration into Omniverse extension workflow. + +.. note:: + + We recommend using the extension template for new projects, as it provides a more streamlined and + efficient workflow. Additionally it ensures that your project remains up-to-date with the latest + features and improvements in Isaac Lab. + + +Installation +------------ + +Install Isaac Lab by following the `installation guide <../../setup/installation/index.html>`_. We recommend using the conda installation as it simplifies calling Python scripts from the terminal. + +Clone the extension template repository separately from the Isaac Lab installation (i.e. outside the IsaacLab directory): + +.. code:: bash + + # Option 1: HTTPS + git clone https://github.com/isaac-sim/IsaacLabExtensionTemplate.git + + # Option 2: SSH + git clone git@github.com:isaac-sim/IsaacLabExtensionTemplate.git + +Throughout the repository, the name ``ext_template`` only serves as an example and we provide a script to rename all the references to it automatically: + +.. code:: bash + + # Enter the repository + cd IsaacLabExtensionTemplate + + # Rename all occurrences of ext_template (in files/directories) to your_fancy_extension_name + python scripts/rename_template.py your_fancy_extension_name + +Using a python interpreter that has Isaac Lab installed, install the library: + +.. code:: bash + + python -m pip install -e exts/ext_template + + +For more details, please follow the instructions in the `extension template repository `_. diff --git a/_sources/source/overview/developer-guide/vs_code.rst b/_sources/source/overview/developer-guide/vs_code.rst new file mode 100644 index 0000000000..0413dd191e --- /dev/null +++ b/_sources/source/overview/developer-guide/vs_code.rst @@ -0,0 +1,65 @@ +Setting up Visual Studio Code +----------------------------- + +The following is only applicable for Isaac Sim installed via the Omniverse Launcher. +The Isaac Lab repository includes the VSCode settings to easily allow setting +up your development environment. These are included in the ``.vscode`` directory +and include the following files: + +.. code-block:: bash + + .vscode + ├── tools + │   ├── launch.template.json + │   ├── settings.template.json + │   └── setup_vscode.py + ├── extensions.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 ``IsaacLab`` 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. + + .. image:: ../../_static/vscode_tasks.png + :width: 600px + :align: center + :alt: VSCode Tasks + +If everything executes correctly, it should create the following files: + +* ``.vscode/launch.json``: Contains the launch configurations for debugging python code. +* ``.vscode/settings.json``: Contains the settings for the python interpreter and the python environment. + +For more information on VSCode support for Omniverse, please refer to the +following links: + +* `Isaac Sim VSCode support `__ +* `Debugging with VSCode `__ + + +Configuring the python interpreter +---------------------------------- + +In the provided configuration, we set the default python interpreter to use the +python executable provided by Omniverse. This is specified in the +``.vscode/settings.json`` file: + +.. code-block:: json + + { + "python.defaultInterpreterPath": "${workspaceFolder}/_isaac_sim/python.sh", + } + +If you want to use a different python interpreter (for instance, from your conda environment), +you need to change the python interpreter used by selecting and activating the python interpreter +of your choice in the bottom left corner of VSCode, or opening the command palette (``Ctrl+Shift+P``) +and selecting ``Python: Select Interpreter``. + +For more information on how to set python interpreter for VSCode, please +refer to the `VSCode documentation `_. diff --git a/_sources/source/overview/environments.rst b/_sources/source/overview/environments.rst new file mode 100644 index 0000000000..531588a8de --- /dev/null +++ b/_sources/source/overview/environments.rst @@ -0,0 +1,543 @@ +.. _environments: + +Available Environments +====================== + +The following lists comprises of all the RL tasks implementations that are available in Isaac Lab. +While we try to keep this list up-to-date, you can always get the latest list of environments by +running the following command: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/environments/list_envs.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\environments\list_envs.py + +We are actively working on adding more environments to the list. If you have any environments that +you would like to add to Isaac Lab, please feel free to open a pull request! + +Single-agent +------------ + +Classic +~~~~~~~ + +Classic environments that are based on IsaacGymEnvs implementation of MuJoCo-style environments. + +.. table:: + :widths: 33 37 30 + + +------------------+-----------------------------+-------------------------------------------------------------------------+ + | World | Environment ID | Description | + +==================+=============================+=========================================================================+ + | |humanoid| | |humanoid-link| | Move towards a direction with the MuJoCo humanoid robot | + | | | | + | | |humanoid-direct-link| | | + +------------------+-----------------------------+-------------------------------------------------------------------------+ + | |ant| | |ant-link| | Move towards a direction with the MuJoCo ant robot | + | | | | + | | |ant-direct-link| | | + +------------------+-----------------------------+-------------------------------------------------------------------------+ + | |cartpole| | |cartpole-link| | Move the cart to keep the pole upwards in the classic cartpole control | + | | | | + | | |cartpole-direct-link| | | + +------------------+-----------------------------+-------------------------------------------------------------------------+ + | |cartpole| | |cartpole-rgb-link| | Move the cart to keep the pole upwards in the classic cartpole control | + | | | and perceptive inputs | + | | |cartpole-depth-link| | | + | | | | + | | |cartpole-rgb-direct-link| | | + | | | | + | | |cartpole-depth-direct-link|| | + +------------------+-----------------------------+-------------------------------------------------------------------------+ + | |cartpole| | |cartpole-resnet-link| | Move the cart to keep the pole upwards in the classic cartpole control | + | | | based off of features extracted from perceptive inputs with pre-trained | + | | |cartpole-theia-link| | frozen vision encoders | + +------------------+-----------------------------+-------------------------------------------------------------------------+ + +.. |humanoid| image:: ../_static/tasks/classic/humanoid.jpg +.. |ant| image:: ../_static/tasks/classic/ant.jpg +.. |cartpole| image:: ../_static/tasks/classic/cartpole.jpg + +.. |humanoid-link| replace:: `Isaac-Humanoid-v0 `__ +.. |ant-link| replace:: `Isaac-Ant-v0 `__ +.. |cartpole-link| replace:: `Isaac-Cartpole-v0 `__ +.. |cartpole-rgb-link| replace:: `Isaac-Cartpole-RGB-v0 `__ +.. |cartpole-depth-link| replace:: `Isaac-Cartpole-Depth-v0 `__ +.. |cartpole-resnet-link| replace:: `Isaac-Cartpole-RGB-ResNet18-v0 `__ +.. |cartpole-theia-link| replace:: `Isaac-Cartpole-RGB-TheiaTiny-v0 `__ + + +.. |humanoid-direct-link| replace:: `Isaac-Humanoid-Direct-v0 `__ +.. |ant-direct-link| replace:: `Isaac-Ant-Direct-v0 `__ +.. |cartpole-direct-link| replace:: `Isaac-Cartpole-Direct-v0 `__ +.. |cartpole-rgb-direct-link| replace:: `Isaac-Cartpole-RGB-Camera-Direct-v0 `__ +.. |cartpole-depth-direct-link| replace:: `Isaac-Cartpole-Depth-Camera-Direct-v0 `__ + +Manipulation +~~~~~~~~~~~~ + +Environments based on fixed-arm manipulation tasks. + +For many of these tasks, we include configurations with different arm action spaces. For example, +for the reach environment: + +* |lift-cube-link|: Franka arm with joint position control +* |lift-cube-ik-abs-link|: Franka arm with absolute IK control +* |lift-cube-ik-rel-link|: Franka arm with relative IK control + +.. table:: + :widths: 33 37 30 + + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +====================+=========================+=============================================================================+ + | |reach-franka| | |reach-franka-link| | Move the end-effector to a sampled target pose with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |reach-ur10| | |reach-ur10-link| | Move the end-effector to a sampled target pose with the UR10 robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |lift-cube| | |lift-cube-link| | Pick a cube and bring it to a sampled target position with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |cabi-franka| | |cabi-franka-link| | Grasp the handle of a cabinet's drawer and open it with the Franka robot | + | | | | + | | |franka-direct-link| | | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |cube-allegro| | |cube-allegro-link| | In-hand reorientation of a cube using Allegro hand | + | | | | + | | |allegro-direct-link| | | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |cube-shadow| | |cube-shadow-link| | In-hand reorientation of a cube using Shadow hand | + | | | | + | | |cube-shadow-ff-link| | | + | | | | + | | |cube-shadow-lstm-link| | | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |cube-shadow| | |cube-shadow-vis-link| | In-hand reorientation of a cube using Shadow hand using perceptive inputs | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + +.. |reach-franka| image:: ../_static/tasks/manipulation/franka_reach.jpg +.. |reach-ur10| image:: ../_static/tasks/manipulation/ur10_reach.jpg +.. |lift-cube| image:: ../_static/tasks/manipulation/franka_lift.jpg +.. |cabi-franka| image:: ../_static/tasks/manipulation/franka_open_drawer.jpg +.. |cube-allegro| image:: ../_static/tasks/manipulation/allegro_cube.jpg +.. |cube-shadow| image:: ../_static/tasks/manipulation/shadow_cube.jpg + +.. |reach-franka-link| replace:: `Isaac-Reach-Franka-v0 `__ +.. |reach-ur10-link| replace:: `Isaac-Reach-UR10-v0 `__ +.. |lift-cube-link| replace:: `Isaac-Lift-Cube-Franka-v0 `__ +.. |lift-cube-ik-abs-link| replace:: `Isaac-Lift-Cube-Franka-IK-Abs-v0 `__ +.. |lift-cube-ik-rel-link| replace:: `Isaac-Lift-Cube-Franka-IK-Rel-v0 `__ +.. |cabi-franka-link| replace:: `Isaac-Open-Drawer-Franka-v0 `__ +.. |franka-direct-link| replace:: `Isaac-Franka-Cabinet-Direct-v0 `__ +.. |cube-allegro-link| replace:: `Isaac-Repose-Cube-Allegro-v0 `__ +.. |allegro-direct-link| replace:: `Isaac-Repose-Cube-Allegro-Direct-v0 `__ + +.. |cube-shadow-link| replace:: `Isaac-Repose-Cube-Shadow-Direct-v0 `__ +.. |cube-shadow-ff-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 `__ +.. |cube-shadow-lstm-link| replace:: `Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 `__ +.. |cube-shadow-vis-link| replace:: `Isaac-Repose-Cube-Shadow-Vision-Direct-v0 `__ + +Locomotion +~~~~~~~~~~ + +Environments based on legged locomotion tasks. + +.. table:: + :widths: 33 37 30 + + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | World | Environment ID | Description | + +==============================+==============================================+==============================================================================+ + | |velocity-flat-anymal-b| | |velocity-flat-anymal-b-link| | Track a velocity command on flat terrain with the Anymal B robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-anymal-b| | |velocity-rough-anymal-b-link| | Track a velocity command on rough terrain with the Anymal B robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-anymal-c| | |velocity-flat-anymal-c-link| | Track a velocity command on flat terrain with the Anymal C robot | + | | | | + | | |velocity-flat-anymal-c-direct-link| | | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-anymal-c| | |velocity-rough-anymal-c-link| | Track a velocity command on rough terrain with the Anymal C robot | + | | | | + | | |velocity-rough-anymal-c-direct-link| | | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-anymal-d| | |velocity-flat-anymal-d-link| | Track a velocity command on flat terrain with the Anymal D robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-anymal-d| | |velocity-rough-anymal-d-link| | Track a velocity command on rough terrain with the Anymal D robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-unitree-a1| | |velocity-flat-unitree-a1-link| | Track a velocity command on flat terrain with the Unitree A1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-unitree-a1| | |velocity-rough-unitree-a1-link| | Track a velocity command on rough terrain with the Unitree A1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-unitree-go1| | |velocity-flat-unitree-go1-link| | Track a velocity command on flat terrain with the Unitree Go1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-unitree-go1| | |velocity-rough-unitree-go1-link| | Track a velocity command on rough terrain with the Unitree Go1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-unitree-go2| | |velocity-flat-unitree-go2-link| | Track a velocity command on flat terrain with the Unitree Go2 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-unitree-go2| | |velocity-rough-unitree-go2-link| | Track a velocity command on rough terrain with the Unitree Go2 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-spot| | |velocity-flat-spot-link| | Track a velocity command on flat terrain with the Boston Dynamics Spot robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-h1| | |velocity-flat-h1-link| | Track a velocity command on flat terrain with the Unitree H1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-h1| | |velocity-rough-h1-link| | Track a velocity command on rough terrain with the Unitree H1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-flat-g1| | |velocity-flat-g1-link| | Track a velocity command on flat terrain with the Unitree G1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + | |velocity-rough-g1| | |velocity-rough-g1-link| | Track a velocity command on rough terrain with the Unitree G1 robot | + +------------------------------+----------------------------------------------+------------------------------------------------------------------------------+ + +.. |velocity-flat-anymal-b-link| replace:: `Isaac-Velocity-Flat-Anymal-B-v0 `__ +.. |velocity-rough-anymal-b-link| replace:: `Isaac-Velocity-Rough-Anymal-B-v0 `__ + +.. |velocity-flat-anymal-c-link| replace:: `Isaac-Velocity-Flat-Anymal-C-v0 `__ +.. |velocity-rough-anymal-c-link| replace:: `Isaac-Velocity-Rough-Anymal-C-v0 `__ + +.. |velocity-flat-anymal-c-direct-link| replace:: `Isaac-Velocity-Flat-Anymal-C-Direct-v0 `__ +.. |velocity-rough-anymal-c-direct-link| replace:: `Isaac-Velocity-Rough-Anymal-C-Direct-v0 `__ + +.. |velocity-flat-anymal-d-link| replace:: `Isaac-Velocity-Flat-Anymal-D-v0 `__ +.. |velocity-rough-anymal-d-link| replace:: `Isaac-Velocity-Rough-Anymal-D-v0 `__ + +.. |velocity-flat-unitree-a1-link| replace:: `Isaac-Velocity-Flat-Unitree-A1-v0 `__ +.. |velocity-rough-unitree-a1-link| replace:: `Isaac-Velocity-Rough-Unitree-A1-v0 `__ + +.. |velocity-flat-unitree-go1-link| replace:: `Isaac-Velocity-Flat-Unitree-Go1-v0 `__ +.. |velocity-rough-unitree-go1-link| replace:: `Isaac-Velocity-Rough-Unitree-Go1-v0 `__ + +.. |velocity-flat-unitree-go2-link| replace:: `Isaac-Velocity-Flat-Unitree-Go2-v0 `__ +.. |velocity-rough-unitree-go2-link| replace:: `Isaac-Velocity-Rough-Unitree-Go2-v0 `__ + +.. |velocity-flat-spot-link| replace:: `Isaac-Velocity-Flat-Spot-v0 `__ + +.. |velocity-flat-h1-link| replace:: `Isaac-Velocity-Flat-H1-v0 `__ +.. |velocity-rough-h1-link| replace:: `Isaac-Velocity-Rough-H1-v0 `__ + +.. |velocity-flat-g1-link| replace:: `Isaac-Velocity-Flat-G1-v0 `__ +.. |velocity-rough-g1-link| replace:: `Isaac-Velocity-Rough-G1-v0 `__ + + +.. |velocity-flat-anymal-b| image:: ../_static/tasks/locomotion/anymal_b_flat.jpg +.. |velocity-rough-anymal-b| image:: ../_static/tasks/locomotion/anymal_b_rough.jpg +.. |velocity-flat-anymal-c| image:: ../_static/tasks/locomotion/anymal_c_flat.jpg +.. |velocity-rough-anymal-c| image:: ../_static/tasks/locomotion/anymal_c_rough.jpg +.. |velocity-flat-anymal-d| image:: ../_static/tasks/locomotion/anymal_d_flat.jpg +.. |velocity-rough-anymal-d| image:: ../_static/tasks/locomotion/anymal_d_rough.jpg +.. |velocity-flat-unitree-a1| image:: ../_static/tasks/locomotion/a1_flat.jpg +.. |velocity-rough-unitree-a1| image:: ../_static/tasks/locomotion/a1_rough.jpg +.. |velocity-flat-unitree-go1| image:: ../_static/tasks/locomotion/go1_flat.jpg +.. |velocity-rough-unitree-go1| image:: ../_static/tasks/locomotion/go1_rough.jpg +.. |velocity-flat-unitree-go2| image:: ../_static/tasks/locomotion/go2_flat.jpg +.. |velocity-rough-unitree-go2| image:: ../_static/tasks/locomotion/go2_rough.jpg +.. |velocity-flat-spot| image:: ../_static/tasks/locomotion/spot_flat.jpg +.. |velocity-flat-h1| image:: ../_static/tasks/locomotion/h1_flat.jpg +.. |velocity-rough-h1| image:: ../_static/tasks/locomotion/h1_rough.jpg +.. |velocity-flat-g1| image:: ../_static/tasks/locomotion/g1_flat.jpg +.. |velocity-rough-g1| image:: ../_static/tasks/locomotion/g1_rough.jpg + +Navigation +~~~~~~~~~~ + +.. table:: + :widths: 33 37 30 + + +----------------+---------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +================+=====================+=============================================================================+ + | |anymal_c_nav| | |anymal_c_nav-link| | Navigate towards a target x-y position and heading with the ANYmal C robot. | + +----------------+---------------------+-----------------------------------------------------------------------------+ + +.. |anymal_c_nav-link| replace:: `Isaac-Navigation-Flat-Anymal-C-v0 `__ + +.. |anymal_c_nav| image:: ../_static/tasks/navigation/anymal_c_nav.jpg + + +Others +~~~~~~ + +.. table:: + :widths: 33 37 30 + + +----------------+---------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +================+=====================+=============================================================================+ + | |quadcopter| | |quadcopter-link| | Fly and hover the Crazyflie copter at a goal point by applying thrust. | + +----------------+---------------------+-----------------------------------------------------------------------------+ + +.. |quadcopter-link| replace:: `Isaac-Quadcopter-Direct-v0 `__ + + +.. |quadcopter| image:: ../_static/tasks/others/quadcopter.jpg + + +Multi-agent +------------ + +.. note:: + + True mutli-agent training is only available with the `skrl` library, see the `Multi-Agents Documentation `_ for more information. + It supports the `IPPO` and `MAPPO` algorithms, which can be activated by adding the command line input ``--algorithm IPPO`` or ``--algorithm MAPPO`` to the train/play script. + If these environments are run with other libraries or without the `IPPO` or `MAPPO` flags, they will be converted to single-agent environments under the hood. + + +Classic +~~~~~~~ + +.. table:: + :widths: 33 37 30 + + +------------------------+------------------------------------+-----------------------------------------------------------------------------------------------------------------------+ + | World | Environment ID | Description | + +========================+====================================+=======================================================================================================================+ + | |cart-double-pendulum| | |cart-double-pendulum-direct-link| | Move the cart and the pendulum to keep the last one upwards in the classic inverted double pendulum on a cart control | + +------------------------+------------------------------------+-----------------------------------------------------------------------------------------------------------------------+ + +.. |cart-double-pendulum| image:: ../_static/tasks/classic/cart_double_pendulum.jpg + +.. |cart-double-pendulum-direct-link| replace:: `Isaac-Cart-Double-Pendulum-Direct-v0 `__ + +Manipulation +~~~~~~~~~~~~ + +Environments based on fixed-arm manipulation tasks. + +.. table:: + :widths: 33 37 30 + + +----------------------+--------------------------------+--------------------------------------------------------+ + | World | Environment ID | Description | + +======================+================================+========================================================+ + | |shadow-hand-over| | |shadow-hand-over-direct-link| | Passing an object from one hand over to the other hand | + +----------------------+--------------------------------+--------------------------------------------------------+ + +.. |shadow-hand-over| image:: ../_static/tasks/manipulation/shadow_hand_over.jpg + +.. |shadow-hand-over-direct-link| replace:: `Isaac-Shadow-Hand-Over-Direct-v0 `__ + +| + +Comprehensive List of Environments +================================== + +.. list-table:: + :widths: 33 25 19 25 + + * - **Task Name** + - **Inference Task Name** + - **Workflow** + - **RL Library** + * - Isaac-Ant-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Ant-v0 + - + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO), **sb3** (PPO) + * - Isaac-Cart-Double-Pendulum-Direct-v0 + - + - Direct + - **rl_games** (PPO), **skrl** (IPPO, MAPPO, PPO) + * - Isaac-Cartpole-Depth-Camera-Direct-v0 + - + - Direct + - **rl_games** (PPO), **skrl** (PPO) + * - Isaac-Cartpole-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) + * - Isaac-Cartpole-RGB-Camera-Direct-v0 + - + - Direct + - **rl_games** (PPO), **skrl** (PPO) + * - Isaac-Cartpole-v0 + - + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) + * - Isaac-Franka-Cabinet-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Humanoid-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Humanoid-v0 + - + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO), **sb3** (PPO) + * - Isaac-Lift-Cube-Franka-IK-Abs-v0 + - + - Manager Based + - + * - Isaac-Lift-Cube-Franka-IK-Rel-v0 + - + - Manager Based + - + * - Isaac-Lift-Cube-Franka-v0 + - Isaac-Lift-Cube-Franka-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO), **rl_games** (PPO) + * - Isaac-Navigation-Flat-Anymal-C-v0 + - Isaac-Navigation-Flat-Anymal-C-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Open-Drawer-Franka-IK-Abs-v0 + - + - Manager Based + - + * - Isaac-Open-Drawer-Franka-IK-Rel-v0 + - + - Manager Based + - + * - Isaac-Open-Drawer-Franka-v0 + - Isaac-Open-Drawer-Franka-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) + * - Isaac-Quadcopter-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Reach-Franka-IK-Abs-v0 + - + - Manager Based + - + * - Isaac-Reach-Franka-IK-Rel-v0 + - + - Manager Based + - + * - Isaac-Reach-Franka-v0 + - Isaac-Reach-Franka-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Reach-UR10-v0 + - Isaac-Reach-UR10-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Repose-Cube-Allegro-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Repose-Cube-Allegro-NoVelObs-v0 + - Isaac-Repose-Cube-Allegro-NoVelObs-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) + * - Isaac-Repose-Cube-Allegro-v0 + - Isaac-Repose-Cube-Allegro-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) + * - Isaac-Repose-Cube-Shadow-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Repose-Cube-Shadow-OpenAI-FF-Direct-v0 + - + - Direct + - **rl_games** (FF), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Repose-Cube-Shadow-OpenAI-LSTM-Direct-v0 + - + - Direct + - **rl_games** (LSTM) + * - Isaac-Repose-Cube-Shadow-Vision-Direct-v0 + - Isaac-Repose-Cube-Shadow-Vision-Direct-Play-v0 + - Direct + - **rsl_rl** (PPO), **rl_games** (VISION) + * - Isaac-Shadow-Hand-Over-Direct-v0 + - + - Direct + - **rl_games** (PPO), **skrl** (IPPO, MAPPO, PPO) + * - Isaac-Velocity-Flat-Anymal-B-v0 + - Isaac-Velocity-Flat-Anymal-B-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Anymal-C-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Anymal-C-v0 + - Isaac-Velocity-Flat-Anymal-C-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **rl_games** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Anymal-D-v0 + - Isaac-Velocity-Flat-Anymal-D-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Cassie-v0 + - Isaac-Velocity-Flat-Cassie-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-G1-v0 + - Isaac-Velocity-Flat-G1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-H1-v0 + - Isaac-Velocity-Flat-H1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Spot-v0 + - Isaac-Velocity-Flat-Spot-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Unitree-A1-v0 + - Isaac-Velocity-Flat-Unitree-A1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Unitree-Go1-v0 + - Isaac-Velocity-Flat-Unitree-Go1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Flat-Unitree-Go2-v0 + - Isaac-Velocity-Flat-Unitree-Go2-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Anymal-B-v0 + - Isaac-Velocity-Rough-Anymal-B-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Anymal-C-Direct-v0 + - + - Direct + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Anymal-C-v0 + - Isaac-Velocity-Rough-Anymal-C-Play-v0 + - Manager Based + - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Anymal-D-v0 + - Isaac-Velocity-Rough-Anymal-D-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Cassie-v0 + - Isaac-Velocity-Rough-Cassie-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-G1-v0 + - Isaac-Velocity-Rough-G1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-H1-v0 + - Isaac-Velocity-Rough-H1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Unitree-A1-v0 + - Isaac-Velocity-Rough-Unitree-A1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Unitree-Go1-v0 + - Isaac-Velocity-Rough-Unitree-Go1-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) + * - Isaac-Velocity-Rough-Unitree-Go2-v0 + - Isaac-Velocity-Rough-Unitree-Go2-Play-v0 + - Manager Based + - **rsl_rl** (PPO), **skrl** (PPO) diff --git a/_sources/source/overview/reinforcement-learning/index.rst b/_sources/source/overview/reinforcement-learning/index.rst new file mode 100644 index 0000000000..378e88da01 --- /dev/null +++ b/_sources/source/overview/reinforcement-learning/index.rst @@ -0,0 +1,14 @@ +Reinforcement Learning +====================== + +Isaac Lab supports multiple reinforcement learning frameworks. +In this section, we show existing scripts for running reinforcement learning +with supported RL libraries and provide a comparison of the supported +learning frameworks. + +.. toctree:: + :maxdepth: 1 + + rl_existing_scripts + rl_frameworks + performance_benchmarks diff --git a/_sources/source/overview/reinforcement-learning/performance_benchmarks.rst b/_sources/source/overview/reinforcement-learning/performance_benchmarks.rst new file mode 100644 index 0000000000..22a630ee5e --- /dev/null +++ b/_sources/source/overview/reinforcement-learning/performance_benchmarks.rst @@ -0,0 +1,151 @@ +Performance Benchmarks +====================== + +Isaac Lab leverages end-to-end GPU training for reinforcement learning workflows, +allowing for fast parallel training across thousands of environments. +In this section, we provide runtime performance benchmark results for reinforcement learning +training of various example environments on different GPU setups. +Multi-GPU and multi-node training performance results are also outlined. + + +Benchmark Results +----------------- + +All benchmarking results were performed with the RL Games library with ``--headless`` flag on Ubuntu 22.04. +``Isaac-Velocity-Rough-G1-v0`` environment benchmarks were performed with the RSL RL library. + + +Memory Consumption +^^^^^^^^^^^^^^^^^^ + ++------------------------------------+----------------+-------------------+----------+-----------+ +| Environment Name | | # of Environments | RAM (GB) | VRAM (GB) | ++====================================+================+===================+==========+===========+ +| Isaac-Cartpole-Direct-v0 | |cartpole| | 4096 | 3.7 | 3.3 | ++------------------------------------+----------------+-------------------+----------+-----------+ +| Isaac-Cartpole-RGB-Camera-Direct-v0| |cartpole-cam| | 1024 | 7.5 | 16.7 | ++------------------------------------+----------------+-------------------+----------+-----------+ +| Isaac-Velocity-Rough-G1-v0 | |g1| | 4096 | 6.5 | 6.1 | ++------------------------------------+----------------+-------------------+----------+-----------+ +| Isaac-Repose-Cube-Shadow-Direct-v0 | |shadow| | 8192 | 6.7 | 6.4 | ++------------------------------------+----------------+-------------------+----------+-----------+ + +.. |cartpole| image:: ../../_static/benchmarks/cartpole.jpg + :width: 80 + :height: 45 +.. |cartpole-cam| image:: ../../_static/benchmarks/cartpole_camera.jpg + :width: 80 + :height: 45 +.. |g1| image:: ../../_static/benchmarks/g1_rough.jpg + :width: 80 + :height: 45 +.. |shadow| image:: ../../_static/benchmarks/shadow.jpg + :width: 80 + :height: 45 + + +Single GPU - RTX 4090 +^^^^^^^^^^^^^^^^^^^^^ + +CPU: AMD Ryzen 9 7950X 16-Core Processor + ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Environment Name | # of Environments | Environment | Environment Step | Environment Step, | +| | | Step FPS | and | Inference, | +| | | | Inference FPS | and Train FPS | ++=====================================+===================+==============+===================+====================+ +| Isaac-Cartpole-Direct-v0 | 4096 | 1100000 | 910000 | 510000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Cartpole-RGB-Camera-Direct-v0 | 1024 | 50000 | 45000 | 32000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Velocity-Rough-G1-v0 | 4096 | 94000 | 88000 | 82000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Repose-Cube-Shadow-Direct-v0 | 8192 | 200000 | 190000 | 170000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ + + +Single GPU - L40 +^^^^^^^^^^^^^^^^ + +CPU: Intel(R) Xeon(R) Platinum 8362 CPU @ 2.80GHz + ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Environment Name | # of Environments | Environment | Environment Step | Environment Step, | +| | | Step FPS | and | Inference, | +| | | | Inference FPS | and Train FPS | ++=====================================+===================+==============+===================+====================+ +| Isaac-Cartpole-Direct-v0 | 4096 | 620000 | 490000 | 260000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Cartpole-RGB-Camera-Direct-v0 | 1024 | 30000 | 28000 | 21000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Velocity-Rough-G1-v0 | 4096 | 72000 | 64000 | 62000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Repose-Cube-Shadow-Direct-v0 | 8192 | 170000 | 140000 | 120000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ + + +Single-Node, 4 x L40 GPUs +^^^^^^^^^^^^^^^^^^^^^^^^^ + +CPU: Intel(R) Xeon(R) Platinum 8362 CPU @ 2.80GHz + ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Environment Name | # of Environments | Environment | Environment Step | Environment Step, | +| | | Step FPS | and | Inference, | +| | | | Inference FPS | and Train FPS | ++=====================================+===================+==============+===================+====================+ +| Isaac-Cartpole-Direct-v0 | 4096 | 2700000 | 2100000 | 950000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Cartpole-RGB-Camera-Direct-v0 | 1024 | 130000 | 120000 | 90000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Velocity-Rough-G1-v0 | 4096 | 290000 | 270000 | 250000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Repose-Cube-Shadow-Direct-v0 | 8192 | 440000 | 420000 | 390000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ + + +4 Nodes, 4 x L40 GPUs per node +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +CPU: Intel(R) Xeon(R) Platinum 8362 CPU @ 2.80GHz + ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Environment Name | # of Environments | Environment | Environment Step | Environment Step, | +| | | Step FPS | and | Inference, | +| | | | Inference FPS | and Train FPS | ++=====================================+===================+==============+===================+====================+ +| Isaac-Cartpole-Direct-v0 | 4096 | 10200000 | 8200000 | 3500000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Cartpole-RGB-Camera-Direct-v0 | 1024 | 530000 | 490000 | 260000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Velocity-Rough-G1-v0 | 4096 | 1200000 | 1100000 | 960000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ +| Isaac-Repose-Cube-Shadow-Direct-v0 | 8192 | 2400000 | 2300000 | 1800000 | ++-------------------------------------+-------------------+--------------+-------------------+--------------------+ + + +Benchmark Scripts +----------------- + +For ease of reproducibility, we provide benchmarking scripts available at ``source/standalone/benchmarks``. +This folder contains individual benchmark scripts that resemble the ``train.py`` script for RL-Games +and RSL RL. In addition, we also provide a benchmarking script that runs only the environment implementation +without any reinforcement learning library. + +Example scripts can be run similarly to training scripts: + +.. code-block:: bash + + # benchmark with RSL RL + python source/standalone/benchmarks/benchmark_rsl_rl.py --task=Isaac-Cartpole-v0 --headless + + # benchmark with RL Games + python source/standalone/benchmarks/benchmark_rlgames.py --task=Isaac-Cartpole-v0 --headless + + # benchmark without RL libraries + python source/standalone/benchmarks/benchmark_non_rl.py --task=Isaac-Cartpole-v0 --headless + +Each script will generate a set of KPI files at the end of the run, which includes data on the +startup times, runtime statistics, such as the time taken for each simulation or rendering step, +as well as overall environment FPS for stepping the environment, performing inference during +rollout, as well as training. diff --git a/_sources/source/overview/reinforcement-learning/rl_existing_scripts.rst b/_sources/source/overview/reinforcement-learning/rl_existing_scripts.rst new file mode 100644 index 0000000000..6cc88137f2 --- /dev/null +++ b/_sources/source/overview/reinforcement-learning/rl_existing_scripts.rst @@ -0,0 +1,232 @@ +Reinforcement Learning Wrappers +=============================== + +We provide wrappers to different reinforcement libraries. These wrappers convert the data +from the environments into the respective libraries function argument and return types. + +Stable-Baselines3 +----------------- + +- Training an agent with + `Stable-Baselines3 `__ + on ``Isaac-Cartpole-v0``: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # install python module (for stable-baselines3) + ./isaaclab.sh -i sb3 + # run script for training + # note: we set the device to cpu since SB3 doesn't optimize for GPU anyway + ./isaaclab.sh -p source/standalone/workflows/sb3/train.py --task Isaac-Cartpole-v0 --headless --device cpu + # run script for playing with 32 environments + ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --num_envs 32 --checkpoint /PATH/TO/model.zip + # run script for recording video of a trained agent (requires installing `ffmpeg`) + ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --headless --video --video_length 200 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: install python module (for stable-baselines3) + isaaclab.bat -i sb3 + :: run script for training + :: note: we set the device to cpu since SB3 doesn't optimize for GPU anyway + isaaclab.bat -p source\standalone\workflows\sb3\train.py --task Isaac-Cartpole-v0 --headless --device cpu + :: run script for playing with 32 environments + isaaclab.bat -p source\standalone\workflows\sb3\play.py --task Isaac-Cartpole-v0 --num_envs 32 --checkpoint /PATH/TO/model.zip + :: run script for recording video of a trained agent (requires installing `ffmpeg`) + isaaclab.bat -p source\standalone\workflows\sb3\play.py --task Isaac-Cartpole-v0 --headless --video --video_length 200 + +SKRL +---- + +- Training an agent with + `SKRL `__ on ``Isaac-Reach-Franka-v0``: + + .. tab-set:: + + .. tab-item:: PyTorch + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # install python module (for skrl) + ./isaaclab.sh -i skrl + # run script for training + ./isaaclab.sh -p source/standalone/workflows/skrl/train.py --task Isaac-Reach-Franka-v0 --headless + # run script for playing with 32 environments + ./isaaclab.sh -p source/standalone/workflows/skrl/play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --checkpoint /PATH/TO/model.pt + # run script for recording video of a trained agent (requires installing `ffmpeg`) + ./isaaclab.sh -p source/standalone/workflows/skrl/play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: install python module (for skrl) + isaaclab.bat -i skrl + :: run script for training + isaaclab.bat -p source\standalone\workflows\skrl\train.py --task Isaac-Reach-Franka-v0 --headless + :: run script for playing with 32 environments + isaaclab.bat -p source\standalone\workflows\skrl\play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --checkpoint /PATH/TO/model.pt + :: run script for recording video of a trained agent (requires installing `ffmpeg`) + isaaclab.bat -p source\standalone\workflows\skrl\play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + + .. tab-item:: JAX + + .. code:: bash + + # install python module (for skrl) + ./isaaclab.sh -i skrl + # install skrl dependencies for JAX. Visit https://skrl.readthedocs.io/en/latest/intro/installation.html for more details + ./isaaclab.sh -p -m pip install skrl["jax"] + # run script for training + ./isaaclab.sh -p source/standalone/workflows/skrl/train.py --task Isaac-Reach-Franka-v0 --headless --ml_framework jax + # run script for playing with 32 environments + ./isaaclab.sh -p source/standalone/workflows/skrl/play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --ml_framework jax --checkpoint /PATH/TO/model.pt + # run script for recording video of a trained agent (requires installing `ffmpeg`) + ./isaaclab.sh -p source/standalone/workflows/skrl/play.py --task Isaac-Reach-Franka-v0 --headless --ml_framework jax --video --video_length 200 + + - Training the multi-agent environment ``Isaac-Shadow-Hand-Over-Direct-v0`` with skrl: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # install python module (for skrl) + ./isaaclab.sh -i skrl + # run script for training with the MAPPO algorithm (IPPO is also supported) + ./isaaclab.sh -p source/standalone/workflows/skrl/train.py --task Isaac-Shadow-Hand-Over-Direct-v0 --headless --algorithm MAPPO + # run script for playing with 32 environments with the MAPPO algorithm (IPPO is also supported) + ./isaaclab.sh -p source/standalone/workflows/skrl/play.py --task Isaac-Shadow-Hand-Over-Direct-v0 --num_envs 32 --algorithm MAPPO --checkpoint /PATH/TO/model.pt + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: install python module (for skrl) + isaaclab.bat -i skrl + :: run script for training with the MAPPO algorithm (IPPO is also supported) + isaaclab.bat -p source\standalone\workflows\skrl\train.py --task Isaac-Shadow-Hand-Over-Direct-v0 --headless --algorithm MAPPO + :: run script for playing with 32 environments with the MAPPO algorithm (IPPO is also supported) + isaaclab.bat -p source\standalone\workflows\skrl\play.py --task Isaac-Shadow-Hand-Over-Direct-v0 --num_envs 32 --algorithm MAPPO --checkpoint /PATH/TO/model.pt + +RL-Games +-------- + +- Training an agent with + `RL-Games `__ on ``Isaac-Ant-v0``: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # install python module (for rl-games) + ./isaaclab.sh -i rl_games + # run script for training + ./isaaclab.sh -p source/standalone/workflows/rl_games/train.py --task Isaac-Ant-v0 --headless + # run script for playing with 32 environments + ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-Ant-v0 --num_envs 32 --checkpoint /PATH/TO/model.pth + # run script for recording video of a trained agent (requires installing `ffmpeg`) + ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-Ant-v0 --headless --video --video_length 200 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: install python module (for rl-games) + isaaclab.bat -i rl_games + :: run script for training + isaaclab.bat -p source\standalone\workflows\rl_games\train.py --task Isaac-Ant-v0 --headless + :: run script for playing with 32 environments + isaaclab.bat -p source\standalone\workflows\rl_games\play.py --task Isaac-Ant-v0 --num_envs 32 --checkpoint /PATH/TO/model.pth + :: run script for recording video of a trained agent (requires installing `ffmpeg`) + isaaclab.bat -p source\standalone\workflows\rl_games\play.py --task Isaac-Ant-v0 --headless --video --video_length 200 + +RSL-RL +------ + +- Training an agent with + `RSL-RL `__ on ``Isaac-Reach-Franka-v0``: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # install python module (for rsl-rl) + ./isaaclab.sh -i rsl_rl + # run script for training + ./isaaclab.sh -p source/standalone/workflows/rsl_rl/train.py --task Isaac-Reach-Franka-v0 --headless + # run script for playing with 32 environments + ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --load_run run_folder_name --checkpoint model.pt + # run script for recording video of a trained agent (requires installing `ffmpeg`) + ./isaaclab.sh -p source/standalone/workflows/rsl_rl/play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: install python module (for rsl-rl) + isaaclab.bat -i rsl_rl + :: run script for training + isaaclab.bat -p source\standalone\workflows\rsl_rl\train.py --task Isaac-Reach-Franka-v0 --headless + :: run script for playing with 32 environments + isaaclab.bat -p source\standalone\workflows\rsl_rl\play.py --task Isaac-Reach-Franka-v0 --num_envs 32 --load_run run_folder_name --checkpoint model.pt + :: run script for recording video of a trained agent (requires installing `ffmpeg`) + isaaclab.bat -p source\standalone\workflows\rsl_rl\play.py --task Isaac-Reach-Franka-v0 --headless --video --video_length 200 + +All the scripts above log the training progress to `Tensorboard`_ in the ``logs`` directory in the root of +the repository. The logs directory follows the pattern ``logs///``, where ```` +is the name of the learning framework, ```` is the task name, and ```` is the timestamp at +which the training script was executed. + +To view the logs, run: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # execute from the root directory of the repository + ./isaaclab.sh -p -m tensorboard.main --logdir=logs + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: execute from the root directory of the repository + isaaclab.bat -p -m tensorboard.main --logdir=logs + +.. _Tensorboard: https://www.tensorflow.org/tensorboard diff --git a/_sources/source/overview/reinforcement-learning/rl_frameworks.rst b/_sources/source/overview/reinforcement-learning/rl_frameworks.rst new file mode 100644 index 0000000000..820f7b8ccd --- /dev/null +++ b/_sources/source/overview/reinforcement-learning/rl_frameworks.rst @@ -0,0 +1,85 @@ +Reinforcement Learning Library Comparison +========================================= + +In this section, we provide an overview of the supported reinforcement learning libraries in Isaac Lab, +along with performance benchmarks across the libraries. + +The supported libraries are: + +- `SKRL `__ +- `RSL-RL `__ +- `RL-Games `__ +- `Stable-Baselines3 `__ + +Feature Comparison +------------------ + +.. list-table:: + :widths: 20 20 20 20 20 + :header-rows: 1 + + * - Feature + - RL-Games + - RSL RL + - SKRL + - Stable Baselines3 + * - Algorithms Included + - PPO, SAC, A2C + - PPO + - `Extensive List `__ + - `Extensive List `__ + * - Vectorized Training + - Yes + - Yes + - Yes + - No + * - Distributed Training + - Yes + - No + - Yes + - No + * - ML Frameworks Supported + - PyTorch + - PyTorch + - PyTorch, JAX + - PyTorch + * - Multi-Agent Support + - PPO + - PPO + - PPO + Multi-Agent algorithms + - External projects support + * - Documentation + - Low + - Low + - Comprehensive + - Extensive + * - Community Support + - Small Community + - Small Community + - Small Community + - Large Community + * - Available Examples in Isaac Lab + - Large + - Large + - Large + - Small + + +Training Performance +-------------------- + +We performed training with each RL library on the same ``Isaac-Humanoid-v0`` environment +with ``--headless`` on a single RTX 4090 GPU +and logged the total training time for 65.5M steps for each RL library. + ++--------------------+-----------------+ +| RL Library | Time in seconds | ++====================+=================+ +| RL-Games | 216 | ++--------------------+-----------------+ +| RSL RL | 215 | ++--------------------+-----------------+ +| SKRL | 321 | ++--------------------+-----------------+ +| Stable-Baselines3 | 6320 | ++--------------------+-----------------+ diff --git a/_sources/source/overview/showroom.rst b/_sources/source/overview/showroom.rst new file mode 100644 index 0000000000..d3d86fd777 --- /dev/null +++ b/_sources/source/overview/showroom.rst @@ -0,0 +1,170 @@ +Showroom Demos +============== + +The main core interface extension in Isaac Lab ``omni.isaac.lab`` provides +the main modules for actuators, objects, robots and sensors. We provide +a list of demo scripts and tutorials. These showcase how to use the provided +interfaces within a code in a minimal way. + +A few quick showroom scripts to run and checkout: + +- Spawn different quadrupeds and make robots stand using position commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/quadrupeds.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\quadrupeds.py + + .. image:: ../_static/demos/quadrupeds.jpg + :width: 100% + :alt: Quadrupeds in Isaac Lab + +- Spawn different arms and apply random joint position commands: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/arms.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\arms.py + + .. image:: ../_static/demos/arms.jpg + :width: 100% + :alt: Arms in Isaac Lab + +- Spawn different hands and command them to open and close: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/hands.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\hands.py + + .. image:: ../_static/demos/hands.jpg + :width: 100% + :alt: Dexterous hands in Isaac Lab + +- Spawn different deformable (soft) bodies and let them fall from a height: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/deformables.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\deformables.py + + .. image:: ../_static/demos/deformables.jpg + :width: 100% + :alt: Deformable primitive-shaped objects in Isaac Lab + +- Use the interactive scene and spawn varying assets in individual environments: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/multi_asset.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\multi_asset.py + + .. image:: ../_static/demos/multi_asset.jpg + :width: 100% + :alt: Multiple assets managed through the same simulation handles + +- Create and spawn procedurally generated terrains with different configurations: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/procedural_terrain.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\procedural_terrain.py + + .. image:: ../_static/demos/procedural_terrain.jpg + :width: 100% + :alt: Procedural Terrains in Isaac Lab + +- Define multiple markers that are useful for visualizations: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/demos/markers.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\demos\markers.py + + .. image:: ../_static/demos/markers.jpg + :width: 100% + :alt: Markers in Isaac Lab diff --git a/_sources/source/overview/simple_agents.rst b/_sources/source/overview/simple_agents.rst new file mode 100644 index 0000000000..07bf7cf8c3 --- /dev/null +++ b/_sources/source/overview/simple_agents.rst @@ -0,0 +1,120 @@ +Simple Agents +============= + +Workflows +--------- + +With Isaac Lab, we also provide a suite of benchmark environments included +in the ``omni.isaac.lab_tasks`` extension. We use the OpenAI Gym registry +to register these environments. For each environment, we provide a default +configuration file that defines the scene, observations, rewards and action spaces. + +The list of environments available registered with OpenAI Gym can be found by running: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/environments/list_envs.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\environments\list_envs.py + +Dummy agents +~~~~~~~~~~~~ + +These include dummy agents that output zero or random agents. They are +useful to ensure that the environments are configured correctly. + +- Zero-action agent on the Cart-pole example + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/environments/zero_agent.py --task Isaac-Cartpole-v0 --num_envs 32 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\environments\zero_agent.py --task Isaac-Cartpole-v0 --num_envs 32 + +- Random-action agent on the Cart-pole example: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/environments/random_agent.py --task Isaac-Cartpole-v0 --num_envs 32 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\environments\random_agent.py --task Isaac-Cartpole-v0 --num_envs 32 + + +State machine +~~~~~~~~~~~~~ + +We include examples on hand-crafted state machines for the environments. These +help in understanding the environment and how to use the provided interfaces. +The state machines are written in `warp `__ which +allows efficient execution for large number of environments using CUDA kernels. + +- Picking up a cube and placing it at a desired pose with a robotic arm: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/environments/state_machine/lift_cube_sm.py --num_envs 32 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\environments\state_machine\lift_cube_sm.py --num_envs 32 + +- Picking up a deformable teddy bear and placing it at a desired pose with a robotic arm: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh -p source/standalone/environments/state_machine/lift_teddy_bear.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat -p source\standalone\environments\state_machine\lift_teddy_bear.py diff --git a/_sources/source/overview/teleop_imitation.rst b/_sources/source/overview/teleop_imitation.rst new file mode 100644 index 0000000000..642aada3fe --- /dev/null +++ b/_sources/source/overview/teleop_imitation.rst @@ -0,0 +1,76 @@ +Teleoperation and Imitation Learning +==================================== + + +Teleoperation +~~~~~~~~~~~~~ + +We provide interfaces for providing commands in SE(2) and SE(3) space +for robot control. In case of SE(2) teleoperation, the returned command +is the linear x-y velocity and yaw rate, while in SE(3), the returned +command is a 6-D vector representing the change in pose. + +To play inverse kinematics (IK) control with a keyboard device: + +.. code:: bash + + ./isaaclab.sh -p source/standalone/environments/teleoperation/teleop_se3_agent.py --task Isaac-Lift-Cube-Franka-IK-Rel-v0 --num_envs 1 --teleop_device keyboard + +The script prints the teleoperation events configured. For keyboard, +these are as follows: + +.. code:: text + + Keyboard Controller for SE(3): Se3Keyboard + Reset all commands: L + Toggle gripper (open/close): K + Move arm along x-axis: W/S + Move arm along y-axis: A/D + Move arm along z-axis: Q/E + Rotate arm along x-axis: Z/X + Rotate arm along y-axis: T/G + Rotate arm along z-axis: C/V + +Imitation Learning +~~~~~~~~~~~~~~~~~~ + +Using the teleoperation devices, it is also possible to collect data for +learning from demonstrations (LfD). For this, we support the learning +framework `Robomimic `__ (Linux only) and allow saving +data in +`HDF5 `__ +format. + +1. Collect demonstrations with teleoperation for the environment + ``Isaac-Lift-Cube-Franka-IK-Rel-v0``: + + .. code:: bash + + # step a: collect data with keyboard + ./isaaclab.sh -p source/standalone/workflows/robomimic/collect_demonstrations.py --task Isaac-Lift-Cube-Franka-IK-Rel-v0 --num_envs 1 --num_demos 10 --teleop_device keyboard + # step b: inspect the collected dataset + ./isaaclab.sh -p source/standalone/workflows/robomimic/tools/inspect_demonstrations.py logs/robomimic/Isaac-Lift-Cube-Franka-IK-Rel-v0/hdf_dataset.hdf5 + +2. Split the dataset into train and validation set: + + .. code:: bash + + # install the dependencies + sudo apt install cmake build-essential + # install python module (for robomimic) + ./isaaclab.sh -i robomimic + # split data + ./isaaclab.sh -p source/standalone/workflows/robomimic/tools/split_train_val.py logs/robomimic/Isaac-Lift-Cube-Franka-IK-Rel-v0/hdf_dataset.hdf5 --ratio 0.2 + +3. Train a BC agent for ``Isaac-Lift-Cube-Franka-IK-Rel-v0`` with + `Robomimic `__: + + .. code:: bash + + ./isaaclab.sh -p source/standalone/workflows/robomimic/train.py --task Isaac-Lift-Cube-Franka-IK-Rel-v0 --algo bc --dataset logs/robomimic/Isaac-Lift-Cube-Franka-IK-Rel-v0/hdf_dataset.hdf5 + +4. Play the learned model to visualize results: + + .. code:: bash + + ./isaaclab.sh -p source/standalone/workflows/robomimic/play.py --task Isaac-Lift-Cube-Franka-IK-Rel-v0 --checkpoint /PATH/TO/model.pth diff --git a/_sources/source/refs/additional_resources.rst b/_sources/source/refs/additional_resources.rst new file mode 100644 index 0000000000..54b29152d6 --- /dev/null +++ b/_sources/source/refs/additional_resources.rst @@ -0,0 +1,34 @@ +Additional Resources +==================== + +Here we provide external links to tools and various resources that you may also find useful. + + +Sim-to-Real Resources +--------------------- + +One of the core goals of the broader Isaac project is to bring real robots to life through the power of NVIDIA technology. There are many ways to do this, and thus, many tools that you could use. These resources are dedicated to helping you navigate these possibilities by providing examples and discussions about closing the Sim-to-Real gap and deploying policies to actual real robots. + +* `Closing the Sim-to-Real Gap: Training Spot Quadruped Locomotion with NVIDIA Isaac Lab `_ is a detailed guide for training a quadruped locomotion policy for the Spot Quadruped from Boston Dynamics, and deploying it to the real robot. + + +LLM Generated Reward Functions +------------------------------ + +Our research endeavor, ``Eureka!``, has resulted in a pipeline for generating and tuning Reinforcement Learning (RL) reward functions using an LLM. These resources are dedicated to helping you utilize this pipeline to create RL based solutions to tasks that were once thought impossible! + +* `Isaac Lab Eureka `_ is a github repository where you can setup your own LLM reward generation pipeline for your direct RL environments built in Isaac Lab! + +* `Eureka! NVIDIA Research Breakthrough Puts New Spin on Robot Learning `_ is a blog post that covers the broad idea of this reward generation process. + + +Simulation Features +------------------- + +At the heart of Isaac Lab is Isaac Sim, which is itself a feature rich tool that is useful for robotics in general, and not only for RL. The stronger your understanding of the simulation, the readily you will be able to exploit its capabilities for your own projects and applications. These resources are dedicated to informing you about the other features of the simulation that may be useful to you given your specific interest in Isaac Lab! + +* `Deploying Policies in Isaac Sim `_ is an Isaac Sim tutorial on how to use trained policies within the simulation. + +* `Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab `_ is a blog post covering the newest features of Isaac Sim 4.0, including ``pip install``, a more advanced physics engine, updated sensor simulations, and more! + +* `Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab `_ is a blog post covering the gamut of features for accelerated robot learning through Isaac Lab. diff --git a/_sources/source/refs/bibliography.rst b/_sources/source/refs/bibliography.rst new file mode 100644 index 0000000000..0d21440d01 --- /dev/null +++ b/_sources/source/refs/bibliography.rst @@ -0,0 +1,4 @@ +Bibliography +============ + +.. bibliography:: diff --git a/_sources/source/refs/changelog.rst b/_sources/source/refs/changelog.rst new file mode 100644 index 0000000000..0538e7c43c --- /dev/null +++ b/_sources/source/refs/changelog.rst @@ -0,0 +1,38 @@ +Extensions Changelog +==================== + +All notable changes to this project are documented in this file. The format is based on +`Keep a Changelog `__ and this project adheres to +`Semantic Versioning `__. For a broader information +about the changes in the framework, please refer to the +`release notes `__. + +Each extension has its own changelog. The changelog for each extension is located in the +``docs`` directory of the extension. The changelog for each extension is also included in +this changelog to make it easier to find the changelog for a specific extension. + +omni.isaac.lab +-------------- + +Extension containing the core framework of Isaac Lab. + +.. include:: ../../../source/extensions/omni.isaac.lab/docs/CHANGELOG.rst + :start-line: 3 + + +omni.isaac.lab_assets +--------------------- + +Extension for configurations of various assets and sensors for Isaac Lab. + +.. include:: ../../../source/extensions/omni.isaac.lab_assets/docs/CHANGELOG.rst + :start-line: 3 + + +omni.isaac.lab_tasks +-------------------- + +Extension containing the environments built using Isaac Lab. + +.. include:: ../../../source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst + :start-line: 3 diff --git a/_sources/source/refs/contributing.rst b/_sources/source/refs/contributing.rst new file mode 100644 index 0000000000..4fff39f8d2 --- /dev/null +++ b/_sources/source/refs/contributing.rst @@ -0,0 +1,270 @@ +Contribution Guidelines +======================= + +We wholeheartedly welcome contributions to the project to make the framework more mature +and useful for everyone. These may happen in forms of: + +* Bug reports: Please report any bugs you find in the `issue tracker `__. +* Feature requests: Please suggest new features you would like to see in the `discussions `__. +* Code contributions: Please submit a `pull request `__. + + * Bug fixes + * New features + * Documentation improvements + * Tutorials and tutorial improvements + + +.. attention:: + + We prefer GitHub `discussions `_ for discussing ideas, + asking questions, conversations and requests for new features. + + Please use the + `issue tracker `_ only to track executable pieces of work + with a definite scope and a clear deliverable. These can be fixing bugs, new features, or general updates. + + +Contributing Code +----------------- + +We use `GitHub `__ for code hosting. Please +follow the following steps to contribute code: + +1. Create an issue in the `issue tracker `__ to discuss + the changes or additions you would like to make. This helps us to avoid duplicate work and to make + sure that the changes are aligned with the roadmap of the project. +2. Fork the repository. +3. Create a new branch for your changes. +4. Make your changes and commit them. +5. Push your changes to your fork. +6. Submit a pull request to the `main branch `__. +7. Ensure all the checks on the pull request template are performed. + +After sending a pull request, the maintainers will review your code and provide feedback. + +Please ensure that your code is well-formatted, documented and passes all the tests. + +.. tip:: + + It is important to keep the pull request as small as possible. This makes it easier for the + maintainers to review your code. If you are making multiple changes, please send multiple pull requests. + Large pull requests are difficult to review and may take a long time to merge. + + +Coding Style +------------ + +We follow the `Google Style +Guides `__ for the +codebase. For Python code, the PEP guidelines are followed. Most +important ones are `PEP-8 `__ +for code comments and layout, +`PEP-484 `__ and +`PEP-585 `__ for +type-hinting. + +For documentation, we adopt the `Google Style Guide `__ +for docstrings. We use `Sphinx `__ for generating the documentation. +Please make sure that your code is well-documented and follows the guidelines. + +Circular Imports +^^^^^^^^^^^^^^^^ + +Circular imports happen when two modules import each other, which is a common issue in Python. +You can prevent circular imports by adhering to the best practices outlined in this +`StackOverflow post `__. + +In general, it is essential to avoid circular imports as they can lead to unpredictable behavior. + +However, in our codebase, we encounter circular imports at a sub-package level. This situation arises +due to our specific code structure. We organize classes or functions and their corresponding configuration +objects into separate files. This separation enhances code readability and maintainability. Nevertheless, +it can result in circular imports because, in many configuration objects, we specify classes or functions +as default values using the attributes ``class_type`` and ``func`` respectively. + +To address circular imports, we leverage the `typing.TYPE_CHECKING +`_ variable. This special variable is +evaluated only during type-checking, allowing us to import classes or functions in the configuration objects +without triggering circular imports. + +It is important to note that this is the sole instance within our codebase where circular imports are used +and are acceptable. In all other scenarios, we adhere to best practices and recommend that you do the same. + +Type-hinting +^^^^^^^^^^^^ + +To make the code more readable, we use `type hints `__ for +all the functions and classes. This helps in understanding the code and makes it easier to maintain. Following +this practice also helps in catching bugs early with static type checkers like `mypy `__. + +To avoid duplication of efforts, we do not specify type hints for the arguments and return values in the docstrings. +However, if your function or class is not self-explanatory, please add a docstring with the type hints. + +Tools +^^^^^ + +We use the following tools for maintaining code quality: + +* `pre-commit `__: Runs a list of formatters and linters over the codebase. +* `black `__: The uncompromising code formatter. +* `flake8 `__: A wrapper around PyFlakes, pycodestyle and + McCabe complexity checker. + +Please check `here `__ for instructions +to set these up. To run over the entire repository, please execute the +following command in the terminal: + +.. code:: bash + + ./isaaclab.sh --format # or "./isaaclab.sh -f" + +Contributing Documentation +-------------------------- + +Contributing to the documentation is as easy as contributing to the codebase. All the source files +for the documentation are located in the ``IsaacLab/docs`` directory. The documentation is written in +`reStructuredText `__ format. + +We use `Sphinx `__ with the +`Book Theme `__ +for maintaining the documentation. + +Sending a pull request for the documentation is the same as sending a pull request for the codebase. +Please follow the steps mentioned in the `Contributing Code`_ section. + +.. caution:: + + To build the documentation, we recommend creating a `virtual environment `__ + to install the dependencies. This can also be a `conda environment `__. + + +To build the documentation, run the following command in the terminal which installs the required python packages and +builds the documentation using the ``docs/Makefile``: + +.. code:: bash + + ./isaaclab.sh --docs # or "./isaaclab.sh -d" + +The documentation is generated in the ``docs/_build`` directory. To view the documentation, open +the ``index.html`` file in the ``html`` directory. This can be done by running the following command +in the terminal: + +.. code:: bash + + xdg-open docs/_build/html/index.html + +.. hint:: + + The ``xdg-open`` command is used to open the ``index.html`` file in the default browser. If you are + using a different operating system, you can use the appropriate command to open the file in the browser. + + +To do a clean build, run the following command in the terminal: + +.. code:: bash + + rm -rf docs/_build && ./isaaclab.sh --docs + + +Contributing assets +------------------- + +Currently, we host the assets for the extensions on `NVIDIA Nucleus Server `__. +Nucleus is a cloud-based storage service that allows users to store and share large files. It is +integrated with the `NVIDIA Omniverse Platform `__. + +Since all assets are hosted on Nucleus, we do not need to include them in the repository. However, +we need to include the links to the assets in the documentation. + +The included assets are part of the `Isaac Sim Content `__. +To use this content, you need to download the files to a Nucleus server or create an **Isaac** Mount on +a Nucleus server. + +Please check the `Isaac Sim documentation `__ +for more information on how to download the assets. + +.. attention:: + + We are currently working on a better way to contribute assets. We will update this section once we + have a solution. In the meantime, please follow the steps mentioned below. + +To host your own assets, the current solution is: + +1. Create a separate repository for the assets and add it over there +2. Make sure the assets are licensed for use and distribution +3. Include images of the assets in the README file of the repository +4. Send a pull request with a link to the repository + +We will then verify the assets, its licensing, and include the assets into the Nucleus server for hosting. +In case you have any questions, please feel free to reach out to us through e-mail or by opening an issue +in the repository. + + +Maintaining a changelog +----------------------- + +Each extension maintains a changelog in the ``CHANGELOG.rst`` file in the ``docs`` directory. The +file is written in `reStructuredText `__ format. It +contains a curated, chronologically ordered list of notable changes for each version of the extension. + +The goal of this changelog is to help users and contributors see precisely what notable changes have +been made between each release (or version) of the extension. This is a *MUST* for every extension. + +For updating the changelog, please follow the following guidelines: + +* Each version should have a section with the version number and the release date. +* The version number is updated according to `Semantic Versioning `__. The + release date is the date on which the version is released. +* Each version is divided into subsections based on the type of changes made. + + * ``Added``: For new features. + * ``Changed``: For changes in existing functionality. + * ``Deprecated``: For soon-to-be removed features. + * ``Removed``: For now removed features. + * ``Fixed``: For any bug fixes. + +* Each change is described in its corresponding sub-section with a bullet point. +* The bullet points are written in the past tense and in imperative mode. + +For example, the following is a sample changelog: + +.. code:: rst + + Changelog + --------- + + 0.1.0 (2021-02-01) + ~~~~~~~~~~~~~~~~~~ + + Added + ^^^^^ + + * Added a new feature. + + Changed + ^^^^^^^ + + * Changed an existing feature. + + Deprecated + ^^^^^^^^^^ + + * Deprecated an existing feature. + + Removed + ^^^^^^^ + + * Removed an existing feature. + + Fixed + ^^^^^ + + * Fixed a bug. + + 0.0.1 (2021-01-01) + ~~~~~~~~~~~~~~~~~~ + + Added + ^^^^^ + + * Added a new feature. diff --git a/_sources/source/refs/issues.rst b/_sources/source/refs/issues.rst new file mode 100644 index 0000000000..0f79690e04 --- /dev/null +++ b/_sources/source/refs/issues.rst @@ -0,0 +1,93 @@ +Known Issues +============ + +.. attention:: + + Please also refer to the `Omniverse Isaac Sim documentation`_ for known issues and workarounds. + +Stale values after resetting the environment +-------------------------------------------- + +When resetting the environment, some of the data fields of assets and sensors are not updated. +These include the poses of links in a kinematic chain, the camera images, the contact sensor readings, +and the lidar point clouds. This is a known issue which has to do with the way the PhysX and +rendering engines work in Omniverse. + +Many physics engines do a simulation step as a two-level call: ``forward()`` and ``simulate()``, +where the kinematic and dynamic states are updated, respectively. Unfortunately, PhysX has only a +single ``step()`` call where the two operations are combined. Due to computations through GPU +kernels, it is not so straightforward for them to split these operations. Thus, at the moment, +it is not possible to set the root and/or joint states and do a forward call to update the +kinematic states of links. This affects both initialization as well as episodic resets. + +Similarly for RTX rendering related sensors (such as cameras), the sensor data is not updated +immediately after setting the state of the sensor. The rendering engine update is bundled with +the simulator's ``step()`` call which only gets called when the simulation is stepped forward. +This means that the sensor data is not updated immediately after a reset and it will hold +outdated values. + +While the above is erroneous, there is currently no direct workaround for it. From our experience in +using IsaacGym, the reset values affect the agent learning critically depending on how frequently +the environment terminates. Eventually if the agent is learning successfully, this number drops +and does not affect the performance that critically. + +We have made a feature request to the respective Omniverse teams to have complete control +over stepping different parts of the simulation app. However, at this point, there is no set +timeline for this feature request. + +.. note:: + With Isaac Lab 1.2, we have introduced a PhysX kinematic update call inside the + :attr:`~omni.isaac.lab.assets.ArticulationData.body_state_w` attribute. This workaround + ensures that the states of the links are updated when the root state or joint state + of an articulation is set. + + +Blank initial frames from the camera +------------------------------------ + +When using the :class:`~omni.isaac.lab.sensors.Camera` sensor in standalone scripts, the first few frames +may be blank. This is a known issue with the simulator where it needs a few steps to load the material +textures properly and fill up the render targets. + +A hack to work around this is to add the following after initializing the camera sensor and setting +its pose: + +.. code-block:: python + + from omni.isaac.lab.sim import SimulationContext + + sim = SimulationContext.instance() + + # note: the number of steps might vary depending on how complicated the scene is. + for _ in range(12): + sim.render() + + +Using instanceable assets for markers +------------------------------------- + +When using `instanceable assets`_ for markers, the markers do not work properly, since Omniverse does not support +instanceable assets when using the :class:`UsdGeom.PointInstancer` schema. This is a known issue and will hopefully +be fixed in a future release. + +If you use an instanceable assets for markers, the marker class removes all the physics properties of the asset. +This is then replicated across other references of the same asset since physics properties of instanceable assets +are stored in the instanceable asset's USD file and not in its stage reference's USD file. + +.. _instanceable assets: https://docs.omniverse.nvidia.com/app_isaacsim/app_isaacsim/tutorial_gym_instanceable_assets.html +.. _Omniverse Isaac Sim documentation: https://docs.omniverse.nvidia.com/isaacsim/latest/known_issues.html + + +Exiting the process +------------------- + +When exiting a process with ``Ctrl+C``, occasionally the below error may appear: + +.. code-block:: bash + + [Error] [omni.physx.plugin] Subscription cannot be changed during the event call. + +This is due to the termination occurring in the middle of a physics event call and +should not affect the functionality of Isaac Lab. It is safe to ignore the error +message and continue with terminating the process. On Windows systems, please use +``Ctrl+Break`` or ``Ctrl+fn+B`` to terminate the process. diff --git a/_sources/source/refs/license.rst b/_sources/source/refs/license.rst new file mode 100644 index 0000000000..4619970952 --- /dev/null +++ b/_sources/source/refs/license.rst @@ -0,0 +1,47 @@ +.. _license: + +License +======== + +NVIDIA Isaac Sim is available freely under `individual license +`_. For more information +about its license terms, please check `here `_. +The license files for all its dependencies and included assets are available in its +`documentation `_. + + +The Isaac Lab framework is open-sourced under the +`BSD-3-Clause license `_. + + +.. code-block:: text + + Copyright (c) 2022-2024, The Isaac Lab 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: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/_sources/source/refs/migration.rst b/_sources/source/refs/migration.rst new file mode 100644 index 0000000000..513af9b136 --- /dev/null +++ b/_sources/source/refs/migration.rst @@ -0,0 +1,90 @@ +Migration Guide (Isaac Sim) +=========================== + +Moving from Isaac Sim 2022.2.1 to 2023.1.0 and later brings in a number of changes to the +APIs and the way the application is built. This document outlines the changes +and how to migrate your code to the new APIs. Many of these changes attribute to +the underlying Omniverse Kit upgrade from 104.2 to 105.1. The new upgrade brings +the following notable changes: + +* Update to USD 22.11 +* Upgrading the Python from 3.7 to 3.10 + + +Renaming of PhysX Flatcache to PhysX Fabric +------------------------------------------- + +The PhysX Flatcache has been renamed to PhysX Fabric. The new name is more +descriptive of the functionality and is consistent with the naming convention +used by Omniverse called `Fabric`_. Consequently, the Python module name has +also been changed from :mod:`omni.physxflatcache` to :mod:`omni.physxfabric`. + +Following this, on the Isaac Sim side, various renaming have occurred: + +* The parameter passed to :class:`SimulationContext` constructor via the keyword :obj:`sim_params` + now expects the key ``use_fabric`` instead of ``use_flatcache``. +* The Python attribute :attr:`SimulationContext.get_physics_context().use_flatcache` is now + :attr:`SimulationContext.get_physics_context().use_fabric`. +* The Python function :meth:`SimulationContext.get_physics_context().enable_flatcache` is now + :meth:`SimulationContext.get_physics_context().enable_fabric`. + + +Renaming of the URDF and MJCF Importers +--------------------------------------- + +Starting from Isaac Sim 2023.1, the URDF and MJCF importers have been renamed to be more consistent +with the other asset importers in Omniverse. The importers are now available on NVIDIA-Omniverse GitHub +as open source projects. + +Due to the extension name change, the Python module names have also been changed: + +* URDF Importer: :mod:`omni.importer.urdf` (previously :mod:`omni.isaac.urdf`) +* MJCF Importer: :mod:`omni.importer.mjcf` (previously :mod:`omni.isaac.mjcf`) + + +Deprecation of :class:`UsdLux.Light` API +---------------------------------------- + +As highlighted in the release notes of `USD 22.11`_, the ``UsdLux.Light`` API has +been deprecated in favor of the new ``UsdLuxLightAPI`` API. In the new API the attributes +are prefixed with ``inputs:``. For example, the ``intensity`` attribute is now available as +``inputs:intensity``. + +The following example shows how to create a sphere light using the old API and +the new API. + +.. dropdown:: Code for Isaac Sim 2022.2.1 and below + :icon: code + + .. code-block:: python + + import omni.isaac.core.utils.prims as prim_utils + + prim_utils.create_prim( + "/World/Light/GreySphere", + "SphereLight", + translation=(4.5, 3.5, 10.0), + attributes={"radius": 2.5, "intensity": 600.0, "color": (0.75, 0.75, 0.75)}, + ) + +.. dropdown:: Code for Isaac Sim 2023.1.0 and above + :icon: code + + .. code-block:: python + + import omni.isaac.core.utils.prims as prim_utils + + prim_utils.create_prim( + "/World/Light/WhiteSphere", + "SphereLight", + translation=(-4.5, 3.5, 10.0), + attributes={ + "inputs:radius": 2.5, + "inputs:intensity": 600.0, + "inputs:color": (1.0, 1.0, 1.0) + }, + ) + + +.. _Fabric: https://docs.omniverse.nvidia.com/kit/docs/usdrt/latest/docs/usd_fabric_usdrt.html +.. _`USD 22.11`: https://github.com/PixarAnimationStudios/OpenUSD/blob/release/CHANGELOG.md diff --git a/_sources/source/refs/reference_architecture/index.rst b/_sources/source/refs/reference_architecture/index.rst new file mode 100644 index 0000000000..338b8a4415 --- /dev/null +++ b/_sources/source/refs/reference_architecture/index.rst @@ -0,0 +1,375 @@ +Reference Architecture +====================== + +This document presents an overview of the end-to-end robot learning process with +Isaac Lab and Isaac Sim. This is demonstrated using a reference architecture that highlights +the major building blocks for training and deployment workflows. It provides a comprehensive, +user-friendly guide on the entire process of developing applications from training to deploying +the trained model in the real world, including links to demos, working examples, and documentation. + +Who is this document for? +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This document is designed to assist robotics developers and researchers working with NVIDIA Isaac Lab +in the robot learning field, including those at research labs, Original Equipment Manufacturers (OEM), +Solutions Providers, Solutions Integrators (SI), and independent software vendors (ISV). It offers +guidance on utilizing Isaac Lab’s robot training framework and workflows as a foundational starting +point for environment configuration, task design, and policy training and testing. + + + +.. image:: ../../_static/reference-architecture/isaac-lab-ra-light.svg + :class: only-light + :align: center + :alt: Isaac Lab Reference Architecture + +.. image:: ../../_static/reference-architecture/isaac-lab-ra-dark.svg + :class: only-dark + :align: center + :alt: Isaac Lab Reference Architecture + + +| + +The reference architecture for Isaac Lab comprises the following components: + +1. :ref:`Asset Input` +2. :ref:`Configuration - Assets & Scene` +3. :ref:`Robot Learning Task Design` +4. :ref:`Register with Gymnasium` +5. :ref:`Environment Wrapping` +6. :ref:`Run Training` +7. :ref:`Run Testing` + + + + +Components +~~~~~~~~~~~ +In this section, we will briefly discuss the individual blocks for creating a +sample reference application in Isaac Lab. + + +.. _ra-asset-input: + +Component 1 - Asset Input +--------------------------- +Isaac Lab accepts URDF, MJCF XML or USD files for the assets. The first step to training using Isaac Lab is to +have the USD file of your asset and the USD or URDF file of your robot. This can be achieved in +the following ways: + + +1. Design your assets or robot in Isaac Sim and export the USD file. + +2. Design your assets or robot in any software of your choice and export it to USD using Isaac Sim converters. Isaac Sim supports the different converters/importers to USD such as the `CAD Converter`_, `URDF Importer`_, `MJCF Importer`_, `Onshape Importer`_, etc. More details are found in the `Importing Assets section`_ in the `Isaac Sim Reference Architecture`_. + +3. If you already have the URDF or MJCF file of your robot, you do not need to convert to USD as Isaac Lab takes URDF and MJCF XML. + + +.. _ra-configuration: + +Component 2 - Configuration (Assets and Scene) +------------------------------------------------------ + +Asset Configuration +^^^^^^^^^^^^^^^^^^^^^^^^ + +Given that you have the asset file for your robot and other assets such as environment objects based on the task, the next step is to import them into Isaac Lab. Isaac Lab uses asset configuration classes to spawn various objects (or prims) into the scene using Python. The first step is to write a configuration class to define the properties for the assets needed to complete the task. For example, a simple go-to-goal task for a mobile robot will include the robot asset, an object like cubes to signify the goal pose visually, lights, ground plane, etc. Isaac Lab understands these assets using the configuration classes. Isaac Lab provides various sim-ready assets such as physically accurate +3D objects that encompass accurate physical properties and behavior. It also provides connected data streams to represent the real world in simulated digital worlds such as `robots `__ +like ANYbotics Anymal, Unitree H1 Humanoid, etc. as well as `sensors `__. We provide these assets configuration classes. Users can also define their own assets using the configuration classes. + +Follow the tutorial on `how to write an Articulation and ArticulationCfg class `__. + +Scene Configuration +^^^^^^^^^^^^^^^^^^^^^^^^ + +Given the individual asset configurations, the next step is to put all the assets together into a +scene. The scene configuration is a simple config class that initializes all the assets in the +scene that are needed for the task and for visualization. This is an example for the +`Cartpole example scene configuration `__, +which includes the cartpole, ground plane, and dome light. + + +.. _ra-robot-learning-task-design: + +Component 3 - Robot Learning Task Design +------------------------------------------------------ +Now, we have the scene for the task, but we need to define the robot learning task. We will focus on +`reinforcement learning (RL) `__ algorithm here. We define the RL task +that the agent is going to do. RL tasks are defined as a Markov Decision Process (MDP), +which is a stochastic decision-making process where optional decisions are made for the agents +considering their current state and environment they interact with. The environment provides the +agents’ current state or observations, and executes the actions provided by the agent. +The environment responds to the agents by providing the next states, reward of taking the +action, done flag and information about the current episode. Therefore, different components +of the MDP formulation (the environment) – states, actions, rewards, reset, done, etc. — must +be defined by the user for the agent to perform the given task. + +In Isaac Lab, we provide two different workflows for designing environments. + +Manager-based +^^^^^^^^^^^^^^^^^ +.. image:: ../../_static/task-workflows/manager-based-light.svg + :class: only-light + :align: center + :alt: Manager-based Task Workflow + +.. image:: ../../_static/task-workflows/manager-based-dark.svg + :class: only-dark + :align: center + :alt: Manager-based Task Workflow + +This workflow is modular, and the environment is decomposed into individual components (or managers) +that handle the different aspects of the environment, such as computing observations, +applying actions, and applying randomization. As a user, you define different configuration classes +for each component. + +- An RL task should have the following configuration classes: + + - Observations Config: Defines the agents’ observations for the task. + - Actions Config: Defines the agent’s action type, i.e. how the output of the agent are mapped to + the robot's control inputs. + - Rewards Config: Defines the reward function for the task + - Terminations Config: Defines the conditions for termination of an episode or when the task + is completed. + +- You can add other optional configuration classes such as Event Config which defines the set of randomizations and noisification for the agent and environment, Curriculum Config for tasks that require `curriculum learning`_ and Commands Config for tasks where the input is from a controller/setpoint controls e.g. a gamepad controller. + +.. tip:: + + To learn more on how you can design your own manager-based environment, see :ref:`tutorial-create-manager-rl-env`. + + + +Direct +^^^^^^^^ +.. image:: ../../_static/task-workflows/direct-based-light.svg + :class: only-light + :align: center + :alt: Direct-based Task Workflow + +.. image:: ../../_static/task-workflows/direct-based-dark.svg + :class: only-dark + :align: center + :alt: Direct-based Task Workflow + +In this workflow, you implement a single class that is responsible for computing observations, applying actions, and computing rewards. This workflow allows for direct control of the environment logic. + +.. tip:: + To learn more on how you can design your own direct environment, see :ref:`tutorial-create-direct-rl-env`. + +Users can choose from Isaac Lab’s large suite of pre-configured environments or users can define +their own environments. For more technical information about the two workflows, please see the +`documentation `__. + + +In addition to designing the RL task, you will need to design your agent’s model, the neural +network policy and value function. To train the RL agent to solve the task, you need to define +the hyperparameters such as number of epochs, learning rate, etc. for training and the +policy/value model architecture. This is defined in the training configuration file specific +to the RL library you want to use. Examples are created under the agent's folder in each task directory. +See an example of `RSL-RL `__ for Anymal-B. + + +.. _ra-register-gym: + +Component 4 - Register with Gymnasium +------------------------------------------------------ + +The next step is to register the environments with the gymnasium registry to allow you to create the environment using the unique environment name. +Registration is a way to make the environment accessible and reusable across different +RL algorithms and experiments. This is common in the RL community. Follow the tutorial on +`Registering an Environment `__ to learn more about how to register in your own environment. + +.. _ra-env-wrap: + +Component 5 - Environment Wrapping +------------------------------------------------------ +In running your RL task, you might want to change the behavior of your environment without +changing the environment itself. For example, you might want to create functions to modify +observations or rewards, record videos, or enforce time limits. Isaac Lab utilizes the API +available in the `gymnasium.Wrapper `__ class to create interfaces to the simulated environments. + +Some wrappers include: + +* `Video Wrappers `__ +* `RL Libraries Wrappers `__ + +Most RL libraries expect their own variation of an environment interface. This means the +data types needed by each library differs. Isaac Lab provides its own wrappers to convert +the environment into the expected interface by the RL library a user wants to use. These are +specified in the `Isaac Lab utils wrapper module `__. + +See the `full list `__ of other wrappers APIs. For more information on how these wrappers work, +please refer to the `Wrapping environments `__ documentation. + +Adding your own wrappers +^^^^^^^^^^^^^^^^^^^^^^^^ + +You can define your own wrappers by adding them to the Isaac Lab utils wrapper module. More information is available `on the GitHub page for wrapping environments `__. + +.. _ra-run-training: + +Component 6 - Run Training +--------------------------- + +Finally, the last step is to run the training of the RL agent. Isaac Lab provides scripts which utilizes four popular RL libraries for training the models (GPU-based training): + +* `StableBaselines3 `__ +* `RSL-RL `__ +* `RL-Games `__ +* `SKRL `__ + + +.. note:: + + Isaac Lab does not provide the implementation of these RL libraries. They are already implemented by different authors. We provide the environments and framework wrappers for the RL libraries. + + + +If you want to integrate a different version of the provided algorithms or your learning library, you can follow +`these instructions `__. + + + +Single GPU Training +^^^^^^^^^^^^^^^^^^^^^^^^ +.. image:: ../../_static/reference-architecture/single-gpu-training-light.svg + :class: only-light + :align: center + :alt: Single GPU Training Data Flow + +.. image:: ../../_static/reference-architecture/single-gpu-training-dark.svg + :class: only-dark + :align: center + :alt: Single GPU Training Data Flow + +Isaac Lab supports training massively parallel environments to speed up RL training and provides rich data for the model to train. +For single GPU training, the following steps show how training works in Isaac Sim and Isaac Lab: + +1. **In Isaac Sim** + +* Isaac Sim provides the asset states such as robot and sensor states, including the observations defined in the task observation config class. + +2. **In Isaac Lab** + +* Randomizations are added to the states defined in the event configuration class to obtain the observation for the task. Randomizations are however optional. If not defined, the states are the observations. +* The observations are computed as PyTorch tensors, and it can optionally include the action provided by the trained model based on the task. + +3. **In the RL library** + +* The observation is passed to the policy. +* The policy is trained to output the right actions for the robot using RL library algorithms such as PPO, TRPO, etc. +* The actions can serve either as a setpoint for a controller that generates the action to the robot or used directly as the action to the robot based on the task. +* Action types such as joint position for a quadruped is an input to a joint controller, velocity of 1 or 0 is used to control the cart directly in the cartpole task, etc. +* In addition, based on how the task is defined, the previous action can be part of the next set of observations that is sent. + +4. **In Isaac Sim** + +* The actions from the policy are sent back to Isaac Sim to control the agent that is learning i.e. the robot. This is the physics simulation (sim) step. This generates the next states in Isaac Sim and the rewards are calculated in Isaac Lab. + +5. **Rendering** + +* The scene can be rendered to produce the cameras' images. + + +The next state is then passed in the flow till the training reaches the specified training steps or epochs. The final product is the trained model/agent. + + + +Multi-GPU and Multi-Node Training +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. image:: ../../_static/reference-architecture/multi-gpu-training-light.svg + :class: only-light + :align: center + :alt: Multi GPU Training Data Flow + +.. image:: ../../_static/reference-architecture/multi-gpu-training-dark.svg + :class: only-dark + :align: center + :alt: Multi GPU Training Data Flow + + +Isaac Lab supports scaling up training by taking advantage of multi-GPU and multi-node training on Linux. Follow the tutorial on `Multi-GPU training `__ and `Multi-Node training `__ to get started. + + +Cloud-Based Training +^^^^^^^^^^^^^^^^^^^^^^^^ +Isaac Lab can be deployed alongside Isaac Sim onto the public clouds with `Isaac Automator `__. AWS, GCP, Azure, and Alibaba Cloud are currently supported. Follow the tutorial on `how to run Isaac Lab in the cloud `__. + +.. note:: + + Both multi-GPU and multi-node jobs can be easily scaled across heterogeneous environments with `OSMO `__, a cloud-native, orchestration platform for scheduling complex multi-stage and multi-container heterogeneous computing workflows. Isaac Lab also provides the tools to run your RL task in Docker. See more details on `container deployment `__. + +.. _ra-run-testing: + +Component 7: Run Testing +----------------------------- +Isaac Lab provides scripts for `testing/playing the trained policy `__ on the environment and functions for converting the trained model from .pt to +.jit and .onnx for deployment. + + +Deployment on Physical Robots +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: ../../_static/reference-architecture/deployment-light.svg + :class: only-light + :align: center + :alt: Isaac Lab Trained Policy Deployment + +.. image:: ../../_static/reference-architecture/deployment-dark.svg + :class: only-dark + :align: center + :alt: Isaac Lab Trained Policy Deployment + + +To deploy your trained model on a real robot, you would need what is shown in the flow diagram. Note, this is a sample reference architecture, hence it can be tweaked for a different application. +First, you need a robot with the required sensors and processing computer such as `NVIDIA Jetson `__ to deploy on. Next, you need a state estimator for your robot. The state estimator should be able to deliver the list of observations used for training. + +Once the observations are extracted, they are passed into the model which delivers the action using the model inferencing runtime. The commanded action from the model serves as setpoints for the action controller. The action controller outputs scaled actions which are then used to control the robot to get to the next state, and this continues till the task is done. + +NVIDIA Isaac platform provides some tools for state estimation, including visual slam and inferencing engines such as `TensorRT `__. Other inferencing runtime includes `OnnxRuntime `__, direct inferencing on the PyTorch model, etc. + + + + +Summary +~~~~~~~~~~~ + +This document presents a reference architecture for Isaac Lab that has undergone SQA testing. We have provided a user-friendly guide to end-to-end robot learning with Isaac Lab and Isaac Sim from training to real-world deployment, including demos, examples, and documentation links. + + +How to Get Started +~~~~~~~~~~~~~~~~~~~~~~ +Check out our resources on using Isaac Lab with your robots. + +Review Our Documentation & Samples Resources + +* `Isaac Lab Tutorials`_ +* `Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab`_ +* `Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab`_ +* `Closing the Sim-to-Real Gap: Training Spot Quadruped Locomotion with NVIDIA Isaac Lab `__ +* `Additional Resources`_ + +Learn More About Featured NVIDIA Solutions + +* `Scale AI-Enabled Robotics Development Workloads with NVIDIA OSMO`_ +* `Parkour and More: How Simulation-Based RL Helps to Push the Boundaries in Legged Locomotion (GTC session) `__ +* `Isaac Perceptor`_ +* `Isaac Manipulator`_ + +.. _curriculum learning: https://arxiv.org/abs/2109.11978 +.. _CAD Converter: https://docs.omniverse.nvidia.com/extensions/latest/ext_cad-converter.html +.. _URDF Importer: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_urdf.html +.. _MJCF Importer: https://docs.omniverse.nvidia.com/isaacsim/latest/advanced_tutorials/tutorial_advanced_import_mjcf.html#import-mjcf +.. _Onshape Importer: https://docs.omniverse.nvidia.com/extensions/latest/ext_onshape.html +.. _Isaac Sim Reference Architecture: https://docs.omniverse.nvidia.com/isaacsim/latest/isaac_sim_reference_architecture.html +.. _Importing Assets section: https://docs.omniverse.nvidia.com/isaacsim/latest/isaac_sim_reference_architecture.html#importing-assets + +.. _Scale AI-Enabled Robotics Development Workloads with NVIDIA OSMO: https://developer.nvidia.com/blog/scale-ai-enabled-robotics-development-workloads-with-nvidia-osmo/ +.. _Isaac Perceptor: https://developer.nvidia.com/isaac/perceptor +.. _Isaac Manipulator: https://developer.nvidia.com/isaac/manipulator +.. _Additional Resources: https://isaac-sim.github.io/IsaacLab/main/source/refs/additional_resources.html +.. _Isaac Lab Tutorials: file:///home/oomotuyi/isaac/IsaacLab/docs/_build/current/source/tutorials/index.html +.. _Fast-Track Robot Learning in Simulation Using NVIDIA Isaac Lab: https://developer.nvidia.com/blog/fast-track-robot-learning-in-simulation-using-nvidia-isaac-lab/ +.. _Supercharge Robotics Workflows with AI and Simulation Using NVIDIA Isaac Sim 4.0 and NVIDIA Isaac Lab: https://developer.nvidia.com/blog/supercharge-robotics-workflows-with-ai-and-simulation-using-nvidia-isaac-sim-4-0-and-nvidia-isaac-lab/ diff --git a/_sources/source/refs/troubleshooting.rst b/_sources/source/refs/troubleshooting.rst new file mode 100644 index 0000000000..91c251c373 --- /dev/null +++ b/_sources/source/refs/troubleshooting.rst @@ -0,0 +1,264 @@ +Tricks and Troubleshooting +========================== + +.. note:: + + The following lists some of the common tricks and troubleshooting methods that we use in our common workflows. + Please also check the `troubleshooting page on Omniverse + `__ for more + assistance. + + +Checking the internal logs from the simulator +--------------------------------------------- + +When running the simulator from a standalone script, it logs warnings and errors to the terminal. At the same time, +it also logs internal messages to a file. These are useful for debugging and understanding the internal state of the +simulator. Depending on your system, the log file can be found in the locations listed +`here `_. + +To obtain the exact location of the log file, you need to check the first few lines of the terminal output when +you run the standalone script. The log file location is printed at the start of the terminal output. For example: + +.. code:: bash + + [INFO] Using python from: /home/${USER}/git/IsaacLab/_isaac_sim/python.sh + ... + Passing the following args to the base kit application: [] + Loading user config located at: '.../data/Kit/Isaac-Sim/2023.1/user.config.json' + [Info] [carb] Logging to file: '.../logs/Kit/Isaac-Sim/2023.1/kit_20240328_183346.log' + + +In the above example, the log file is located at ``.../logs/Kit/Isaac-Sim/2023.1/kit_20240328_183346.log``, +``...`` is the path to the user's log directory. The log file is named ``kit_20240328_183346.log`` + +You can open this file to check the internal logs from the simulator. Also when reporting issues, please include +this log file to help us debug the issue. + +Changing logging channel levels for the simulator +------------------------------------------------- + +By default, the simulator logs messages at the ``WARN`` level and above on the terminal. You can change the logging +channel levels to get more detailed logs. The logging channel levels can be set through Omniverse's logging system. + +To obtain more detailed logs, you can run your application with the following flags: + +* ``--info``: This flag logs messages at the ``INFO`` level and above. +* ``--verbose``: This flag logs messages at the ``VERBOSE`` level and above. + +For instance, to run a standalone script with verbose logging, you can use the following command: + +.. code-block:: bash + + # Run the standalone script with info logging + ./isaaclab.sh -p source/standalone/tutorials/00_sim/create_empty.py --headless --info + +For more fine-grained control, you can modify the logging channels through the ``omni.log`` module. +For more information, please refer to its `documentation `__. + +Using CPU Scaling Governor for performance +------------------------------------------ + +By default on many systems, the CPU frequency governor is set to +“powersave” mode, which sets the CPU to lowest static frequency. To +increase the maximum performance, we recommend setting the CPU frequency +governor to “performance” mode. For more details, please check the the +link +`here `__. + +.. warning:: + We advice not to set the governor to “performance” mode on a system with poor + cooling (such as laptops), since it may cause the system to overheat. + +- To view existing ``scaling_governor`` value per CPU: + + .. code:: bash + + cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + +- To change the governor to “performance” mode for each CPU: + + .. code:: bash + + echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor + + +Observing long load times at the start of the simulation +-------------------------------------------------------- + +The first time you run the simulator, it will take a long time to load up. This is because the +simulator is compiling shaders and loading assets. Subsequent runs should be faster to start up, +but may still take some time. + +Please note that once the Isaac Sim app loads, the environment creation time may scale linearly with +the number of environments. Please expect a longer load time if running with thousands of +environments or if each environment contains a larger number of assets. We are continually working +on improving the time needed for this. + +When an instance of Isaac Sim is already running, launching another Isaac Sim instance in a different +process may appear to hang at startup for the first time. Please be patient and give it some time as +the second process will take longer to start up due to slower shader compilation. + + +Receiving a “PhysX error” when running simulation on GPU +-------------------------------------------------------- + +When using the GPU pipeline, the buffers used for the physics simulation are allocated on the GPU only +once at the start of the simulation. This means that they do not grow dynamically as the number of +collisions or objects in the scene changes. If the number of collisions or objects in the scene +exceeds the size of the buffers, the simulation will fail with an error such as the following: + +.. code:: bash + + PhysX error: the application need to increase the PxgDynamicsMemoryConfig::foundLostPairsCapacity + parameter to 3072, otherwise the simulation will miss interactions + +In this case, you need to increase the size of the buffers passed to the +:class:`~omni.isaac.lab.sim.SimulationContext` class. The size of the buffers can be increased by setting +the :attr:`~omni.isaac.lab.sim.PhysxCfg.gpu_found_lost_pairs_capacity` parameter in the +:class:`~omni.isaac.lab.sim.PhysxCfg` class. For example, to increase the size of the buffers to +4096, you can use the following code: + +.. code:: python + + import omni.isaac.lab.sim as sim_utils + + sim_cfg = sim_utils.SimulationConfig() + sim_cfg.physx.gpu_found_lost_pairs_capacity = 4096 + sim = SimulationContext(sim_params=sim_cfg) + +Please see the documentation for :class:`~omni.isaac.lab.sim.SimulationCfg` for more details +on the parameters that can be used to configure the simulation. + + +Preventing memory leaks in the simulator +---------------------------------------- + +Memory leaks in the Isaac Sim simulator can occur when C++ callbacks are registered with Python objects. +This happens when callback functions within classes maintain references to the Python objects they are +associated with. As a result, Python's garbage collection is unable to reclaim memory associated with +these objects, preventing the corresponding C++ objects from being destroyed. Over time, this can lead +to memory leaks and increased resource usage. + +To prevent memory leaks in the Isaac Sim simulator, it is essential to use weak references when registering +callbacks with the simulator. This ensures that Python objects can be garbage collected when they are no +longer needed, thereby avoiding memory leaks. The `weakref `_ +module from the Python standard library can be employed for this purpose. + + +For example, consider a class with a callback function ``on_event_callback`` that needs to be registered +with the simulator. If you use a strong reference to the ``MyClass`` object when passing the callback, +the reference count of the ``MyClass`` object will be incremented. This prevents the ``MyClass`` object +from being garbage collected when it is no longer needed, i.e., the ``__del__`` destructor will not be +called. + +.. code:: python + + import omni.kit + + class MyClass: + def __init__(self): + app_interface = omni.kit.app.get_app_interface() + self._handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + self.on_event_callback + ) + + def __del__(self): + self._handle.unsubscribe() + self._handle = None + + def on_event_callback(self, event): + # do something with the message + + +To fix this issue, it's crucial to employ weak references when registering the callback. While this approach +adds some verbosity to the code, it ensures that the ``MyClass`` object can be garbage collected when no longer +in use. Here's the modified code: + +.. code:: python + + import omni.kit + import weakref + + class MyClass: + def __init__(self): + app_interface = omni.kit.app.get_app_interface() + self._handle = app_interface.get_post_update_event_stream().create_subscription_to_pop( + lambda event, obj=weakref.proxy(self): obj.on_event_callback(event) + ) + + def __del__(self): + self._handle.unsubscribe() + self._handle = None + + def on_event_callback(self, event): + # do something with the message + + +In this revised code, the weak reference ``weakref.proxy(self)`` is used when registering the callback, +allowing the ``MyClass`` object to be properly garbage collected. + +By following this pattern, you can prevent memory leaks and maintain a more efficient and stable simulation. + + +Understanding the error logs from crashes +----------------------------------------- + +Many times the simulator crashes due to a bug in the implementation. +This swamps the terminal with exceptions, some of which are coming from +the python interpreter calling ``__del__()`` destructor of the +simulation application. These typically look like the following: + +.. code:: bash + + ... + + [INFO]: Completed setting up the environment... + + Traceback (most recent call last): + File "source/standalone/workflows/robomimic/collect_demonstrations.py", line 166, in + main() + File "source/standalone/workflows/robomimic/collect_demonstrations.py", line 126, in main + actions = pre_process_actions(delta_pose, gripper_command) + File "source/standalone/workflows/robomimic/collect_demonstrations.py", line 57, in pre_process_actions + return torch.concat([delta_pose, gripper_vel], dim=1) + TypeError: expected Tensor as element 1 in argument 0, but got int + Exception ignored in: ._Registry.__del__ at 0x7f94ac097f80> + Traceback (most recent call last): + File "../IsaacLab/_isaac_sim/kit/extscore/omni.kit.viewport.registry/omni/kit/viewport/registry/registry.py", line 103, in __del__ + File "../IsaacLab/_isaac_sim/kit/extscore/omni.kit.viewport.registry/omni/kit/viewport/registry/registry.py", line 98, in destroy + TypeError: 'NoneType' object is not callable + Exception ignored in: ._Registry.__del__ at 0x7f94ac097f80> + Traceback (most recent call last): + File "../IsaacLab/_isaac_sim/kit/extscore/omni.kit.viewport.registry/omni/kit/viewport/registry/registry.py", line 103, in __del__ + File "../IsaacLab/_isaac_sim/kit/extscore/omni.kit.viewport.registry/omni/kit/viewport/registry/registry.py", line 98, in destroy + TypeError: 'NoneType' object is not callable + Exception ignored in: + Traceback (most recent call last): + File "../IsaacLab/_isaac_sim/kit/kernel/py/omni/kit/app/_impl/__init__.py", line 114, in __del__ + AttributeError: 'NoneType' object has no attribute 'get_settings' + Exception ignored in: + Traceback (most recent call last): + File "../IsaacLab/_isaac_sim/extscache/omni.kit.viewport.menubar.lighting-104.0.7/omni/kit/viewport/menubar/lighting/actions.py", line 345, in __del__ + File "../IsaacLab/_isaac_sim/extscache/omni.kit.viewport.menubar.lighting-104.0.7/omni/kit/viewport/menubar/lighting/actions.py", line 350, in destroy + TypeError: 'NoneType' object is not callable + 2022-12-02 15:41:54 [18,514ms] [Warning] [carb.audio.context] 1 contexts were leaked + ../IsaacLab/_isaac_sim/python.sh: line 41: 414372 Segmentation fault (core dumped) $python_exe "$@" $args + There was an error running python + +This is a known error with running standalone scripts with the Isaac Sim +simulator. Please scroll above the exceptions thrown with +``registry`` to see the actual error log. + +In the above case, the actual error is: + +.. code:: bash + + Traceback (most recent call last): + File "source/standalone/workflows/robomimic/tools/collect_demonstrations.py", line 166, in + main() + File "source/standalone/workflows/robomimic/tools/collect_demonstrations.py", line 126, in main + actions = pre_process_actions(delta_pose, gripper_command) + File "source/standalone/workflows/robomimic/tools/collect_demonstrations.py", line 57, in pre_process_actions + return torch.concat([delta_pose, gripper_vel], dim=1) + TypeError: expected Tensor as element 1 in argument 0, but got int diff --git a/_sources/source/setup/ecosystem.rst b/_sources/source/setup/ecosystem.rst new file mode 100644 index 0000000000..107c95843d --- /dev/null +++ b/_sources/source/setup/ecosystem.rst @@ -0,0 +1,32 @@ +Isaac Lab Ecosystem +=================== + +Isaac Lab 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). 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. + +Working with Isaac Lab requires the installation of Isaac Sim, which is packaged with core robotics tools +that Isaac Lab depends on, including URDF and MJCF importers, simulation managers, and ROS features. Isaac +Sim also builds on top of the Nvidia Omniverse platform, leveraging advanced physics simulation from PhysX, +photorealistic rendering technologies, and Universal Scene Description (USD) for scene creation. + +Isaac Lab 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. + +.. image:: ../_static/setup/ecosystem-light.jpg + :class: only-light + :align: center + :alt: The Isaac Lab, Isaac Sim, and Nvidia Omniverse ecosystem + +.. image:: ../_static/setup/ecosystem-dark.jpg + :class: only-dark + :align: center + :alt: The Isaac Lab, Isaac Sim, and Nvidia Omniverse ecosystem + + +For a detailed explanation of Nvidia's development journey of robot learning frameworks, please visit +the `FAQ page `_. diff --git a/_sources/source/setup/faq.rst b/_sources/source/setup/faq.rst new file mode 100644 index 0000000000..f088854cf9 --- /dev/null +++ b/_sources/source/setup/faq.rst @@ -0,0 +1,87 @@ +Frequently Asked Questions +========================== + +Where does Isaac Lab fit in the Isaac ecosystem? +------------------------------------------------ + +Over the years, NVIDIA has developed a number of tools for robotics and AI. These tools leverage +the power of GPUs to accelerate the simulation both in terms of speed and realism. They show great +promise in the field of simulation technology and are being used by many researchers and companies +worldwide. + +`Isaac Gym`_ :cite:`makoviychuk2021isaac` provides a high performance GPU-based physics simulation +for robot learning. It is built on top of `PhysX`_ which supports GPU-accelerated simulation of rigid bodies +and a Python API to directly access physics simulation data. Through an end-to-end GPU pipeline, it is possible +to achieve high frame rates compared to CPU-based physics engines. The tool has been used successfully in a +number of research projects, including legged locomotion :cite:`rudin2022learning` :cite:`rudin2022advanced`, +in-hand manipulation :cite:`handa2022dextreme` :cite:`allshire2022transferring`, and industrial assembly +:cite:`narang2022factory`. + +Despite the success of Isaac Gym, it is not designed to be a general purpose simulator for +robotics. For example, it does not include interaction between deformable and rigid objects, high-fidelity +rendering, and support for ROS. The tool has been primarily designed as a preview release to showcase the +capabilities of the underlying physics engine. With the release of `Isaac Sim`_, NVIDIA is building +a general purpose simulator for robotics and has integrated the functionalities of Isaac Gym into +Isaac Sim. + +`Isaac Sim`_ is a robot simulation toolkit built on top of Omniverse, which is a general purpose platform +that aims to unite complex 3D workflows. Isaac Sim leverages the latest advances in graphics and +physics simulation to provide a high-fidelity simulation environment for robotics. It supports +ROS/ROS2, various sensor simulation, tools for domain randomization and synthetic data creation. +Tiled rendering support in Isaac Sim allows for vectorized rendering across environments, along with +support for running in the cloud using `Isaac Automator`_. +Overall, it is a powerful tool for roboticists and is a huge step forward in the field of robotics +simulation. + +With the release of above two tools, NVIDIA also released an open-sourced set of environments called +`IsaacGymEnvs`_ and `OmniIsaacGymEnvs`_, that have been built on top of Isaac Gym and Isaac Sim respectively. +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 Isaac Lab comes in. + +Isaac Lab 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). 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. + +Isaac Lab replaces the previous `IsaacGymEnvs`_, `OmniIsaacGymEnvs`_ and `Orbit`_ frameworks and will +be the single robot learning framework for Isaac Sim. Previously released frameworks are deprecated +and we encourage users to follow our `migration guides`_ to transition over to Isaac Lab. + + +Why should I use Isaac Lab? +--------------------------- + +Since Isaac Sim remains closed-sourced, it is difficult for users to contribute to the simulator and build a +common framework for research. On its current path, we see the community using the simulator will simply +develop their own frameworks that will result in scattered efforts with a lot of duplication of work. +This has happened in the past with other simulators, and we believe that it is not the best way to move +forward as a community. + +Isaac Lab provides an open-sourced platform for the community to drive progress with consolidated efforts +toward designing benchmarks and robot learning systems as a joint initiative. This allows us to reuse +existing components and algorithms, and to build on top of each other's work. Doing so not only saves +time and effort, but also allows us to focus on the more important aspects of research. Our hope with +Isaac Lab is that it becomes the de-facto platform for robot learning research and an environment *zoo* +that leverages Isaac Sim. As the framework matures, we foresee it benefitting hugely from the latest +simulation developments (as part of internal developments at NVIDIA and collaborating partners) +and research in robotics. + +We are already working with labs in universities and research institutions to integrate their work into Isaac Lab +and hope that others in the community will join us too in this effort. If you are interested in contributing +to Isaac Lab, please reach out to us. + + +.. _PhysX: https://developer.nvidia.com/physx-sdk +.. _Isaac Sim: https://developer.nvidia.com/isaac-sim +.. _Isaac Gym: https://developer.nvidia.com/isaac-gym +.. _IsaacGymEnvs: https://github.com/isaac-sim/IsaacGymEnvs +.. _OmniIsaacGymEnvs: https://github.com/isaac-sim/OmniIsaacGymEnvs +.. _Orbit: https://isaac-orbit.github.io/ +.. _Isaac Automator: https://github.com/isaac-sim/IsaacAutomator +.. _migration guides: ../migration/index.html diff --git a/_sources/source/setup/installation/binaries_installation.rst b/_sources/source/setup/installation/binaries_installation.rst new file mode 100644 index 0000000000..73138ebd17 --- /dev/null +++ b/_sources/source/setup/installation/binaries_installation.rst @@ -0,0 +1,420 @@ +.. _isaacsim-binaries-installation: + + +Installation using Isaac Sim Binaries +===================================== + +.. note:: + + If you use Conda, we recommend using `Miniconda `_. + +Installing Isaac Sim +-------------------- + +Downloading pre-built binaries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Please follow the Isaac Sim +`documentation `__ +to install the latest Isaac Sim release. + +To check the minimum system requirements,refer to the documentation +`here `__. + +.. note:: + We have tested Isaac Lab with Isaac Sim 4.1 release on Ubuntu + 20.04LTS with NVIDIA driver 525.147. + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + On Linux systems, by default, Isaac Sim is installed in the directory + ``${HOME}/.local/share/ov/pkg/isaac_sim-*``, with ``*`` corresponding to the Isaac Sim version. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + On Windows systems, by default,Isaac Sim is installed in the directory + ``%USERPROFILE%\AppData\Local\ov\pkg\isaac_sim-*``, with ``*`` corresponding to the Isaac Sim version. + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To avoid the overhead of finding and locating the Isaac Sim installation +directory every time, we recommend exporting the following environment +variables to your terminal for the remaining of the installation instructions: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Isaac Sim root directory + export ISAACSIM_PATH="${HOME}/.local/share/ov/pkg/isaac-sim-4.2.0" + # Isaac Sim python executable + export ISAACSIM_PYTHON_EXE="${ISAACSIM_PATH}/python.sh" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Isaac Sim root directory + set ISAACSIM_PATH="%USERPROFILE%\AppData\Local\ov\pkg\isaac-sim-4.2.0" + :: Isaac Sim python executable + set ISAACSIM_PYTHON_EXE="%ISAACSIM_PATH:"=%\python.bat" + + +For more information on common paths, please check the Isaac Sim +`documentation `__. + + +- Check that the simulator runs as expected: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + ${ISAACSIM_PATH}/isaac-sim.sh + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: note: you can pass the argument "--help" to see all arguments possible. + %ISAACSIM_PATH%\isaac-sim.bat + + +- Check that the simulator runs from a standalone python script: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # checks that python path is set correctly + ${ISAACSIM_PYTHON_EXE} -c "print('Isaac Sim configuration is now complete.')" + # checks that Isaac Sim can be launched from python + ${ISAACSIM_PYTHON_EXE} ${ISAACSIM_PATH}/standalone_examples/api/omni.isaac.core/add_cubes.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: checks that python path is set correctly + %ISAACSIM_PYTHON_EXE% -c "print('Isaac Sim configuration is now complete.')" + :: checks that Isaac Sim can be launched from python + %ISAACSIM_PYTHON_EXE% %ISAACSIM_PATH%\standalone_examples\api\omni.isaac.core\add_cubes.py + + +.. caution:: + + If you have been using a previous version of Isaac Sim, you need to run the following command for the *first* + time after installation to remove all the old user data and cached variables: + + .. tab-set:: + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + + .. code:: bash + + ${ISAACSIM_PATH}/isaac-sim.sh --reset-user + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + + .. code:: batch + + %ISAACSIM_PATH%\isaac-sim.bat --reset-user + + +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 `__ +and the +`forums `__. + + +Installing Isaac Lab +-------------------- + +Cloning Isaac Lab +~~~~~~~~~~~~~~~~~ + +.. note:: + + We recommend making a `fork `_ of the Isaac Lab repository to contribute + to the project but this is not mandatory to use the framework. If you + make a fork, please replace ``isaac-sim`` with your username + in the following instructions. + +Clone the Isaac Lab repository into your workspace: + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:isaac-sim/IsaacLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacLab.git + + +.. note:: + We provide a helper executable `isaaclab.sh `_ that provides + utilities to manage extensions: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: text + + ./isaaclab.sh --help + + usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl-games, rsl-rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. + -t, --test Run all python unittest tests. + -o, --docker Run the docker container helper script (docker/container.sh). + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'isaaclab'. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: text + + isaaclab.bat --help + + usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl-games, rsl-rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. + -t, --test Run all python unittest tests. + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'isaaclab'. + + +Creating the Isaac Sim Symbolic Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set up a symbolic link between the installed Isaac Sim root folder +and ``_isaac_sim`` in the Isaac Lab directory. This makes it convenient +to index the python modules and look for extensions shipped with Isaac Sim. + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # enter the cloned repository + cd IsaacLab + # create a symbolic link + ln -s path_to_isaac_sim _isaac_sim + # For example: ln -s /home/nvidia/.local/share/ov/pkg/isaac-sim-4.2.0 _isaac_sim + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: enter the cloned repository + cd IsaacLab + :: create a symbolic link - requires launching Command Prompt with Administrator access + mklink /D _isaac_sim path_to_isaac_sim + :: For example: mklink /D _isaac_sim C:/Users/nvidia/AppData/Local/ov/pkg/isaac-sim-4.2.0 + + +Setting up the conda environment (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. attention:: + This step is optional. If you are using the bundled python with Isaac Sim, you can skip this step. + +The executable ``isaaclab.sh`` automatically fetches the python bundled with Isaac +Sim, using ``./isaaclab.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 `__. + +Although using a virtual environment is optional, we recommend using ``conda``. To install +``conda``, please follow the instructions `here `__. +In case you want to use ``conda`` to create a virtual environment, you can +use the following command: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Default name for conda environment is 'isaaclab' + ./isaaclab.sh --conda # or "./isaaclab.sh -c" + # Option 2: Custom name for conda environment + ./isaaclab.sh --conda my_env # or "./isaaclab.sh -c my_env" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Default name for conda environment is 'isaaclab' + isaaclab.bat --conda :: or "isaaclab.bat -c" + :: Option 2: Custom name for conda environment + isaaclab.bat --conda my_env :: or "isaaclab.bat -c my_env" + + +If you are using ``conda`` to create a virtual environment, make sure to +activate the environment before running any scripts. For example: + +.. code:: bash + + conda activate isaaclab # or "conda activate my_env" + +Once you are in the virtual environment, you do not need to use ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` +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 ``./isaaclab.sh -p`` / ``isaaclab.bat -p`` to run python scripts. This command +is equivalent to running ``python`` or ``python3`` in your virtual environment. + + +Installation +~~~~~~~~~~~~ + +- Install dependencies using ``apt`` (on Linux only): + + .. code:: bash + + # these dependency are needed by robomimic which is not available on Windows + sudo apt install cmake build-essential + +- Run the install command that iterates over all the extensions in ``source/extensions`` directory and installs them + using pip (with ``--editable`` flag): + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install # or "./isaaclab.sh -i" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat --install :: or "isaaclab.bat -i" + +.. note:: + + By default, the above will install all the learning frameworks. If you want to install only a specific framework, you can + pass the name of the framework as an argument. For example, to install only the ``rl_games`` framework, you can run + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" + + The valid options are ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, ``none``. + +Verifying the Isaac Lab installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To verify that the installation was successful, run the following command from the +top of the repository: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Using the isaaclab.sh executable + # note: this works for both the bundled python and the virtual environment + ./isaaclab.sh -p source/standalone/tutorials/00_sim/create_empty.py + + # Option 2: Using python in your virtual environment + python source/standalone/tutorials/00_sim/create_empty.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Using the isaaclab.bat executable + :: note: this works for both the bundled python and the virtual environment + isaaclab.bat -p source\standalone\tutorials\00_sim\create_empty.py + + :: Option 2: Using python in your virtual environment + python source\standalone\tutorials\00_sim\create_empty.py + + +The above command should launch the simulator and display a window with a black +viewport. You can exit the script by pressing ``Ctrl+C`` on your terminal. +On Windows machines, please terminate the process from Command Prompt using +``Ctrl+Break`` or ``Ctrl+fn+B``. + +.. figure:: ../../_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + +If you see this, then the installation was successful! |:tada:| diff --git a/_sources/source/setup/installation/cloud_installation.rst b/_sources/source/setup/installation/cloud_installation.rst new file mode 100644 index 0000000000..00703d2290 --- /dev/null +++ b/_sources/source/setup/installation/cloud_installation.rst @@ -0,0 +1,155 @@ +Running Isaac Lab in the Cloud +============================== + +Isaac Lab can be run in various cloud infrastructures with the use of `Isaac Automator `__. +Isaac Automator allows for quick deployment of Isaac Sim and Isaac Lab onto the public clouds (AWS, GCP, Azure, and Alibaba Cloud are currently supported). + +The result is a fully configured remote desktop cloud workstation, which can be used for development and testing of Isaac Lab within minutes and on a budget. Isaac Automator supports variety of GPU instances and stop-start functionality to save on cloud costs and a variety of tools to aid the workflow (like uploading and downloading data, autorun, deployment management, etc). + + +Installing Isaac Automator +-------------------------- + +For the most update-to-date and complete installation instructions, please refer to `Isaac Automator `__. + +To use Isaac Automator, first clone the repo: + +.. code-block:: bash + + git clone https://github.com/isaac-sim/IsaacAutomator.git + +Isaac Automator requires having ``docker`` pre-installed on the system. + +* To install Docker, please follow the instructions for your operating system on the `Docker website`_. +* Follow the post-installation steps for Docker on the `post-installation steps`_ page. These steps allow you to run + Docker without using ``sudo``. + +Isaac Automator also requires obtaining a NGC API key. + +* Get access to the `Isaac Sim container`_ by joining the NVIDIA Developer Program credentials. +* Generate your `NGC API key`_ to access locked container images from NVIDIA GPU Cloud (NGC). + + * This step requires you to create an NGC account if you do not already have one. + * Once you have your generated API key, you need to log in to NGC + from the terminal. + + .. code:: bash + + docker login nvcr.io + + * For the username, enter ``$oauthtoken`` exactly as shown. It is a special username that is used to + authenticate with NGC. + + .. code:: text + + Username: $oauthtoken + Password: + + +Running Isaac Automator +----------------------- + +To run Isaac Automator, first build the Isaac Automator container: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + ./build + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + docker build --platform linux/x86_64 -t isa . + +Next, enter the automator container: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + ./run + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + docker run --platform linux/x86_64 -it --rm -v .:/app isa bash + +Next, run the deployed script for your preferred cloud: + +.. code-block:: bash + + # AWS + ./deploy-aws + # Azure + ./deploy-azure + # GCP + ./deploy-gcp + # Alibaba Cloud + ./deploy-alicloud + +Follow the prompts for entering information regarding the environment setup and credentials. +Once successful, instructions for connecting to the cloud instance will be available in the terminal. +Connections can be made using SSH, noVCN, or NoMachine. + +For details on the credentials and setup required for each cloud, please visit the +`Isaac Automator `__ +page for more instructions. + + +Running Isaac Lab on the Cloud +------------------------------ + +Once connected to the cloud instance, the desktop will have an icon showing ``isaaclab.sh``. +Launch the ``isaaclab.sh`` executable, which will open a new Terminal. Within the terminal, +Isaac Lab commands can be executed in the same way as running locally. + +For example: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + ./isaaclab.bat -p source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-v0 + + +Destroying a Development +------------------------- + +To save costs, deployments can be destroyed when not being used. +This can be done from within the Automator container, which can be entered with command ``./run``. + +To destroy a deployment, run: + +.. code:: bash + + ./destroy + + +.. _`Docker website`: https://docs.docker.com/desktop/install/linux-install/ +.. _`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 diff --git a/_sources/source/setup/installation/index.rst b/_sources/source/setup/installation/index.rst new file mode 100644 index 0000000000..90c8937cfc --- /dev/null +++ b/_sources/source/setup/installation/index.rst @@ -0,0 +1,39 @@ +Local Installation +================== + +.. image:: https://img.shields.io/badge/IsaacSim-4.2.0-silver.svg + :target: https://developer.nvidia.com/isaac-sim + :alt: IsaacSim 4.2.0 + +.. 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-orange.svg + :target: https://releases.ubuntu.com/20.04/ + :alt: Ubuntu 20.04 + +.. image:: https://img.shields.io/badge/platform-windows--64-orange.svg + :target: https://www.microsoft.com/en-ca/windows/windows-11 + :alt: Windows 11 + +.. caution:: + + We have dropped support for Isaac Sim versions 4.0.0 and below. We recommend using the latest + Isaac Sim 4.2.0 release to benefit from the latest features and improvements. + + For more information, please refer to the + `Isaac Sim release notes `__. + +.. note:: + + We recommend system requirements with at least 32GB RAM and 16GB VRAM for Isaac Lab. + For the full list of system requirements for Isaac Sim, please refer to the + `Isaac Sim system requirements `_. + + +.. toctree:: + :maxdepth: 2 + + Pip installation (recommended for Ubuntu 22.04 and Windows) + Binary installation (recommended for Ubuntu 20.04) diff --git a/_sources/source/setup/installation/pip_installation.rst b/_sources/source/setup/installation/pip_installation.rst new file mode 100644 index 0000000000..b0ddf378d0 --- /dev/null +++ b/_sources/source/setup/installation/pip_installation.rst @@ -0,0 +1,331 @@ +.. _isaacsim-pip-installation: + +Installation using Isaac Sim pip +================================ + +.. note:: + + If you use Conda, we recommend using `Miniconda `_. + +Installing Isaac Sim +-------------------- + +From Isaac Sim 4.0 release, it is possible to install Isaac Sim using pip. This approach is experimental and may have +compatibility issues with some Linux distributions. If you encounter any issues, please report them to the +`Isaac Sim Forums `_. + +.. attention:: + + Installing Isaac Sim with pip requires GLIBC 2.34+ version compatibility. + To check the GLIBC version on your system, use command ``ldd --version``. + + This may pose compatibility issues with some Linux distributions. For instance, Ubuntu 20.04 LTS has GLIBC 2.31 + by default. If you encounter compatibility issues, we recommend following the + :ref:`Isaac Sim Binaries Installation ` approach. + +.. attention:: + + On Windows with CUDA 12, the GPU driver version 552.86 is required. + +- To use the pip installation approach for Isaac Sim, we recommend first creating a virtual environment. + Ensure that the python version of the virtual environment is **Python 3.10**. + + .. tab-set:: + + .. tab-item:: conda environment + + .. code-block:: bash + + conda create -n isaaclab python=3.10 + conda activate isaaclab + + .. tab-item:: venv environment + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code-block:: bash + + # create a virtual environment named isaaclab with python3.10 + python3.10 -m venv isaaclab + # activate the virtual environment + source isaaclab/bin/activate + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code-block:: batch + + # create a virtual environment named isaaclab with python3.10 + python3.10 -m venv isaaclab + # activate the virtual environment + isaaclab\Scripts\activate + + +- Next, install a CUDA-enabled PyTorch 2.4.0 build based on the CUDA version available on your system. This step is optional for Linux, but required for Windows to ensure a CUDA-compatible version of PyTorch is installed. + + .. tab-set:: + + .. tab-item:: CUDA 11 + + .. code-block:: bash + + pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/cu118 + + .. tab-item:: CUDA 12 + + .. code-block:: bash + + pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/cu121 + +- Before installing Isaac Sim, ensure the latest pip version is installed. To update pip, run + + .. code-block:: bash + + pip install --upgrade pip + + +- Then, install the Isaac Sim package + + .. code-block:: bash + + pip install isaacsim==4.2.0.2 --extra-index-url https://pypi.nvidia.com + + +- To install a minimal set of packages for running Isaac Lab only, the following command can be used. Note that you cannot run ``isaacsim`` with this. + + .. code-block:: bash + + pip install isaacsim-rl isaacsim-replicator isaacsim-extscache-physics isaacsim-extscache-kit-sdk isaacsim-extscache-kit isaacsim-app --extra-index-url https://pypi.nvidia.com + + +Verifying the Isaac Sim installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Make sure that your virtual environment is activated (if applicable) + + +- Check that the simulator runs as expected: + + .. code:: bash + + # note: you can pass the argument "--help" to see all arguments possible. + isaacsim + + By default, this will launch an empty mini Kit window. + +- To run with a specific experience file, run: + + .. code:: bash + + # experience files can be absolute path, or relative path searched in isaacsim/apps or omni/apps + isaacsim omni.isaac.sim.python.kit + + +.. attention:: + + When running Isaac Sim for the first time, all dependent extensions will be pulled from the registry. + This process can take upwards of 10 minutes and is required on the first run of each experience file. + Once the extensions are pulled, consecutive runs using the same experience file will use the cached extensions. + + In addition, the first run will prompt users to accept the Nvidia Omniverse License Agreement. + To accept the EULA, reply ``Yes`` when prompted with the below message: + + .. code:: bash + + By installing or using Isaac Sim, I agree to the terms of NVIDIA OMNIVERSE LICENSE AGREEMENT (EULA) + in https://docs.omniverse.nvidia.com/isaacsim/latest/common/NVIDIA_Omniverse_License_Agreement.html + + Do you accept the EULA? (Yes/No): Yes + + +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 `__ +and the +`forums `__. + + + +Installing Isaac Lab +-------------------- + +Cloning Isaac Lab +~~~~~~~~~~~~~~~~~ + +.. note:: + + We recommend making a `fork `_ of the Isaac Lab repository to contribute + to the project but this is not mandatory to use the framework. If you + make a fork, please replace ``isaac-sim`` with your username + in the following instructions. + +Clone the Isaac Lab repository into your workspace: + +.. tab-set:: + + .. tab-item:: SSH + + .. code:: bash + + git clone git@github.com:isaac-sim/IsaacLab.git + + .. tab-item:: HTTPS + + .. code:: bash + + git clone https://github.com/isaac-sim/IsaacLab.git + + +.. note:: + We provide a helper executable `isaaclab.sh `_ that provides + utilities to manage extensions: + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: text + + ./isaaclab.sh --help + + usage: isaaclab.sh [-h] [-i] [-f] [-p] [-s] [-t] [-o] [-v] [-d] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.sh) provided by Isaac Sim. + -t, --test Run all python unittest tests. + -o, --docker Run the docker container helper script (docker/container.sh). + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'isaaclab'. + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: text + + isaaclab.bat --help + + usage: isaaclab.bat [-h] [-i] [-f] [-p] [-s] [-v] [-d] [-c] -- Utility to manage Isaac Lab. + + optional arguments: + -h, --help Display the help content. + -i, --install [LIB] Install the extensions inside Isaac Lab and learning frameworks (rl_games, rsl_rl, sb3, skrl) as extra dependencies. Default is 'all'. + -f, --format Run pre-commit to format the code and check lints. + -p, --python Run the python executable provided by Isaac Sim or virtual environment (if active). + -s, --sim Run the simulator executable (isaac-sim.bat) provided by Isaac Sim. + -t, --test Run all python unittest tests. + -v, --vscode Generate the VSCode settings file from template. + -d, --docs Build the documentation from source using sphinx. + -c, --conda [NAME] Create the conda environment for Isaac Lab. Default name is 'isaaclab'. + +Installation +~~~~~~~~~~~~ + +- Install dependencies using ``apt`` (on Ubuntu): + + .. code:: bash + + sudo apt install cmake build-essential + +- Run the install command that iterates over all the extensions in ``source/extensions`` directory and installs them + using pip (with ``--editable`` flag): + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install # or "./isaaclab.sh -i" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: bash + + isaaclab.bat --install :: or "isaaclab.bat -i" + +.. note:: + + By default, this will install all the learning frameworks. If you want to install only a specific framework, you can + pass the name of the framework as an argument. For example, to install only the ``rl_games`` framework, you can run + + .. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + ./isaaclab.sh --install rl_games # or "./isaaclab.sh -i rl_games" + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: bash + + isaaclab.bat --install rl_games :: or "isaaclab.bat -i rl_games" + + The valid options are ``rl_games``, ``rsl_rl``, ``sb3``, ``skrl``, ``robomimic``, ``none``. + +Verifying the Isaac Lab installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To verify that the installation was successful, run the following command from the +top of the repository: + +.. tab-set:: + :sync-group: os + + .. tab-item:: :icon:`fa-brands fa-linux` Linux + :sync: linux + + .. code:: bash + + # Option 1: Using the isaaclab.sh executable + # note: this works for both the bundled python and the virtual environment + ./isaaclab.sh -p source/standalone/tutorials/00_sim/create_empty.py + + # Option 2: Using python in your virtual environment + python source/standalone/tutorials/00_sim/create_empty.py + + .. tab-item:: :icon:`fa-brands fa-windows` Windows + :sync: windows + + .. code:: batch + + :: Option 1: Using the isaaclab.bat executable + :: note: this works for both the bundled python and the virtual environment + isaaclab.bat -p source\standalone\tutorials\00_sim\create_empty.py + + :: Option 2: Using python in your virtual environment + python source\standalone\tutorials\00_sim\create_empty.py + + +The above command should launch the simulator and display a window with a black +viewport as shown below. You can exit the script by pressing ``Ctrl+C`` on your terminal. +On Windows machines, please terminate the process from Command Prompt using +``Ctrl+Break`` or ``Ctrl+fn+B``. + + +.. figure:: ../../_static/setup/verify_install.jpg + :align: center + :figwidth: 100% + :alt: Simulator with a black window. + + +If you see this, then the installation was successful! |:tada:| diff --git a/_sources/source/setup/translation.rst b/_sources/source/setup/translation.rst new file mode 100644 index 0000000000..7e1d17facd --- /dev/null +++ b/_sources/source/setup/translation.rst @@ -0,0 +1,108 @@ +关于翻译 +========================= + +.. attention:: + + 本翻译项目不属于 NVIDIA 或 IsaacLab 官方文档,由 `范子琦 `__ 提供中文翻译,仅供学习交流使用,禁止转载或用于商业用途。 + + 官方文档引入了版本系统,可以查看历史版本的文档。译者精力有限,故不提供历史版本翻译,本站只同步更新main分支的文档。 + +IsaacLab原版英文文档网站: `https://isaac-sim.github.io/IsaacLab `__ + +IsaacLab中文翻译文档网站: `https://docs.robotsfan.com/isaaclab `__ + +IsaacSim中文文档网站(API自动翻译): `https://docs.robotsfan.com/isaacsim `__ + +所有翻译均开源在 `fan-ziqi/IsaacLab `__ ,译者: `github@fan-ziqi `__ 。如果你对此翻译项目有疑问,请通过 fanziqi614@gmail.com 联系译者。 + +.. note:: + + 随着本站用户的增多,轻量服务器访问负载日渐增加。如果您认可本站的工作,可以通过下面的赞赏码打赏。收到的赞赏用于服务器升级,感谢您的支持! + + 赞赏名单: **H\*R** 、 **\*彡** 、 **b\*k** 、 **\*涛** 、 **\*航** 、 **\*靖** 、 **李\*坤** 、 **\*玉** 、 **胡\*泽** 、 **\*塔** 、 **王\*辉** 、 **\*崇**、 **\*熠*** + + .. figure:: ../_static/thanks.png + :width: 450px + :align: center + :alt: 赞赏码 + +翻译过程 +----------------------------- + +本翻译项目的第一个版本使用ChatGPT-3.5-API编写了批量翻译脚本,对文档进行粗略翻译,但未翻译 ``API`` 和 ``CHANGELOG`` 。使用Github-Action实时监测官方文档变化并自动触发增量翻译,自动构建HTML,上传到gh-pages-zhcn分支,并触发webhook自动同步到阿里云境内服务器,保证国内用户的访问速度。随着文档更新越来越频繁以及为了确保翻译准确,之后的更新均为作者手动翻译,不再大面积使用GPT辅助。 + +重建文档的步骤: + +.. code-block:: bash + + # install python packages + pip install setuptools polib==1.2.0 openai==v1.3.6 python-dotenv==1.0.0 pytest==8.2.2 sphinx-intl sphinx-book-theme==1.0.1 myst-parser sphinxcontrib-bibtex==2.5.0 autodocsumm sphinx-copybutton sphinx-icon sphinx_design sphinxemoji numpy matplotlib warp-lang gymnasium sphinx-tabs sphinx-multiversion==0.2.4 + # merge upstream changes + git remote add upstream https://github.com/isaac-sim/IsaacLab.git + git fetch upstream + git merge upstream/main --strategy-option ours --allow-unrelated-histories --verbose + # make pot files + sphinx-build -M gettext . _build + # make po files + sphinx-intl update -p _build/gettext -l zh_CN + # transtale to zh_CN + python po_translator.py --folder ./locale --lang zh_CN --folder-language --bulk --fuzzy + # make translated html files + sphinx-build -M html . _build -D language='zh_CN' + # open on default browser + xdg-open _build/html/index.html + +过程中还需要: + +* 使用搜索 ``#, fuzzy`` 找到变动的地方,校对后删除 ``#, fuzzy`` 标志。 +* 通过搜索 ``#~`` 找到并删除已废弃的翻译。 + +译者说 +----------------------------- + +绝大部分报错都可以在 `Linux Troubleshooting `__ 中找到解决方案。下面补充一些官方文档中没有的解决方案: + +如何更新? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +如IsaacLab/IsaacSim有更新,pull最新的IsaacLab执行下述步骤即可解决。(仅限pip安装的Isaac系列) + +.. code-block:: bash + + pip install --upgrade isaacsim-rl isaacsim-replicator isaacsim-extscache-physics isaacsim-extscache-kit-sdk isaacsim-extscache-kit isaacsim-app --extra-index-url https://pypi.nvidia.com + ./isaaclab.sh --install + + +Ubuntu20.04使用pip安装Isaac Sim +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +使用pip安装Isaac Sim只支持 ``GLIBC>=2.34`` 。 `bug link `__ ,如果你使用的是Ubuntu20.04,使用 ``ldd --version`` 查看GLIBC版本,如果版本低于 ``2.34`` 则需要升级GLIBC。 请注意,升级GLIBC是一个危险操作可能会导致无法与其的问题,请谨慎升级! + +首先在 ``/etc/apt/sources.list`` 中添加 ``deb http://th.archive.ubuntu.com/ubuntu jammy main`` + +.. code-block:: bash + + sudo apt update + sudo apt install libc6 + +然后使用 ``ldd --version`` 查看升级后的GLIBC版本。 + +最后从 ``/etc/apt/sources.list`` 中删除 ``deb http://th.archive.ubuntu.com/ubuntu jammy main`` ,升级完成,可继续使用Pip进行安装。 + +通过pip安装的isaacsim打开后报错 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +报错类似如下: + +.. code-block:: + + [omni.isaac.sim.python-x.x.x] dependency: 'omni.isaac.xxx' = { version='^' } can't be satisfied. + +这是因为Isaac Lab只安装RL所需的“Isaac Sim - Python packages”。安装完整版本的“Isaac Sim - Python packages”即可解决,这样您将安装所有扩展(与Isaac Lab 100%兼容)。 + +.. code-block:: bash + + pip install isaacsim --extra-index-url https://pypi.nvidia.com + +需要升级的话加上 ``--upgrade`` 即可。 + diff --git a/_sources/source/setup/wechat.rst b/_sources/source/setup/wechat.rst new file mode 100644 index 0000000000..3410f54a7d --- /dev/null +++ b/_sources/source/setup/wechat.rst @@ -0,0 +1,11 @@ +微信交流群 +========================= + +一群已满,现开启二群。为保证群聊质量,进群后请按照 **单位-姓名或昵称-研究方向** 修改备注。 + +.. figure:: ../_static/wechat-group2-1121.jpg + :width: 500px + :align: center + :alt: 微信交流群二维码 + +更新日期:2024.11.21 \ No newline at end of file diff --git a/_sources/source/tutorials/00_sim/create_empty.rst b/_sources/source/tutorials/00_sim/create_empty.rst new file mode 100644 index 0000000000..4f4aba0511 --- /dev/null +++ b/_sources/source/tutorials/00_sim/create_empty.rst @@ -0,0 +1,168 @@ +Creating an empty scene +======================= + +.. currentmodule:: omni.isaac.lab + +This tutorial shows how to launch and control Isaac Sim simulator from a standalone Python script. It sets up an +empty scene in Isaac Lab and introduces the two main classes used in the framework, :class:`app.AppLauncher` and +:class:`sim.SimulationContext`. + +Please review `Isaac Sim Interface`_ and `Isaac Sim Workflows`_ prior to beginning this tutorial to get +an initial understanding of working with the simulator. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``create_empty.py`` script in the ``source/standalone/tutorials/00_sim`` directory. + +.. dropdown:: Code for create_empty.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/00_sim/create_empty.py + :language: python + :emphasize-lines: 18-30,34,40-44,46-47,51-54,60-61 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Launching the simulator +----------------------- + +The first step when working with standalone Python scripts is to launch the simulation application. +This is necessary to do at the start since various dependency modules of Isaac Sim are only available +after the simulation app is running. + +This can be done by importing the :class:`app.AppLauncher` class. This utility class wraps around +:class:`omni.isaac.kit.SimulationApp` class to launch the simulator. It provides mechanisms to +configure the simulator using command-line arguments and environment variables. + +For this tutorial, we mainly look at adding the command-line options to a user-defined +:class:`argparse.ArgumentParser`. This is done by passing the parser instance to the +:meth:`app.AppLauncher.add_app_launcher_args` method, which appends different parameters +to it. These include launching the app headless, configuring different Livestream options, +and enabling off-screen rendering. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/create_empty.py + :language: python + :start-at: import argparse + :end-at: simulation_app = app_launcher.app + +Importing python modules +------------------------ + +Once the simulation app is running, it is possible to import different Python modules from +Isaac Sim and other libraries. Here we import the following module: + +* :mod:`omni.isaac.lab.sim`: A sub-package in Isaac Lab for all the core simulator-related operations. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/create_empty.py + :language: python + :start-at: from omni.isaac.lab.sim import SimulationCfg, SimulationContext + :end-at: from omni.isaac.lab.sim import SimulationCfg, SimulationContext + + +Configuring the simulation context +---------------------------------- + +When launching the simulator from a standalone script, the user has complete control over playing, +pausing and stepping the simulator. All these operations are handled through the **simulation +context**. It takes care of various timeline events and also configures the `physics scene`_ for +simulation. + +In Isaac Lab, the :class:`sim.SimulationContext` class inherits from Isaac Sim's +:class:`omni.isaac.core.simulation_context.SimulationContext` to allow configuring the simulation +through Python's ``dataclass`` object and handle certain intricacies of the simulation stepping. + +For this tutorial, we set the physics and rendering time step to 0.01 seconds. This is done +by passing these quantities to the :class:`sim.SimulationCfg`, which is then used to create an +instance of the simulation context. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/create_empty.py + :language: python + :start-at: # Initialize the simulation context + :end-at: sim.set_camera_view([2.5, 2.5, 2.5], [0.0, 0.0, 0.0]) + + +Following the creation of the simulation context, we have only configured the physics acting on the +simulated scene. This includes the device to use for simulation, the gravity vector, and other advanced +solver parameters. There are now two main steps remaining to run the simulation: + +1. Designing the simulation scene: Adding sensors, robots and other simulated objects +2. Running the simulation loop: Stepping the simulator, and setting and getting data from the simulator + +In this tutorial, we look at Step 2 first for an empty scene to focus on the simulation control first. +In the following tutorials, we will look into Step 1 and working with simulation handles for interacting +with the simulator. + +Running the simulation +---------------------- + +The first thing, after setting up the simulation scene, is to call the :meth:`sim.SimulationContext.reset` +method. This method plays the timeline and initializes the physics handles in the simulator. It must always +be called the first time before stepping the simulator. Otherwise, the simulation handles are not initialized +properly. + +.. note:: + + :meth:`sim.SimulationContext.reset` is different from :meth:`sim.SimulationContext.play` method as the latter + only plays the timeline and does not initializes the physics handles. + +After playing the simulation timeline, we set up a simple simulation loop where the simulator is stepped repeatedly +while the simulation app is running. The method :meth:`sim.SimulationContext.step` takes in as argument :attr:`render`, +which dictates whether the step includes updating the rendering-related events or not. By default, this flag is +set to True. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/create_empty.py + :language: python + :start-at: # Play the simulator + :end-at: sim.step() + +Exiting the simulation +---------------------- + +Lastly, the simulation application is stopped and its window is closed by calling +:meth:`omni.isaac.kit.SimulationApp.close` method. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/create_empty.py + :language: python + :start-at: # close sim app + :end-at: simulation_app.close() + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/00_sim/create_empty.py + + +The simulation should be playing, and the stage should be rendering. To stop the simulation, +you can either close the window, or press ``Ctrl+C`` in the terminal. + +.. figure:: ../../_static/tutorials/tutorial_create_empty.jpg + :align: center + :figwidth: 100% + :alt: result of create_empty.py + +Passing ``--help`` to the above script will show the different command-line arguments added +earlier by the :class:`app.AppLauncher` class. To run the script headless, you can execute the +following: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/00_sim/create_empty.py --headless + + +Now that we have a basic understanding of how to run a simulation, let's move on to the +following tutorial where we will learn how to add assets to the stage. + +.. _`Isaac Sim Interface`: https://docs.omniverse.nvidia.com/isaacsim/latest/introductory_tutorials/tutorial_intro_interface.html#isaac-sim-app-tutorial-intro-interface +.. _`Isaac Sim Workflows`: https://docs.omniverse.nvidia.com/isaacsim/latest/introductory_tutorials/tutorial_intro_workflows.html +.. _carb: https://docs.omniverse.nvidia.com/kit/docs/carbonite/latest/index.html +.. _`physics scene`: https://docs.omniverse.nvidia.com/prod_extensions/prod_extensions/ext_physics.html#physics-scene diff --git a/_sources/source/tutorials/00_sim/launch_app.rst b/_sources/source/tutorials/00_sim/launch_app.rst new file mode 100644 index 0000000000..335ad587da --- /dev/null +++ b/_sources/source/tutorials/00_sim/launch_app.rst @@ -0,0 +1,176 @@ +Deep-dive into AppLauncher +========================== + +.. currentmodule:: omni.isaac.lab + +In this tutorial, we will dive into the :class:`app.AppLauncher` class to configure the simulator using +CLI arguments and environment variables (envars). Particularly, we will demonstrate how to use +:class:`~app.AppLauncher` to enable livestreaming and configure the :class:`omni.isaac.kit.SimulationApp` +instance it wraps, while also allowing user-provided options. + +The :class:`~app.AppLauncher` is a wrapper for :class:`~omni.isaac.kit.SimulationApp` to simplify +its configuration. The :class:`~omni.isaac.kit.SimulationApp` has many extensions that must be +loaded to enable different capabilities, and some of these extensions are order- and inter-dependent. +Additionally, there are startup options such as ``headless`` which must be set at instantiation time, +and which have an implied relationship with some extensions, e.g. the livestreaming extensions. +The :class:`~app.AppLauncher` presents an interface that can handle these extensions and startup +options in a portable manner across a variety of use cases. To achieve this, we offer CLI and envar +flags which can be merged with user-defined CLI args, while passing forward arguments intended +for :class:`~omni.isaac.kit.SimulationApp`. + + +The Code +-------- + +The tutorial corresponds to the ``launch_app.py`` script in the +``source/standalone/tutorials/00_sim`` directory. + +.. dropdown:: Code for launch_app.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/00_sim/launch_app.py + :language: python + :emphasize-lines: 18-40 + :linenos: + +The Code Explained +------------------ + +Adding arguments to the argparser +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:class:`~app.AppLauncher` is designed to be compatible with custom CLI args that users need for +their own scripts, while still providing a portable CLI interface. + +In this tutorial, a standard :class:`argparse.ArgumentParser` is instantiated and given the +script-specific ``--size`` argument, as well as the arguments ``--height`` and ``--width``. +The latter are ingested by :class:`~omni.isaac.kit.SimulationApp`. + +The argument ``--size`` is not used by :class:`~app.AppLauncher`, but will merge seamlessly +with the :class:`~app.AppLauncher` interface. In-script arguments can be merged with the +:class:`~app.AppLauncher` interface via the :meth:`~app.AppLauncher.add_app_launcher_args` method, +which will return a modified :class:`~argparse.ArgumentParser` with the :class:`~app.AppLauncher` +arguments appended. This can then be processed into an :class:`argparse.Namespace` using the +standard :meth:`argparse.ArgumentParser.parse_args` method and passed directly to +:class:`~app.AppLauncher` for instantiation. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/launch_app.py + :language: python + :start-at: import argparse + :end-at: simulation_app = app_launcher.app + +The above only illustrates only one of several ways of passing arguments to :class:`~app.AppLauncher`. +Please consult its documentation page to see further options. + +Understanding the output of --help +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +While executing the script, we can pass the ``--help`` argument and see the combined outputs of the +custom arguments and those from :class:`~app.AppLauncher`. + +.. code-block:: console + + ./isaaclab.sh -p source/standalone/tutorials/00_sim/launch_app.py --help + + [INFO] Using python from: /isaac-sim/python.sh + [INFO][AppLauncher]: The argument 'width' will be used to configure the SimulationApp. + [INFO][AppLauncher]: The argument 'height' will be used to configure the SimulationApp. + usage: launch_app.py [-h] [--size SIZE] [--width WIDTH] [--height HEIGHT] [--headless] [--livestream {0,1,2}] + [--enable_cameras] [--verbose] [--experience EXPERIENCE] + + Tutorial on running IsaacSim via the AppLauncher. + + options: + -h, --help show this help message and exit + --size SIZE Side-length of cuboid + --width WIDTH Width of the viewport and generated images. Defaults to 1280 + --height HEIGHT Height of the viewport and generated images. Defaults to 720 + + app_launcher arguments: + --headless Force display off at all times. + --livestream {0,1,2} + Force enable livestreaming. Mapping corresponds to that for the "LIVESTREAM" environment variable. + --enable_cameras Enable cameras when running without a GUI. + --verbose Enable verbose terminal logging from the SimulationApp. + --experience EXPERIENCE + The experience file to load when launching the SimulationApp. + + * If an empty string is provided, the experience file is determined based on the headless flag. + * If a relative path is provided, it is resolved relative to the `apps` folder in Isaac Sim and + Isaac Lab (in that order). + +This readout details the ``--size``, ``--height``, and ``--width`` arguments defined in the script directly, +as well as the :class:`~app.AppLauncher` arguments. + +The ``[INFO]`` messages preceding the help output also reads out which of these arguments are going +to be interpreted as arguments to the :class:`~omni.isaac.kit.SimulationApp` instance which the +:class:`~app.AppLauncher` class wraps. In this case, it is ``--height`` and ``--width``. These +are classified as such because they match the name and type of an argument which can be processed +by :class:`~omni.isaac.kit.SimulationApp`. Please refer to the `specification`_ for such arguments +for more examples. + +Using environment variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As noted in the help message, the :class:`~app.AppLauncher` arguments (``--livestream``, ``--headless``) +have corresponding environment variables (envar) as well. These are detailed in :mod:`omni.isaac.lab.app` +documentation. Providing any of these arguments through CLI is equivalent to running the script in a shell +environment where the corresponding envar is set. + +The support for :class:`~app.AppLauncher` envars are simply a convenience to provide session-persistent +configurations, and can be set in the user's ``${HOME}/.bashrc`` for persistent settings between sessions. +In the case where these arguments are provided from the CLI, they will override their corresponding envar, +as we will demonstrate later in this tutorial. + +These arguments can be used with any script that starts the simulation using :class:`~app.AppLauncher`, +with one exception, ``--enable_cameras``. This setting sets the rendering pipeline to use the +offscreen renderer. However, this setting is only compatible with the :class:`omni.isaac.lab.sim.SimulationContext`. +It will not work with Isaac Sim's :class:`omni.isaac.core.simulation_context.SimulationContext` class. +For more information on this flag, please see the :class:`~app.AppLauncher` API documentation. + + +The Code Execution +------------------ + +We will now run the example script: + +.. code-block:: console + + LIVESTREAM=1 ./isaaclab.sh -p source/standalone/tutorials/00_sim/launch_app.py --size 0.5 + +This will spawn a 0.5m\ :sup:`3` volume cuboid in the simulation. No GUI will appear, equivalent +to if we had passed the ``--headless`` flag because headlessness is implied by our ``LIVESTREAM`` +envar. If a visualization is desired, we could get one via Isaac's `Native Livestreaming`_. Streaming +is currently the only supported method of visualization from within the container. The +process can be killed by pressing ``Ctrl+C`` in the launching terminal. + +.. figure:: ../../_static/tutorials/tutorial_launch_app.jpg + :align: center + :figwidth: 100% + :alt: result of launch_app.py + +Now, let's look at how :class:`~app.AppLauncher` handles conflicting commands: + +.. code-block:: console + + LIVESTREAM=0 ./isaaclab.sh -p source/standalone/tutorials/00_sim/launch_app.py --size 0.5 --livestream 1 + +This will cause the same behavior as in the previous run, because although we have set ``LIVESTREAM=0`` +in our envars, CLI args such as ``--livestream`` take precedence in determining behavior. The process can +be killed by pressing ``Ctrl+C`` in the launching terminal. + +Finally, we will examine passing arguments to :class:`~omni.isaac.kit.SimulationApp` through +:class:`~app.AppLauncher`: + +.. code-block:: console + + LIVESTREAM=1 ./isaaclab.sh -p source/standalone/tutorials/00_sim/launch_app.py --size 0.5 --width 1920 --height 1080 + +This will cause the same behavior as before, but now the viewport will be rendered at 1920x1080p resolution. +This can be useful when we want to gather high-resolution video, or we can specify a lower resolution if we +want our simulation to be more performant. The process can be killed by pressing ``Ctrl+C`` in the launching +terminal. + + +.. _specification: https://docs.omniverse.nvidia.com/py/isaacsim/source/extensions/omni.isaac.kit/docs/index.html#omni.isaac.kit.SimulationApp.DEFAULT_LAUNCHER_CONFIG +.. _Native Livestreaming: https://docs.omniverse.nvidia.com/isaacsim/latest/installation/manual_livestream_clients.html#omniverse-streaming-client diff --git a/_sources/source/tutorials/00_sim/spawn_prims.rst b/_sources/source/tutorials/00_sim/spawn_prims.rst new file mode 100644 index 0000000000..1f7ed89ccc --- /dev/null +++ b/_sources/source/tutorials/00_sim/spawn_prims.rst @@ -0,0 +1,192 @@ +.. _tutorial-spawn-prims: + + +Spawning prims into the scene +============================= + +.. currentmodule:: omni.isaac.lab + +This tutorial explores how to spawn various objects (or prims) into the scene in Isaac Lab from Python. +It builds upon the previous tutorial on running the simulator from a standalone script and +demonstrates how to spawn a ground plane, lights, primitive shapes, and meshes from USD files. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``spawn_prims.py`` script in the ``source/standalone/tutorials/00_sim`` directory. +Let's take a look at the Python script: + +.. dropdown:: Code for spawn_prims.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :emphasize-lines: 40-88, 100-101 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Scene designing in Omniverse is built around a software system and file format called USD (Universal Scene Description). +It allows describing 3D scenes in a hierarchical manner, similar to a file system. Since USD is a comprehensive framework, +we recommend reading the `USD documentation`_ to learn more about it. + +For completeness, we introduce the must know concepts of USD in this tutorial. + +* **Primitives (Prims)**: These are the basic building blocks of a USD scene. They can be thought of as nodes in a scene + graph. Each node can be a mesh, a light, a camera, or a transform. It can also be a group of other prims under it. +* **Attributes**: These are the properties of a prim. They can be thought of as key-value pairs. For example, a prim can + have an attribute called ``color`` with a value of ``red``. +* **Relationships**: These are the connections between prims. They can be thought of as pointers to other prims. For + example, a mesh prim can have a relationship to a material prim for shading. + +A collection of these prims, with their attributes and relationships, is called a **USD stage**. It can be thought of +as a container for all prims in a scene. When we say we are designing a scene, we are actually designing a USD stage. + +While working with direct USD APIs provides a lot of flexibility, it can be cumbersome to learn and use. To make it +easier to design scenes, Isaac Lab builds on top of the USD APIs to provide a configuration-driven interface to spawn prims +into a scene. These are included in the :mod:`sim.spawners` module. + +When spawning prims into the scene, each prim requires a configuration class instance that defines the prim's attributes +and relationships (through material and shading information). The configuration class is then passed to its respective +function where the prim name and transformation are specified. The function then spawns the prim into the scene. + +At a high-level, this is how it works: + +.. code-block:: python + + # Create a configuration class instance + cfg = MyPrimCfg() + prim_path = "/path/to/prim" + + # Spawn the prim into the scene using the corresponding spawner function + spawn_my_prim(prim_path, cfg, translation=[0, 0, 0], orientation=[1, 0, 0, 0], scale=[1, 1, 1]) + # OR + # Use the spawner function directly from the configuration class + cfg.func(prim_path, cfg, translation=[0, 0, 0], orientation=[1, 0, 0, 0], scale=[1, 1, 1]) + + +In this tutorial, we demonstrate the spawning of various different prims into the scene. For more +information on the available spawners, please refer to the :mod:`sim.spawners` module in Isaac Lab. + +.. attention:: + + All the scene designing must happen before the simulation starts. Once the simulation starts, we recommend keeping + the scene frozen and only altering the properties of the prim. This is particularly important for GPU simulation + as adding new prims during simulation may alter the physics simulation buffers on GPU and lead to unexpected + behaviors. + + +Spawning a ground plane +----------------------- + +The :class:`~sim.spawners.from_files.GroundPlaneCfg` configures a grid-like ground plane with +modifiable properties such as its appearance and size. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # Ground-plane + :end-at: cfg_ground.func("/World/defaultGroundPlane", cfg_ground) + + +Spawning lights +--------------- + +It is possible to spawn `different light prims`_ into the stage. These include distant lights, sphere lights, disk +lights, and cylinder lights. In this tutorial, we spawn a distant light which is a light that is infinitely far away +from the scene and shines in a single direction. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # spawn distant light + :end-at: cfg_light_distant.func("/World/lightDistant", cfg_light_distant, translation=(1, 0, 10)) + + +Spawning primitive shapes +------------------------- + +Before spawning primitive shapes, we introduce the concept of a transform prim or Xform. A transform prim is a prim that +contains only transformation properties. It is used to group other prims under it and to transform them as a group. +Here we make an Xform prim to group all the primitive shapes under it. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # create a new xform prim for all objects to be spawned under + :end-at: prim_utils.create_prim("/World/Objects", "Xform") + +Next, we spawn a cone using the :class:`~sim.spawners.shapes.ConeCfg` class. It is possible to specify +the radius, height, physics properties, and material properties of the cone. By default, the physics and material +properties are disabled. + +The first two cones we spawn ``Cone1`` and ``Cone2`` are visual elements and do not have physics enabled. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # spawn a red cone + :end-at: cfg_cone.func("/World/Objects/Cone2", cfg_cone, translation=(-1.0, -1.0, 1.0)) + +For the third cone ``ConeRigid``, we add rigid body physics to it by setting the attributes for that in the configuration +class. Through these attributes, we can specify the mass, friction, and restitution of the cone. If unspecified, they +default to the default values set by USD Physics. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # spawn a green cone with colliders and rigid body + :end-before: # spawn a blue cuboid with deformable body + +Lastly, we spawn a cuboid ``CuboidDeformable`` which contains deformable body physics properties. Unlike the +rigid body simulation, a deformable body can have relative motion between its vertices. This is useful for simulating +soft bodies like cloth, rubber, or jello. It is important to note that deformable bodies are only supported in +GPU simulation and require a mesh object to be spawned with the deformable body physics properties. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # spawn a blue cuboid with deformable body + :end-before: # spawn a usd file of a table into the scene + +Spawning from another file +-------------------------- + +Lastly, it is possible to spawn prims from other file formats such as other USD, URDF, or OBJ files. In this tutorial, +we spawn a USD file of a table into the scene. The table is a mesh prim and has a material prim associated with it. +All of this information is stored in its USD file. + +.. literalinclude:: ../../../../source/standalone/tutorials/00_sim/spawn_prims.py + :language: python + :start-at: # spawn a usd file of a table into the scene + :end-at: cfg.func("/World/Objects/Table", cfg, translation=(0.0, 0.0, 1.05)) + +The table above is added as a reference to the scene. In layman terms, this means that the table is not actually added +to the scene, but a ``pointer`` to the table asset is added. This allows us to modify the table asset and have the changes +reflected in the scene in a non-destructive manner. For example, we can change the material of the table without +actually modifying the underlying file for the table asset directly. Only the changes are stored in the USD stage. + + +Executing the Script +~~~~~~~~~~~~~~~~~~~~ + +Similar to the tutorial before, to run the script, execute the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/00_sim/spawn_prims.py + +Once the simulation starts, you should see a window with a ground plane, a light, some cones, and a table. +The green cone, which has rigid body physics enabled, should fall and collide with the table and the ground +plane. The other cones are visual elements and should not move. To stop the simulation, you can close the window, +or press ``Ctrl+C`` in the terminal. + +.. figure:: ../../_static/tutorials/tutorial_spawn_prims.jpg + :align: center + :figwidth: 100% + :alt: result of spawn_prims.py + +This tutorial provided a foundation for spawning various prims into the scene in Isaac Lab. Although simple, it +demonstrates the basic concepts of scene designing in Isaac Lab and how to use the spawners. In the coming tutorials, +we will now look at how to interact with the scene and the simulation. + + +.. _`USD documentation`: https://graphics.pixar.com/usd/docs/index.html +.. _`different light prims`: https://youtu.be/c7qyI8pZvF4?feature=shared diff --git a/_sources/source/tutorials/01_assets/run_articulation.rst b/_sources/source/tutorials/01_assets/run_articulation.rst new file mode 100644 index 0000000000..dc8d14c669 --- /dev/null +++ b/_sources/source/tutorials/01_assets/run_articulation.rst @@ -0,0 +1,146 @@ +.. _tutorial-interact-articulation: + +Interacting with an articulation +================================ + +.. currentmodule:: omni.isaac.lab + + +This tutorial shows how to interact with an articulated robot in the simulation. It is a continuation of the +:ref:`tutorial-interact-rigid-object` tutorial, where we learned how to interact with a rigid object. +On top of setting the root state, we will see how to set the joint state and apply commands to the articulated +robot. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``run_articulation.py`` script in the ``source/standalone/tutorials/01_assets`` +directory. + +.. dropdown:: Code for run_articulation.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_articulation.py + :language: python + :emphasize-lines: 58-69, 91-104, 108-111, 116-117 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Designing the scene +------------------- + +Similar to the previous tutorial, we populate the scene with a ground plane and a distant light. Instead of +spawning rigid objects, we now spawn a cart-pole articulation from its USD file. The cart-pole is a simple robot +consisting of a cart and a pole attached to it. The cart is free to move along the x-axis, and the pole is free to +rotate about the cart. The USD file for the cart-pole contains the robot's geometry, joints, and other physical +properties. + +For the cart-pole, we use its pre-defined configuration object, which is an instance of the +:class:`assets.ArticulationCfg` class. This class contains information about the articulation's spawning strategy, +default initial state, actuator models for different joints, and other meta-information. A deeper-dive into how to +create this configuration object is provided in the :ref:`how-to-write-articulation-config` tutorial. + +As seen in the previous tutorial, we can spawn the articulation into the scene in a similar fashion by creating +an instance of the :class:`assets.Articulation` class by passing the configuration object to its constructor. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_articulation.py + :language: python + :start-at: # Create separate groups called "Origin1", "Origin2" + :end-at: cartpole = Articulation(cfg=cartpole_cfg) + + +Running the simulation loop +--------------------------- + +Continuing from the previous tutorial, we reset the simulation at regular intervals, set commands to the articulation, +step the simulation, and update the articulation's internal buffers. + +Resetting the simulation +"""""""""""""""""""""""" + +Similar to a rigid object, an articulation also has a root state. This state corresponds to the root body in the +articulation tree. On top of the root state, an articulation also has joint states. These states correspond to the +joint positions and velocities. + +To reset the articulation, we first set the root state by calling the :meth:`Articulation.write_root_state_to_sim` +method. Similarly, we set the joint states by calling the :meth:`Articulation.write_joint_state_to_sim` method. +Finally, we call the :meth:`Articulation.reset` method to reset any internal buffers and caches. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_articulation.py + :language: python + :start-at: # reset the scene entities + :end-at: robot.reset() + +Stepping the simulation +""""""""""""""""""""""" + +Applying commands to the articulation involves two steps: + +1. *Setting the joint targets*: This sets the desired joint position, velocity, or effort targets for the articulation. +2. *Writing the data to the simulation*: Based on the articulation's configuration, this step handles any + :ref:`actuation conversions ` and writes the converted values to the PhysX buffer. + +In this tutorial, we control the articulation using joint effort commands. For this to work, we need to set the +articulation's stiffness and damping parameters to zero. This is done a-priori inside the cart-pole's pre-defined +configuration object. + +At every step, we randomly sample joint efforts and set them to the articulation by calling the +:meth:`Articulation.set_joint_effort_target` method. After setting the targets, we call the +:meth:`Articulation.write_data_to_sim` method to write the data to the PhysX buffer. Finally, we step +the simulation. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_articulation.py + :language: python + :start-at: # Apply random action + :end-at: robot.write_data_to_sim() + + +Updating the state +"""""""""""""""""" + +Every articulation class contains a :class:`assets.ArticulationData` object. This stores the state of the +articulation. To update the state inside the buffer, we call the :meth:`assets.Articulation.update` method. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_articulation.py + :language: python + :start-at: # Update buffers + :end-at: robot.update(sim_dt) + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + + +To run the code and see the results, let's run the script from the terminal: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/01_assets/run_articulation.py + + +This command should open a stage with a ground plane, lights, and two cart-poles that are moving around randomly. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal. + +.. figure:: ../../_static/tutorials/tutorial_run_articulation.jpg + :align: center + :figwidth: 100% + :alt: result of run_articulation.py + +In this tutorial, we learned how to create and interact with a simple articulation. We saw how to set the state +of an articulation (its root and joint state) and how to apply commands to it. We also saw how to update its +buffers to read the latest state from the simulation. + +In addition to this tutorial, we also provide a few other scripts that spawn different robots.These are included +in the ``source/standalone/demos`` directory. You can run these scripts as: + +.. code-block:: bash + + # Spawn many different single-arm manipulators + ./isaaclab.sh -p source/standalone/demos/arms.py + + # Spawn many different quadrupeds + ./isaaclab.sh -p source/standalone/demos/quadrupeds.py diff --git a/_sources/source/tutorials/01_assets/run_deformable_object.rst b/_sources/source/tutorials/01_assets/run_deformable_object.rst new file mode 100644 index 0000000000..4041d7e7ed --- /dev/null +++ b/_sources/source/tutorials/01_assets/run_deformable_object.rst @@ -0,0 +1,181 @@ +.. _tutorial-interact-deformable-object: + + +Interacting with a deformable object +==================================== + +.. currentmodule:: omni.isaac.lab + +While deformable objects sometimes refer to a broader class of objects, such as cloths, fluids and soft bodies, +in PhysX, deformable objects syntactically correspond to soft bodies. Unlike rigid objects, soft bodies can deform +under external forces and collisions. + +Soft bodies are simulated using Finite Element Method (FEM) in PhysX. The soft body comprises of two tetrahedral +meshes -- a simulation mesh and a collision mesh. The simulation mesh is used to simulate the deformations of +the soft body, while the collision mesh is used to detect collisions with other objects in the scene. +For more details, please check the `PhysX documentation`_. + +This tutorial shows how to interact with a deformable object in the simulation. We will spawn a +set of soft cubes and see how to set their nodal positions and velocities, along with apply kinematic +commands to the mesh nodes to move the soft body. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``run_deformable_object.py`` script in the ``source/standalone/tutorials/01_assets`` directory. + +.. dropdown:: Code for run_deformable_object.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :emphasize-lines: 61-73, 75-77, 102-110, 112-115, 117-118, 123-130, 132-133, 139-140 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Designing the scene +------------------- + +Similar to the :ref:`tutorial-interact-rigid-object` tutorial, we populate the scene with a ground plane +and a light source. In addition, we add a deformable object to the scene using the :class:`assets.DeformableObject` +class. This class is responsible for spawning the prims at the input path and initializes their corresponding +deformable body physics handles. + +In this tutorial, we create a cubical soft object using the spawn configuration similar to the deformable cube +in the :ref:`Spawn Objects ` tutorial. The only difference is that now we wrap +the spawning configuration into the :class:`assets.DeformableObjectCfg` class. This class contains information about +the asset's spawning strategy and default initial state. When this class is passed to +the :class:`assets.DeformableObject` class, it spawns the object and initializes the corresponding physics handles +when the simulation is played. + +.. note:: + The deformable object is only supported in GPU simulation and requires a mesh object to be spawned with the + deformable body physics properties on it. + + +As seen in the rigid body tutorial, we can spawn the deformable object into the scene in a similar fashion by creating +an instance of the :class:`assets.DeformableObject` class by passing the configuration object to its constructor. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # Create separate groups called "Origin1", "Origin2", "Origin3" + :end-at: cube_object = DeformableObject(cfg=cfg) + +Running the simulation loop +--------------------------- + +Continuing from the rigid body tutorial, we reset the simulation at regular intervals, apply kinematic commands +to the deformable body, step the simulation, and update the deformable object's internal buffers. + +Resetting the simulation state +"""""""""""""""""""""""""""""" + +Unlike rigid bodies and articulations, deformable objects have a different state representation. The state of a +deformable object is defined by the nodal positions and velocities of the mesh. The nodal positions and velocities +are defined in the **simulation world frame** and are stored in the :attr:`assets.DeformableObject.data` attribute. + +We use the :attr:`assets.DeformableObject.data.default_nodal_state_w` attribute to get the default nodal state of the +spawned object prims. This default state can be configured from the :attr:`assets.DeformableObjectCfg.init_state` +attribute, which we left as identity in this tutorial. + +.. attention:: + The initial state in the configuration :attr:`assets.DeformableObjectCfg` specifies the pose + of the deformable object at the time of spawning. Based on this initial state, the default nodal state is + obtained when the simulation is played for the first time. + +We apply transformations to the nodal positions to randomize the initial state of the deformable object. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # reset the nodal state of the object + :end-at: nodal_state[..., :3] = cube_object.transform_nodal_pos(nodal_state[..., :3], pos_w, quat_w) + +To reset the deformable object, we first set the nodal state by calling the :meth:`assets.DeformableObject.write_nodal_state_to_sim` +method. This method writes the nodal state of the deformable object prim into the simulation buffer. +Additionally, we free all the kinematic targets set for the nodes in the previous simulation step by calling +the :meth:`assets.DeformableObject.write_nodal_kinematic_target_to_sim` method. We explain the +kinematic targets in the next section. + +Finally, we call the :meth:`assets.DeformableObject.reset` method to reset any internal buffers and caches. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # write nodal state to simulation + :end-at: cube_object.reset() + +Stepping the simulation +""""""""""""""""""""""" + +Deformable bodies support user-driven kinematic control where a user can specify position targets for some of +the mesh nodes while the rest of the nodes are simulated using the FEM solver. This `partial kinematic`_ control +is useful for applications where the user wants to interact with the deformable object in a controlled manner. + +In this tutorial, we apply kinematic commands to two out of the four cubes in the scene. We set the position +targets for the node at index 0 (bottom-left corner) to move the cube along the z-axis. + +At every step, we increment the kinematic position target for the node by a small value. Additionally, +we set the flag to indicate that the target is a kinematic target for that node in the simulation buffer. +These are set into the simulation buffer by calling the :meth:`assets.DeformableObject.write_nodal_kinematic_target_to_sim` +method. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # update the kinematic target for cubes at index 0 and 3 + :end-at: cube_object.write_nodal_kinematic_target_to_sim(nodal_kinematic_target) + +Similar to the rigid object and articulation, we perform the :meth:`assets.DeformableObject.write_data_to_sim` method +before stepping the simulation. For deformable objects, this method does not apply any external forces to the object. +However, we keep this method for completeness and future extensions. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # write internal data to simulation + :end-at: cube_object.write_data_to_sim() + +Updating the state +"""""""""""""""""" + +After stepping the simulation, we update the internal buffers of the deformable object prims to reflect their new state +inside the :class:`assets.DeformableObject.data` attribute. This is done using the :meth:`assets.DeformableObject.update` method. + +At a fixed interval, we print the root position of the deformable object to the terminal. As mentioned +earlier, there is no concept of a root state for deformable objects. However, we compute the root position as +the average position of all the nodes in the mesh. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_deformable_object.py + :language: python + :start-at: # update buffers + :end-at: print(f"Root position (in world): {cube_object.data.root_pos_w[:, :3]}") + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/01_assets/run_deformable_object.py + + +This should open a stage with a ground plane, lights, and several green cubes. Two of the four cubes must be dropping +from a height and settling on to the ground. Meanwhile the other two cubes must be moving along the z-axis. You +should see a marker showing the kinematic target position for the nodes at the bottom-left corner of the cubes. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal + +.. figure:: ../../_static/tutorials/tutorial_run_deformable_object.jpg + :align: center + :figwidth: 100% + :alt: result of run_deformable_object.py + +This tutorial showed how to spawn deformable objects and wrap them in a :class:`DeformableObject` class to initialize their +physics handles which allows setting and obtaining their state. We also saw how to apply kinematic commands to the +deformable object to move the mesh nodes in a controlled manner. In the next tutorial, we will see how to create +a scene using the :class:`InteractiveScene` class. + +.. _PhysX documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html +.. _partial kinematic: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html#kinematic-soft-bodies diff --git a/_sources/source/tutorials/01_assets/run_rigid_object.rst b/_sources/source/tutorials/01_assets/run_rigid_object.rst new file mode 100644 index 0000000000..f9fe141467 --- /dev/null +++ b/_sources/source/tutorials/01_assets/run_rigid_object.rst @@ -0,0 +1,153 @@ +.. _tutorial-interact-rigid-object: + + +Interacting with a rigid object +=============================== + +.. currentmodule:: omni.isaac.lab + +In the previous tutorials, we learned the essential workings of the standalone script and how to +spawn different objects (or *prims*) into the simulation. This tutorial shows how to create and interact +with a rigid object. For this, we will use the :class:`assets.RigidObject` class provided in Isaac Lab. + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``run_rigid_object.py`` script in the ``source/standalone/tutorials/01_assets`` directory. + +.. dropdown:: Code for run_rigid_object.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_rigid_object.py + :language: python + :emphasize-lines: 55-74, 76-78, 98-108, 111-112, 118-119, 132-134, 139-140 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +In this script, we split the ``main`` function into two separate functions, which highlight the two main +steps of setting up any simulation in the simulator: + +1. **Design scene**: As the name suggests, this part is responsible for adding all the prims to the scene. +2. **Run simulation**: This part is responsible for stepping the simulator, interacting with the prims + in the scene, e.g., changing their poses, and applying any commands to them. + +A distinction between these two steps is necessary because the second step only happens after the first step +is complete and the simulator is reset. Once the simulator is reset (which automatically plays the simulation), +no new (physics-enabled) prims should be added to the scene as it may lead to unexpected behaviors. However, +the prims can be interacted with through their respective handles. + + +Designing the scene +------------------- + +Similar to the previous tutorial, we populate the scene with a ground plane and a light source. In addition, +we add a rigid object to the scene using the :class:`assets.RigidObject` class. This class is responsible for +spawning the prims at the input path and initializes their corresponding rigid body physics handles. + +In this tutorial, we create a conical rigid object using the spawn configuration similar to the rigid cone +in the :ref:`Spawn Objects ` tutorial. The only difference is that now we wrap +the spawning configuration into the :class:`assets.RigidObjectCfg` class. This class contains information about +the asset's spawning strategy, default initial state, and other meta-information. When this class is passed to +the :class:`assets.RigidObject` class, it spawns the object and initializes the corresponding physics handles +when the simulation is played. + +As an example on spawning the rigid object prim multiple times, we create its parent Xform prims, +``/World/Origin{i}``, that correspond to different spawn locations. When the regex expression +``/World/Origin*/Cone`` is passed to the :class:`assets.RigidObject` class, it spawns the rigid object prim at +each of the ``/World/Origin{i}`` locations. For instance, if ``/World/Origin1`` and ``/World/Origin2`` are +present in the scene, the rigid object prims are spawned at the locations ``/World/Origin1/Cone`` and +``/World/Origin2/Cone`` respectively. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_rigid_object.py + :language: python + :start-at: # Create separate groups called "Origin1", "Origin2", "Origin3" + :end-at: cone_object = RigidObject(cfg=cone_cfg) + +Since we want to interact with the rigid object, we pass this entity back to the main function. This entity +is then used to interact with the rigid object in the simulation loop. In later tutorials, we will see a more +convenient way to handle multiple scene entities using the :class:`scene.InteractiveScene` class. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_rigid_object.py + :language: python + :start-at: # return the scene information + :end-at: return scene_entities, origins + + +Running the simulation loop +--------------------------- + +We modify the simulation loop to interact with the rigid object to include three steps -- resetting the +simulation state at fixed intervals, stepping the simulation, and updating the internal buffers of the +rigid object. For the convenience of this tutorial, we extract the rigid object's entity from the scene +dictionary and store it in a variable. + +Resetting the simulation state +"""""""""""""""""""""""""""""" + +To reset the simulation state of the spawned rigid object prims, we need to set their pose and velocity. +Together they define the root state of the spawned rigid objects. It is important to note that this state +is defined in the **simulation world frame**, and not of their parent Xform prim. This is because the physics +engine only understands the world frame and not the parent Xform prim's frame. Thus, we need to transform +desired state of the rigid object prim into the world frame before setting it. + +We use the :attr:`assets.RigidObject.data.default_root_state` attribute to get the default root state of the +spawned rigid object prims. This default state can be configured from the :attr:`assets.RigidObjectCfg.init_state` +attribute, which we left as identity in this tutorial. We then randomize the translation of the root state and +set the desired state of the rigid object prim using the :meth:`assets.RigidObject.write_root_state_to_sim` method. +As the name suggests, this method writes the root state of the rigid object prim into the simulation buffer. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_rigid_object.py + :language: python + :start-at: # reset root state + :end-at: cone_object.reset() + +Stepping the simulation +""""""""""""""""""""""" + +Before stepping the simulation, we perform the :meth:`assets.RigidObject.write_data_to_sim` method. This method +writes other data, such as external forces, into the simulation buffer. In this tutorial, we do not apply any +external forces to the rigid object, so this method is not necessary. However, it is included for completeness. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_rigid_object.py + :language: python + :start-at: # apply sim data + :end-at: cone_object.write_data_to_sim() + +Updating the state +"""""""""""""""""" + +After stepping the simulation, we update the internal buffers of the rigid object prims to reflect their new state +inside the :class:`assets.RigidObject.data` attribute. This is done using the :meth:`assets.RigidObject.update` method. + +.. literalinclude:: ../../../../source/standalone/tutorials/01_assets/run_rigid_object.py + :language: python + :start-at: # update buffers + :end-at: cone_object.update(sim_dt) + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/01_assets/run_rigid_object.py + + +This should open a stage with a ground plane, lights, and several green cones. The cones must be dropping from +a random height and settling on to the ground. 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 + +.. figure:: ../../_static/tutorials/tutorial_run_rigid_object.jpg + :align: center + :figwidth: 100% + :alt: result of run_rigid_object.py + + +This tutorial showed how to spawn rigid objects and wrap them in a :class:`RigidObject` class to initialize their +physics handles which allows setting and obtaining their state. In the next tutorial, we will see how to interact +with an articulated object which is a collection of rigid objects connected by joints. diff --git a/_sources/source/tutorials/02_scene/create_scene.rst b/_sources/source/tutorials/02_scene/create_scene.rst new file mode 100644 index 0000000000..dbe6faea97 --- /dev/null +++ b/_sources/source/tutorials/02_scene/create_scene.rst @@ -0,0 +1,169 @@ +.. _tutorial-interactive-scene: + +Using the Interactive Scene +=========================== + +.. currentmodule:: omni.isaac.lab + +So far in the tutorials, we manually spawned assets into the simulation and created +object instances to interact with them. However, as the complexity of the scene +increases, it becomes tedious to perform these tasks manually. In this tutorial, +we will introduce the :class:`scene.InteractiveScene` class, which provides a convenient +interface for spawning prims and managing them in the simulation. + +At a high-level, the interactive scene is a collection of scene entities. Each entity +can be either a non-interactive prim (e.g. ground plane, light source), an interactive +prim (e.g. articulation, rigid object), or a sensor (e.g. camera, lidar). The interactive +scene provides a convenient interface for spawning these entities and managing them +in the simulation. + +Compared the manual approach, it provides the following benefits: + +* Alleviates the user needing to spawn each asset separately as this is handled implicitly. +* Enables user-friendly cloning of scene prims for multiple environments. +* Collects all the scene entities into a single object, which makes them easier to manage. + +In this tutorial, we take the cartpole example from the :ref:`tutorial-interact-articulation` +tutorial and replace the ``design_scene`` function with an :class:`scene.InteractiveScene` object. +While it may seem like overkill to use the interactive scene for this simple example, it will +become more useful in the future as more assets and sensors are added to the scene. + + +The Code +~~~~~~~~ + +This tutorial corresponds to the ``create_scene.py`` script within +``source/standalone/tutorials/02_scene``. + +.. dropdown:: Code for create_scene.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/02_scene/create_scene.py + :language: python + :emphasize-lines: 50-63, 68-70, 91-92, 99-100, 105-106, 116-118 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +While the code is similar to the previous tutorial, there are a few key differences +that we will go over in detail. + +Scene configuration +------------------- + +The scene is composed of a collection of entities, each with their own configuration. +These are specified in a configuration class that inherits from :class:`scene.InteractiveSceneCfg`. +The configuration class is then passed to the :class:`scene.InteractiveScene` constructor +to create the scene. + +For the cartpole example, we specify the same scene as in the previous tutorial, but list +them now in the configuration class :class:`CartpoleSceneCfg` instead of manually spawning them. + +.. literalinclude:: ../../../../source/standalone/tutorials/02_scene/create_scene.py + :language: python + :pyobject: CartpoleSceneCfg + +The variable names in the configuration class are used as keys to access the corresponding +entity from the :class:`scene.InteractiveScene` object. For example, the cartpole can +be accessed via ``scene["cartpole"]``. However, we will get to that later. First, let's +look at how individual scene entities are configured. + +Similar to how a rigid object and articulation were configured in the previous tutorials, +the configurations are specified using a configuration class. However, there is a key +difference between the configurations for the ground plane and light source and the +configuration for the cartpole. The ground plane and light source are non-interactive +prims, while the cartpole is an interactive prim. This distinction is reflected in the +configuration classes used to specify them. The configurations for the ground plane and +light source are specified using an instance of the :class:`assets.AssetBaseCfg` class +while the cartpole is configured using an instance of the :class:`assets.ArticulationCfg`. +Anything that is not an interactive prim (i.e., neither an asset nor a sensor) is not +*handled* by the scene during simulation steps. + +Another key difference to note is in the specification of the prim paths for the +different prims: + +* Ground plane: ``/World/defaultGroundPlane`` +* Light source: ``/World/Light`` +* Cartpole: ``{ENV_REGEX_NS}/Robot`` + +As we learned earlier, Omniverse creates a graph of prims in the USD stage. The prim +paths are used to specify the location of the prim in the graph. The ground plane and +light source are specified using absolute paths, while the cartpole is specified using +a relative path. The relative path is specified using the ``ENV_REGEX_NS`` variable, +which is a special variable that is replaced with the environment name during scene creation. +Any entity that has the ``ENV_REGEX_NS`` variable in its prim path will be cloned for each +environment. This path is replaced by the scene object with ``/World/envs/env_{i}`` where +``i`` is the environment index. + +Scene instantiation +------------------- + +Unlike before where we called the ``design_scene`` function to create the scene, we now +create an instance of the :class:`scene.InteractiveScene` class and pass in the configuration +object to its constructor. While creating the configuration instance of ``CartpoleSceneCfg`` +we specify how many environment copies we want to create using the ``num_envs`` argument. +This will be used to clone the scene for each environment. + +.. literalinclude:: ../../../../source/standalone/tutorials/02_scene/create_scene.py + :language: python + :start-at: # Design scene + :end-at: scene = InteractiveScene(scene_cfg) + +Accessing scene elements +------------------------ + +Similar to how entities were accessed from a dictionary in the previous tutorials, the +scene elements can be accessed from the :class:`InteractiveScene` object using the +``[]`` operator. The operator takes in a string key and returns the corresponding +entity. The key is specified through the configuration class for each entity. For example, +the cartpole is specified using the key ``"cartpole"`` in the configuration class. + +.. literalinclude:: ../../../../source/standalone/tutorials/02_scene/create_scene.py + :language: python + :start-at: # Extract scene entities + :end-at: robot = scene["cartpole"] + +Running the simulation loop +--------------------------- + +The rest of the script looks similar to previous scripts that interfaced with :class:`assets.Articulation`, +with a few small differences in the methods called: + +* :meth:`assets.Articulation.reset` ⟶ :meth:`scene.InteractiveScene.reset` +* :meth:`assets.Articulation.write_data_to_sim` ⟶ :meth:`scene.InteractiveScene.write_data_to_sim` +* :meth:`assets.Articulation.update` ⟶ :meth:`scene.InteractiveScene.update` + +Under the hood, the methods of :class:`scene.InteractiveScene` call the corresponding +methods of the entities in the scene. + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + + + +Let's run the script to simulate 32 cartpoles in the scene. We can do this by passing +the ``--num_envs`` argument to the script. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/02_scene/create_scene.py --num_envs 32 + +This should open a stage with 32 cartpoles swinging around randomly. You can use the +mouse to rotate the camera and the arrow keys to move around the scene. + + +.. figure:: ../../_static/tutorials/tutorial_creating_a_scene.jpg + :align: center + :figwidth: 100% + :alt: result of create_scene.py + +In this tutorial, we saw how to use :class:`scene.InteractiveScene` to create a +scene with multiple assets. We also saw how to use the ``num_envs`` argument +to clone the scene for multiple environments. + +There are many more example usages of the :class:`scene.InteractiveSceneCfg` in the tasks found +under the ``omni.isaac.lab_tasks`` extension. Please check out the source code to see +how they are used for more complex scenes. diff --git a/_sources/source/tutorials/03_envs/create_direct_rl_env.rst b/_sources/source/tutorials/03_envs/create_direct_rl_env.rst new file mode 100644 index 0000000000..a4b945be9d --- /dev/null +++ b/_sources/source/tutorials/03_envs/create_direct_rl_env.rst @@ -0,0 +1,339 @@ +.. _tutorial-create-direct-rl-env: + + +Creating a Direct Workflow RL Environment +========================================= + +.. currentmodule:: omni.isaac.lab + +In addition to the :class:`envs.ManagerBasedRLEnv` class, which encourages the use of configuration classes +for more modular environments, the :class:`~omni.isaac.lab.envs.DirectRLEnv` class allows for more direct control +in the scripting of environment. + +Instead of using Manager classes for defining rewards and observations, the direct workflow tasks +implement the full reward and observation functions directly in the task script. +This allows for more control in the implementation of the methods, such as using pytorch jit +features, and provides a less abstracted framework that makes it easier to find the various +pieces of code. + +In this tutorial, we will configure the cartpole environment using the direct workflow implementation to create a task +for balancing the pole upright. We will learn how to specify the task using by implementing functions +for scene creation, actions, resets, rewards and observations. + + +The Code +~~~~~~~~ + +For this tutorial, we use the cartpole environment defined in ``omni.isaac.lab_tasks.direct.cartpole`` module. + +.. dropdown:: Code for cartpole_env.py + :icon: code + + .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Similar to the manager-based environments, a configuration class is defined for the task to hold settings +for the simulation parameters, the scene, the actors, and the task. With the direct workflow implementation, +the :class:`envs.DirectRLEnvCfg` class is used as the base class for configurations. +Since the direct workflow implementation does not use Action and Observation managers, the task +config should define the number of actions and observations for the environment. + +.. code-block:: python + + @configclass + class CartpoleEnvCfg(DirectRLEnvCfg): + ... + action_space = 1 + observation_space = 4 + state_space = 0 + +The config class can also be used to define task-specific attributes, such as scaling for reward terms +and thresholds for reset conditions. + +.. code-block:: python + + @configclass + class CartpoleEnvCfg(DirectRLEnvCfg): + ... + # reset + max_cart_pos = 3.0 + initial_pole_angle_range = [-0.25, 0.25] + + # reward scales + rew_scale_alive = 1.0 + rew_scale_terminated = -2.0 + rew_scale_pole_pos = -1.0 + rew_scale_cart_vel = -0.01 + rew_scale_pole_vel = -0.005 + +When creating a new environment, the code should define a new class that inherits from :class:`~omni.isaac.lab.envs.DirectRLEnv`. + +.. code-block:: python + + class CartpoleEnv(DirectRLEnv): + cfg: CartpoleEnvCfg + + def __init__(self, cfg: CartpoleEnvCfg, render_mode: str | None = None, **kwargs): + super().__init__(cfg, render_mode, **kwargs) + +The class can also hold class variables that are accessible by all functions in the class, +including functions for applying actions, computing resets, rewards, and observations. + +Scene Creation +-------------- + +In contrast to manager-based environments where the scene creation is taken care of by the framework, +the direct workflow implementation provides flexibility for users to implement their own scene creation +function. This includes adding actors into the stage, cloning the environments, filtering collisions +between the environments, adding the actors into the scene, and adding any additional props to the +scene, such as ground plane and lights. These operations should be implemented in the +``_setup_scene(self)`` method. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._setup_scene + +Defining Rewards +---------------- + +Reward function should be defined in the ``_get_rewards(self)`` API, which returns the reward +buffer as a return value. Within this function, the task is free to implement the logic of +the reward function. In this example, we implement a Pytorch JIT function that computes +the various components of the reward function. + +.. code-block:: python + + def _get_rewards(self) -> torch.Tensor: + total_reward = compute_rewards( + self.cfg.rew_scale_alive, + self.cfg.rew_scale_terminated, + self.cfg.rew_scale_pole_pos, + self.cfg.rew_scale_cart_vel, + self.cfg.rew_scale_pole_vel, + self.joint_pos[:, self._pole_dof_idx[0]], + self.joint_vel[:, self._pole_dof_idx[0]], + self.joint_pos[:, self._cart_dof_idx[0]], + self.joint_vel[:, self._cart_dof_idx[0]], + self.reset_terminated, + ) + return total_reward + + @torch.jit.script + def compute_rewards( + rew_scale_alive: float, + rew_scale_terminated: float, + rew_scale_pole_pos: float, + rew_scale_cart_vel: float, + rew_scale_pole_vel: float, + pole_pos: torch.Tensor, + pole_vel: torch.Tensor, + cart_pos: torch.Tensor, + cart_vel: torch.Tensor, + reset_terminated: torch.Tensor, + ): + rew_alive = rew_scale_alive * (1.0 - reset_terminated.float()) + rew_termination = rew_scale_terminated * reset_terminated.float() + rew_pole_pos = rew_scale_pole_pos * torch.sum(torch.square(pole_pos), dim=-1) + rew_cart_vel = rew_scale_cart_vel * torch.sum(torch.abs(cart_vel), dim=-1) + rew_pole_vel = rew_scale_pole_vel * torch.sum(torch.abs(pole_vel), dim=-1) + total_reward = rew_alive + rew_termination + rew_pole_pos + rew_cart_vel + rew_pole_vel + return total_reward + + +Defining Observations +--------------------- + +The observation buffer should be computed in the ``_get_observations(self)`` function, +which constructs the observation buffer for the environment. At the end of this API, +a dictionary should be returned that contains ``policy`` as the key, and the full +observation buffer as the value. For asymmetric policies, the dictionary should also +include the key ``critic`` and the states buffer as the value. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._get_observations + +Computing Dones and Performing Resets +------------------------------------- + +Populating the ``dones`` buffer should be done in the ``_get_dones(self)`` method. +This method is free to implement logic that computes which environments would need to be reset +and which environments have reached the episode length limit. Both results should be +returned by the ``_get_dones(self)`` function, in the form of a tuple of boolean tensors. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._get_dones + +Once the indices for environments requiring reset have been computed, the ``_reset_idx(self, env_ids)`` +function performs the reset operations on those environments. Within this function, new states +for the environments requiring reset should be set directly into simulation. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._reset_idx + +Applying Actions +---------------- + +There are two APIs that are designed for working with actions. The ``_pre_physics_step(self, actions)`` takes in actions +from the policy as an argument and is called once per RL step, prior to taking any physics steps. This function can +be used to process the actions buffer from the policy and cache the data in a class variable for the environment. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._pre_physics_step + +The ``_apply_action(self)`` API is called ``decimation`` number of times for each RL step, prior to taking +each physics step. This provides more flexibility for environments where actions should be applied +for each physics step. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/cartpole_env.py + :language: python + :pyobject: CartpoleEnv._apply_action + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +To run training for the direct workflow Cartpole environment, we can use the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/rl_games/train.py --task=Isaac-Cartpole-Direct-v0 + +.. figure:: ../../_static/tutorials/tutorial_create_direct_workflow.jpg + :align: center + :figwidth: 100% + :alt: result of train.py + +All direct workflow tasks have the suffix ``-Direct`` added to the task name to differentiate the implementation style. + + +Domain Randomization +~~~~~~~~~~~~~~~~~~~~ + +In the direct workflow, domain randomization configuration uses the :class:`~omni.isaac.lab.utils.configclass` module +to specify a configuration class consisting of :class:`~managers.EventTermCfg` variables. + +Below is an example of a configuration class for domain randomization: + +.. code-block:: python + + @configclass + class EventCfg: + robot_physics_material = EventTerm( + func=mdp.randomize_rigid_body_material, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", body_names=".*"), + "static_friction_range": (0.7, 1.3), + "dynamic_friction_range": (1.0, 1.0), + "restitution_range": (1.0, 1.0), + "num_buckets": 250, + }, + ) + robot_joint_stiffness_and_damping = EventTerm( + func=mdp.randomize_actuator_gains, + mode="reset", + params={ + "asset_cfg": SceneEntityCfg("robot", joint_names=".*"), + "stiffness_distribution_params": (0.75, 1.5), + "damping_distribution_params": (0.3, 3.0), + "operation": "scale", + "distribution": "log_uniform", + }, + ) + reset_gravity = EventTerm( + func=mdp.randomize_physics_scene_gravity, + mode="interval", + is_global_time=True, + interval_range_s=(36.0, 36.0), # time_s = num_steps * (decimation * dt) + params={ + "gravity_distribution_params": ([0.0, 0.0, 0.0], [0.0, 0.0, 0.4]), + "operation": "add", + "distribution": "gaussian", + }, + ) + +Each ``EventTerm`` object is of the :class:`~managers.EventTermCfg` class and takes in a ``func`` parameter +for specifying the function to call during randomization, a ``mode`` parameter, which can be ``startup``, +``reset`` or ``interval``. THe ``params`` dictionary should provide the necessary arguments to the +function that is specified in the ``func`` parameter. +Functions specified as ``func`` for the ``EventTerm`` can be found in the :class:`~envs.mdp.events` module. + +Note that as part of the ``"asset_cfg": SceneEntityCfg("robot", body_names=".*")`` parameter, the name of +the actor ``"robot"`` is provided, along with the body or joint names specified as a regex expression, +which will be the actors and bodies/joints that will have randomization applied. + +Once the ``configclass`` for the randomization terms have been set up, the class must be added +to the base config class for the task and be assigned to the variable ``events``. + +.. code-block:: python + + @configclass + class MyTaskConfig: + events: EventCfg = EventCfg() + + +Action and Observation Noise +---------------------------- + +Actions and observation noise can also be added using the :class:`~utils.configclass` module. +Action and observation noise configs must be added to the main task config using the +``action_noise_model`` and ``observation_noise_model`` variables: + +.. code-block:: python + + @configclass + class MyTaskConfig: + + # at every time-step add gaussian noise + bias. The bias is a gaussian sampled at reset + action_noise_model: NoiseModelWithAdditiveBiasCfg = NoiseModelWithAdditiveBiasCfg( + noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.05, operation="add"), + bias_noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.015, operation="abs"), + ) + + # at every time-step add gaussian noise + bias. The bias is a gaussian sampled at reset + observation_noise_model: NoiseModelWithAdditiveBiasCfg = NoiseModelWithAdditiveBiasCfg( + noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.002, operation="add"), + bias_noise_cfg=GaussianNoiseCfg(mean=0.0, std=0.0001, operation="abs"), + ) + + +:class:`~.utils.noise.NoiseModelWithAdditiveBiasCfg` can be used to sample both uncorrelated noise +per step as well as correlated noise that is re-sampled at reset time. + +The ``noise_cfg`` term specifies the Gaussian distribution that will be sampled at each +step for all environments. This noise will be added to the corresponding actions and +observations buffers at every step. + +The ``bias_noise_cfg`` term specifies the Gaussian distribution for the correlated noise +that will be sampled at reset time for the environments being reset. The same noise +will be applied each step for the remaining of the episode for the environments and +resampled at the next reset. + +If only per-step noise is desired, :class:`~utils.noise.GaussianNoiseCfg` can be used +to specify an additive Gaussian distribution that adds the sampled noise to the input buffer. + +.. code-block:: python + + @configclass + class MyTaskConfig: + action_noise_model: GaussianNoiseCfg = GaussianNoiseCfg(mean=0.0, std=0.05, operation="add") + + + + +In this tutorial, we learnt how to create a direct workflow task environment for reinforcement learning. We do this +by extending the base environment to include the scene setup, actions, dones, reset, reward and observaion functions. + +While it is possible to manually create an instance of :class:`~omni.isaac.lab.envs.DirectRLEnv` class for a desired task, +this is not scalable as it requires specialized scripts for each task. Thus, we exploit the +:meth:`gymnasium.make` function to create the environment with the gym interface. We will learn how to do this +in the next tutorial. diff --git a/_sources/source/tutorials/03_envs/create_manager_base_env.rst b/_sources/source/tutorials/03_envs/create_manager_base_env.rst new file mode 100644 index 0000000000..f54641f178 --- /dev/null +++ b/_sources/source/tutorials/03_envs/create_manager_base_env.rst @@ -0,0 +1,220 @@ +.. _tutorial-create-manager-base-env: + + +Creating a Manager-Based Base Environment +========================================= + +.. currentmodule:: omni.isaac.lab + +Environments bring together different aspects of the simulation such as +the scene, observations and actions spaces, reset events etc. to create a +coherent interface for various applications. In Isaac Lab, manager-based environments are +implemented as :class:`envs.ManagerBasedEnv` and :class:`envs.ManagerBasedRLEnv` classes. +The two classes are very similar, but :class:`envs.ManagerBasedRLEnv` is useful for +reinforcement learning tasks and contains rewards, terminations, curriculum +and command generation. The :class:`envs.ManagerBasedEnv` class is useful for +traditional robot control and doesn't contain rewards and terminations. + +In this tutorial, we will look at the base class :class:`envs.ManagerBasedEnv` and its +corresponding configuration class :class:`envs.ManagerBasedEnvCfg` for the manager-based workflow. +We will use the +cartpole environment from earlier to illustrate the different components +in creating a new :class:`envs.ManagerBasedEnv` environment. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``create_cartpole_base_env`` script in the ``source/standalone/tutorials/03_envs`` +directory. + +.. dropdown:: Code for create_cartpole_base_env.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/03_envs/create_cartpole_base_env.py + :language: python + :emphasize-lines: 47-51, 54-71, 74-108, 111-130, 135-139, 144, 148, 153-154, 160-161 + :linenos: + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +The base class :class:`envs.ManagerBasedEnv` wraps around many intricacies of the simulation interaction +and provides a simple interface for the user to run the simulation and interact with it. It +is composed of the following components: + +* :class:`scene.InteractiveScene` - The scene that is used for the simulation. +* :class:`managers.ActionManager` - The manager that handles actions. +* :class:`managers.ObservationManager` - The manager that handles observations. +* :class:`managers.EventManager` - The manager that schedules operations (such as domain randomization) + at specified simulation events. For instance, at startup, on resets, or periodic intervals. + +By configuring these components, the user can create different variations of the same environment +with minimal effort. In this tutorial, we will go through the different components of the +:class:`envs.ManagerBasedEnv` class and how to configure them to create a new environment. + +Designing the scene +------------------- + +The first step in creating a new environment is to configure its scene. For the cartpole +environment, we will be using the scene from the previous tutorial. Thus, we omit the +scene configuration here. For more details on how to configure a scene, see +:ref:`tutorial-interactive-scene`. + +Defining actions +---------------- + +In the previous tutorial, we directly input the action to the cartpole using +the :meth:`assets.Articulation.set_joint_effort_target` method. In this tutorial, we will +use the :class:`managers.ActionManager` to handle the actions. + +The action manager can comprise of multiple :class:`managers.ActionTerm`. Each action term +is responsible for applying *control* over a specific aspect of the environment. For instance, +for robotic arm, we can have two action terms -- one for controlling the joints of the arm, +and the other for controlling the gripper. This composition allows the user to define +different control schemes for different aspects of the environment. + +In the cartpole environment, we want to control the force applied to the cart to balance the pole. +Thus, we will create an action term that controls the force applied to the cart. + +.. literalinclude:: ../../../../source/standalone/tutorials/03_envs/create_cartpole_base_env.py + :language: python + :pyobject: ActionsCfg + +Defining observations +--------------------- + +While the scene defines the state of the environment, the observations define the states +that are observable by the agent. These observations are used by the agent to make decisions +on what actions to take. In Isaac Lab, the observations are computed by the +:class:`managers.ObservationManager` class. + +Similar to the action manager, the observation manager can comprise of multiple observation terms. +These are further grouped into observation groups which are used to define different observation +spaces for the environment. For instance, for hierarchical control, we may want to define +two observation groups -- one for the low level controller and the other for the high level +controller. It is assumed that all the observation terms in a group have the same dimensions. + +For this tutorial, we will only define one observation group named ``"policy"``. While not completely +prescriptive, this group is a necessary requirement for various wrappers in Isaac Lab. +We define a group by inheriting from the :class:`managers.ObservationGroupCfg` class. This class +collects different observation terms and help define common properties for the group, such +as enabling noise corruption or concatenating the observations into a single tensor. + +The individual terms are defined by inheriting from the :class:`managers.ObservationTermCfg` class. +This class takes in the :attr:`managers.ObservationTermCfg.func` that specifies the function or +callable class that computes the observation for that term. It includes other parameters for +defining the noise model, clipping, scaling, etc. However, we leave these parameters to their +default values for this tutorial. + +.. literalinclude:: ../../../../source/standalone/tutorials/03_envs/create_cartpole_base_env.py + :language: python + :pyobject: ObservationsCfg + +Defining events +--------------- + +At this point, we have defined the scene, actions and observations for the cartpole environment. +The general idea for all these components is to define the configuration classes and then +pass them to the corresponding managers. The event manager is no different. + +The :class:`managers.EventManager` class is responsible for events corresponding to changes +in the simulation state. This includes resetting (or randomizing) the scene, randomizing physical +properties (such as mass, friction, etc.), and varying visual properties (such as colors, textures, etc.). +Each of these are specified through the :class:`managers.EventTermCfg` class, which +takes in the :attr:`managers.EventTermCfg.func` that specifies the function or callable +class that performs the event. + +Additionally, it expects the **mode** of the event. The mode specifies when the event term should be applied. +It is possible to specify your own mode. For this, you'll need to adapt the :class:`~envs.ManagerBasedEnv` class. +However, out of the box, Isaac Lab provides three commonly used modes: + +* ``"startup"`` - Event that takes place only once at environment startup. +* ``"reset"`` - Event that occurs on environment termination and reset. +* ``"interval"`` - Event that are executed at a given interval, i.e., periodically after a certain number of steps. + +For this example, we define events that randomize the pole's mass on startup. This is done only once since this +operation is expensive and we don't want to do it on every reset. We also create an event to randomize the initial +joint state of the cartpole and the pole at every reset. + +.. literalinclude:: ../../../../source/standalone/tutorials/03_envs/create_cartpole_base_env.py + :language: python + :pyobject: EventCfg + +Tying it all together +--------------------- + +Having defined the scene and manager configurations, we can now define the environment configuration +through the :class:`envs.ManagerBasedEnvCfg` class. This class takes in the scene, action, observation and +event configurations. + +In addition to these, it also takes in the :attr:`envs.ManagerBasedEnvCfg.sim` which defines the simulation +parameters such as the timestep, gravity, etc. This is initialized to the default values, but can +be modified as needed. We recommend doing so by defining the :meth:`__post_init__` method in the +:class:`envs.ManagerBasedEnvCfg` class, which is called after the configuration is initialized. + +.. literalinclude:: ../../../../source/standalone/tutorials/03_envs/create_cartpole_base_env.py + :language: python + :pyobject: CartpoleEnvCfg + +Running the simulation +---------------------- + +Lastly, we revisit the simulation execution loop. This is now much simpler since we have +abstracted away most of the details into the environment configuration. We only need to +call the :meth:`envs.ManagerBasedEnv.reset` method to reset the environment and :meth:`envs.ManagerBasedEnv.step` +method to step the environment. Both these functions return the observation and an info dictionary +which may contain additional information provided by the environment. These can be used by an +agent for decision-making. + +The :class:`envs.ManagerBasedEnv` class does not have any notion of terminations since that concept is +specific for episodic tasks. Thus, the user is responsible for defining the termination condition +for the environment. In this tutorial, we reset the simulation at regular intervals. + +.. literalinclude:: ../../../../source/standalone/tutorials/03_envs/create_cartpole_base_env.py + :language: python + :pyobject: main + +An important thing to note above is that the entire simulation loop is wrapped inside the +:meth:`torch.inference_mode` context manager. This is because the environment uses PyTorch +operations under-the-hood and we want to ensure that the simulation is not slowed down by +the overhead of PyTorch's autograd engine and gradients are not computed for the simulation +operations. + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +To run the base environment made in this tutorial, you can use the following command: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/03_envs/create_cartpole_base_env.py --num_envs 32 + +This should open a stage with a ground plane, light source, and cartpoles. The simulation should be +playing with random actions on the cartpole. Additionally, it opens a UI window on the bottom +right corner of the screen named ``"Isaac Lab"``. This window contains different UI elements that +can be used for debugging and visualization. + + +.. figure:: ../../_static/tutorials/tutorial_create_manager_rl_env.jpg + :align: center + :figwidth: 100% + :alt: result of create_cartpole_base_env.py + +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal where you +started the simulation. + +In this tutorial, we learned about the different managers that help define a base environment. We +include more examples of defining the base environment in the ``source/standalone/tutorials/03_envs`` +directory. For completeness, they can be run using the following commands: + +.. code-block:: bash + + # Floating cube environment with custom action term for PD control + ./isaaclab.sh -p source/standalone/tutorials/03_envs/create_cube_base_env.py --num_envs 32 + + # Quadrupedal locomotion environment with a policy that interacts with the environment + ./isaaclab.sh -p source/standalone/tutorials/03_envs/create_quadruped_base_env.py --num_envs 32 + +In the following tutorial, we will look at the :class:`envs.ManagerBasedRLEnv` class and how to use it +to create a Markovian Decision Process (MDP). diff --git a/_sources/source/tutorials/03_envs/create_manager_rl_env.rst b/_sources/source/tutorials/03_envs/create_manager_rl_env.rst new file mode 100644 index 0000000000..63f710965b --- /dev/null +++ b/_sources/source/tutorials/03_envs/create_manager_rl_env.rst @@ -0,0 +1,190 @@ +.. _tutorial-create-manager-rl-env: + + +Creating a Manager-Based RL Environment +======================================= + +.. currentmodule:: omni.isaac.lab + +Having learnt how to create a base environment in :ref:`tutorial-create-manager-base-env`, we will now look at how to create a manager-based +task environment for reinforcement learning. + +The base environment is designed as an sense-act environment where the agent can send commands to the environment +and receive observations from the environment. This minimal interface is sufficient for many applications such as +traditional motion planning and controls. However, many applications require a task-specification which often +serves as the learning objective for the agent. For instance, in a navigation task, the agent may be required to +reach a goal location. To this end, we use the :class:`envs.ManagerBasedRLEnv` class which extends the base environment +to include a task specification. + +Similar to other components in Isaac Lab, instead of directly modifying the base class :class:`envs.ManagerBasedRLEnv`, we +encourage users to simply implement a configuration :class:`envs.ManagerBasedRLEnvCfg` for their task environment. +This practice allows us to separate the task specification from the environment implementation, making it easier +to reuse components of the same environment for different tasks. + +In this tutorial, we will configure the cartpole environment using the :class:`envs.ManagerBasedRLEnvCfg` to create a manager-based task +for balancing the pole upright. We will learn how to specify the task using reward terms, termination criteria, +curriculum and commands. + + +The Code +~~~~~~~~ + +For this tutorial, we use the cartpole environment defined in ``omni.isaac.lab_tasks.manager_based.classic.cartpole`` module. + +.. dropdown:: Code for cartpole_env_cfg.py + :icon: code + + .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :emphasize-lines: 117-141, 144-154, 172-174 + :linenos: + +The script for running the environment ``run_cartpole_rl_env.py`` is present in the +``isaaclab/source/standalone/tutorials/03_envs`` directory. The script is similar to the +``cartpole_base_env.py`` script in the previous tutorial, except that it uses the +:class:`envs.ManagerBasedRLEnv` instead of the :class:`envs.ManagerBasedEnv`. + +.. dropdown:: Code for run_cartpole_rl_env.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/03_envs/run_cartpole_rl_env.py + :language: python + :emphasize-lines: 38-42, 56-57 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +We already went through parts of the above in the :ref:`tutorial-create-manager-base-env` tutorial to learn +about how to specify the scene, observations, actions and events. Thus, in this tutorial, we +will focus only on the RL components of the environment. + +In Isaac Lab, we provide various implementations of different terms in the :mod:`envs.mdp` module. We will use +some of these terms in this tutorial, but users are free to define their own terms as well. These +are usually placed in their task-specific sub-package +(for instance, in :mod:`omni.isaac.lab_tasks.manager_based.classic.cartpole.mdp`). + + +Defining rewards +---------------- + +The :class:`managers.RewardManager` is used to compute the reward terms for the agent. Similar to the other +managers, its terms are configured using the :class:`managers.RewardTermCfg` class. The +:class:`managers.RewardTermCfg` class specifies the function or callable class that computes the reward +as well as the weighting associated with it. It also takes in dictionary of arguments, ``"params"`` +that are passed to the reward function when it is called. + +For the cartpole task, we will use the following reward terms: + +* **Alive Reward**: Encourage the agent to stay alive for as long as possible. +* **Terminating Reward**: Similarly penalize the agent for terminating. +* **Pole Angle Reward**: Encourage the agent to keep the pole at the desired upright position. +* **Cart Velocity Reward**: Encourage the agent to keep the cart velocity as small as possible. +* **Pole Velocity Reward**: Encourage the agent to keep the pole velocity as small as possible. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :pyobject: RewardsCfg + +Defining termination criteria +----------------------------- + +Most learning tasks happen over a finite number of steps that we call an episode. For instance, in the cartpole +task, we want the agent to balance the pole for as long as possible. However, if the agent reaches an unstable +or unsafe state, we want to terminate the episode. On the other hand, if the agent is able to balance the pole +for a long time, we want to terminate the episode and start a new one so that the agent can learn to balance the +pole from a different starting configuration. + +The :class:`managers.TerminationsCfg` configures what constitutes for an episode to terminate. In this example, +we want the task to terminate when either of the following conditions is met: + +* **Episode Length** The episode length is greater than the defined max_episode_length +* **Cart out of bounds** The cart goes outside of the bounds [-3, 3] + +The flag :attr:`managers.TerminationsCfg.time_out` specifies whether the term is a time-out (truncation) term +or terminated term. These are used to indicate the two types of terminations as described in `Gymnasium's documentation +`_. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :pyobject: TerminationsCfg + +Defining commands +----------------- + +For various goal-conditioned tasks, it is useful to specify the goals or commands for the agent. These are +handled through the :class:`managers.CommandManager`. The command manager handles resampling and updating the +commands at each step. It can also be used to provide the commands as an observation to the agent. + +For this simple task, we do not use any commands. Hence, we leave this attribute as its default value, which is None. +You can see an example of how to define a command manager in the other locomotion or manipulation tasks. + +Defining curriculum +------------------- + +Often times when training a learning agent, it helps to start with a simple task and gradually increase the +tasks's difficulty as the agent training progresses. This is the idea behind curriculum learning. In Isaac Lab, +we provide a :class:`managers.CurriculumManager` class that can be used to define a curriculum for your environment. + +In this tutorial we don't implement a curriculum for simplicity, but you can see an example of a +curriculum definition in the other locomotion or manipulation tasks. + +Tying it all together +--------------------- + +With all the above components defined, we can now create the :class:`ManagerBasedRLEnvCfg` configuration for the +cartpole environment. This is similar to the :class:`ManagerBasedEnvCfg` defined in :ref:`tutorial-create-manager-base-env`, +only with the added RL components explained in the above sections. + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/cartpole_env_cfg.py + :language: python + :pyobject: CartpoleEnvCfg + +Running the simulation loop +--------------------------- + +Coming back to the ``run_cartpole_rl_env.py`` script, the simulation loop is similar to the previous tutorial. +The only difference is that we create an instance of :class:`envs.ManagerBasedRLEnv` instead of the +:class:`envs.ManagerBasedEnv`. Consequently, now the :meth:`envs.ManagerBasedRLEnv.step` method returns additional signals +such as the reward and termination status. The information dictionary also maintains logging of quantities +such as the reward contribution from individual terms, the termination status of each term, the episode length etc. + +.. literalinclude:: ../../../../source/standalone/tutorials/03_envs/run_cartpole_rl_env.py + :language: python + :pyobject: main + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + + +Similar to the previous tutorial, we can run the environment by executing the ``run_cartpole_rl_env.py`` script. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/03_envs/run_cartpole_rl_env.py --num_envs 32 + + +This should open a similar simulation as in the previous tutorial. However, this time, the environment +returns more signals that specify the reward and termination status. Additionally, the individual +environments reset themselves when they terminate based on the termination criteria specified in the +configuration. + +.. figure:: ../../_static/tutorials/tutorial_create_manager_rl_env.jpg + :align: center + :figwidth: 100% + :alt: result of run_cartpole_rl_env.py + +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal +where you started the simulation. + +In this tutorial, we learnt how to create a task environment for reinforcement learning. We do this +by extending the base environment to include the rewards, terminations, commands and curriculum terms. +We also learnt how to use the :class:`envs.ManagerBasedRLEnv` class to run the environment and receive various +signals from it. + +While it is possible to manually create an instance of :class:`envs.ManagerBasedRLEnv` class for a desired task, +this is not scalable as it requires specialized scripts for each task. Thus, we exploit the +:meth:`gymnasium.make` function to create the environment with the gym interface. We will learn how to do this +in the next tutorial. diff --git a/_sources/source/tutorials/03_envs/modify_direct_rl_env.rst b/_sources/source/tutorials/03_envs/modify_direct_rl_env.rst new file mode 100644 index 0000000000..1cae1f6802 --- /dev/null +++ b/_sources/source/tutorials/03_envs/modify_direct_rl_env.rst @@ -0,0 +1,133 @@ +.. _tutorial-modify-direct-rl-env: + + +Modifying an existing Direct RL Environment +=========================================== + +.. currentmodule:: omni.isaac.lab + +Having learnt how to create a task in :ref:`tutorial-create-direct-rl-env`, register it in :ref:`tutorial-register-rl-env-gym`, +and train it in :ref:`tutorial-run-rl-training`, we will now look at how to make minor modifications to an existing task. + +Sometimes it is necessary to create, due to complexity or variations from existing examples, tasks from scratch. However, in certain situations, +it is possible to start from the existing code and introduce minor changes, one by one, to transform them according to our needs. + +In this tutorial, we will make minor modifications to the direct workflow Humanoid task to change the simple +humanoid model to the Unitree H1 humanoid robot without affecting the original code. + + +The Base Code +~~~~~~~~~~~~~ + +For this tutorial, we start from the direct workflow Humanoid environment defined in ``omni.isaac.lab_tasks.direct.humanoid`` module. + +.. dropdown:: Code for humanoid_env.py + :icon: code + + .. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid/humanoid_env.py + :language: python + :linenos: + + +The Changes Explained +~~~~~~~~~~~~~~~~~~~~~ + +Duplicating the file and registering a new task +----------------------------------------------- + +To avoid modifying the code of the existing task, we will make a copy of the file containing the Python +code and perform the modification on this copy. Then, in the Isaac Lab project +``source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/humanoid`` +folder we make a copy of the ``humanoid_env.py`` file and rename it to ``h1_env.py``. + +Open the ``h1_env.py`` file in a code editor and replace all the humanoid task name (``HumanoidEnv``) and its configuration +(``HumanoidEnvCfg``) instances to ``H1Env`` and ``H1EnvCfg`` respectively. +This is necessary to avoid name conflicts during import when registering the environment. + +Once the name change has been made, we proceed to add a new entry to register the task under the name ``Isaac-H1-Direct-v0``. +To do this, we modify the ``__init__.py`` file in the same working folder and add the following entry. +Refer to the :ref:`tutorial-register-rl-env-gym` tutorial for more details about environment registrations. + +.. hint:: + + If the changes in the task are minimal, it is very likely that the same RL library agent configurations can be used to train it successfully. + Otherwise, it is advisable to create new configuration files (adjusting their name during registration under the ``kwargs`` parameter) + to avoid altering the original configurations. + + +.. literalinclude:: ../../refs/snippets/tutorial_modify_direct_rl_env.py + :language: python + :start-after: [start-init-import] + :end-before: [end-init-import] + +.. literalinclude:: ../../refs/snippets/tutorial_modify_direct_rl_env.py + :language: python + :start-after: [start-init-register] + :end-before: [end-init-register] + +Changing the robot +------------------ + +The ``H1EnvCfg`` class (in the new created ``h1_env.py`` file) encapsulates the configuration values of the environment, +including the assets to be instantiated. Particularly in this example, the ``robot`` property holds the target articulation configuration. + +Since the Unitree H1 robot is included in the Isaac Lab assets extension (``omni.isaac.lab_assets``) we can just import it +and do the replacement directly (under the ``H1EnvCfg.robot`` property), as shown below. Note that we also need to modify the +``joint_gears`` property as it holds robot-specific configuration values. + +.. |franka-direct-link| replace:: `Isaac-Franka-Cabinet-Direct-v0 `__ + +.. hint:: + + If the target robot is not included in the Isaac Lab assets extension, it is possible to load and configure it, from a USD file, + by using the :class:`~omni.isaac.lab.assets.ArticulationCfg` class. + + * See the |franka-direct-link| source code for an example of loading and configuring a robot from a USD file. + * Refer to the `Importing a New Asset <../../how-to/import_new_asset.html>`_ tutorial for details on how to import an asset from URDF or MJCF file, and other formats. + +.. literalinclude:: ../../refs/snippets/tutorial_modify_direct_rl_env.py + :language: python + :start-after: [start-h1_env-import] + :end-before: [end-h1_env-import] + +.. literalinclude:: ../../refs/snippets/tutorial_modify_direct_rl_env.py + :language: python + :start-after: [start-h1_env-robot] + :end-before: [end-h1_env-robot] + +The robot changed, and with it the number of joints to control or the number of rigid bodies that compose the articulation, for example. +Therefore, it is also necessary to adjust other values in the environment configuration that depend on the characteristics of the robot, +such as the number of elements in the observation and action space. + +.. literalinclude:: ../../refs/snippets/tutorial_modify_direct_rl_env.py + :language: python + :start-after: [start-h1_env-spaces] + :end-before: [end-h1_env-spaces] + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +After the minor modification has been done, and similar to the previous tutorial, we can train on the task using one of the available RL workflows for such task. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/rl_games/train.py --task Isaac-H1-Direct-v0 --headless + +When the training is finished, we can visualize the result with the following command. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal +where you started the simulation. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/rl_games/play.py --task Isaac-H1-Direct-v0 --num_envs 64 + +.. figure:: ../../_static/tutorials/tutorial_modify_direct_rl_env.jpg + :align: center + :figwidth: 100% + :alt: result of training Isaac-H1-Direct-v0 task + +In this tutorial, we learnt how to make minor modifications to an existing environment without affecting the original code. + +It is important to note, however, that while the changes to be made may be small, they may not always work on the first try, +as there may be deeper dependencies on the original assets in the environment being modified. +In these cases, it is advisable to analyze the code of the available examples in detail in order to make an appropriate adjustment. diff --git a/_sources/source/tutorials/03_envs/register_rl_env_gym.rst b/_sources/source/tutorials/03_envs/register_rl_env_gym.rst new file mode 100644 index 0000000000..cbec780399 --- /dev/null +++ b/_sources/source/tutorials/03_envs/register_rl_env_gym.rst @@ -0,0 +1,172 @@ +.. _tutorial-register-rl-env-gym: + +Registering an Environment +========================== + +.. currentmodule:: omni.isaac.lab + +In the previous tutorial, we learned how to create a custom cartpole environment. We manually +created an instance of the environment by importing the environment class and its configuration +class. + +.. dropdown:: Environment creation in the previous tutorial + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/03_envs/run_cartpole_rl_env.py + :language: python + :start-at: # create environment configuration + :end-at: env = ManagerBasedRLEnv(cfg=env_cfg) + +While straightforward, this approach is not scalable as we have a large suite of environments. +In this tutorial, we will show how to use the :meth:`gymnasium.register` method to register +environments with the ``gymnasium`` registry. This allows us to create the environment through +the :meth:`gymnasium.make` function. + + +.. dropdown:: Environment creation in this tutorial + :icon: code + + .. literalinclude:: ../../../../source/standalone/environments/random_agent.py + :language: python + :lines: 36-47 + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``random_agent.py`` script in the ``source/standalone/environments`` directory. + +.. dropdown:: Code for random_agent.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/environments/random_agent.py + :language: python + :emphasize-lines: 36-37, 42-47 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +The :class:`envs.ManagerBasedRLEnv` class inherits from the :class:`gymnasium.Env` class to follow +a standard interface. However, unlike the traditional Gym environments, the :class:`envs.ManagerBasedRLEnv` +implements a *vectorized* environment. This means that multiple environment instances +are running simultaneously in the same process, and all the data is returned in a batched +fashion. + +Similarly, the :class:`envs.DirectRLEnv` class also inherits from the :class:`gymnasium.Env` class +for the direct workflow. For :class:`envs.DirectMARLEnv`, although it does not inherit +from Gymnasium, it can be registered and created in the same way. + +Using the gym registry +---------------------- + +To register an environment, we use the :meth:`gymnasium.register` method. This method takes +in the environment name, the entry point to the environment class, and the entry point to the +environment configuration class. + +.. note:: + The :mod:`gymnasium` registry is a global registry. Hence, it is important to ensure that the + environment names are unique. Otherwise, the registry will throw an error when registering + the environment. + +Manager-Based Environments +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For manager-based environments, the following shows the registration +call for the cartpole environment in the ``omni.isaac.lab_tasks.manager_based.classic.cartpole`` sub-package: + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/manager_based/classic/cartpole/__init__.py + :language: python + :lines: 10- + :emphasize-lines: 4, 11, 12, 15 + +The ``id`` argument is the name of the environment. As a convention, we name all the environments +with the prefix ``Isaac-`` to make it easier to search for them in the registry. The name of the +environment is typically followed by the name of the task, and then the name of the robot. +For instance, for legged locomotion with ANYmal C on flat terrain, the environment is called +``Isaac-Velocity-Flat-Anymal-C-v0``. The version number ``v`` is typically used to specify different +variations of the same environment. Otherwise, the names of the environments can become too long +and difficult to read. + +The ``entry_point`` argument is the entry point to the environment class. The entry point is a string +of the form ``:``. In the case of the cartpole environment, the entry point is +``omni.isaac.lab.envs:ManagerBasedRLEnv``. The entry point is used to import the environment class +when creating the environment instance. + +The ``env_cfg_entry_point`` argument specifies the default configuration for the environment. The default +configuration is loaded using the :meth:`omni.isaac.lab_tasks.utils.parse_env_cfg` function. +It is then passed to the :meth:`gymnasium.make` function to create the environment instance. +The configuration entry point can be both a YAML file or a python configuration class. + +Direct Environments +^^^^^^^^^^^^^^^^^^^ + +For direct-based environments, the environment registration follows a similar pattern. Instead of +registering the environment's entry point as the :class:`~omni.isaac.lab.envs.ManagerBasedRLEnv` class, +we register the environment's entry point as the implementation class of the environment. +Additionally, we add the suffix ``-Direct`` to the environment name to differentiate it from the +manager-based environments. + +As an example, the following shows the registration call for the cartpole environment in the +``omni.isaac.lab_tasks.direct.cartpole`` sub-package: + +.. literalinclude:: ../../../../source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/cartpole/__init__.py + :language: python + :lines: 10-31 + :emphasize-lines: 5, 12, 13, 16 + + +Creating the environment +------------------------ + +To inform the ``gym`` registry with all the environments provided by the ``omni.isaac.lab_tasks`` +extension, we must import the module at the start of the script. This will execute the ``__init__.py`` +file which iterates over all the sub-packages and registers their respective environments. + +.. literalinclude:: ../../../../source/standalone/environments/random_agent.py + :language: python + :start-at: import omni.isaac.lab_tasks # noqa: F401 + :end-at: import omni.isaac.lab_tasks # noqa: F401 + +In this tutorial, the task name is read from the command line. The task name is used to parse +the default configuration as well as to create the environment instance. In addition, other +parsed command line arguments such as the number of environments, the simulation device, +and whether to render, are used to override the default configuration. + +.. literalinclude:: ../../../../source/standalone/environments/random_agent.py + :language: python + :start-at: # create environment configuration + :end-at: env = gym.make(args_cli.task, cfg=env_cfg) + +Once creating the environment, the rest of the execution follows the standard resetting and stepping. + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/environments/random_agent.py --task Isaac-Cartpole-v0 --num_envs 32 + + +This should open a stage with everything similar to the :ref:`tutorial-create-manager-rl-env` tutorial. +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal. + + +.. figure:: ../../_static/tutorials/tutorial_register_environment.jpg + :align: center + :figwidth: 100% + :alt: result of random_agent.py + + +In addition, you can also change the simulation device from GPU to CPU by setting the value of the ``--device`` flag explicitly: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/environments/random_agent.py --task Isaac-Cartpole-v0 --num_envs 32 --device cpu + +With the ``--device cpu`` flag, the simulation will run on the CPU. This is useful for debugging the simulation. +However, the simulation will run much slower than on the GPU. diff --git a/_sources/source/tutorials/03_envs/run_rl_training.rst b/_sources/source/tutorials/03_envs/run_rl_training.rst new file mode 100644 index 0000000000..de01ee9f28 --- /dev/null +++ b/_sources/source/tutorials/03_envs/run_rl_training.rst @@ -0,0 +1,156 @@ +.. _tutorial-run-rl-training: + +Training with an RL Agent +========================= + +.. currentmodule:: omni.isaac.lab + +In the previous tutorials, we covered how to define an RL task environment, register +it into the ``gym`` registry, and interact with it using a random agent. We now move +on to the next step: training an RL agent to solve the task. + +Although the :class:`envs.ManagerBasedRLEnv` conforms to the :class:`gymnasium.Env` interface, +it is not exactly a ``gym`` environment. The input and outputs of the environment are +not numpy arrays, but rather based on torch tensors with the first dimension being the +number of environment instances. + +Additionally, most RL libraries expect their own variation of an environment interface. +For example, `Stable-Baselines3`_ expects the environment to conform to its +`VecEnv API`_ which expects a list of numpy arrays instead of a single tensor. Similarly, +`RSL-RL`_, `RL-Games`_ and `SKRL`_ expect a different interface. Since there is no one-size-fits-all +solution, we do not base the :class:`envs.ManagerBasedRLEnv` on any particular learning library. +Instead, we implement wrappers to convert the environment into the expected interface. +These are specified in the :mod:`omni.isaac.lab_tasks.utils.wrappers` module. + +In this tutorial, we will use `Stable-Baselines3`_ to train an RL agent to solve the +cartpole balancing task. + +.. caution:: + + 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 :class:`gymnasium.Env`. + +The Code +-------- + +For this tutorial, we use the training script from `Stable-Baselines3`_ workflow in the +``source/standalone/workflows/sb3`` directory. + +.. dropdown:: Code for train.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/workflows/sb3/train.py + :language: python + :emphasize-lines: 57, 66, 68-70, 81, 90-98, 100, 105-113, 115-116, 121-126, 133-136 + :linenos: + +The Code Explained +------------------ + +.. currentmodule:: omni.isaac.lab_tasks.utils + +Most of the code above is boilerplate code to create logging directories, saving the parsed configurations, +and setting up different Stable-Baselines3 components. For this tutorial, the important part is creating +the environment and wrapping it with the Stable-Baselines3 wrapper. + +There are three wrappers used in the code above: + +1. :class:`gymnasium.wrappers.RecordVideo`: This wrapper records a video of the environment + and saves it to the specified directory. This is useful for visualizing the agent's behavior + during training. +2. :class:`wrappers.sb3.Sb3VecEnvWrapper`: This wrapper converts the environment + into a Stable-Baselines3 compatible environment. +3. `stable_baselines3.common.vec_env.VecNormalize`_: This wrapper normalizes the + environment's observations and rewards. + +Each of these wrappers wrap around the previous wrapper by following ``env = wrapper(env, *args, **kwargs)`` +repeatedly. The final environment is then used to train the agent. For more information on how these +wrappers work, please refer to the :ref:`how-to-env-wrappers` documentation. + +The Code Execution +------------------ + +We train a PPO agent from Stable-Baselines3 to solve the cartpole balancing task. + +Training the agent +~~~~~~~~~~~~~~~~~~ + +There are three main ways to train the agent. Each of them has their own advantages and disadvantages. +It is up to you to decide which one you prefer based on your use case. + +Headless execution +"""""""""""""""""" + +If the ``--headless`` flag is set, the simulation is not rendered during training. This is useful +when training on a remote server or when you do not want to see the simulation. Typically, it speeds +up the training process since only physics simulation step is performed. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/sb3/train.py --task Isaac-Cartpole-v0 --num_envs 64 --headless + + +Headless execution with off-screen render +""""""""""""""""""""""""""""""""""""""""" + +Since the above command does not render the simulation, it is not possible to visualize the agent's +behavior during training. To visualize the agent's behavior, we pass the ``--enable_cameras`` which +enables off-screen rendering. Additionally, we pass the flag ``--video`` which records a video of the +agent's behavior during training. + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/sb3/train.py --task Isaac-Cartpole-v0 --num_envs 64 --headless --video + +The videos are saved to the ``logs/sb3/Isaac-Cartpole-v0//videos/train`` directory. You can open these videos +using any video player. + +Interactive execution +""""""""""""""""""""" + +.. currentmodule:: omni.isaac.lab + +While the above two methods are useful for training the agent, they don't allow you to interact with the +simulation to see what is happening. In this case, you can ignore the ``--headless`` flag and run the +training script as follows: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/workflows/sb3/train.py --task Isaac-Cartpole-v0 --num_envs 64 + +This will open the Isaac Sim window and you can see the agent training in the environment. However, this +will slow down the training process since the simulation is rendered on the screen. As a workaround, you +can switch between different render modes in the ``"Isaac Lab"`` window that is docked on the bottom-right +corner of the screen. To learn more about these render modes, please check the +:class:`sim.SimulationContext.RenderMode` class. + +Viewing the logs +~~~~~~~~~~~~~~~~ + +On a separate terminal, you can monitor the training progress by executing the following command: + +.. code:: bash + + # execute from the root directory of the repository + ./isaaclab.sh -p -m tensorboard.main --logdir logs/sb3/Isaac-Cartpole-v0 + +Playing the trained agent +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once the training is complete, you can visualize the trained agent by executing the following command: + +.. code:: bash + + # execute from the root directory of the repository + ./isaaclab.sh -p source/standalone/workflows/sb3/play.py --task Isaac-Cartpole-v0 --num_envs 32 --use_last_checkpoint + +The above command will load the latest checkpoint from the ``logs/sb3/Isaac-Cartpole-v0`` +directory. You can also specify a specific checkpoint by passing the ``--checkpoint`` flag. + +.. _Stable-Baselines3: https://stable-baselines3.readthedocs.io/en/master/ +.. _VecEnv API: https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html#vecenv-api-vs-gym-api +.. _`stable_baselines3.common.vec_env.VecNormalize`: https://stable-baselines3.readthedocs.io/en/master/guide/vec_envs.html#vecnormalize +.. _RL-Games: https://github.com/Denys88/rl_games +.. _RSL-RL: https://github.com/leggedrobotics/rsl_rl +.. _SKRL: https://skrl.readthedocs.io diff --git a/_sources/source/tutorials/04_sensors/add_sensors_on_robot.rst b/_sources/source/tutorials/04_sensors/add_sensors_on_robot.rst new file mode 100644 index 0000000000..3ddd39bf48 --- /dev/null +++ b/_sources/source/tutorials/04_sensors/add_sensors_on_robot.rst @@ -0,0 +1,210 @@ +.. _tutorial-add-sensors-on-robot: + +Adding sensors on a robot +========================= + +.. currentmodule:: omni.isaac.lab + + +While the asset classes allow us to create and simulate the physical embodiment of the robot, +sensors help in obtaining information about the environment. They typically update at a lower +frequency than the simulation and are useful for obtaining different proprioceptive and +exteroceptive information. For example, a camera sensor can be used to obtain the visual +information of the environment, and a contact sensor can be used to obtain the contact +information of the robot with the environment. + +In this tutorial, we will see how to add different sensors to a robot. We will use the +ANYmal-C robot for this tutorial. The ANYmal-C robot is a quadrupedal robot with 12 degrees +of freedom. It has 4 legs, each with 3 degrees of freedom. The robot has the following +sensors: + +- A camera sensor on the head of the robot which provides RGB-D images +- A height scanner sensor that provides terrain height information +- Contact sensors on the feet of the robot that provide contact information + +We continue this tutorial from the previous tutorial on :ref:`tutorial-interactive-scene`, +where we learned about the :class:`scene.InteractiveScene` class. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``add_sensors_on_robot.py`` script in the +``source/standalone/tutorials/04_sensors`` directory. + +.. dropdown:: Code for add_sensors_on_robot.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/04_sensors/add_sensors_on_robot.py + :language: python + :emphasize-lines: 72-95, 143-153, 167-168 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +Similar to the previous tutorials, where we added assets to the scene, the sensors are also added +to the scene using the scene configuration. All sensors inherit from the :class:`sensors.SensorBase` class +and are configured through their respective config classes. Each sensor instance can define its own +update period, which is the frequency at which the sensor is updated. The update period is specified +in seconds through the :attr:`sensors.SensorBaseCfg.update_period` attribute. + +Depending on the specified path and the sensor type, the sensors are attached to the prims in the scene. +They may have an associated prim that is created in the scene or they may be attached to an existing prim. +For instance, the camera sensor has a corresponding prim that is created in the scene, whereas for the +contact sensor, the activating the contact reporting is a property on a rigid body prim. + +In the following, we introduce the different sensors we use in this tutorial and how they are configured. +For more description about them, please check the :mod:`sensors` module. + +Camera sensor +------------- + +A camera is defined using the :class:`sensors.CameraCfg`. It is based on the USD Camera sensor and +the different data types are captured using Omniverse Replicator API. Since it has a corresponding prim +in the scene, the prims are created in the scene at the specified prim path. + +The configuration of the camera sensor includes the following parameters: + +* :attr:`~sensors.CameraCfg.spawn`: The type of USD camera to create. This can be either + :class:`~sim.spawners.sensors.PinholeCameraCfg` or :class:`~sim.spawners.sensors.FisheyeCameraCfg`. +* :attr:`~sensors.CameraCfg.offset`: The offset of the camera sensor from the parent prim. +* :attr:`~sensors.CameraCfg.data_types`: The data types to capture. This can be ``rgb``, + ``distance_to_image_plane``, ``normals``, or other types supported by the USD Camera sensor. + +To attach an RGB-D camera sensor to the head of the robot, we specify an offset relative to the base +frame of the robot. The offset is specified as a translation and rotation relative to the base frame, +and the :attr:`~sensors.CameraCfg.OffsetCfg.convention` in which the offset is specified. + +In the following, we show the configuration of the camera sensor used in this tutorial. We set the +update period to 0.1s, which means that the camera sensor is updated at 10Hz. The prim path expression is +set to ``{ENV_REGEX_NS}/Robot/base/front_cam`` where the ``{ENV_REGEX_NS}`` is the environment namespace, +``"Robot"`` is the name of the robot, ``"base"`` is the name of the prim to which the camera is attached, +and ``"front_cam"`` is the name of the prim associated with the camera sensor. + +.. literalinclude:: ../../../../source/standalone/tutorials/04_sensors/add_sensors_on_robot.py + :language: python + :start-at: camera = CameraCfg( + :end-before: height_scanner = RayCasterCfg( + +Height scanner +-------------- + +The height-scanner is implemented as a virtual sensor using the NVIDIA Warp ray-casting kernels. +Through the :class:`sensors.RayCasterCfg`, we can specify the pattern of rays to cast and the +meshes against which to cast the rays. Since they are virtual sensors, there is no corresponding +prim created in the scene for them. Instead they are attached to a prim in the scene, which is +used to specify the location of the sensor. + +For this tutorial, the ray-cast based height scanner is attached to the base frame of the robot. +The pattern of rays is specified using the :attr:`~sensors.RayCasterCfg.pattern` attribute. For +a uniform grid pattern, we specify the pattern using :class:`~sensors.patterns.GridPatternCfg`. +Since we only care about the height information, we do not need to consider the roll and pitch +of the robot. Hence, we set the :attr:`~sensors.RayCasterCfg.attach_yaw_only` to true. + +For the height-scanner, you can visualize the points where the rays hit the mesh. This is done +by setting the :attr:`~sensors.SensorBaseCfg.debug_vis` attribute to true. + +The entire configuration of the height-scanner is as follows: + +.. literalinclude:: ../../../../source/standalone/tutorials/04_sensors/add_sensors_on_robot.py + :language: python + :start-at: height_scanner = RayCasterCfg( + :end-before: contact_forces = ContactSensorCfg( + +Contact sensor +-------------- + +Contact sensors wrap around the PhysX contact reporting API to obtain the contact information of the robot +with the environment. Since it relies of PhysX, the contact sensor expects the contact reporting API +to be enabled on the rigid bodies of the robot. This can be done by setting the +:attr:`~sim.spawners.RigidObjectSpawnerCfg.activate_contact_sensors` to true in the asset configuration. + +Through the :class:`sensors.ContactSensorCfg`, it is possible to specify the prims for which we want to +obtain the contact information. Additional flags can be set to obtain more information about +the contact, such as the contact air time, contact forces between filtered prims, etc. + +In this tutorial, we attach the contact sensor to the feet of the robot. The feet of the robot are +named ``"LF_FOOT"``, ``"RF_FOOT"``, ``"LH_FOOT"``, and ``"RF_FOOT"``. We pass a Regex expression +``".*_FOOT"`` to simplify the prim path specification. This Regex expression matches all prims that +end with ``"_FOOT"``. + +We set the update period to 0 to update the sensor at the same frequency as the simulation. Additionally, +for contact sensors, we can specify the history length of the contact information to store. For this +tutorial, we set the history length to 6, which means that the contact information for the last 6 +simulation steps is stored. + +The entire configuration of the contact sensor is as follows: + +.. literalinclude:: ../../../../source/standalone/tutorials/04_sensors/add_sensors_on_robot.py + :language: python + :start-at: contact_forces = ContactSensorCfg( + :lines: 1-3 + +Running the simulation loop +--------------------------- + +Similar to when using assets, the buffers and physics handles for the sensors are initialized only +when the simulation is played, i.e., it is important to call ``sim.reset()`` after creating the scene. + +.. literalinclude:: ../../../../source/standalone/tutorials/04_sensors/add_sensors_on_robot.py + :language: python + :start-at: # Play the simulator + :end-at: sim.reset() + +Besides that, the simulation loop is similar to the previous tutorials. The sensors are updated as part +of the scene update and they internally handle the updating of their buffers based on their update +periods. + +The data from the sensors can be accessed through their ``data`` attribute. As an example, we show how +to access the data for the different sensors created in this tutorial: + +.. literalinclude:: ../../../../source/standalone/tutorials/04_sensors/add_sensors_on_robot.py + :language: python + :start-at: # print information from the sensors + :end-at: print("Received max contact force of: ", torch.max(scene["contact_forces"].data.net_forces_w).item()) + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/add_sensors_on_robot.py --num_envs 2 --enable_cameras + + +This command should open a stage with a ground plane, lights, and two quadrupedal robots. +Around the robots, you should see red spheres that indicate the points where the rays hit the mesh. +Additionally, you can switch the viewport to the camera view to see the RGB image captured by the +camera sensor. Please check `here `_ for more information +on how to switch the viewport to the camera view. + +.. figure:: ../../_static/tutorials/tutorial_add_sensors. jpg + :align: center + :figwidth: 100% + :alt: result of add_sensors_on_robot.py + +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal. + +While in this tutorial, we went over creating and using different sensors, there are many more sensors +available in the :mod:`sensors` module. We include minimal examples of using these sensors in the +``source/standalone/tutorials/04_sensors`` directory. For completeness, these scripts can be run using the +following commands: + +.. code-block:: bash + + # Frame Transformer + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_frame_transformer.py + + # Ray Caster + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_ray_caster.py + + # Ray Caster Camera + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_ray_caster_camera.py + + # USD Camera + ./isaaclab.sh -p source/standalone/tutorials/04_sensors/run_usd_camera.py diff --git a/_sources/source/tutorials/05_controllers/run_diff_ik.rst b/_sources/source/tutorials/05_controllers/run_diff_ik.rst new file mode 100644 index 0000000000..99600b0591 --- /dev/null +++ b/_sources/source/tutorials/05_controllers/run_diff_ik.rst @@ -0,0 +1,159 @@ +Using a task-space controller +============================= + +.. currentmodule:: omni.isaac.lab + +In the previous tutorials, we have joint-space controllers to control the robot. However, in many +cases, it is more intuitive to control the robot using a task-space controller. For example, if we +want to teleoperate the robot, it is easier to specify the desired end-effector pose rather than +the desired joint positions. + +In this tutorial, we will learn how to use a task-space controller to control the robot. +We will use the :class:`controllers.DifferentialIKController` class to track a desired +end-effector pose command. + + +The Code +~~~~~~~~ + +The tutorial corresponds to the ``run_diff_ik.py`` script in the +``source/standalone/tutorials/05_controllers`` directory. + + +.. dropdown:: Code for run_diff_ik.py + :icon: code + + .. literalinclude:: ../../../../source/standalone/tutorials/05_controllers/run_diff_ik.py + :language: python + :emphasize-lines: 98-100, 121-136, 155-157, 161-171 + :linenos: + + +The Code Explained +~~~~~~~~~~~~~~~~~~ + +While using any task-space controller, it is important to ensure that the provided +quantities are in the correct frames. When parallelizing environment instances, they are +all existing in the same unique simulation world frame. However, typically, we want each +environment itself to have its own local frame. This is accessible through the +:attr:`scene.InteractiveScene.env_origins` attribute. + +In our APIs, we use the following notation for frames: + +- The simulation world frame (denoted as ``w``), which is the frame of the entire simulation. +- The local environment frame (denoted as ``e``), which is the frame of the local environment. +- The robot's base frame (denoted as ``b``), which is the frame of the robot's base link. + +Since the asset instances are not "aware" of the local environment frame, they return +their states in the simulation world frame. Thus, we need to convert the obtained +quantities to the local environment frame. This is done by subtracting the local environment +origin from the obtained quantities. + + +Creating an IK controller +------------------------- + +The :class:`~controllers.DifferentialIKController` class computes the desired joint +positions for a robot to reach a desired end-effector pose. The included implementation +performs the computation in a batched format and uses PyTorch operations. It supports +different types of inverse kinematics solvers, including the damped least-squares method +and the pseudo-inverse method. These solvers can be specified using the +:attr:`~controllers.DifferentialIKControllerCfg.ik_method` argument. +Additionally, the controller can handle commands as both relative and absolute poses. + +In this tutorial, we will use the damped least-squares method to compute the desired +joint positions. Additionally, since we want to track desired end-effector poses, we +will use the absolute pose command mode. + +.. literalinclude:: ../../../../source/standalone/tutorials/05_controllers/run_diff_ik.py + :language: python + :start-at: # Create controller + :end-at: diff_ik_controller = DifferentialIKController(diff_ik_cfg, num_envs=scene.num_envs, device=sim.device) + +Obtaining the robot's joint and body indices +-------------------------------------------- + +The IK controller implementation is a computation-only class. Thus, it expects the +user to provide the necessary information about the robot. This includes the robot's +joint positions, current end-effector pose, and the Jacobian matrix. + +While the attribute :attr:`assets.ArticulationData.joint_pos` provides the joint positions, +we only want the joint positions of the robot's arm, and not the gripper. Similarly, while +the attribute :attr:`assets.ArticulationData.body_state_w` provides the state of all the +robot's bodies, we only want the state of the robot's end-effector. Thus, we need to +index into these arrays to obtain the desired quantities. + +For this, the articulation class provides the methods :meth:`~assets.Articulation.find_joints` +and :meth:`~assets.Articulation.find_bodies`. These methods take in the names of the joints +and bodies and return their corresponding indices. + +While you may directly use these methods to obtain the indices, we recommend using the +:attr:`~managers.SceneEntityCfg` class to resolve the indices. This class is used in various +places in the APIs to extract certain information from a scene entity. Internally, it +calls the above methods to obtain the indices. However, it also performs some additional +checks to ensure that the provided names are valid. Thus, it is a safer option to use +this class. + +.. literalinclude:: ../../../../source/standalone/tutorials/05_controllers/run_diff_ik.py + :language: python + :start-at: # Specify robot-specific parameters + :end-before: # Define simulation stepping + + +Computing robot command +----------------------- + +The IK controller separates the operation of setting the desired command and +computing the desired joint positions. This is done to allow for the user to +run the IK controller at a different frequency than the robot's control frequency. + +The :meth:`~controllers.DifferentialIKController.set_command` method takes in +the desired end-effector pose as a single batched array. The pose is specified in +the robot's base frame. + +.. literalinclude:: ../../../../source/standalone/tutorials/05_controllers/run_diff_ik.py + :language: python + :start-at: # reset controller + :end-at: diff_ik_controller.set_command(ik_commands) + +We can then compute the desired joint positions using the +:meth:`~controllers.DifferentialIKController.compute` method. +The method takes in the current end-effector pose (in base frame), Jacobian, and +current joint positions. We read the Jacobian matrix from the robot's data, which uses +its value computed from the physics engine. + + +.. literalinclude:: ../../../../source/standalone/tutorials/05_controllers/run_diff_ik.py + :language: python + :start-at: # obtain quantities from simulation + :end-at: joint_pos_des = diff_ik_controller.compute(ee_pos_b, ee_quat_b, jacobian, joint_pos) + +The computed joint position targets can then be applied on the robot, as done in the +previous tutorials. + +.. literalinclude:: ../../../../source/standalone/tutorials/05_controllers/run_diff_ik.py + :language: python + :start-at: # apply actions + :end-at: scene.write_data_to_sim() + + +The Code Execution +~~~~~~~~~~~~~~~~~~ + + +Now that we have gone through the code, let's run the script and see the result: + +.. code-block:: bash + + ./isaaclab.sh -p source/standalone/tutorials/05_controllers/run_diff_ik.py --robot franka_panda --num_envs 128 + +The script will start a simulation with 128 robots. The robots will be controlled using the IK controller. +The current and desired end-effector poses should be displayed using frame markers. When the robot reaches +the desired pose, the command should cycle through to the next pose specified in the script. + +.. figure:: ../../_static/tutorials/tutorial_task_space_controller.jpg + :align: center + :figwidth: 100% + :alt: result of run_diff_ik.py + +To stop the simulation, you can either close the window, or press ``Ctrl+C`` in the terminal. diff --git a/_sources/source/tutorials/index.rst b/_sources/source/tutorials/index.rst new file mode 100644 index 0000000000..d3d582a5c4 --- /dev/null +++ b/_sources/source/tutorials/index.rst @@ -0,0 +1,103 @@ +Tutorials +========= + +Welcome to the Isaac Lab tutorials! These tutorials provide a step-by-step guide to help you understand +and use various features of the framework. All the tutorials are written as Python scripts. You can +find the source code for each tutorial in the ``source/standalone/tutorials`` directory of the Isaac Lab +repository. + +.. note:: + + We would love to extend the tutorials to cover more topics and use cases, so please let us know if + you have any suggestions. + +We recommend that you go through the tutorials in the order they are listed here. + + +Setting up a Simple Simulation +------------------------------- + +These tutorials show you how to launch the simulation with different settings and spawn objects in the +simulated scene. They cover the following APIs: :class:`~omni.isaac.lab.app.AppLauncher`, +:class:`~omni.isaac.lab.sim.SimulationContext`, and :class:`~omni.isaac.lab.sim.spawners`. + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + 00_sim/create_empty + 00_sim/spawn_prims + 00_sim/launch_app + +Interacting with Assets +----------------------- + +Having spawned objects in the scene, these tutorials show you how to create physics handles for these +objects and interact with them. These revolve around the :class:`~omni.isaac.lab.assets.AssetBase` +class and its derivatives such as :class:`~omni.isaac.lab.assets.RigidObject`, +:class:`~omni.isaac.lab.assets.Articulation` and :class:`~omni.isaac.lab.assets.DeformableObject`. + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + 01_assets/run_rigid_object + 01_assets/run_articulation + 01_assets/run_deformable_object + +Creating a Scene +---------------- + +With the basic concepts of the framework covered, the tutorials move to a more intuitive scene +interface that uses the :class:`~omni.isaac.lab.scene.InteractiveScene` class. This class +provides a higher level abstraction for creating scenes easily. + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + 02_scene/create_scene + +Designing an Environment +------------------------ + +The following tutorials introduce the concept of manager-based environments: :class:`~omni.isaac.lab.envs.ManagerBasedEnv` +and its derivative :class:`~omni.isaac.lab.envs.ManagerBasedRLEnv`, as well as the direct workflow base class +:class:`~omni.isaac.lab.envs.DirectRLEnv`. These environments bring-in together +different aspects of the framework to create a simulation environment for agent interaction. + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + 03_envs/create_manager_base_env + 03_envs/create_manager_rl_env + 03_envs/create_direct_rl_env + 03_envs/register_rl_env_gym + 03_envs/run_rl_training + 03_envs/modify_direct_rl_env + +Integrating Sensors +------------------- + +The following tutorial shows you how to integrate sensors into the simulation environment. The +tutorials introduce the :class:`~omni.isaac.lab.sensors.SensorBase` class and its derivatives +such as :class:`~omni.isaac.lab.sensors.Camera` and :class:`~omni.isaac.lab.sensors.RayCaster`. + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + 04_sensors/add_sensors_on_robot + +Using motion generators +----------------------- + +While the robots in the simulation environment can be controlled at the joint-level, the following +tutorials show you how to use motion generators to control the robots at the task-level. + +.. toctree:: + :maxdepth: 1 + :titlesonly: + + 05_controllers/run_diff_ik diff --git a/_sphinx_design_static/design-tabs.js b/_sphinx_design_static/design-tabs.js new file mode 100644 index 0000000000..b25bd6a4fa --- /dev/null +++ b/_sphinx_design_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_sphinx_design_static/sphinx-design.4cbf315f70debaebd550c87a6162cf0f.min.css b/_sphinx_design_static/sphinx-design.4cbf315f70debaebd550c87a6162cf0f.min.css new file mode 100644 index 0000000000..860c36da0f --- /dev/null +++ b/_sphinx_design_static/sphinx-design.4cbf315f70debaebd550c87a6162cf0f.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative;font-size:var(--sd-fontsize-dropdown)}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary.sd-summary-title{padding:.5em .6em .5em 1em;font-size:var(--sd-fontsize-dropdown-title);font-weight:var(--sd-fontweight-dropdown-title);user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;list-style:none;display:inline-flex;justify-content:space-between}details.sd-dropdown summary.sd-summary-title::-webkit-details-marker{display:none}details.sd-dropdown summary.sd-summary-title:focus{outline:none}details.sd-dropdown summary.sd-summary-title .sd-summary-icon{margin-right:.6em;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary.sd-summary-title .sd-summary-text{flex-grow:1;line-height:1.5;padding-right:.5rem}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker{pointer-events:none;display:inline-flex;align-items:center}details.sd-dropdown summary.sd-summary-title .sd-summary-state-marker svg{opacity:.6}details.sd-dropdown summary.sd-summary-title:hover .sd-summary-state-marker svg{opacity:1;transform:scale(1.1)}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown .sd-summary-chevron-right{transition:.25s}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-right{transform:rotate(90deg)}details.sd-dropdown[open]>.sd-summary-title .sd-summary-chevron-down{transform:rotate(180deg)}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-bg: rgba(0, 113, 188, 0.2);--sd-color-secondary-bg: rgba(108, 117, 125, 0.2);--sd-color-success-bg: rgba(40, 167, 69, 0.2);--sd-color-info-bg: rgba(23, 162, 184, 0.2);--sd-color-warning-bg: rgba(240, 179, 126, 0.2);--sd-color-danger-bg: rgba(220, 53, 69, 0.2);--sd-color-light-bg: rgba(248, 249, 250, 0.2);--sd-color-muted-bg: rgba(108, 117, 125, 0.2);--sd-color-dark-bg: rgba(33, 37, 41, 0.2);--sd-color-black-bg: rgba(0, 0, 0, 0.2);--sd-color-white-bg: rgba(255, 255, 255, 0.2);--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem;--sd-fontsize-dropdown: inherit;--sd-fontsize-dropdown-title: 1rem;--sd-fontweight-dropdown-title: 700} diff --git a/_static/NVIDIA-logo-black.png b/_static/NVIDIA-logo-black.png new file mode 100644 index 0000000000..12e95458e7 Binary files /dev/null and b/_static/NVIDIA-logo-black.png differ diff --git a/_static/NVIDIA-logo-white.png b/_static/NVIDIA-logo-white.png new file mode 100644 index 0000000000..1143326b85 Binary files /dev/null and b/_static/NVIDIA-logo-white.png differ diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000000..61572969d1 --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,903 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/check-solid.svg b/_static/check-solid.svg new file mode 100644 index 0000000000..92fad4b5c0 --- /dev/null +++ b/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/_static/clipboard.min.js b/_static/clipboard.min.js new file mode 100644 index 0000000000..54b3c46381 --- /dev/null +++ b/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/_static/copybutton.css b/_static/copybutton.css new file mode 100644 index 0000000000..f1916ec7d1 --- /dev/null +++ b/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 0000000000..2ea7ff3e21 --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 0000000000..dbe1aaad79 --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/custom.css b/_static/custom.css new file mode 100644 index 0000000000..4cc0b914ff --- /dev/null +++ b/_static/custom.css @@ -0,0 +1,84 @@ +/* + * For reference: https://pydata-sphinx-theme.readthedocs.io/en/v0.9.0/user_guide/customizing.html + * For colors: https://clrs.cc/ + */ + +/* anything related to the light theme */ +html[data-theme="light"] { + --pst-color-primary: #76B900; + --pst-color-secondary: #5b8e03; + --pst-color-secondary-highlight: #5b8e03; + --pst-color-inline-code-links: #76B900; + --pst-color-info: var(--pst-color-primary); + --pst-color-info-highlight: var(--pst-color-primary); + --pst-color-info-bg: #daedb9; + --pst-color-attention: #ffc107; + --pst-color-text-base: #323232; + --pst-color-text-muted: #646464; + --pst-color-shadow: #d8d8d8; + --pst-color-border: #c9c9c9; + --pst-color-inline-code: #76B900; + --pst-color-target: #fbe54e; + --pst-color-background: #fff; + --pst-color-on-background: #fff; + --pst-color-surface: #f5f5f5; + --pst-color-on-surface: #e1e1e1; + --pst-color-link: var(--pst-color-primary); + --pst-color-link-hover: #789841; + --pst-color-table-row-hover-bg: #daedb9; + --pst-color-accent: var(--pst-color-primary); +} + +/* anything related to the dark theme */ +html[data-theme="dark"] { + --pst-color-primary: #76B900; + --pst-color-secondary: #c2f26f; + --pst-color-secondary-highlight: #c2f26f; + --pst-color-inline-code-links: #b6e664; + --pst-color-info: var(--pst-color-primary); + --pst-color-info-highlight: var(--pst-color-primary); + --pst-color-info-bg: #3a550b; + --pst-color-attention: #dca90f; + --pst-color-text-base: #cecece; + --pst-color-text-muted: #a6a6a6; + --pst-color-shadow: #212121; + --pst-color-border: silver; + --pst-color-inline-code: #76B900; + --pst-color-target: #472700; + --pst-color-background: #121212; + --pst-color-on-background: #1e1e1e; + --pst-color-surface: #212121; + --pst-color-on-surface: #373737; + --pst-color-link: var(--pst-color-primary); + --pst-color-link-hover: #aee354; + --pst-color-table-row-hover-bg: #3a550b; + --pst-color-accent: var(--pst-color-primary); +} + +a { + text-decoration: none !important; +} + +/* for the announcement link */ +.bd-header-announcement a, +.bd-header-version-warning a { + color: #7FDBFF; +} + +/* for the search box in the navbar */ +.form-control { + border-radius: 0 !important; + border: none !important; + outline: none !important; +} + +/* reduce padding for logo */ +.navbar-brand { + padding-top: 0.0rem !important; + padding-bottom: 0.0rem !important; +} + +.navbar-icon-links { + padding-top: 0.0rem !important; + padding-bottom: 0.0rem !important; +} diff --git a/_static/design-tabs.js b/_static/design-tabs.js new file mode 100644 index 0000000000..b25bd6a4fa --- /dev/null +++ b/_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record} + */ +let sd_id_to_elements = {}; +const storageKeyPrefix = "sphinx-design-tab-id-"; + +/** + * Create a key for a tab element. + * @param {HTMLElement} el - The tab element. + * @returns {[string, string, string] | null} - The key. + * + */ +function create_key(el) { + let syncId = el.getAttribute("data-sync-id"); + let syncGroup = el.getAttribute("data-sync-group"); + if (!syncId || !syncGroup) return null; + return [syncGroup, syncId, syncGroup + "--" + syncId]; +} + +/** + * Initialize the tab selection. + * + */ +function ready() { + // Find all tabs with sync data + + /** @type {string[]} */ + let groups = []; + + document.querySelectorAll(".sd-tab-label").forEach((label) => { + if (label instanceof HTMLElement) { + let data = create_key(label); + if (data) { + let [group, id, key] = data; + + // add click event listener + // @ts-ignore + label.onclick = onSDLabelClick; + + // store map of key to elements + if (!sd_id_to_elements[key]) { + sd_id_to_elements[key] = []; + } + sd_id_to_elements[key].push(label); + + if (groups.indexOf(group) === -1) { + groups.push(group); + // Check if a specific tab has been selected via URL parameter + const tabParam = new URLSearchParams(window.location.search).get( + group + ); + if (tabParam) { + console.log( + "sphinx-design: Selecting tab id for group '" + + group + + "' from URL parameter: " + + tabParam + ); + window.sessionStorage.setItem(storageKeyPrefix + group, tabParam); + } + } + + // Check is a specific tab has been selected previously + let previousId = window.sessionStorage.getItem( + storageKeyPrefix + group + ); + if (previousId === id) { + // console.log( + // "sphinx-design: Selecting tab from session storage: " + id + // ); + // @ts-ignore + label.previousElementSibling.checked = true; + } + } + } + }); +} + +/** + * Activate other tabs with the same sync id. + * + * @this {HTMLElement} - The element that was clicked. + */ +function onSDLabelClick() { + let data = create_key(this); + if (!data) return; + let [group, id, key] = data; + for (const label of sd_id_to_elements[key]) { + if (label === this) continue; + // @ts-ignore + label.previousElementSibling.checked = true; + } + window.sessionStorage.setItem(storageKeyPrefix + group, id); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000000..d06a71d751 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 0000000000..4d08804caa --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '', + LANGUAGE: 'zh-CN', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/favicon.ico b/_static/favicon.ico new file mode 100644 index 0000000000..7b39f8af7a Binary files /dev/null and b/_static/favicon.ico differ diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000000..a858a410e4 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/images/logo_binder.svg b/_static/images/logo_binder.svg new file mode 100644 index 0000000000..45fecf7511 --- /dev/null +++ b/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + + +logo + + + + + + + + diff --git a/_static/images/logo_colab.png b/_static/images/logo_colab.png new file mode 100644 index 0000000000..b7560ec216 Binary files /dev/null and b/_static/images/logo_colab.png differ diff --git a/_static/images/logo_deepnote.svg b/_static/images/logo_deepnote.svg new file mode 100644 index 0000000000..fa77ebfc25 --- /dev/null +++ b/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/_static/images/logo_jupyterhub.svg b/_static/images/logo_jupyterhub.svg new file mode 100644 index 0000000000..60cfe9f222 --- /dev/null +++ b/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ +logo_jupyterhubHub diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 0000000000..250f5665fa --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.mo b/_static/locales/ar/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..15541a6a37 Binary files /dev/null and b/_static/locales/ar/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.po b/_static/locales/ar/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..2e8d682024 --- /dev/null +++ b/_static/locales/ar/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ar\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "أقترح تحرير" + +msgid "Last updated on" +msgstr "آخر تحديث في" + +msgid "Edit this page" +msgstr "قم بتحرير هذه الصفحة" + +msgid "Launch" +msgstr "إطلاق" + +msgid "Print to PDF" +msgstr "طباعة إلى PDF" + +msgid "open issue" +msgstr "قضية مفتوحة" + +msgid "Download notebook file" +msgstr "تنزيل ملف دفتر الملاحظات" + +msgid "Toggle navigation" +msgstr "تبديل التنقل" + +msgid "Source repository" +msgstr "مستودع المصدر" + +msgid "By the" +msgstr "بواسطة" + +msgid "next page" +msgstr "الصفحة التالية" + +msgid "repository" +msgstr "مخزن" + +msgid "Sphinx Book Theme" +msgstr "موضوع كتاب أبو الهول" + +msgid "Download source file" +msgstr "تنزيل ملف المصدر" + +msgid "Contents" +msgstr "محتويات" + +msgid "By" +msgstr "بواسطة" + +msgid "Copyright" +msgstr "حقوق النشر" + +msgid "Fullscreen mode" +msgstr "وضع ملء الشاشة" + +msgid "Open an issue" +msgstr "افتح قضية" + +msgid "previous page" +msgstr "الصفحة السابقة" + +msgid "Download this page" +msgstr "قم بتنزيل هذه الصفحة" + +msgid "Theme by the" +msgstr "موضوع بواسطة" diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.mo b/_static/locales/bg/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..da95120037 Binary files /dev/null and b/_static/locales/bg/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.po b/_static/locales/bg/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..56ef0ebdfa --- /dev/null +++ b/_static/locales/bg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "предложи редактиране" + +msgid "Last updated on" +msgstr "Последна актуализация на" + +msgid "Edit this page" +msgstr "Редактирайте тази страница" + +msgid "Launch" +msgstr "Стартиране" + +msgid "Print to PDF" +msgstr "Печат в PDF" + +msgid "open issue" +msgstr "отворен брой" + +msgid "Download notebook file" +msgstr "Изтеглете файла на бележника" + +msgid "Toggle navigation" +msgstr "Превключване на навигацията" + +msgid "Source repository" +msgstr "Хранилище на източника" + +msgid "By the" +msgstr "По" + +msgid "next page" +msgstr "Следваща страница" + +msgid "repository" +msgstr "хранилище" + +msgid "Sphinx Book Theme" +msgstr "Тема на книгата Sphinx" + +msgid "Download source file" +msgstr "Изтеглете изходния файл" + +msgid "Contents" +msgstr "Съдържание" + +msgid "By" +msgstr "От" + +msgid "Copyright" +msgstr "Авторско право" + +msgid "Fullscreen mode" +msgstr "Режим на цял екран" + +msgid "Open an issue" +msgstr "Отворете проблем" + +msgid "previous page" +msgstr "предишна страница" + +msgid "Download this page" +msgstr "Изтеглете тази страница" + +msgid "Theme by the" +msgstr "Тема от" diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.mo b/_static/locales/bn/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..6b96639b72 Binary files /dev/null and b/_static/locales/bn/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.po b/_static/locales/bn/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..243ca31f73 --- /dev/null +++ b/_static/locales/bn/LC_MESSAGES/booktheme.po @@ -0,0 +1,63 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bn\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Last updated on" +msgstr "সর্বশেষ আপডেট" + +msgid "Edit this page" +msgstr "এই পৃষ্ঠাটি সম্পাদনা করুন" + +msgid "Launch" +msgstr "শুরু করা" + +msgid "Print to PDF" +msgstr "পিডিএফ প্রিন্ট করুন" + +msgid "open issue" +msgstr "খোলা সমস্যা" + +msgid "Download notebook file" +msgstr "নোটবুক ফাইল ডাউনলোড করুন" + +msgid "Toggle navigation" +msgstr "নেভিগেশন টগল করুন" + +msgid "Source repository" +msgstr "উত্স সংগ্রহস্থল" + +msgid "By the" +msgstr "দ্বারা" + +msgid "next page" +msgstr "পরবর্তী পৃষ্ঠা" + +msgid "Sphinx Book Theme" +msgstr "স্পিনিক্স বুক থিম" + +msgid "Download source file" +msgstr "উত্স ফাইল ডাউনলোড করুন" + +msgid "By" +msgstr "দ্বারা" + +msgid "Copyright" +msgstr "কপিরাইট" + +msgid "Open an issue" +msgstr "একটি সমস্যা খুলুন" + +msgid "previous page" +msgstr "আগের পৃষ্ঠা" + +msgid "Download this page" +msgstr "এই পৃষ্ঠাটি ডাউনলোড করুন" + +msgid "Theme by the" +msgstr "থিম দ্বারা" diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.mo b/_static/locales/ca/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..a4dd30e9bd Binary files /dev/null and b/_static/locales/ca/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.po b/_static/locales/ca/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..b27a13db9c --- /dev/null +++ b/_static/locales/ca/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "suggerir edició" + +msgid "Last updated on" +msgstr "Darrera actualització el" + +msgid "Edit this page" +msgstr "Editeu aquesta pàgina" + +msgid "Launch" +msgstr "Llançament" + +msgid "Print to PDF" +msgstr "Imprimeix a PDF" + +msgid "open issue" +msgstr "número obert" + +msgid "Download notebook file" +msgstr "Descarregar fitxer de quadern" + +msgid "Toggle navigation" +msgstr "Commuta la navegació" + +msgid "Source repository" +msgstr "Dipòsit de fonts" + +msgid "By the" +msgstr "Per la" + +msgid "next page" +msgstr "pàgina següent" + +msgid "Sphinx Book Theme" +msgstr "Tema del llibre Esfinx" + +msgid "Download source file" +msgstr "Baixeu el fitxer font" + +msgid "By" +msgstr "Per" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Open an issue" +msgstr "Obriu un número" + +msgid "previous page" +msgstr "Pàgina anterior" + +msgid "Download this page" +msgstr "Descarregueu aquesta pàgina" + +msgid "Theme by the" +msgstr "Tema del" diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.mo b/_static/locales/cs/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..c39e01a6ae Binary files /dev/null and b/_static/locales/cs/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.po b/_static/locales/cs/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..3818df9765 --- /dev/null +++ b/_static/locales/cs/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "navrhnout úpravy" + +msgid "Last updated on" +msgstr "Naposledy aktualizováno" + +msgid "Edit this page" +msgstr "Upravit tuto stránku" + +msgid "Launch" +msgstr "Zahájení" + +msgid "Print to PDF" +msgstr "Tisk do PDF" + +msgid "open issue" +msgstr "otevřené číslo" + +msgid "Download notebook file" +msgstr "Stáhnout soubor poznámkového bloku" + +msgid "Toggle navigation" +msgstr "Přepnout navigaci" + +msgid "Source repository" +msgstr "Zdrojové úložiště" + +msgid "By the" +msgstr "Podle" + +msgid "next page" +msgstr "další strana" + +msgid "repository" +msgstr "úložiště" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "Download source file" +msgstr "Stáhněte si zdrojový soubor" + +msgid "Contents" +msgstr "Obsah" + +msgid "By" +msgstr "Podle" + +msgid "Copyright" +msgstr "autorská práva" + +msgid "Fullscreen mode" +msgstr "Režim celé obrazovky" + +msgid "Open an issue" +msgstr "Otevřete problém" + +msgid "previous page" +msgstr "předchozí stránka" + +msgid "Download this page" +msgstr "Stáhněte si tuto stránku" + +msgid "Theme by the" +msgstr "Téma od" diff --git a/_static/locales/da/LC_MESSAGES/booktheme.mo b/_static/locales/da/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..f43157d70c Binary files /dev/null and b/_static/locales/da/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/da/LC_MESSAGES/booktheme.po b/_static/locales/da/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..7f20a3bd09 --- /dev/null +++ b/_static/locales/da/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "Last updated on" +msgstr "Sidst opdateret den" + +msgid "Edit this page" +msgstr "Rediger denne side" + +msgid "Launch" +msgstr "Start" + +msgid "Print to PDF" +msgstr "Udskriv til PDF" + +msgid "open issue" +msgstr "åbent nummer" + +msgid "Download notebook file" +msgstr "Download notesbog-fil" + +msgid "Toggle navigation" +msgstr "Skift navigation" + +msgid "Source repository" +msgstr "Kildelager" + +msgid "By the" +msgstr "Ved" + +msgid "next page" +msgstr "Næste side" + +msgid "repository" +msgstr "lager" + +msgid "Sphinx Book Theme" +msgstr "Sphinx bogtema" + +msgid "Download source file" +msgstr "Download kildefil" + +msgid "Contents" +msgstr "Indhold" + +msgid "By" +msgstr "Ved" + +msgid "Copyright" +msgstr "ophavsret" + +msgid "Fullscreen mode" +msgstr "Fuldskærmstilstand" + +msgid "Open an issue" +msgstr "Åbn et problem" + +msgid "previous page" +msgstr "forrige side" + +msgid "Download this page" +msgstr "Download denne side" + +msgid "Theme by the" +msgstr "Tema af" diff --git a/_static/locales/de/LC_MESSAGES/booktheme.mo b/_static/locales/de/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..648b565c78 Binary files /dev/null and b/_static/locales/de/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/de/LC_MESSAGES/booktheme.po b/_static/locales/de/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..c0027d3ab0 --- /dev/null +++ b/_static/locales/de/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "vorschlagen zu bearbeiten" + +msgid "Last updated on" +msgstr "Zuletzt aktualisiert am" + +msgid "Edit this page" +msgstr "Bearbeite diese Seite" + +msgid "Launch" +msgstr "Starten" + +msgid "Print to PDF" +msgstr "In PDF drucken" + +msgid "open issue" +msgstr "offenes Thema" + +msgid "Download notebook file" +msgstr "Notebook-Datei herunterladen" + +msgid "Toggle navigation" +msgstr "Navigation umschalten" + +msgid "Source repository" +msgstr "Quell-Repository" + +msgid "By the" +msgstr "Bis zum" + +msgid "next page" +msgstr "Nächste Seite" + +msgid "repository" +msgstr "Repository" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-Buch-Thema" + +msgid "Download source file" +msgstr "Quelldatei herunterladen" + +msgid "Contents" +msgstr "Inhalt" + +msgid "By" +msgstr "Durch" + +msgid "Copyright" +msgstr "Urheberrechte ©" + +msgid "Fullscreen mode" +msgstr "Vollbildmodus" + +msgid "Open an issue" +msgstr "Öffnen Sie ein Problem" + +msgid "previous page" +msgstr "vorherige Seite" + +msgid "Download this page" +msgstr "Laden Sie diese Seite herunter" + +msgid "Theme by the" +msgstr "Thema von der" diff --git a/_static/locales/el/LC_MESSAGES/booktheme.mo b/_static/locales/el/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..fca6e9355f Binary files /dev/null and b/_static/locales/el/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/el/LC_MESSAGES/booktheme.po b/_static/locales/el/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..bdeb3270aa --- /dev/null +++ b/_static/locales/el/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "προτείνω επεξεργασία" + +msgid "Last updated on" +msgstr "Τελευταία ενημέρωση στις" + +msgid "Edit this page" +msgstr "Επεξεργαστείτε αυτήν τη σελίδα" + +msgid "Launch" +msgstr "Εκτόξευση" + +msgid "Print to PDF" +msgstr "Εκτύπωση σε PDF" + +msgid "open issue" +msgstr "ανοιχτό ζήτημα" + +msgid "Download notebook file" +msgstr "Λήψη αρχείου σημειωματάριου" + +msgid "Toggle navigation" +msgstr "Εναλλαγή πλοήγησης" + +msgid "Source repository" +msgstr "Αποθήκη πηγής" + +msgid "By the" +msgstr "Από το" + +msgid "next page" +msgstr "επόμενη σελίδα" + +msgid "repository" +msgstr "αποθήκη" + +msgid "Sphinx Book Theme" +msgstr "Θέμα βιβλίου Sphinx" + +msgid "Download source file" +msgstr "Λήψη αρχείου προέλευσης" + +msgid "Contents" +msgstr "Περιεχόμενα" + +msgid "By" +msgstr "Με" + +msgid "Copyright" +msgstr "Πνευματική ιδιοκτησία" + +msgid "Fullscreen mode" +msgstr "ΛΕΙΤΟΥΡΓΙΑ ΠΛΗΡΟΥΣ ΟΘΟΝΗΣ" + +msgid "Open an issue" +msgstr "Ανοίξτε ένα ζήτημα" + +msgid "previous page" +msgstr "προηγούμενη σελίδα" + +msgid "Download this page" +msgstr "Λήψη αυτής της σελίδας" + +msgid "Theme by the" +msgstr "Θέμα από το" diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.mo b/_static/locales/eo/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..d1072bbec6 Binary files /dev/null and b/_static/locales/eo/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.po b/_static/locales/eo/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..6749f3a34a --- /dev/null +++ b/_static/locales/eo/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: eo\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "sugesti redaktadon" + +msgid "Last updated on" +msgstr "Laste ĝisdatigita la" + +msgid "Edit this page" +msgstr "Redaktu ĉi tiun paĝon" + +msgid "Launch" +msgstr "Lanĉo" + +msgid "Print to PDF" +msgstr "Presi al PDF" + +msgid "open issue" +msgstr "malferma numero" + +msgid "Download notebook file" +msgstr "Elŝutu kajeran dosieron" + +msgid "Toggle navigation" +msgstr "Ŝalti navigadon" + +msgid "Source repository" +msgstr "Fonto-deponejo" + +msgid "By the" +msgstr "Per la" + +msgid "next page" +msgstr "sekva paĝo" + +msgid "repository" +msgstr "deponejo" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa Libro-Temo" + +msgid "Download source file" +msgstr "Elŝutu fontodosieron" + +msgid "Contents" +msgstr "Enhavo" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "Kopirajto" + +msgid "Fullscreen mode" +msgstr "Plenekrana reĝimo" + +msgid "Open an issue" +msgstr "Malfermu numeron" + +msgid "previous page" +msgstr "antaŭa paĝo" + +msgid "Download this page" +msgstr "Elŝutu ĉi tiun paĝon" + +msgid "Theme by the" +msgstr "Temo de la" diff --git a/_static/locales/es/LC_MESSAGES/booktheme.mo b/_static/locales/es/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..ba2ee4dc22 Binary files /dev/null and b/_static/locales/es/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/es/LC_MESSAGES/booktheme.po b/_static/locales/es/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..71dde37f27 --- /dev/null +++ b/_static/locales/es/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "sugerir editar" + +msgid "Last updated on" +msgstr "Ultima actualización en" + +msgid "Edit this page" +msgstr "Edita esta página" + +msgid "Launch" +msgstr "Lanzamiento" + +msgid "Print to PDF" +msgstr "Imprimir en PDF" + +msgid "open issue" +msgstr "Tema abierto" + +msgid "Download notebook file" +msgstr "Descargar archivo de cuaderno" + +msgid "Toggle navigation" +msgstr "Navegación de palanca" + +msgid "Source repository" +msgstr "Repositorio de origen" + +msgid "By the" +msgstr "Por el" + +msgid "next page" +msgstr "siguiente página" + +msgid "repository" +msgstr "repositorio" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro de la esfinge" + +msgid "Download source file" +msgstr "Descargar archivo fuente" + +msgid "Contents" +msgstr "Contenido" + +msgid "By" +msgstr "Por" + +msgid "Copyright" +msgstr "Derechos de autor" + +msgid "Fullscreen mode" +msgstr "Modo de pantalla completa" + +msgid "Open an issue" +msgstr "Abrir un problema" + +msgid "previous page" +msgstr "pagina anterior" + +msgid "Download this page" +msgstr "Descarga esta pagina" + +msgid "Theme by the" +msgstr "Tema por el" diff --git a/_static/locales/et/LC_MESSAGES/booktheme.mo b/_static/locales/et/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..983b82391f Binary files /dev/null and b/_static/locales/et/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/et/LC_MESSAGES/booktheme.po b/_static/locales/et/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..cdcd07c7d8 --- /dev/null +++ b/_static/locales/et/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "soovita muuta" + +msgid "Last updated on" +msgstr "Viimati uuendatud" + +msgid "Edit this page" +msgstr "Muutke seda lehte" + +msgid "Launch" +msgstr "Käivitage" + +msgid "Print to PDF" +msgstr "Prindi PDF-i" + +msgid "open issue" +msgstr "avatud küsimus" + +msgid "Download notebook file" +msgstr "Laadige sülearvuti fail alla" + +msgid "Toggle navigation" +msgstr "Lülita navigeerimine sisse" + +msgid "Source repository" +msgstr "Allikahoidla" + +msgid "By the" +msgstr "Autor" + +msgid "next page" +msgstr "järgmine leht" + +msgid "repository" +msgstr "hoidla" + +msgid "Sphinx Book Theme" +msgstr "Sfinksiraamatu teema" + +msgid "Download source file" +msgstr "Laadige alla lähtefail" + +msgid "Contents" +msgstr "Sisu" + +msgid "By" +msgstr "Kõrval" + +msgid "Copyright" +msgstr "Autoriõigus" + +msgid "Fullscreen mode" +msgstr "Täisekraanirežiim" + +msgid "Open an issue" +msgstr "Avage probleem" + +msgid "previous page" +msgstr "eelmine leht" + +msgid "Download this page" +msgstr "Laadige see leht alla" + +msgid "Theme by the" +msgstr "Teema" diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.mo b/_static/locales/fi/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..d8ac054597 Binary files /dev/null and b/_static/locales/fi/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.po b/_static/locales/fi/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..3c3dd08962 --- /dev/null +++ b/_static/locales/fi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "ehdottaa muokkausta" + +msgid "Last updated on" +msgstr "Viimeksi päivitetty" + +msgid "Edit this page" +msgstr "Muokkaa tätä sivua" + +msgid "Launch" +msgstr "Tuoda markkinoille" + +msgid "Print to PDF" +msgstr "Tulosta PDF-tiedostoon" + +msgid "open issue" +msgstr "avoin ongelma" + +msgid "Download notebook file" +msgstr "Lataa muistikirjatiedosto" + +msgid "Toggle navigation" +msgstr "Vaihda navigointia" + +msgid "Source repository" +msgstr "Lähteen arkisto" + +msgid "By the" +msgstr "Mukaan" + +msgid "next page" +msgstr "seuraava sivu" + +msgid "repository" +msgstr "arkisto" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-kirjan teema" + +msgid "Download source file" +msgstr "Lataa lähdetiedosto" + +msgid "Contents" +msgstr "Sisällys" + +msgid "By" +msgstr "Tekijä" + +msgid "Copyright" +msgstr "Tekijänoikeus" + +msgid "Fullscreen mode" +msgstr "Koko näytön tila" + +msgid "Open an issue" +msgstr "Avaa ongelma" + +msgid "previous page" +msgstr "Edellinen sivu" + +msgid "Download this page" +msgstr "Lataa tämä sivu" + +msgid "Theme by the" +msgstr "Teeman tekijä" diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.mo b/_static/locales/fr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..f663d39f0f Binary files /dev/null and b/_static/locales/fr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.po b/_static/locales/fr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..b57d2fe745 --- /dev/null +++ b/_static/locales/fr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "suggestion de modification" + +msgid "Last updated on" +msgstr "Dernière mise à jour le" + +msgid "Edit this page" +msgstr "Modifier cette page" + +msgid "Launch" +msgstr "lancement" + +msgid "Print to PDF" +msgstr "Imprimer au format PDF" + +msgid "open issue" +msgstr "signaler un problème" + +msgid "Download notebook file" +msgstr "Télécharger le fichier notebook" + +msgid "Toggle navigation" +msgstr "Basculer la navigation" + +msgid "Source repository" +msgstr "Dépôt source" + +msgid "By the" +msgstr "Par le" + +msgid "next page" +msgstr "page suivante" + +msgid "repository" +msgstr "dépôt" + +msgid "Sphinx Book Theme" +msgstr "Thème du livre Sphinx" + +msgid "Download source file" +msgstr "Télécharger le fichier source" + +msgid "Contents" +msgstr "Contenu" + +msgid "By" +msgstr "Par" + +msgid "Copyright" +msgstr "droits d'auteur" + +msgid "Fullscreen mode" +msgstr "Mode plein écran" + +msgid "Open an issue" +msgstr "Ouvrez un problème" + +msgid "previous page" +msgstr "page précédente" + +msgid "Download this page" +msgstr "Téléchargez cette page" + +msgid "Theme by the" +msgstr "Thème par le" diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.mo b/_static/locales/hr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..eca4a1a284 Binary files /dev/null and b/_static/locales/hr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.po b/_static/locales/hr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..4c425e89ae --- /dev/null +++ b/_static/locales/hr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "predloži uređivanje" + +msgid "Last updated on" +msgstr "Posljednje ažuriranje:" + +msgid "Edit this page" +msgstr "Uredite ovu stranicu" + +msgid "Launch" +msgstr "Pokrenite" + +msgid "Print to PDF" +msgstr "Ispis u PDF" + +msgid "open issue" +msgstr "otvoreno izdanje" + +msgid "Download notebook file" +msgstr "Preuzmi datoteku bilježnice" + +msgid "Toggle navigation" +msgstr "Uključi / isključi navigaciju" + +msgid "Source repository" +msgstr "Izvorno spremište" + +msgid "By the" +msgstr "Od strane" + +msgid "next page" +msgstr "sljedeća stranica" + +msgid "repository" +msgstr "spremište" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "Download source file" +msgstr "Preuzmi izvornu datoteku" + +msgid "Contents" +msgstr "Sadržaj" + +msgid "By" +msgstr "Po" + +msgid "Copyright" +msgstr "Autorska prava" + +msgid "Fullscreen mode" +msgstr "Način preko cijelog zaslona" + +msgid "Open an issue" +msgstr "Otvorite izdanje" + +msgid "previous page" +msgstr "Prethodna stranica" + +msgid "Download this page" +msgstr "Preuzmite ovu stranicu" + +msgid "Theme by the" +msgstr "Tema autora" diff --git a/_static/locales/id/LC_MESSAGES/booktheme.mo b/_static/locales/id/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..d07a06a9d2 Binary files /dev/null and b/_static/locales/id/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/id/LC_MESSAGES/booktheme.po b/_static/locales/id/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..5db2ae1474 --- /dev/null +++ b/_static/locales/id/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "menyarankan edit" + +msgid "Last updated on" +msgstr "Terakhir diperbarui saat" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "Launch" +msgstr "Meluncurkan" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "open issue" +msgstr "masalah terbuka" + +msgid "Download notebook file" +msgstr "Unduh file notebook" + +msgid "Toggle navigation" +msgstr "Alihkan navigasi" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "By the" +msgstr "Oleh" + +msgid "next page" +msgstr "halaman selanjutnya" + +msgid "repository" +msgstr "gudang" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "Download source file" +msgstr "Unduh file sumber" + +msgid "Contents" +msgstr "Isi" + +msgid "By" +msgstr "Oleh" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Fullscreen mode" +msgstr "Mode layar penuh" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "Download this page" +msgstr "Unduh halaman ini" + +msgid "Theme by the" +msgstr "Tema oleh" diff --git a/_static/locales/it/LC_MESSAGES/booktheme.mo b/_static/locales/it/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..53ba476edd Binary files /dev/null and b/_static/locales/it/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/it/LC_MESSAGES/booktheme.po b/_static/locales/it/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..7d54fdefa8 --- /dev/null +++ b/_static/locales/it/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "suggerisci modifica" + +msgid "Last updated on" +msgstr "Ultimo aggiornamento il" + +msgid "Edit this page" +msgstr "Modifica questa pagina" + +msgid "Launch" +msgstr "Lanciare" + +msgid "Print to PDF" +msgstr "Stampa in PDF" + +msgid "open issue" +msgstr "questione aperta" + +msgid "Download notebook file" +msgstr "Scarica il file del taccuino" + +msgid "Toggle navigation" +msgstr "Attiva / disattiva la navigazione" + +msgid "Source repository" +msgstr "Repository di origine" + +msgid "By the" +msgstr "Dal" + +msgid "next page" +msgstr "pagina successiva" + +msgid "repository" +msgstr "repository" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro della Sfinge" + +msgid "Download source file" +msgstr "Scarica il file sorgente" + +msgid "Contents" +msgstr "Contenuti" + +msgid "By" +msgstr "Di" + +msgid "Copyright" +msgstr "Diritto d'autore" + +msgid "Fullscreen mode" +msgstr "Modalità schermo intero" + +msgid "Open an issue" +msgstr "Apri un problema" + +msgid "previous page" +msgstr "pagina precedente" + +msgid "Download this page" +msgstr "Scarica questa pagina" + +msgid "Theme by the" +msgstr "Tema di" diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.mo b/_static/locales/iw/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..a45c6575e4 Binary files /dev/null and b/_static/locales/iw/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.po b/_static/locales/iw/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..32b017cf69 --- /dev/null +++ b/_static/locales/iw/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: iw\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "מציע לערוך" + +msgid "Last updated on" +msgstr "עודכן לאחרונה ב" + +msgid "Edit this page" +msgstr "ערוך דף זה" + +msgid "Launch" +msgstr "לְהַשִׁיק" + +msgid "Print to PDF" +msgstr "הדפס לקובץ PDF" + +msgid "open issue" +msgstr "בעיה פתוחה" + +msgid "Download notebook file" +msgstr "הורד קובץ מחברת" + +msgid "Toggle navigation" +msgstr "החלף ניווט" + +msgid "Source repository" +msgstr "מאגר המקורות" + +msgid "By the" +msgstr "דרך" + +msgid "next page" +msgstr "עמוד הבא" + +msgid "repository" +msgstr "מאגר" + +msgid "Sphinx Book Theme" +msgstr "נושא ספר ספינקס" + +msgid "Download source file" +msgstr "הורד את קובץ המקור" + +msgid "Contents" +msgstr "תוכן" + +msgid "By" +msgstr "על ידי" + +msgid "Copyright" +msgstr "זכויות יוצרים" + +msgid "Fullscreen mode" +msgstr "מצב מסך מלא" + +msgid "Open an issue" +msgstr "פתח גיליון" + +msgid "previous page" +msgstr "עמוד קודם" + +msgid "Download this page" +msgstr "הורד דף זה" + +msgid "Theme by the" +msgstr "נושא מאת" diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.mo b/_static/locales/ja/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..1cefd29ce3 Binary files /dev/null and b/_static/locales/ja/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.po b/_static/locales/ja/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..16924e1972 --- /dev/null +++ b/_static/locales/ja/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ja\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "編集を提案する" + +msgid "Last updated on" +msgstr "最終更新日" + +msgid "Edit this page" +msgstr "このページを編集" + +msgid "Launch" +msgstr "起動" + +msgid "Print to PDF" +msgstr "PDFに印刷" + +msgid "open issue" +msgstr "未解決の問題" + +msgid "Download notebook file" +msgstr "ノートブックファイルをダウンロード" + +msgid "Toggle navigation" +msgstr "ナビゲーションを切り替え" + +msgid "Source repository" +msgstr "ソースリポジトリ" + +msgid "By the" +msgstr "によって" + +msgid "next page" +msgstr "次のページ" + +msgid "repository" +msgstr "リポジトリ" + +msgid "Sphinx Book Theme" +msgstr "スフィンクスの本のテーマ" + +msgid "Download source file" +msgstr "ソースファイルをダウンロード" + +msgid "Contents" +msgstr "目次" + +msgid "By" +msgstr "著者" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Fullscreen mode" +msgstr "全画面モード" + +msgid "Open an issue" +msgstr "問題を報告" + +msgid "previous page" +msgstr "前のページ" + +msgid "Download this page" +msgstr "このページをダウンロード" + +msgid "Theme by the" +msgstr "のテーマ" diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.mo b/_static/locales/ko/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..06c7ec938b Binary files /dev/null and b/_static/locales/ko/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.po b/_static/locales/ko/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..69dd18f773 --- /dev/null +++ b/_static/locales/ko/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ko\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "편집 제안" + +msgid "Last updated on" +msgstr "마지막 업데이트" + +msgid "Edit this page" +msgstr "이 페이지 편집" + +msgid "Launch" +msgstr "시작하다" + +msgid "Print to PDF" +msgstr "PDF로 인쇄" + +msgid "open issue" +msgstr "열린 문제" + +msgid "Download notebook file" +msgstr "노트북 파일 다운로드" + +msgid "Toggle navigation" +msgstr "탐색 전환" + +msgid "Source repository" +msgstr "소스 저장소" + +msgid "By the" +msgstr "에 의해" + +msgid "next page" +msgstr "다음 페이지" + +msgid "repository" +msgstr "저장소" + +msgid "Sphinx Book Theme" +msgstr "스핑크스 도서 테마" + +msgid "Download source file" +msgstr "소스 파일 다운로드" + +msgid "Contents" +msgstr "내용" + +msgid "By" +msgstr "으로" + +msgid "Copyright" +msgstr "저작권" + +msgid "Fullscreen mode" +msgstr "전체 화면으로보기" + +msgid "Open an issue" +msgstr "이슈 열기" + +msgid "previous page" +msgstr "이전 페이지" + +msgid "Download this page" +msgstr "이 페이지 다운로드" + +msgid "Theme by the" +msgstr "테마별" diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.mo b/_static/locales/lt/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..4468ba04bc Binary files /dev/null and b/_static/locales/lt/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.po b/_static/locales/lt/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..9f037752c0 --- /dev/null +++ b/_static/locales/lt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "pasiūlyti redaguoti" + +msgid "Last updated on" +msgstr "Paskutinį kartą atnaujinta" + +msgid "Edit this page" +msgstr "Redaguoti šį puslapį" + +msgid "Launch" +msgstr "Paleiskite" + +msgid "Print to PDF" +msgstr "Spausdinti į PDF" + +msgid "open issue" +msgstr "atviras klausimas" + +msgid "Download notebook file" +msgstr "Atsisiųsti nešiojamojo kompiuterio failą" + +msgid "Toggle navigation" +msgstr "Perjungti naršymą" + +msgid "Source repository" +msgstr "Šaltinio saugykla" + +msgid "By the" +msgstr "Prie" + +msgid "next page" +msgstr "Kitas puslapis" + +msgid "repository" +msgstr "saugykla" + +msgid "Sphinx Book Theme" +msgstr "Sfinkso knygos tema" + +msgid "Download source file" +msgstr "Atsisiųsti šaltinio failą" + +msgid "Contents" +msgstr "Turinys" + +msgid "By" +msgstr "Iki" + +msgid "Copyright" +msgstr "Autorių teisės" + +msgid "Fullscreen mode" +msgstr "Pilno ekrano režimas" + +msgid "Open an issue" +msgstr "Atidarykite problemą" + +msgid "previous page" +msgstr "Ankstesnis puslapis" + +msgid "Download this page" +msgstr "Atsisiųskite šį puslapį" + +msgid "Theme by the" +msgstr "Tema" diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.mo b/_static/locales/lv/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..74aa4d8985 Binary files /dev/null and b/_static/locales/lv/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.po b/_static/locales/lv/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..c9633b545c --- /dev/null +++ b/_static/locales/lv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "ieteikt rediģēt" + +msgid "Last updated on" +msgstr "Pēdējoreiz atjaunināts" + +msgid "Edit this page" +msgstr "Rediģēt šo lapu" + +msgid "Launch" +msgstr "Uzsākt" + +msgid "Print to PDF" +msgstr "Drukāt PDF formātā" + +msgid "open issue" +msgstr "atklāts jautājums" + +msgid "Download notebook file" +msgstr "Lejupielādēt piezīmju grāmatiņu" + +msgid "Toggle navigation" +msgstr "Pārslēgt navigāciju" + +msgid "Source repository" +msgstr "Avota krātuve" + +msgid "By the" +msgstr "Ar" + +msgid "next page" +msgstr "nākamā lapaspuse" + +msgid "repository" +msgstr "krātuve" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa grāmatas tēma" + +msgid "Download source file" +msgstr "Lejupielādēt avota failu" + +msgid "Contents" +msgstr "Saturs" + +msgid "By" +msgstr "Autors" + +msgid "Copyright" +msgstr "Autortiesības" + +msgid "Fullscreen mode" +msgstr "Pilnekrāna režīms" + +msgid "Open an issue" +msgstr "Atveriet problēmu" + +msgid "previous page" +msgstr "iepriekšējā lapa" + +msgid "Download this page" +msgstr "Lejupielādējiet šo lapu" + +msgid "Theme by the" +msgstr "Autora tēma" diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.mo b/_static/locales/ml/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..2736e8fcf6 Binary files /dev/null and b/_static/locales/ml/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.po b/_static/locales/ml/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..9a6a41e8ec --- /dev/null +++ b/_static/locales/ml/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ml\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "എഡിറ്റുചെയ്യാൻ നിർദ്ദേശിക്കുക" + +msgid "Last updated on" +msgstr "അവസാനം അപ്‌ഡേറ്റുചെയ്‌തത്" + +msgid "Edit this page" +msgstr "ഈ പേജ് എഡിറ്റുചെയ്യുക" + +msgid "Launch" +msgstr "സമാരംഭിക്കുക" + +msgid "Print to PDF" +msgstr "PDF- ലേക്ക് പ്രിന്റുചെയ്യുക" + +msgid "open issue" +msgstr "തുറന്ന പ്രശ്നം" + +msgid "Download notebook file" +msgstr "നോട്ട്ബുക്ക് ഫയൽ ഡൺലോഡ് ചെയ്യുക" + +msgid "Toggle navigation" +msgstr "നാവിഗേഷൻ ടോഗിൾ ചെയ്യുക" + +msgid "Source repository" +msgstr "ഉറവിട ശേഖരം" + +msgid "By the" +msgstr "എഴുതിയത്" + +msgid "next page" +msgstr "അടുത്ത പേജ്" + +msgid "Sphinx Book Theme" +msgstr "സ്ഫിങ്ക്സ് പുസ്തക തീം" + +msgid "Download source file" +msgstr "ഉറവിട ഫയൽ ഡൗൺലോഡുചെയ്യുക" + +msgid "By" +msgstr "എഴുതിയത്" + +msgid "Copyright" +msgstr "പകർപ്പവകാശം" + +msgid "Open an issue" +msgstr "ഒരു പ്രശ്നം തുറക്കുക" + +msgid "previous page" +msgstr "മുൻപത്തെ താൾ" + +msgid "Download this page" +msgstr "ഈ പേജ് ഡൗൺലോഡുചെയ്യുക" + +msgid "Theme by the" +msgstr "പ്രമേയം" diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.mo b/_static/locales/mr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..fe530100d7 Binary files /dev/null and b/_static/locales/mr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.po b/_static/locales/mr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..ef72d8c6bc --- /dev/null +++ b/_static/locales/mr/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: mr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "संपादन सुचवा" + +msgid "Last updated on" +msgstr "अखेरचे अद्यतनित" + +msgid "Edit this page" +msgstr "हे पृष्ठ संपादित करा" + +msgid "Launch" +msgstr "लाँच करा" + +msgid "Print to PDF" +msgstr "पीडीएफवर मुद्रित करा" + +msgid "open issue" +msgstr "खुला मुद्दा" + +msgid "Download notebook file" +msgstr "नोटबुक फाईल डाउनलोड करा" + +msgid "Toggle navigation" +msgstr "नेव्हिगेशन टॉगल करा" + +msgid "Source repository" +msgstr "स्त्रोत भांडार" + +msgid "By the" +msgstr "द्वारा" + +msgid "next page" +msgstr "पुढील पृष्ठ" + +msgid "Sphinx Book Theme" +msgstr "स्फिंक्स बुक थीम" + +msgid "Download source file" +msgstr "स्त्रोत फाइल डाउनलोड करा" + +msgid "By" +msgstr "द्वारा" + +msgid "Copyright" +msgstr "कॉपीराइट" + +msgid "Open an issue" +msgstr "एक मुद्दा उघडा" + +msgid "previous page" +msgstr "मागील पान" + +msgid "Download this page" +msgstr "हे पृष्ठ डाउनलोड करा" + +msgid "Theme by the" +msgstr "द्वारा थीम" diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.mo b/_static/locales/ms/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..f02603fa25 Binary files /dev/null and b/_static/locales/ms/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.po b/_static/locales/ms/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..e29cbe2ec2 --- /dev/null +++ b/_static/locales/ms/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ms\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "cadangkan edit" + +msgid "Last updated on" +msgstr "Terakhir dikemas kini pada" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "Launch" +msgstr "Lancarkan" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "open issue" +msgstr "isu terbuka" + +msgid "Download notebook file" +msgstr "Muat turun fail buku nota" + +msgid "Toggle navigation" +msgstr "Togol navigasi" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "By the" +msgstr "Oleh" + +msgid "next page" +msgstr "muka surat seterusnya" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "Download source file" +msgstr "Muat turun fail sumber" + +msgid "By" +msgstr "Oleh" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "Download this page" +msgstr "Muat turun halaman ini" + +msgid "Theme by the" +msgstr "Tema oleh" diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.mo b/_static/locales/nl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..e59e7ecb30 Binary files /dev/null and b/_static/locales/nl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.po b/_static/locales/nl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..e4844d7c98 --- /dev/null +++ b/_static/locales/nl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "suggereren bewerken" + +msgid "Last updated on" +msgstr "Laatst geupdate op" + +msgid "Edit this page" +msgstr "bewerk deze pagina" + +msgid "Launch" +msgstr "Lancering" + +msgid "Print to PDF" +msgstr "Afdrukken naar pdf" + +msgid "open issue" +msgstr "open probleem" + +msgid "Download notebook file" +msgstr "Download notebookbestand" + +msgid "Toggle navigation" +msgstr "Schakel navigatie" + +msgid "Source repository" +msgstr "Bronopslagplaats" + +msgid "By the" +msgstr "Door de" + +msgid "next page" +msgstr "volgende bladzijde" + +msgid "repository" +msgstr "repository" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-boekthema" + +msgid "Download source file" +msgstr "Download het bronbestand" + +msgid "Contents" +msgstr "Inhoud" + +msgid "By" +msgstr "Door" + +msgid "Copyright" +msgstr "auteursrechten" + +msgid "Fullscreen mode" +msgstr "Volledig scherm" + +msgid "Open an issue" +msgstr "Open een probleem" + +msgid "previous page" +msgstr "vorige pagina" + +msgid "Download this page" +msgstr "Download deze pagina" + +msgid "Theme by the" +msgstr "Thema door de" diff --git a/_static/locales/no/LC_MESSAGES/booktheme.mo b/_static/locales/no/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..6cd15c88de Binary files /dev/null and b/_static/locales/no/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/no/LC_MESSAGES/booktheme.po b/_static/locales/no/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..d079dd9b09 --- /dev/null +++ b/_static/locales/no/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: no\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "Last updated on" +msgstr "Sist oppdatert den" + +msgid "Edit this page" +msgstr "Rediger denne siden" + +msgid "Launch" +msgstr "Start" + +msgid "Print to PDF" +msgstr "Skriv ut til PDF" + +msgid "open issue" +msgstr "åpent nummer" + +msgid "Download notebook file" +msgstr "Last ned notatbokfilen" + +msgid "Toggle navigation" +msgstr "Bytt navigasjon" + +msgid "Source repository" +msgstr "Kildedepot" + +msgid "By the" +msgstr "Ved" + +msgid "next page" +msgstr "neste side" + +msgid "repository" +msgstr "oppbevaringssted" + +msgid "Sphinx Book Theme" +msgstr "Sphinx boktema" + +msgid "Download source file" +msgstr "Last ned kildefilen" + +msgid "Contents" +msgstr "Innhold" + +msgid "By" +msgstr "Av" + +msgid "Copyright" +msgstr "opphavsrett" + +msgid "Fullscreen mode" +msgstr "Fullskjerm-modus" + +msgid "Open an issue" +msgstr "Åpne et problem" + +msgid "previous page" +msgstr "forrige side" + +msgid "Download this page" +msgstr "Last ned denne siden" + +msgid "Theme by the" +msgstr "Tema av" diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.mo b/_static/locales/pl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..9ebb584f77 Binary files /dev/null and b/_static/locales/pl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.po b/_static/locales/pl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..fcac51d329 --- /dev/null +++ b/_static/locales/pl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "zaproponuj edycję" + +msgid "Last updated on" +msgstr "Ostatnia aktualizacja" + +msgid "Edit this page" +msgstr "Edytuj tę strone" + +msgid "Launch" +msgstr "Uruchomić" + +msgid "Print to PDF" +msgstr "Drukuj do PDF" + +msgid "open issue" +msgstr "otwarty problem" + +msgid "Download notebook file" +msgstr "Pobierz plik notatnika" + +msgid "Toggle navigation" +msgstr "Przełącz nawigację" + +msgid "Source repository" +msgstr "Repozytorium źródłowe" + +msgid "By the" +msgstr "Przez" + +msgid "next page" +msgstr "Następna strona" + +msgid "repository" +msgstr "magazyn" + +msgid "Sphinx Book Theme" +msgstr "Motyw książki Sphinx" + +msgid "Download source file" +msgstr "Pobierz plik źródłowy" + +msgid "Contents" +msgstr "Zawartość" + +msgid "By" +msgstr "Przez" + +msgid "Copyright" +msgstr "prawa autorskie" + +msgid "Fullscreen mode" +msgstr "Pełny ekran" + +msgid "Open an issue" +msgstr "Otwórz problem" + +msgid "previous page" +msgstr "Poprzednia strona" + +msgid "Download this page" +msgstr "Pobierz tę stronę" + +msgid "Theme by the" +msgstr "Motyw autorstwa" diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.mo b/_static/locales/pt/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..d0ddb8728e Binary files /dev/null and b/_static/locales/pt/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.po b/_static/locales/pt/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..1761db08ae --- /dev/null +++ b/_static/locales/pt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "sugerir edição" + +msgid "Last updated on" +msgstr "Última atualização em" + +msgid "Edit this page" +msgstr "Edite essa página" + +msgid "Launch" +msgstr "Lançamento" + +msgid "Print to PDF" +msgstr "Imprimir em PDF" + +msgid "open issue" +msgstr "questão aberta" + +msgid "Download notebook file" +msgstr "Baixar arquivo de notebook" + +msgid "Toggle navigation" +msgstr "Alternar de navegação" + +msgid "Source repository" +msgstr "Repositório fonte" + +msgid "By the" +msgstr "Pelo" + +msgid "next page" +msgstr "próxima página" + +msgid "repository" +msgstr "repositório" + +msgid "Sphinx Book Theme" +msgstr "Tema do livro Sphinx" + +msgid "Download source file" +msgstr "Baixar arquivo fonte" + +msgid "Contents" +msgstr "Conteúdo" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "direito autoral" + +msgid "Fullscreen mode" +msgstr "Modo tela cheia" + +msgid "Open an issue" +msgstr "Abra um problema" + +msgid "previous page" +msgstr "página anterior" + +msgid "Download this page" +msgstr "Baixe esta página" + +msgid "Theme by the" +msgstr "Tema por" diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.mo b/_static/locales/ro/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..3c36ab1df7 Binary files /dev/null and b/_static/locales/ro/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.po b/_static/locales/ro/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..db865c8f65 --- /dev/null +++ b/_static/locales/ro/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ro\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "sugerează editare" + +msgid "Last updated on" +msgstr "Ultima actualizare la" + +msgid "Edit this page" +msgstr "Editați această pagină" + +msgid "Launch" +msgstr "Lansa" + +msgid "Print to PDF" +msgstr "Imprimați în PDF" + +msgid "open issue" +msgstr "problema deschisă" + +msgid "Download notebook file" +msgstr "Descărcați fișierul notebook" + +msgid "Toggle navigation" +msgstr "Comutare navigare" + +msgid "Source repository" +msgstr "Depozit sursă" + +msgid "By the" +msgstr "Langa" + +msgid "next page" +msgstr "pagina următoare" + +msgid "repository" +msgstr "repertoriu" + +msgid "Sphinx Book Theme" +msgstr "Tema Sphinx Book" + +msgid "Download source file" +msgstr "Descărcați fișierul sursă" + +msgid "Contents" +msgstr "Cuprins" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "Drepturi de autor" + +msgid "Fullscreen mode" +msgstr "Modul ecran întreg" + +msgid "Open an issue" +msgstr "Deschideți o problemă" + +msgid "previous page" +msgstr "pagina anterioară" + +msgid "Download this page" +msgstr "Descarcă această pagină" + +msgid "Theme by the" +msgstr "Tema de" diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.mo b/_static/locales/ru/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..6b8ca41f36 Binary files /dev/null and b/_static/locales/ru/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.po b/_static/locales/ru/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..84ab6eb531 --- /dev/null +++ b/_static/locales/ru/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "предложить редактировать" + +msgid "Last updated on" +msgstr "Последнее обновление" + +msgid "Edit this page" +msgstr "Редактировать эту страницу" + +msgid "Launch" +msgstr "Запуск" + +msgid "Print to PDF" +msgstr "Распечатать в PDF" + +msgid "open issue" +msgstr "открытый вопрос" + +msgid "Download notebook file" +msgstr "Скачать файл записной книжки" + +msgid "Toggle navigation" +msgstr "Переключить навигацию" + +msgid "Source repository" +msgstr "Исходный репозиторий" + +msgid "By the" +msgstr "Посредством" + +msgid "next page" +msgstr "Следующая страница" + +msgid "repository" +msgstr "хранилище" + +msgid "Sphinx Book Theme" +msgstr "Тема книги Сфинкс" + +msgid "Download source file" +msgstr "Скачать исходный файл" + +msgid "Contents" +msgstr "Содержание" + +msgid "By" +msgstr "По" + +msgid "Copyright" +msgstr "авторское право" + +msgid "Fullscreen mode" +msgstr "Полноэкранный режим" + +msgid "Open an issue" +msgstr "Открыть вопрос" + +msgid "previous page" +msgstr "Предыдущая страница" + +msgid "Download this page" +msgstr "Загрузите эту страницу" + +msgid "Theme by the" +msgstr "Тема от" diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.mo b/_static/locales/sk/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..59bd0ddfa3 Binary files /dev/null and b/_static/locales/sk/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.po b/_static/locales/sk/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..e44878b50e --- /dev/null +++ b/_static/locales/sk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "navrhnúť úpravu" + +msgid "Last updated on" +msgstr "Posledná aktualizácia dňa" + +msgid "Edit this page" +msgstr "Upraviť túto stránku" + +msgid "Launch" +msgstr "Spustiť" + +msgid "Print to PDF" +msgstr "Tlač do PDF" + +msgid "open issue" +msgstr "otvorené vydanie" + +msgid "Download notebook file" +msgstr "Stiahnite si zošit" + +msgid "Toggle navigation" +msgstr "Prepnúť navigáciu" + +msgid "Source repository" +msgstr "Zdrojové úložisko" + +msgid "By the" +msgstr "Podľa" + +msgid "next page" +msgstr "ďalšia strana" + +msgid "repository" +msgstr "Úložisko" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "Download source file" +msgstr "Stiahnite si zdrojový súbor" + +msgid "Contents" +msgstr "Obsah" + +msgid "By" +msgstr "Autor:" + +msgid "Copyright" +msgstr "Autorské práva" + +msgid "Fullscreen mode" +msgstr "Režim celej obrazovky" + +msgid "Open an issue" +msgstr "Otvorte problém" + +msgid "previous page" +msgstr "predchádzajúca strana" + +msgid "Download this page" +msgstr "Stiahnite si túto stránku" + +msgid "Theme by the" +msgstr "Téma od" diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.mo b/_static/locales/sl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..87bf26de68 Binary files /dev/null and b/_static/locales/sl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.po b/_static/locales/sl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..228939bcdd --- /dev/null +++ b/_static/locales/sl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "predlagajte urejanje" + +msgid "Last updated on" +msgstr "Nazadnje posodobljeno dne" + +msgid "Edit this page" +msgstr "Uredite to stran" + +msgid "Launch" +msgstr "Kosilo" + +msgid "Print to PDF" +msgstr "Natisni v PDF" + +msgid "open issue" +msgstr "odprto vprašanje" + +msgid "Download notebook file" +msgstr "Prenesite datoteko zvezka" + +msgid "Toggle navigation" +msgstr "Preklopi navigacijo" + +msgid "Source repository" +msgstr "Izvorno skladišče" + +msgid "By the" +msgstr "Avtor" + +msgid "next page" +msgstr "Naslednja stran" + +msgid "repository" +msgstr "odlagališče" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "Download source file" +msgstr "Prenesite izvorno datoteko" + +msgid "Contents" +msgstr "Vsebina" + +msgid "By" +msgstr "Avtor" + +msgid "Copyright" +msgstr "avtorske pravice" + +msgid "Fullscreen mode" +msgstr "Celozaslonski način" + +msgid "Open an issue" +msgstr "Odprite številko" + +msgid "previous page" +msgstr "Prejšnja stran" + +msgid "Download this page" +msgstr "Prenesite to stran" + +msgid "Theme by the" +msgstr "Tema avtorja" diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.mo b/_static/locales/sr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..ec740f4852 Binary files /dev/null and b/_static/locales/sr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.po b/_static/locales/sr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..1a712a18d8 --- /dev/null +++ b/_static/locales/sr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "предложи уређивање" + +msgid "Last updated on" +msgstr "Последње ажурирање" + +msgid "Edit this page" +msgstr "Уредите ову страницу" + +msgid "Launch" +msgstr "Лансирање" + +msgid "Print to PDF" +msgstr "Испис у ПДФ" + +msgid "open issue" +msgstr "отворено издање" + +msgid "Download notebook file" +msgstr "Преузмите датотеку бележнице" + +msgid "Toggle navigation" +msgstr "Укључи / искључи навигацију" + +msgid "Source repository" +msgstr "Изворно спремиште" + +msgid "By the" +msgstr "Од" + +msgid "next page" +msgstr "Следећа страна" + +msgid "repository" +msgstr "спремиште" + +msgid "Sphinx Book Theme" +msgstr "Тема књиге Спхинк" + +msgid "Download source file" +msgstr "Преузми изворну датотеку" + +msgid "Contents" +msgstr "Садржај" + +msgid "By" +msgstr "Од стране" + +msgid "Copyright" +msgstr "Ауторско право" + +msgid "Fullscreen mode" +msgstr "Режим целог екрана" + +msgid "Open an issue" +msgstr "Отворите издање" + +msgid "previous page" +msgstr "Претходна страница" + +msgid "Download this page" +msgstr "Преузмите ову страницу" + +msgid "Theme by the" +msgstr "Тхеме би" diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.mo b/_static/locales/sv/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..be951bec20 Binary files /dev/null and b/_static/locales/sv/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.po b/_static/locales/sv/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..7d2b56d949 --- /dev/null +++ b/_static/locales/sv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "föreslå redigering" + +msgid "Last updated on" +msgstr "Senast uppdaterad den" + +msgid "Edit this page" +msgstr "Redigera den här sidan" + +msgid "Launch" +msgstr "Lansera" + +msgid "Print to PDF" +msgstr "Skriv ut till PDF" + +msgid "open issue" +msgstr "öppet problem" + +msgid "Download notebook file" +msgstr "Ladda ner anteckningsbokfilen" + +msgid "Toggle navigation" +msgstr "Växla navigering" + +msgid "Source repository" +msgstr "Källförvar" + +msgid "By the" +msgstr "Vid" + +msgid "next page" +msgstr "nästa sida" + +msgid "repository" +msgstr "förvar" + +msgid "Sphinx Book Theme" +msgstr "Sphinx boktema" + +msgid "Download source file" +msgstr "Ladda ner källfil" + +msgid "Contents" +msgstr "Innehåll" + +msgid "By" +msgstr "Förbi" + +msgid "Copyright" +msgstr "upphovsrätt" + +msgid "Fullscreen mode" +msgstr "Fullskärmsläge" + +msgid "Open an issue" +msgstr "Öppna ett problem" + +msgid "previous page" +msgstr "föregående sida" + +msgid "Download this page" +msgstr "Ladda ner den här sidan" + +msgid "Theme by the" +msgstr "Tema av" diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.mo b/_static/locales/ta/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..29f52e1f6f Binary files /dev/null and b/_static/locales/ta/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.po b/_static/locales/ta/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..c75ffe192c --- /dev/null +++ b/_static/locales/ta/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ta\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "திருத்த பரிந்துரைக்கவும்" + +msgid "Last updated on" +msgstr "கடைசியாக புதுப்பிக்கப்பட்டது" + +msgid "Edit this page" +msgstr "இந்தப் பக்கத்தைத் திருத்தவும்" + +msgid "Launch" +msgstr "தொடங்க" + +msgid "Print to PDF" +msgstr "PDF இல் அச்சிடுக" + +msgid "open issue" +msgstr "திறந்த பிரச்சினை" + +msgid "Download notebook file" +msgstr "நோட்புக் கோப்பைப் பதிவிறக்கவும்" + +msgid "Toggle navigation" +msgstr "வழிசெலுத்தலை நிலைமாற்று" + +msgid "Source repository" +msgstr "மூல களஞ்சியம்" + +msgid "By the" +msgstr "மூலம்" + +msgid "next page" +msgstr "அடுத்த பக்கம்" + +msgid "Sphinx Book Theme" +msgstr "ஸ்பிங்க்ஸ் புத்தக தீம்" + +msgid "Download source file" +msgstr "மூல கோப்பைப் பதிவிறக்குக" + +msgid "By" +msgstr "வழங்கியவர்" + +msgid "Copyright" +msgstr "பதிப்புரிமை" + +msgid "Open an issue" +msgstr "சிக்கலைத் திறக்கவும்" + +msgid "previous page" +msgstr "முந்தைய பக்கம்" + +msgid "Download this page" +msgstr "இந்தப் பக்கத்தைப் பதிவிறக்கவும்" + +msgid "Theme by the" +msgstr "வழங்கிய தீம்" diff --git a/_static/locales/te/LC_MESSAGES/booktheme.mo b/_static/locales/te/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..0a5f4b46ad Binary files /dev/null and b/_static/locales/te/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/te/LC_MESSAGES/booktheme.po b/_static/locales/te/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..2595c03590 --- /dev/null +++ b/_static/locales/te/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: te\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "సవరించమని సూచించండి" + +msgid "Last updated on" +msgstr "చివరిగా నవీకరించబడింది" + +msgid "Edit this page" +msgstr "ఈ పేజీని సవరించండి" + +msgid "Launch" +msgstr "ప్రారంభించండి" + +msgid "Print to PDF" +msgstr "PDF కి ముద్రించండి" + +msgid "open issue" +msgstr "ఓపెన్ ఇష్యూ" + +msgid "Download notebook file" +msgstr "నోట్బుక్ ఫైల్ను డౌన్లోడ్ చేయండి" + +msgid "Toggle navigation" +msgstr "నావిగేషన్‌ను టోగుల్ చేయండి" + +msgid "Source repository" +msgstr "మూల రిపోజిటరీ" + +msgid "By the" +msgstr "ద్వారా" + +msgid "next page" +msgstr "తరువాతి పేజీ" + +msgid "Sphinx Book Theme" +msgstr "సింహిక పుస్తక థీమ్" + +msgid "Download source file" +msgstr "మూల ఫైల్‌ను డౌన్‌లోడ్ చేయండి" + +msgid "By" +msgstr "ద్వారా" + +msgid "Copyright" +msgstr "కాపీరైట్" + +msgid "Open an issue" +msgstr "సమస్యను తెరవండి" + +msgid "previous page" +msgstr "ముందు పేజి" + +msgid "Download this page" +msgstr "ఈ పేజీని డౌన్‌లోడ్ చేయండి" + +msgid "Theme by the" +msgstr "ద్వారా థీమ్" diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.mo b/_static/locales/tg/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..b21c6c6340 Binary files /dev/null and b/_static/locales/tg/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.po b/_static/locales/tg/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..73cd30ea97 --- /dev/null +++ b/_static/locales/tg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "пешниҳод вироиш" + +msgid "Last updated on" +msgstr "Last навсозӣ дар" + +msgid "Edit this page" +msgstr "Ин саҳифаро таҳрир кунед" + +msgid "Launch" +msgstr "Оғоз" + +msgid "Print to PDF" +msgstr "Чоп ба PDF" + +msgid "open issue" +msgstr "барориши кушод" + +msgid "Download notebook file" +msgstr "Файли дафтарро зеркашӣ кунед" + +msgid "Toggle navigation" +msgstr "Гузаришро иваз кунед" + +msgid "Source repository" +msgstr "Анбори манбаъ" + +msgid "By the" +msgstr "Бо" + +msgid "next page" +msgstr "саҳифаи оянда" + +msgid "repository" +msgstr "анбор" + +msgid "Sphinx Book Theme" +msgstr "Сфинкс Мавзӯи китоб" + +msgid "Download source file" +msgstr "Файли манбаъро зеркашӣ кунед" + +msgid "Contents" +msgstr "Мундариҷа" + +msgid "By" +msgstr "Бо" + +msgid "Copyright" +msgstr "Ҳуқуқи муаллиф" + +msgid "Fullscreen mode" +msgstr "Ҳолати экрани пурра" + +msgid "Open an issue" +msgstr "Масъаларо кушоед" + +msgid "previous page" +msgstr "саҳифаи қаблӣ" + +msgid "Download this page" +msgstr "Ин саҳифаро зеркашӣ кунед" + +msgid "Theme by the" +msgstr "Мавзӯъи аз" diff --git a/_static/locales/th/LC_MESSAGES/booktheme.mo b/_static/locales/th/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..abede98aa1 Binary files /dev/null and b/_static/locales/th/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/th/LC_MESSAGES/booktheme.po b/_static/locales/th/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..0392b4ad39 --- /dev/null +++ b/_static/locales/th/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: th\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "แนะนำแก้ไข" + +msgid "Last updated on" +msgstr "ปรับปรุงล่าสุดเมื่อ" + +msgid "Edit this page" +msgstr "แก้ไขหน้านี้" + +msgid "Launch" +msgstr "เปิด" + +msgid "Print to PDF" +msgstr "พิมพ์เป็น PDF" + +msgid "open issue" +msgstr "เปิดปัญหา" + +msgid "Download notebook file" +msgstr "ดาวน์โหลดไฟล์สมุดบันทึก" + +msgid "Toggle navigation" +msgstr "ไม่ต้องสลับช่องทาง" + +msgid "Source repository" +msgstr "ที่เก็บซอร์ส" + +msgid "By the" +msgstr "โดย" + +msgid "next page" +msgstr "หน้าต่อไป" + +msgid "repository" +msgstr "ที่เก็บ" + +msgid "Sphinx Book Theme" +msgstr "ธีมหนังสือสฟิงซ์" + +msgid "Download source file" +msgstr "ดาวน์โหลดไฟล์ต้นฉบับ" + +msgid "Contents" +msgstr "สารบัญ" + +msgid "By" +msgstr "โดย" + +msgid "Copyright" +msgstr "ลิขสิทธิ์" + +msgid "Fullscreen mode" +msgstr "โหมดเต็มหน้าจอ" + +msgid "Open an issue" +msgstr "เปิดปัญหา" + +msgid "previous page" +msgstr "หน้าที่แล้ว" + +msgid "Download this page" +msgstr "ดาวน์โหลดหน้านี้" + +msgid "Theme by the" +msgstr "ธีมโดย" diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.mo b/_static/locales/tl/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..8df1b73310 Binary files /dev/null and b/_static/locales/tl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.po b/_static/locales/tl/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..c8375b5431 --- /dev/null +++ b/_static/locales/tl/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "iminumungkahi i-edit" + +msgid "Last updated on" +msgstr "Huling na-update noong" + +msgid "Edit this page" +msgstr "I-edit ang pahinang ito" + +msgid "Launch" +msgstr "Ilunsad" + +msgid "Print to PDF" +msgstr "I-print sa PDF" + +msgid "open issue" +msgstr "bukas na isyu" + +msgid "Download notebook file" +msgstr "Mag-download ng file ng notebook" + +msgid "Toggle navigation" +msgstr "I-toggle ang pag-navigate" + +msgid "Source repository" +msgstr "Pinagmulan ng imbakan" + +msgid "By the" +msgstr "Sa pamamagitan ng" + +msgid "next page" +msgstr "Susunod na pahina" + +msgid "Sphinx Book Theme" +msgstr "Tema ng Sphinx Book" + +msgid "Download source file" +msgstr "Mag-download ng file ng pinagmulan" + +msgid "By" +msgstr "Ni" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Open an issue" +msgstr "Magbukas ng isyu" + +msgid "previous page" +msgstr "Nakaraang pahina" + +msgid "Download this page" +msgstr "I-download ang pahinang ito" + +msgid "Theme by the" +msgstr "Tema ng" diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.mo b/_static/locales/tr/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..029ae18afb Binary files /dev/null and b/_static/locales/tr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.po b/_static/locales/tr/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..47d7bdf7f5 --- /dev/null +++ b/_static/locales/tr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "düzenleme öner" + +msgid "Last updated on" +msgstr "Son güncelleme tarihi" + +msgid "Edit this page" +msgstr "Bu sayfayı düzenle" + +msgid "Launch" +msgstr "Başlatmak" + +msgid "Print to PDF" +msgstr "PDF olarak yazdır" + +msgid "open issue" +msgstr "Açık konu" + +msgid "Download notebook file" +msgstr "Defter dosyasını indirin" + +msgid "Toggle navigation" +msgstr "Gezinmeyi değiştir" + +msgid "Source repository" +msgstr "Kaynak kod deposu" + +msgid "By the" +msgstr "Tarafından" + +msgid "next page" +msgstr "sonraki Sayfa" + +msgid "repository" +msgstr "depo" + +msgid "Sphinx Book Theme" +msgstr "Sfenks Kitap Teması" + +msgid "Download source file" +msgstr "Kaynak dosyayı indirin" + +msgid "Contents" +msgstr "İçindekiler" + +msgid "By" +msgstr "Tarafından" + +msgid "Copyright" +msgstr "Telif hakkı" + +msgid "Fullscreen mode" +msgstr "Tam ekran modu" + +msgid "Open an issue" +msgstr "Bir sorunu açın" + +msgid "previous page" +msgstr "önceki sayfa" + +msgid "Download this page" +msgstr "Bu sayfayı indirin" + +msgid "Theme by the" +msgstr "Tarafından tema" diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.mo b/_static/locales/uk/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..16ab78909c Binary files /dev/null and b/_static/locales/uk/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.po b/_static/locales/uk/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..e85f6f16ac --- /dev/null +++ b/_static/locales/uk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: uk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "запропонувати редагувати" + +msgid "Last updated on" +msgstr "Останнє оновлення:" + +msgid "Edit this page" +msgstr "Редагувати цю сторінку" + +msgid "Launch" +msgstr "Запуск" + +msgid "Print to PDF" +msgstr "Друк у форматі PDF" + +msgid "open issue" +msgstr "відкритий випуск" + +msgid "Download notebook file" +msgstr "Завантажте файл блокнота" + +msgid "Toggle navigation" +msgstr "Переключити навігацію" + +msgid "Source repository" +msgstr "Джерело сховища" + +msgid "By the" +msgstr "По" + +msgid "next page" +msgstr "Наступна сторінка" + +msgid "repository" +msgstr "сховище" + +msgid "Sphinx Book Theme" +msgstr "Тема книги \"Сфінкс\"" + +msgid "Download source file" +msgstr "Завантажити вихідний файл" + +msgid "Contents" +msgstr "Зміст" + +msgid "By" +msgstr "Автор" + +msgid "Copyright" +msgstr "Авторське право" + +msgid "Fullscreen mode" +msgstr "Повноекранний режим" + +msgid "Open an issue" +msgstr "Відкрийте випуск" + +msgid "previous page" +msgstr "Попередня сторінка" + +msgid "Download this page" +msgstr "Завантажте цю сторінку" + +msgid "Theme by the" +msgstr "Тема від" diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.mo b/_static/locales/ur/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..de8c84b935 Binary files /dev/null and b/_static/locales/ur/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.po b/_static/locales/ur/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..0f90726c12 --- /dev/null +++ b/_static/locales/ur/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ur\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "ترمیم کی تجویز کریں" + +msgid "Last updated on" +msgstr "آخری بار تازہ کاری ہوئی" + +msgid "Edit this page" +msgstr "اس صفحے میں ترمیم کریں" + +msgid "Launch" +msgstr "لانچ کریں" + +msgid "Print to PDF" +msgstr "پی ڈی ایف پرنٹ کریں" + +msgid "open issue" +msgstr "کھلا مسئلہ" + +msgid "Download notebook file" +msgstr "نوٹ بک فائل ڈاؤن لوڈ کریں" + +msgid "Toggle navigation" +msgstr "نیویگیشن ٹوگل کریں" + +msgid "Source repository" +msgstr "ماخذ ذخیرہ" + +msgid "By the" +msgstr "کی طرف" + +msgid "next page" +msgstr "اگلا صفحہ" + +msgid "Sphinx Book Theme" +msgstr "سپنکس بک تھیم" + +msgid "Download source file" +msgstr "سورس فائل ڈاؤن لوڈ کریں" + +msgid "By" +msgstr "بذریعہ" + +msgid "Copyright" +msgstr "کاپی رائٹ" + +msgid "Open an issue" +msgstr "ایک مسئلہ کھولیں" + +msgid "previous page" +msgstr "سابقہ ​​صفحہ" + +msgid "Download this page" +msgstr "اس صفحے کو ڈاؤن لوڈ کریں" + +msgid "Theme by the" +msgstr "کے ذریعہ تھیم" diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.mo b/_static/locales/vi/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..2bb32555c3 Binary files /dev/null and b/_static/locales/vi/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.po b/_static/locales/vi/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..2cb5cf3b82 --- /dev/null +++ b/_static/locales/vi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: vi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "đề nghị chỉnh sửa" + +msgid "Last updated on" +msgstr "Cập nhật lần cuối vào" + +msgid "Edit this page" +msgstr "chỉnh sửa trang này" + +msgid "Launch" +msgstr "Phóng" + +msgid "Print to PDF" +msgstr "In sang PDF" + +msgid "open issue" +msgstr "vấn đề mở" + +msgid "Download notebook file" +msgstr "Tải xuống tệp sổ tay" + +msgid "Toggle navigation" +msgstr "Chuyển đổi điều hướng thành" + +msgid "Source repository" +msgstr "Kho nguồn" + +msgid "By the" +msgstr "Bằng" + +msgid "next page" +msgstr "Trang tiếp theo" + +msgid "repository" +msgstr "kho" + +msgid "Sphinx Book Theme" +msgstr "Chủ đề sách nhân sư" + +msgid "Download source file" +msgstr "Tải xuống tệp nguồn" + +msgid "Contents" +msgstr "Nội dung" + +msgid "By" +msgstr "Bởi" + +msgid "Copyright" +msgstr "Bản quyền" + +msgid "Fullscreen mode" +msgstr "Chế độ toàn màn hình" + +msgid "Open an issue" +msgstr "Mở một vấn đề" + +msgid "previous page" +msgstr "trang trước" + +msgid "Download this page" +msgstr "Tải xuống trang này" + +msgid "Theme by the" +msgstr "Chủ đề của" diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..0e3235d090 Binary files /dev/null and b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.po b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..f91f3ba0a2 --- /dev/null +++ b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "提出修改建议" + +msgid "Last updated on" +msgstr "上次更新时间:" + +msgid "Edit this page" +msgstr "编辑此页面" + +msgid "Launch" +msgstr "启动" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "open issue" +msgstr "创建议题" + +msgid "Download notebook file" +msgstr "下载笔记本文件" + +msgid "Toggle navigation" +msgstr "显示或隐藏导航栏" + +msgid "Source repository" +msgstr "源码库" + +msgid "By the" +msgstr "作者:" + +msgid "next page" +msgstr "下一页" + +msgid "repository" +msgstr "仓库" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 主题" + +msgid "Download source file" +msgstr "下载源文件" + +msgid "Contents" +msgstr "目录" + +msgid "By" +msgstr "作者:" + +msgid "Copyright" +msgstr "版权" + +msgid "Fullscreen mode" +msgstr "全屏模式" + +msgid "Open an issue" +msgstr "创建议题" + +msgid "previous page" +msgstr "上一页" + +msgid "Download this page" +msgstr "下载此页面" + +msgid "Theme by the" +msgstr "主题作者:" diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo new file mode 100644 index 0000000000..9116fa95d0 Binary files /dev/null and b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.po b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po new file mode 100644 index 0000000000..7833d90432 --- /dev/null +++ b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "suggest edit" +msgstr "提出修改建議" + +msgid "Last updated on" +msgstr "最後更新時間:" + +msgid "Edit this page" +msgstr "編輯此頁面" + +msgid "Launch" +msgstr "啟動" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "open issue" +msgstr "公開的問題" + +msgid "Download notebook file" +msgstr "下載 Notebook 檔案" + +msgid "Toggle navigation" +msgstr "顯示或隱藏導覽列" + +msgid "Source repository" +msgstr "來源儲存庫" + +msgid "By the" +msgstr "作者:" + +msgid "next page" +msgstr "下一頁" + +msgid "repository" +msgstr "儲存庫" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 佈景主題" + +msgid "Download source file" +msgstr "下載原始檔" + +msgid "Contents" +msgstr "目錄" + +msgid "By" +msgstr "作者:" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Fullscreen mode" +msgstr "全螢幕模式" + +msgid "Open an issue" +msgstr "開啟議題" + +msgid "previous page" +msgstr "上一頁" + +msgid "Download this page" +msgstr "下載此頁面" + +msgid "Theme by the" +msgstr "佈景主題作者:" diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000000..d96755fdaf Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000000..7107cec93a Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 0000000000..012e6a00a4 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #fae4c2 } +html[data-theme="light"] .highlight { background: #fefefe; color: #080808 } +html[data-theme="light"] .highlight .c { color: #515151 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #a12236 } /* Error */ +html[data-theme="light"] .highlight .k { color: #6730c5 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #7f4707 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #080808 } /* Name */ +html[data-theme="light"] .highlight .o { color: #00622f } /* Operator */ +html[data-theme="light"] .highlight .p { color: #080808 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #515151 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #515151 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #515151 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #515151 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #515151 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #515151 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #005b82 } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #005b82 } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #005b82 } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #6730c5 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #6730c5 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #6730c5 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #6730c5 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #6730c5 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #7f4707 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #7f4707 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #7f4707 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #00622f } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #912583 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #7f4707 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #005b82 } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #005b82 } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #7f4707 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #00622f } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #6730c5 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #005b82 } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #7f4707 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #080808 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #080808 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #005b82 } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #005b82 } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #a12236 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #6730c5 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #080808 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #080808 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #7f4707 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #7f4707 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #7f4707 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #7f4707 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #7f4707 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #00622f } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #00622f } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #00622f } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #00622f } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #00622f } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #00622f } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #00622f } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #00622f } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #00622f } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #00622f } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #a12236 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #00622f } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #005b82 } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #7f4707 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #005b82 } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #a12236 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #a12236 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #a12236 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #7f4707 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #7f4707 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/sbt-webpack-macros.html b/_static/sbt-webpack-macros.html new file mode 100644 index 0000000000..6cbf559faa --- /dev/null +++ b/_static/sbt-webpack-macros.html @@ -0,0 +1,11 @@ + +{% macro head_pre_bootstrap() %} + +{% endmacro %} + +{% macro body_post() %} + +{% endmacro %} diff --git a/_static/scripts/bootstrap.js b/_static/scripts/bootstrap.js new file mode 100644 index 0000000000..c8178debbc --- /dev/null +++ b/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>li,Collapse:()=>Ei,Dropdown:()=>Ki,Modal:()=>Ln,Offcanvas:()=>Kn,Popover:()=>bs,ScrollSpy:()=>Ls,Tab:()=>Js,Toast:()=>po,Tooltip:()=>fs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every((function(t){return t}))){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find((function(e){var i=D.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some((function(e){return t[e]>=0}))}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Tt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),T=!A,C=Q(E),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,W="y"===C?"height":"width",z=x[C],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=T?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=T?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[C])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[C]=ht,M[C]=ht-z}if(u){var dt,ut="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),jt=t=>{t.dispatchEvent(new Event(Pt))},Ft=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Ft(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Bt=t=>{if(!Ft(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),zt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?zt(t.parentNode):null},Rt=()=>{},qt=t=>{t.offsetHeight},Vt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Yt=[],Kt=()=>"rtl"===document.documentElement.dir,Qt=t=>{var e;e=()=>{const e=Vt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Yt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Yt)t()})),Yt.push(e)):e()},Xt=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Ut=(t,e,i=!0)=>{if(!i)return void Xt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Xt(t))};e.addEventListener(Pt,o),setTimeout((()=>{s||jt(e)}),n)},Gt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Jt=/[^.]*(?=\..*)\.|.*/,Zt=/\..*/,te=/::\d+$/,ee={};let ie=1;const ne={mouseenter:"mouseover",mouseleave:"mouseout"},se=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function oe(t,e){return e&&`${e}::${ie++}`||t.uidEvent||ie++}function re(t){const e=oe(t);return t.uidEvent=e,ee[e]=ee[e]||{},ee[e]}function ae(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function le(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=ue(t);return se.has(o)||(o=t),[n,s,o]}function ce(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=le(e,i,n);if(e in ne){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=re(t),c=l[a]||(l[a]={}),h=ae(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=oe(r,e.replace(Jt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return pe(s,{delegateTarget:r}),n.oneOff&&fe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return pe(n,{delegateTarget:t}),i.oneOff&&fe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function he(t,e,i,n,s){const o=ae(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function de(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&he(t,e,i,r.callable,r.delegationSelector)}function ue(t){return t=t.replace(Zt,""),ne[t]||t}const fe={on(t,e,i,n){ce(t,e,i,n,!1)},one(t,e,i,n){ce(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=le(e,i,n),a=r!==e,l=re(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))de(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(te,"");a&&!e.includes(s)||he(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;he(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Vt();let s=null,o=!0,r=!0,a=!1;e!==ue(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=pe(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function pe(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ge(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const _e={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ge(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ge(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${ge(e)}`))};class be{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ft(e)?_e.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ft(e)?_e.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],o=Ft(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${o}" but expected type "${s}".`)}var i}}class ve extends be{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),fe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Ut(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.3"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const ye=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e?e.split(",").map((t=>Mt(t))).join(","):null},we={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))},getSelectorFromElement(t){const e=ye(t);return e&&we.findOne(e)?e:null},getElementFromSelector(t){const e=ye(t);return e?we.findOne(e):null},getMultipleElementsFromSelector(t){const e=ye(t);return e?we.find(e):[]}},Ee=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;fe.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=we.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Te=`close${Ae}`,Ce=`closed${Ae}`;class Oe extends ve{static get NAME(){return"alert"}close(){if(fe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),fe.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Oe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}Ee(Oe,"close"),Qt(Oe);const xe='[data-bs-toggle="button"]';class ke extends ve{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=ke.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}fe.on(document,"click.bs.button.data-api",xe,(t=>{t.preventDefault();const e=t.target.closest(xe);ke.getOrCreateInstance(e).toggle()})),Qt(ke);const Le=".bs.swipe",Se=`touchstart${Le}`,De=`touchmove${Le}`,$e=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},Me={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class je extends be{constructor(t,e){super(),this._element=t,t&&je.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return Me}static get NAME(){return"swipe"}dispose(){fe.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Xt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Xt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(fe.on(this._element,Ie,(t=>this._start(t))),fe.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(fe.on(this._element,Se,(t=>this._start(t))),fe.on(this._element,De,(t=>this._move(t))),fe.on(this._element,$e,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Fe=".bs.carousel",He=".data-api",Be="ArrowLeft",We="ArrowRight",ze="next",Re="prev",qe="left",Ve="right",Ye=`slide${Fe}`,Ke=`slid${Fe}`,Qe=`keydown${Fe}`,Xe=`mouseenter${Fe}`,Ue=`mouseleave${Fe}`,Ge=`dragstart${Fe}`,Je=`load${Fe}${He}`,Ze=`click${Fe}${He}`,ti="carousel",ei="active",ii=".active",ni=".carousel-item",si=ii+ni,oi={[Be]:Ve,[We]:qe},ri={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},ai={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class li extends ve{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=we.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===ti&&this.cycle()}static get Default(){return ri}static get DefaultType(){return ai}static get NAME(){return"carousel"}next(){this._slide(ze)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(Re)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?fe.one(this._element,Ke,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void fe.one(this._element,Ke,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ze:Re;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&fe.on(this._element,Qe,(t=>this._keydown(t))),"hover"===this._config.pause&&(fe.on(this._element,Xe,(()=>this.pause())),fe.on(this._element,Ue,(()=>this._maybeEnableCycle()))),this._config.touch&&je.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of we.find(".carousel-item img",this._element))fe.on(t,Ge,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(qe)),rightCallback:()=>this._slide(this._directionToOrder(Ve)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new je(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=oi[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=we.findOne(ii,this._indicatorsElement);e.classList.remove(ei),e.removeAttribute("aria-current");const i=we.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(ei),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ze,s=e||Gt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>fe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(Ye).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(ei),i.classList.remove(ei,c,l),this._isSliding=!1,r(Ke)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return we.findOne(si,this._element)}_getItems(){return we.find(ni,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Kt()?t===qe?Re:ze:t===qe?ze:Re}_orderToDirection(t){return Kt()?t===Re?qe:Ve:t===Re?Ve:qe}static jQueryInterface(t){return this.each((function(){const e=li.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}fe.on(document,Ze,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=we.getElementFromSelector(this);if(!e||!e.classList.contains(ti))return;t.preventDefault();const i=li.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===_e.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),fe.on(window,Je,(()=>{const t=we.find('[data-bs-ride="carousel"]');for(const e of t)li.getOrCreateInstance(e)})),Qt(li);const ci=".bs.collapse",hi=`show${ci}`,di=`shown${ci}`,ui=`hide${ci}`,fi=`hidden${ci}`,pi=`click${ci}.data-api`,mi="show",gi="collapse",_i="collapsing",bi=`:scope .${gi} .${gi}`,vi='[data-bs-toggle="collapse"]',yi={parent:null,toggle:!0},wi={parent:"(null|element)",toggle:"boolean"};class Ei extends ve{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=we.find(vi);for(const t of i){const e=we.getSelectorFromElement(t),i=we.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return yi}static get DefaultType(){return wi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>Ei.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(fe.trigger(this._element,hi).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(gi),this._element.classList.add(_i),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi,mi),this._element.style[e]="",fe.trigger(this._element,di)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(fe.trigger(this._element,ui).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(_i),this._element.classList.remove(gi,mi);for(const t of this._triggerArray){const e=we.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(_i),this._element.classList.add(gi),fe.trigger(this._element,fi)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(mi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(vi);for(const e of t){const t=we.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=we.find(bi,this._config.parent);return we.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Ei.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}fe.on(document,pi,vi,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of we.getMultipleElementsFromSelector(this))Ei.getOrCreateInstance(t,{toggle:!1}).toggle()})),Qt(Ei);const Ai="dropdown",Ti=".bs.dropdown",Ci=".data-api",Oi="ArrowUp",xi="ArrowDown",ki=`hide${Ti}`,Li=`hidden${Ti}`,Si=`show${Ti}`,Di=`shown${Ti}`,$i=`click${Ti}${Ci}`,Ii=`keydown${Ti}${Ci}`,Ni=`keyup${Ti}${Ci}`,Pi="show",Mi='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',ji=`${Mi}.${Pi}`,Fi=".dropdown-menu",Hi=Kt()?"top-end":"top-start",Bi=Kt()?"top-start":"top-end",Wi=Kt()?"bottom-end":"bottom-start",zi=Kt()?"bottom-start":"bottom-end",Ri=Kt()?"left-start":"right-start",qi=Kt()?"right-start":"left-start",Vi={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},Yi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Ki extends ve{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=we.next(this._element,Fi)[0]||we.prev(this._element,Fi)[0]||we.findOne(Fi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Vi}static get DefaultType(){return Yi}static get NAME(){return Ai}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!fe.trigger(this._element,Si,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Pi),this._element.classList.add(Pi),fe.trigger(this._element,Di,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!fe.trigger(this._element,ki,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Pi),this._element.classList.remove(Pi),this._element.setAttribute("aria-expanded","false"),_e.removeDataAttribute(this._menu,"popper"),fe.trigger(this._element,Li,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ft(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ai.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Ft(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Pi)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Ri;if(t.classList.contains("dropstart"))return qi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Bi:Hi:e?zi:Wi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(_e.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Xt(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=we.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Gt(i,e,t===xi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Ki.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=we.find(ji);for(const i of e){const e=Ki.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Oi,xi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Mi)?this:we.prev(this,Mi)[0]||we.next(this,Mi)[0]||we.findOne(Mi,t.delegateTarget.parentNode),o=Ki.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}fe.on(document,Ii,Mi,Ki.dataApiKeydownHandler),fe.on(document,Ii,Fi,Ki.dataApiKeydownHandler),fe.on(document,$i,Ki.clearMenus),fe.on(document,Ni,Ki.clearMenus),fe.on(document,$i,Mi,(function(t){t.preventDefault(),Ki.getOrCreateInstance(this).toggle()})),Qt(Ki);const Qi="backdrop",Xi="show",Ui=`mousedown.bs.${Qi}`,Gi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ji={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zi extends be{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return Qi}show(t){if(!this._config.isVisible)return void Xt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Xi),this._emulateAnimation((()=>{Xt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Xi),this._emulateAnimation((()=>{this.dispose(),Xt(t)}))):Xt(t)}dispose(){this._isAppended&&(fe.off(this._element,Ui),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),fe.on(t,Ui,(()=>{Xt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Ut(t,this._getElement(),this._config.isAnimated)}}const tn=".bs.focustrap",en=`focusin${tn}`,nn=`keydown.tab${tn}`,sn="backward",on={autofocus:!0,trapElement:null},rn={autofocus:"boolean",trapElement:"element"};class an extends be{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return on}static get DefaultType(){return rn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),fe.off(document,tn),fe.on(document,en,(t=>this._handleFocusin(t))),fe.on(document,nn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,fe.off(document,tn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=we.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===sn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?sn:"forward")}}const ln=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",cn=".sticky-top",hn="padding-right",dn="margin-right";class un{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hn,(e=>e+t)),this._setElementAttributes(ln,hn,(e=>e+t)),this._setElementAttributes(cn,dn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hn),this._resetElementAttributes(ln,hn),this._resetElementAttributes(cn,dn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&_e.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=_e.getDataAttribute(t,e);null!==i?(_e.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Ft(t))e(t);else for(const i of we.find(t,this._element))e(i)}}const fn=".bs.modal",pn=`hide${fn}`,mn=`hidePrevented${fn}`,gn=`hidden${fn}`,_n=`show${fn}`,bn=`shown${fn}`,vn=`resize${fn}`,yn=`click.dismiss${fn}`,wn=`mousedown.dismiss${fn}`,En=`keydown.dismiss${fn}`,An=`click${fn}.data-api`,Tn="modal-open",Cn="show",On="modal-static",xn={backdrop:!0,focus:!0,keyboard:!0},kn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ln extends ve{constructor(t,e){super(t,e),this._dialog=we.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new un,this._addEventListeners()}static get Default(){return xn}static get DefaultType(){return kn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||fe.trigger(this._element,_n,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(fe.trigger(this._element,pn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Cn),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){fe.off(window,fn),fe.off(this._dialog,fn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new an({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=we.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(Cn),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,fe.trigger(this._element,bn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){fe.on(this._element,En,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),fe.on(window,vn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),fe.on(this._element,wn,(t=>{fe.one(this._element,yn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),fe.trigger(this._element,gn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(fe.trigger(this._element,mn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(On)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(On),this._queueCallback((()=>{this._element.classList.remove(On),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Kt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Kt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}fe.on(document,An,'[data-bs-toggle="modal"]',(function(t){const e=we.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),fe.one(e,_n,(t=>{t.defaultPrevented||fe.one(e,gn,(()=>{Bt(this)&&this.focus()}))}));const i=we.findOne(".modal.show");i&&Ln.getInstance(i).hide(),Ln.getOrCreateInstance(e).toggle(this)})),Ee(Ln),Qt(Ln);const Sn=".bs.offcanvas",Dn=".data-api",$n=`load${Sn}${Dn}`,In="show",Nn="showing",Pn="hiding",Mn=".offcanvas.show",jn=`show${Sn}`,Fn=`shown${Sn}`,Hn=`hide${Sn}`,Bn=`hidePrevented${Sn}`,Wn=`hidden${Sn}`,zn=`resize${Sn}`,Rn=`click${Sn}${Dn}`,qn=`keydown.dismiss${Sn}`,Vn={backdrop:!0,keyboard:!0,scroll:!1},Yn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Kn extends ve{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Vn}static get DefaultType(){return Yn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||fe.trigger(this._element,jn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new un).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Nn),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(In),this._element.classList.remove(Nn),fe.trigger(this._element,Fn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(fe.trigger(this._element,Hn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Pn),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(In,Pn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new un).reset(),fe.trigger(this._element,Wn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Zi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():fe.trigger(this._element,Bn)}:null})}_initializeFocusTrap(){return new an({trapElement:this._element})}_addEventListeners(){fe.on(this._element,qn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():fe.trigger(this._element,Bn))}))}static jQueryInterface(t){return this.each((function(){const e=Kn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}fe.on(document,Rn,'[data-bs-toggle="offcanvas"]',(function(t){const e=we.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;fe.one(e,Wn,(()=>{Bt(this)&&this.focus()}));const i=we.findOne(Mn);i&&i!==e&&Kn.getInstance(i).hide(),Kn.getOrCreateInstance(e).toggle(this)})),fe.on(window,$n,(()=>{for(const t of we.find(Mn))Kn.getOrCreateInstance(t).show()})),fe.on(window,zn,(()=>{for(const t of we.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Kn.getOrCreateInstance(t).hide()})),Ee(Kn),Qt(Kn);const Qn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Xn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Un=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Gn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Xn.has(i)||Boolean(Un.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Jn={allowList:Qn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Zn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ts={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends be{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Jn}static get DefaultType(){return Zn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ts)}_setContent(t,e,i){const n=we.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ft(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Gn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Xt(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const is=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",ss="show",os=".tooltip-inner",rs=".modal",as="hide.bs.modal",ls="hover",cs="focus",hs={AUTO:"auto",TOP:"top",RIGHT:Kt()?"left":"right",BOTTOM:"bottom",LEFT:Kt()?"right":"left"},ds={allowList:Qn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},us={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class fs extends ve{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),fe.off(this._element.closest(rs),as,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=fe.trigger(this._element,this.constructor.eventName("show")),e=(zt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),fe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._queueCallback((()=>{fe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!fe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._activeTrigger.click=!1,this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),fe.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ns,ss),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ns),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new es({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[os]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(ss)}_createPopper(t){const e=Xt(this._config.placement,[this,t,this._element]),i=hs[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Xt(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Xt(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)fe.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===ls?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ls?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");fe.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?cs:ls]=!0,e._enter()})),fe.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?cs:ls]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},fe.on(this._element.closest(rs),as,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=_e.getDataAttributes(this._element);for(const t of Object.keys(e))is.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(fs);const ps=".popover-header",ms=".popover-body",gs={...fs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},_s={...fs.DefaultType,content:"(null|string|element|function)"};class bs extends fs{static get Default(){return gs}static get DefaultType(){return _s}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ps]:this._getTitle(),[ms]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=bs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(bs);const vs=".bs.scrollspy",ys=`activate${vs}`,ws=`click${vs}`,Es=`load${vs}.data-api`,As="active",Ts="[href]",Cs=".nav-link",Os=`${Cs}, .nav-item > ${Cs}, .list-group-item`,xs={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},ks={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ls extends ve{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return xs}static get DefaultType(){return ks}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(fe.off(this._config.target,ws),fe.on(this._config.target,ws,Ts,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=we.find(Ts,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=we.findOne(decodeURI(e.hash),this._element);Bt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(As),this._activateParents(t),fe.trigger(this._element,ys,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))we.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(As);else for(const e of we.parents(t,".nav, .list-group"))for(const t of we.prev(e,Os))t.classList.add(As)}_clearActiveClass(t){t.classList.remove(As);const e=we.find(`${Ts}.${As}`,t);for(const t of e)t.classList.remove(As)}static jQueryInterface(t){return this.each((function(){const e=Ls.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(window,Es,(()=>{for(const t of we.find('[data-bs-spy="scroll"]'))Ls.getOrCreateInstance(t)})),Qt(Ls);const Ss=".bs.tab",Ds=`hide${Ss}`,$s=`hidden${Ss}`,Is=`show${Ss}`,Ns=`shown${Ss}`,Ps=`click${Ss}`,Ms=`keydown${Ss}`,js=`load${Ss}`,Fs="ArrowLeft",Hs="ArrowRight",Bs="ArrowUp",Ws="ArrowDown",zs="Home",Rs="End",qs="active",Vs="fade",Ys="show",Ks=".dropdown-toggle",Qs=`:not(${Ks})`,Xs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Us=`.nav-link${Qs}, .list-group-item${Qs}, [role="tab"]${Qs}, ${Xs}`,Gs=`.${qs}[data-bs-toggle="tab"], .${qs}[data-bs-toggle="pill"], .${qs}[data-bs-toggle="list"]`;class Js extends ve{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),fe.on(this._element,Ms,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?fe.trigger(e,Ds,{relatedTarget:t}):null;fe.trigger(t,Is,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(qs),this._activate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),fe.trigger(t,Ns,{relatedTarget:e})):t.classList.add(Ys)}),t,t.classList.contains(Vs)))}_deactivate(t,e){t&&(t.classList.remove(qs),t.blur(),this._deactivate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),fe.trigger(t,$s,{relatedTarget:e})):t.classList.remove(Ys)}),t,t.classList.contains(Vs)))}_keydown(t){if(![Fs,Hs,Bs,Ws,zs,Rs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!Wt(t)));let i;if([zs,Rs].includes(t.key))i=e[t.key===zs?0:e.length-1];else{const n=[Hs,Ws].includes(t.key);i=Gt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Js.getOrCreateInstance(i).show())}_getChildren(){return we.find(Us,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=we.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=we.findOne(t,i);s&&s.classList.toggle(n,e)};n(Ks,qs),n(".dropdown-menu",Ys),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(qs)}_getInnerElement(t){return t.matches(Us)?t:we.findOne(Us,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Js.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(document,Ps,Xs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Js.getOrCreateInstance(this).show()})),fe.on(window,js,(()=>{for(const t of we.find(Gs))Js.getOrCreateInstance(t)})),Qt(Js);const Zs=".bs.toast",to=`mouseover${Zs}`,eo=`mouseout${Zs}`,io=`focusin${Zs}`,no=`focusout${Zs}`,so=`hide${Zs}`,oo=`hidden${Zs}`,ro=`show${Zs}`,ao=`shown${Zs}`,lo="hide",co="show",ho="showing",uo={animation:"boolean",autohide:"boolean",delay:"number"},fo={animation:!0,autohide:!0,delay:5e3};class po extends ve{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return fo}static get DefaultType(){return uo}static get NAME(){return"toast"}show(){fe.trigger(this._element,ro).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(lo),qt(this._element),this._element.classList.add(co,ho),this._queueCallback((()=>{this._element.classList.remove(ho),fe.trigger(this._element,ao),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(fe.trigger(this._element,so).defaultPrevented||(this._element.classList.add(ho),this._queueCallback((()=>{this._element.classList.add(lo),this._element.classList.remove(ho,co),fe.trigger(this._element,oo)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(co),super.dispose()}isShown(){return this._element.classList.contains(co)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){fe.on(this._element,to,(t=>this._onInteraction(t,!0))),fe.on(this._element,eo,(t=>this._onInteraction(t,!1))),fe.on(this._element,io,(t=>this._onInteraction(t,!0))),fe.on(this._element,no,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=po.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function mo(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ee(po),Qt(po),mo((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new fs(t,{delay:{show:500,hide:100}})}))})),mo((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),mo((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))})),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/_static/scripts/bootstrap.js.LICENSE.txt b/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 0000000000..28755c2c5b --- /dev/null +++ b/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.3 (https://getbootstrap.com/) + * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/_static/scripts/bootstrap.js.map b/_static/scripts/bootstrap.js.map new file mode 100644 index 0000000000..4a3502aeb2 --- /dev/null +++ b/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":";mBACA,IAAIA,EAAsB,CCA1BA,EAAwB,CAACC,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXF,EAAoBI,EAAEF,EAAYC,KAASH,EAAoBI,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDH,EAAwB,CAACS,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFV,EAAyBC,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,01BCLvD,IAAI,EAAM,MACNC,EAAS,SACTC,EAAQ,QACRC,EAAO,OACPC,EAAO,OACPC,EAAiB,CAAC,EAAKJ,EAAQC,EAAOC,GACtCG,EAAQ,QACRC,EAAM,MACNC,EAAkB,kBAClBC,EAAW,WACXC,EAAS,SACTC,EAAY,YACZC,EAAmCP,EAAeQ,QAAO,SAAUC,EAAKC,GACjF,OAAOD,EAAIE,OAAO,CAACD,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAChE,GAAG,IACQ,EAA0B,GAAGS,OAAOX,EAAgB,CAACD,IAAOS,QAAO,SAAUC,EAAKC,GAC3F,OAAOD,EAAIE,OAAO,CAACD,EAAWA,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAC3E,GAAG,IAEQU,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAc,cACdC,EAAQ,QACRC,EAAa,aACbC,EAAiB,CAACT,EAAYC,EAAMC,EAAWC,EAAYC,EAAMC,EAAWC,EAAaC,EAAOC,GC9B5F,SAASE,EAAYC,GAClC,OAAOA,GAAWA,EAAQC,UAAY,IAAIC,cAAgB,IAC5D,CCFe,SAASC,EAAUC,GAChC,GAAY,MAARA,EACF,OAAOC,OAGT,GAAwB,oBAApBD,EAAKE,WAAkC,CACzC,IAAIC,EAAgBH,EAAKG,cACzB,OAAOA,GAAgBA,EAAcC,aAAwBH,MAC/D,CAEA,OAAOD,CACT,CCTA,SAASK,EAAUL,GAEjB,OAAOA,aADUD,EAAUC,GAAMM,SACIN,aAAgBM,OACvD,CAEA,SAASC,EAAcP,GAErB,OAAOA,aADUD,EAAUC,GAAMQ,aACIR,aAAgBQ,WACvD,CAEA,SAASC,EAAaT,GAEpB,MAA0B,oBAAfU,aAKJV,aADUD,EAAUC,GAAMU,YACIV,aAAgBU,WACvD,CCwDA,SACEC,KAAM,cACNC,SAAS,EACTC,MAAO,QACPC,GA5EF,SAAqBC,GACnB,IAAIC,EAAQD,EAAKC,MACjB3D,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIS,EAAQJ,EAAMK,OAAOV,IAAS,CAAC,EAC/BW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EACxCf,EAAUoB,EAAME,SAASP,GAExBJ,EAAcX,IAAaD,EAAYC,KAO5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUR,GACxC,IAAI3C,EAAQsD,EAAWX,IAET,IAAV3C,EACF4B,EAAQ4B,gBAAgBb,GAExBf,EAAQ6B,aAAad,GAAgB,IAAV3C,EAAiB,GAAKA,EAErD,IACF,GACF,EAoDE0D,OAlDF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MACdY,EAAgB,CAClBlD,OAAQ,CACNmD,SAAUb,EAAMc,QAAQC,SACxB5D,KAAM,IACN6D,IAAK,IACLC,OAAQ,KAEVC,MAAO,CACLL,SAAU,YAEZlD,UAAW,CAAC,GASd,OAPAtB,OAAOkE,OAAOP,EAAME,SAASxC,OAAO0C,MAAOQ,EAAclD,QACzDsC,EAAMK,OAASO,EAEXZ,EAAME,SAASgB,OACjB7E,OAAOkE,OAAOP,EAAME,SAASgB,MAAMd,MAAOQ,EAAcM,OAGnD,WACL7E,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIf,EAAUoB,EAAME,SAASP,GACzBW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EAGxCS,EAFkB/D,OAAO4D,KAAKD,EAAMK,OAAOzD,eAAe+C,GAAQK,EAAMK,OAAOV,GAAQiB,EAAcjB,IAE7E9B,QAAO,SAAUuC,EAAOe,GAElD,OADAf,EAAMe,GAAY,GACXf,CACT,GAAG,CAAC,GAECb,EAAcX,IAAaD,EAAYC,KAI5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUiB,GACxCxC,EAAQ4B,gBAAgBY,EAC1B,IACF,GACF,CACF,EASEC,SAAU,CAAC,kBCjFE,SAASC,EAAiBvD,GACvC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCHO,IAAI,EAAMC,KAAKC,IACX,EAAMD,KAAKE,IACXC,EAAQH,KAAKG,MCFT,SAASC,IACtB,IAAIC,EAASC,UAAUC,cAEvB,OAAc,MAAVF,GAAkBA,EAAOG,QAAUC,MAAMC,QAAQL,EAAOG,QACnDH,EAAOG,OAAOG,KAAI,SAAUC,GACjC,OAAOA,EAAKC,MAAQ,IAAMD,EAAKE,OACjC,IAAGC,KAAK,KAGHT,UAAUU,SACnB,CCTe,SAASC,IACtB,OAAQ,iCAAiCC,KAAKd,IAChD,CCCe,SAASe,EAAsB/D,EAASgE,EAAcC,QAC9C,IAAjBD,IACFA,GAAe,QAGO,IAApBC,IACFA,GAAkB,GAGpB,IAAIC,EAAalE,EAAQ+D,wBACrBI,EAAS,EACTC,EAAS,EAETJ,GAAgBrD,EAAcX,KAChCmE,EAASnE,EAAQqE,YAAc,GAAItB,EAAMmB,EAAWI,OAAStE,EAAQqE,aAAmB,EACxFD,EAASpE,EAAQuE,aAAe,GAAIxB,EAAMmB,EAAWM,QAAUxE,EAAQuE,cAAoB,GAG7F,IACIE,GADOhE,EAAUT,GAAWG,EAAUH,GAAWK,QAC3BoE,eAEtBC,GAAoBb,KAAsBI,EAC1CU,GAAKT,EAAW3F,MAAQmG,GAAoBD,EAAiBA,EAAeG,WAAa,IAAMT,EAC/FU,GAAKX,EAAW9B,KAAOsC,GAAoBD,EAAiBA,EAAeK,UAAY,IAAMV,EAC7FE,EAAQJ,EAAWI,MAAQH,EAC3BK,EAASN,EAAWM,OAASJ,EACjC,MAAO,CACLE,MAAOA,EACPE,OAAQA,EACRpC,IAAKyC,EACLvG,MAAOqG,EAAIL,EACXjG,OAAQwG,EAAIL,EACZjG,KAAMoG,EACNA,EAAGA,EACHE,EAAGA,EAEP,CCrCe,SAASE,EAAc/E,GACpC,IAAIkE,EAAaH,EAAsB/D,GAGnCsE,EAAQtE,EAAQqE,YAChBG,EAASxE,EAAQuE,aAUrB,OARI3B,KAAKoC,IAAId,EAAWI,MAAQA,IAAU,IACxCA,EAAQJ,EAAWI,OAGjB1B,KAAKoC,IAAId,EAAWM,OAASA,IAAW,IAC1CA,EAASN,EAAWM,QAGf,CACLG,EAAG3E,EAAQ4E,WACXC,EAAG7E,EAAQ8E,UACXR,MAAOA,EACPE,OAAQA,EAEZ,CCvBe,SAASS,EAASC,EAAQC,GACvC,IAAIC,EAAWD,EAAME,aAAeF,EAAME,cAE1C,GAAIH,EAAOD,SAASE,GAClB,OAAO,EAEJ,GAAIC,GAAYvE,EAAauE,GAAW,CACzC,IAAIE,EAAOH,EAEX,EAAG,CACD,GAAIG,GAAQJ,EAAOK,WAAWD,GAC5B,OAAO,EAITA,EAAOA,EAAKE,YAAcF,EAAKG,IACjC,OAASH,EACX,CAGF,OAAO,CACT,CCrBe,SAAS,EAAiBtF,GACvC,OAAOG,EAAUH,GAAS0F,iBAAiB1F,EAC7C,CCFe,SAAS2F,EAAe3F,GACrC,MAAO,CAAC,QAAS,KAAM,MAAM4F,QAAQ7F,EAAYC,KAAa,CAChE,CCFe,SAAS6F,EAAmB7F,GAEzC,QAASS,EAAUT,GAAWA,EAAQO,cACtCP,EAAQ8F,WAAazF,OAAOyF,UAAUC,eACxC,CCFe,SAASC,EAAchG,GACpC,MAA6B,SAAzBD,EAAYC,GACPA,EAMPA,EAAQiG,cACRjG,EAAQwF,aACR3E,EAAab,GAAWA,EAAQyF,KAAO,OAEvCI,EAAmB7F,EAGvB,CCVA,SAASkG,EAAoBlG,GAC3B,OAAKW,EAAcX,IACoB,UAAvC,EAAiBA,GAASiC,SAInBjC,EAAQmG,aAHN,IAIX,CAwCe,SAASC,EAAgBpG,GAItC,IAHA,IAAIK,EAASF,EAAUH,GACnBmG,EAAeD,EAAoBlG,GAEhCmG,GAAgBR,EAAeQ,IAA6D,WAA5C,EAAiBA,GAAclE,UACpFkE,EAAeD,EAAoBC,GAGrC,OAAIA,IAA+C,SAA9BpG,EAAYoG,IAA0D,SAA9BpG,EAAYoG,IAAwE,WAA5C,EAAiBA,GAAclE,UAC3H5B,EAGF8F,GAhDT,SAA4BnG,GAC1B,IAAIqG,EAAY,WAAWvC,KAAKd,KAGhC,GAFW,WAAWc,KAAKd,MAEfrC,EAAcX,IAII,UAFX,EAAiBA,GAEnBiC,SACb,OAAO,KAIX,IAAIqE,EAAcN,EAAchG,GAMhC,IAJIa,EAAayF,KACfA,EAAcA,EAAYb,MAGrB9E,EAAc2F,IAAgB,CAAC,OAAQ,QAAQV,QAAQ7F,EAAYuG,IAAgB,GAAG,CAC3F,IAAIC,EAAM,EAAiBD,GAI3B,GAAsB,SAAlBC,EAAIC,WAA4C,SAApBD,EAAIE,aAA0C,UAAhBF,EAAIG,UAAiF,IAA1D,CAAC,YAAa,eAAed,QAAQW,EAAII,aAAsBN,GAAgC,WAAnBE,EAAII,YAA2BN,GAAaE,EAAIK,QAAyB,SAAfL,EAAIK,OACjO,OAAON,EAEPA,EAAcA,EAAYd,UAE9B,CAEA,OAAO,IACT,CAgByBqB,CAAmB7G,IAAYK,CACxD,CCpEe,SAASyG,EAAyB3H,GAC/C,MAAO,CAAC,MAAO,UAAUyG,QAAQzG,IAAc,EAAI,IAAM,GAC3D,CCDO,SAAS4H,EAAOjE,EAAK1E,EAAOyE,GACjC,OAAO,EAAQC,EAAK,EAAQ1E,EAAOyE,GACrC,CCFe,SAASmE,EAAmBC,GACzC,OAAOxJ,OAAOkE,OAAO,CAAC,ECDf,CACLS,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GDHuC0I,EACjD,CEHe,SAASC,EAAgB9I,EAAOiD,GAC7C,OAAOA,EAAKpC,QAAO,SAAUkI,EAAS5J,GAEpC,OADA4J,EAAQ5J,GAAOa,EACR+I,CACT,GAAG,CAAC,EACN,CC4EA,SACEpG,KAAM,QACNC,SAAS,EACTC,MAAO,OACPC,GApEF,SAAeC,GACb,IAAIiG,EAEAhG,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZmB,EAAUf,EAAKe,QACfmF,EAAejG,EAAME,SAASgB,MAC9BgF,EAAgBlG,EAAMmG,cAAcD,cACpCE,EAAgB9E,EAAiBtB,EAAMjC,WACvCsI,EAAOX,EAAyBU,GAEhCE,EADa,CAACnJ,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAClC,SAAW,QAElC,GAAKH,GAAiBC,EAAtB,CAIA,IAAIL,EAxBgB,SAAyBU,EAASvG,GAItD,OAAO4F,EAAsC,iBAH7CW,EAA6B,mBAAZA,EAAyBA,EAAQlK,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CAC/EzI,UAAWiC,EAAMjC,aACbwI,GACkDA,EAAUT,EAAgBS,EAASlJ,GAC7F,CAmBsBoJ,CAAgB3F,EAAQyF,QAASvG,GACjD0G,EAAY/C,EAAcsC,GAC1BU,EAAmB,MAATN,EAAe,EAAMlJ,EAC/ByJ,EAAmB,MAATP,EAAepJ,EAASC,EAClC2J,EAAU7G,EAAMwG,MAAM7I,UAAU2I,GAAOtG,EAAMwG,MAAM7I,UAAU0I,GAAQH,EAAcG,GAAQrG,EAAMwG,MAAM9I,OAAO4I,GAC9GQ,EAAYZ,EAAcG,GAAQrG,EAAMwG,MAAM7I,UAAU0I,GACxDU,EAAoB/B,EAAgBiB,GACpCe,EAAaD,EAA6B,MAATV,EAAeU,EAAkBE,cAAgB,EAAIF,EAAkBG,aAAe,EAAI,EAC3HC,EAAoBN,EAAU,EAAIC,EAAY,EAG9CpF,EAAMmE,EAAcc,GACpBlF,EAAMuF,EAAaN,EAAUJ,GAAOT,EAAce,GAClDQ,EAASJ,EAAa,EAAIN,EAAUJ,GAAO,EAAIa,EAC/CE,EAAS1B,EAAOjE,EAAK0F,EAAQ3F,GAE7B6F,EAAWjB,EACfrG,EAAMmG,cAAcxG,KAASqG,EAAwB,CAAC,GAAyBsB,GAAYD,EAAQrB,EAAsBuB,aAAeF,EAASD,EAAQpB,EAnBzJ,CAoBF,EAkCEtF,OAhCF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MAEdwH,EADU7G,EAAMG,QACWlC,QAC3BqH,OAAoC,IAArBuB,EAA8B,sBAAwBA,EAErD,MAAhBvB,IAKwB,iBAAjBA,IACTA,EAAejG,EAAME,SAASxC,OAAO+J,cAAcxB,MAOhDpC,EAAS7D,EAAME,SAASxC,OAAQuI,KAIrCjG,EAAME,SAASgB,MAAQ+E,EACzB,EASE5E,SAAU,CAAC,iBACXqG,iBAAkB,CAAC,oBCxFN,SAASC,EAAa5J,GACnC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCOA,IAAIqG,GAAa,CACf5G,IAAK,OACL9D,MAAO,OACPD,OAAQ,OACRE,KAAM,QAeD,SAAS0K,GAAYlH,GAC1B,IAAImH,EAEApK,EAASiD,EAAMjD,OACfqK,EAAapH,EAAMoH,WACnBhK,EAAY4C,EAAM5C,UAClBiK,EAAYrH,EAAMqH,UAClBC,EAAUtH,EAAMsH,QAChBpH,EAAWF,EAAME,SACjBqH,EAAkBvH,EAAMuH,gBACxBC,EAAWxH,EAAMwH,SACjBC,EAAezH,EAAMyH,aACrBC,EAAU1H,EAAM0H,QAChBC,EAAaL,EAAQ1E,EACrBA,OAAmB,IAAf+E,EAAwB,EAAIA,EAChCC,EAAaN,EAAQxE,EACrBA,OAAmB,IAAf8E,EAAwB,EAAIA,EAEhCC,EAAgC,mBAAjBJ,EAA8BA,EAAa,CAC5D7E,EAAGA,EACHE,IACG,CACHF,EAAGA,EACHE,GAGFF,EAAIiF,EAAMjF,EACVE,EAAI+E,EAAM/E,EACV,IAAIgF,EAAOR,EAAQrL,eAAe,KAC9B8L,EAAOT,EAAQrL,eAAe,KAC9B+L,EAAQxL,EACRyL,EAAQ,EACRC,EAAM5J,OAEV,GAAIkJ,EAAU,CACZ,IAAIpD,EAAeC,EAAgBtH,GAC/BoL,EAAa,eACbC,EAAY,cAEZhE,IAAiBhG,EAAUrB,IAGmB,WAA5C,EAFJqH,EAAeN,EAAmB/G,IAECmD,UAAsC,aAAbA,IAC1DiI,EAAa,eACbC,EAAY,gBAOZhL,IAAc,IAAQA,IAAcZ,GAAQY,IAAcb,IAAU8K,IAAczK,KACpFqL,EAAQ3L,EAGRwG,IAFc4E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeD,OACzF2B,EAAa+D,IACEf,EAAW3E,OAC1BK,GAAKyE,EAAkB,GAAK,GAG1BnK,IAAcZ,IAASY,IAAc,GAAOA,IAAcd,GAAW+K,IAAczK,KACrFoL,EAAQzL,EAGRqG,IAFc8E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeH,MACzF6B,EAAagE,IACEhB,EAAW7E,MAC1BK,GAAK2E,EAAkB,GAAK,EAEhC,CAEA,IAgBMc,EAhBFC,EAAe5M,OAAOkE,OAAO,CAC/BM,SAAUA,GACTsH,GAAYP,IAEXsB,GAAyB,IAAjBd,EAlFd,SAA2BrI,EAAM8I,GAC/B,IAAItF,EAAIxD,EAAKwD,EACTE,EAAI1D,EAAK0D,EACT0F,EAAMN,EAAIO,kBAAoB,EAClC,MAAO,CACL7F,EAAG5B,EAAM4B,EAAI4F,GAAOA,GAAO,EAC3B1F,EAAG9B,EAAM8B,EAAI0F,GAAOA,GAAO,EAE/B,CA0EsCE,CAAkB,CACpD9F,EAAGA,EACHE,GACC1E,EAAUrB,IAAW,CACtB6F,EAAGA,EACHE,GAMF,OAHAF,EAAI2F,EAAM3F,EACVE,EAAIyF,EAAMzF,EAENyE,EAGK7L,OAAOkE,OAAO,CAAC,EAAG0I,IAAeD,EAAiB,CAAC,GAAkBJ,GAASF,EAAO,IAAM,GAAIM,EAAeL,GAASF,EAAO,IAAM,GAAIO,EAAe5D,WAAayD,EAAIO,kBAAoB,IAAM,EAAI,aAAe7F,EAAI,OAASE,EAAI,MAAQ,eAAiBF,EAAI,OAASE,EAAI,SAAUuF,IAG5R3M,OAAOkE,OAAO,CAAC,EAAG0I,IAAenB,EAAkB,CAAC,GAAmBc,GAASF,EAAOjF,EAAI,KAAO,GAAIqE,EAAgBa,GAASF,EAAOlF,EAAI,KAAO,GAAIuE,EAAgB1C,UAAY,GAAI0C,GAC9L,CA4CA,UACEnI,KAAM,gBACNC,SAAS,EACTC,MAAO,cACPC,GA9CF,SAAuBwJ,GACrB,IAAItJ,EAAQsJ,EAAMtJ,MACdc,EAAUwI,EAAMxI,QAChByI,EAAwBzI,EAAQoH,gBAChCA,OAA4C,IAA1BqB,GAA0CA,EAC5DC,EAAoB1I,EAAQqH,SAC5BA,OAAiC,IAAtBqB,GAAsCA,EACjDC,EAAwB3I,EAAQsH,aAChCA,OAAyC,IAA1BqB,GAA0CA,EACzDR,EAAe,CACjBlL,UAAWuD,EAAiBtB,EAAMjC,WAClCiK,UAAWL,EAAa3H,EAAMjC,WAC9BL,OAAQsC,EAAME,SAASxC,OACvBqK,WAAY/H,EAAMwG,MAAM9I,OACxBwK,gBAAiBA,EACjBG,QAAoC,UAA3BrI,EAAMc,QAAQC,UAGgB,MAArCf,EAAMmG,cAAcD,gBACtBlG,EAAMK,OAAO3C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAO3C,OAAQmK,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACvGhB,QAASjI,EAAMmG,cAAcD,cAC7BrF,SAAUb,EAAMc,QAAQC,SACxBoH,SAAUA,EACVC,aAAcA,OAIe,MAA7BpI,EAAMmG,cAAcjF,QACtBlB,EAAMK,OAAOa,MAAQ7E,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAOa,MAAO2G,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACrGhB,QAASjI,EAAMmG,cAAcjF,MAC7BL,SAAU,WACVsH,UAAU,EACVC,aAAcA,OAIlBpI,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,wBAAyBsC,EAAMjC,WAEnC,EAQE2L,KAAM,CAAC,GCrKT,IAAIC,GAAU,CACZA,SAAS,GAsCX,UACEhK,KAAM,iBACNC,SAAS,EACTC,MAAO,QACPC,GAAI,WAAe,EACnBY,OAxCF,SAAgBX,GACd,IAAIC,EAAQD,EAAKC,MACb4J,EAAW7J,EAAK6J,SAChB9I,EAAUf,EAAKe,QACf+I,EAAkB/I,EAAQgJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAkBjJ,EAAQkJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7C9K,EAASF,EAAUiB,EAAME,SAASxC,QAClCuM,EAAgB,GAAGjM,OAAOgC,EAAMiK,cAActM,UAAWqC,EAAMiK,cAAcvM,QAYjF,OAVIoM,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaC,iBAAiB,SAAUP,EAASQ,OAAQT,GAC3D,IAGEK,GACF/K,EAAOkL,iBAAiB,SAAUP,EAASQ,OAAQT,IAG9C,WACDG,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaG,oBAAoB,SAAUT,EAASQ,OAAQT,GAC9D,IAGEK,GACF/K,EAAOoL,oBAAoB,SAAUT,EAASQ,OAAQT,GAE1D,CACF,EASED,KAAM,CAAC,GC/CT,IAAIY,GAAO,CACTnN,KAAM,QACND,MAAO,OACPD,OAAQ,MACR+D,IAAK,UAEQ,SAASuJ,GAAqBxM,GAC3C,OAAOA,EAAUyM,QAAQ,0BAA0B,SAAUC,GAC3D,OAAOH,GAAKG,EACd,GACF,CCVA,IAAI,GAAO,CACTnN,MAAO,MACPC,IAAK,SAEQ,SAASmN,GAA8B3M,GACpD,OAAOA,EAAUyM,QAAQ,cAAc,SAAUC,GAC/C,OAAO,GAAKA,EACd,GACF,CCPe,SAASE,GAAgB3L,GACtC,IAAI6J,EAAM9J,EAAUC,GAGpB,MAAO,CACL4L,WAHe/B,EAAIgC,YAInBC,UAHcjC,EAAIkC,YAKtB,CCNe,SAASC,GAAoBpM,GAQ1C,OAAO+D,EAAsB8B,EAAmB7F,IAAUzB,KAAOwN,GAAgB/L,GAASgM,UAC5F,CCXe,SAASK,GAAerM,GAErC,IAAIsM,EAAoB,EAAiBtM,GACrCuM,EAAWD,EAAkBC,SAC7BC,EAAYF,EAAkBE,UAC9BC,EAAYH,EAAkBG,UAElC,MAAO,6BAA6B3I,KAAKyI,EAAWE,EAAYD,EAClE,CCLe,SAASE,GAAgBtM,GACtC,MAAI,CAAC,OAAQ,OAAQ,aAAawF,QAAQ7F,EAAYK,KAAU,EAEvDA,EAAKG,cAAcoM,KAGxBhM,EAAcP,IAASiM,GAAejM,GACjCA,EAGFsM,GAAgB1G,EAAc5F,GACvC,CCJe,SAASwM,GAAkB5M,EAAS6M,GACjD,IAAIC,OAES,IAATD,IACFA,EAAO,IAGT,IAAIvB,EAAeoB,GAAgB1M,GAC/B+M,EAASzB,KAAqE,OAAlDwB,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,MACpH1C,EAAM9J,EAAUmL,GAChB0B,EAASD,EAAS,CAAC9C,GAAK7K,OAAO6K,EAAIxF,gBAAkB,GAAI4H,GAAef,GAAgBA,EAAe,IAAMA,EAC7G2B,EAAcJ,EAAKzN,OAAO4N,GAC9B,OAAOD,EAASE,EAChBA,EAAY7N,OAAOwN,GAAkB5G,EAAcgH,IACrD,CCzBe,SAASE,GAAiBC,GACvC,OAAO1P,OAAOkE,OAAO,CAAC,EAAGwL,EAAM,CAC7B5O,KAAM4O,EAAKxI,EACXvC,IAAK+K,EAAKtI,EACVvG,MAAO6O,EAAKxI,EAAIwI,EAAK7I,MACrBjG,OAAQ8O,EAAKtI,EAAIsI,EAAK3I,QAE1B,CCqBA,SAAS4I,GAA2BpN,EAASqN,EAAgBlL,GAC3D,OAAOkL,IAAmBxO,EAAWqO,GCzBxB,SAAyBlN,EAASmC,GAC/C,IAAI8H,EAAM9J,EAAUH,GAChBsN,EAAOzH,EAAmB7F,GAC1ByE,EAAiBwF,EAAIxF,eACrBH,EAAQgJ,EAAKhF,YACb9D,EAAS8I,EAAKjF,aACd1D,EAAI,EACJE,EAAI,EAER,GAAIJ,EAAgB,CAClBH,EAAQG,EAAeH,MACvBE,EAASC,EAAeD,OACxB,IAAI+I,EAAiB1J,KAEjB0J,IAAmBA,GAA+B,UAAbpL,KACvCwC,EAAIF,EAAeG,WACnBC,EAAIJ,EAAeK,UAEvB,CAEA,MAAO,CACLR,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EAAIyH,GAAoBpM,GAC3B6E,EAAGA,EAEP,CDDwD2I,CAAgBxN,EAASmC,IAAa1B,EAAU4M,GAdxG,SAAoCrN,EAASmC,GAC3C,IAAIgL,EAAOpJ,EAAsB/D,GAAS,EAAoB,UAAbmC,GASjD,OARAgL,EAAK/K,IAAM+K,EAAK/K,IAAMpC,EAAQyN,UAC9BN,EAAK5O,KAAO4O,EAAK5O,KAAOyB,EAAQ0N,WAChCP,EAAK9O,OAAS8O,EAAK/K,IAAMpC,EAAQqI,aACjC8E,EAAK7O,MAAQ6O,EAAK5O,KAAOyB,EAAQsI,YACjC6E,EAAK7I,MAAQtE,EAAQsI,YACrB6E,EAAK3I,OAASxE,EAAQqI,aACtB8E,EAAKxI,EAAIwI,EAAK5O,KACd4O,EAAKtI,EAAIsI,EAAK/K,IACP+K,CACT,CAG0HQ,CAA2BN,EAAgBlL,GAAY+K,GEtBlK,SAAyBlN,GACtC,IAAI8M,EAEAQ,EAAOzH,EAAmB7F,GAC1B4N,EAAY7B,GAAgB/L,GAC5B2M,EAA0D,OAAlDG,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,KAChGrI,EAAQ,EAAIgJ,EAAKO,YAAaP,EAAKhF,YAAaqE,EAAOA,EAAKkB,YAAc,EAAGlB,EAAOA,EAAKrE,YAAc,GACvG9D,EAAS,EAAI8I,EAAKQ,aAAcR,EAAKjF,aAAcsE,EAAOA,EAAKmB,aAAe,EAAGnB,EAAOA,EAAKtE,aAAe,GAC5G1D,GAAKiJ,EAAU5B,WAAaI,GAAoBpM,GAChD6E,GAAK+I,EAAU1B,UAMnB,MAJiD,QAA7C,EAAiBS,GAAQW,GAAMS,YACjCpJ,GAAK,EAAI2I,EAAKhF,YAAaqE,EAAOA,EAAKrE,YAAc,GAAKhE,GAGrD,CACLA,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EACHE,EAAGA,EAEP,CFCkMmJ,CAAgBnI,EAAmB7F,IACrO,CG1Be,SAASiO,GAAe9M,GACrC,IAOIkI,EAPAtK,EAAYoC,EAAKpC,UACjBiB,EAAUmB,EAAKnB,QACfb,EAAYgC,EAAKhC,UACjBqI,EAAgBrI,EAAYuD,EAAiBvD,GAAa,KAC1DiK,EAAYjK,EAAY4J,EAAa5J,GAAa,KAClD+O,EAAUnP,EAAU4F,EAAI5F,EAAUuF,MAAQ,EAAItE,EAAQsE,MAAQ,EAC9D6J,EAAUpP,EAAU8F,EAAI9F,EAAUyF,OAAS,EAAIxE,EAAQwE,OAAS,EAGpE,OAAQgD,GACN,KAAK,EACH6B,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI7E,EAAQwE,QAE3B,MAEF,KAAKnG,EACHgL,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI9F,EAAUyF,QAE7B,MAEF,KAAKlG,EACH+K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI5F,EAAUuF,MAC3BO,EAAGsJ,GAEL,MAEF,KAAK5P,EACH8K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI3E,EAAQsE,MACzBO,EAAGsJ,GAEL,MAEF,QACE9E,EAAU,CACR1E,EAAG5F,EAAU4F,EACbE,EAAG9F,EAAU8F,GAInB,IAAIuJ,EAAW5G,EAAgBV,EAAyBU,GAAiB,KAEzE,GAAgB,MAAZ4G,EAAkB,CACpB,IAAI1G,EAAmB,MAAb0G,EAAmB,SAAW,QAExC,OAAQhF,GACN,KAAK1K,EACH2K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAC7E,MAEF,KAAK/I,EACH0K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAKnF,CAEA,OAAO2B,CACT,CC3De,SAASgF,GAAejN,EAAOc,QAC5B,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACXqM,EAAqBD,EAASnP,UAC9BA,OAAmC,IAAvBoP,EAAgCnN,EAAMjC,UAAYoP,EAC9DC,EAAoBF,EAASnM,SAC7BA,OAAiC,IAAtBqM,EAA+BpN,EAAMe,SAAWqM,EAC3DC,EAAoBH,EAASI,SAC7BA,OAAiC,IAAtBD,EAA+B7P,EAAkB6P,EAC5DE,EAAwBL,EAASM,aACjCA,OAAyC,IAA1BD,EAAmC9P,EAAW8P,EAC7DE,EAAwBP,EAASQ,eACjCA,OAA2C,IAA1BD,EAAmC/P,EAAS+P,EAC7DE,EAAuBT,EAASU,YAChCA,OAAuC,IAAzBD,GAA0CA,EACxDE,EAAmBX,EAAS3G,QAC5BA,OAA+B,IAArBsH,EAA8B,EAAIA,EAC5ChI,EAAgBD,EAAsC,iBAAZW,EAAuBA,EAAUT,EAAgBS,EAASlJ,IACpGyQ,EAAaJ,IAAmBhQ,EAASC,EAAYD,EACrDqK,EAAa/H,EAAMwG,MAAM9I,OACzBkB,EAAUoB,EAAME,SAAS0N,EAAcE,EAAaJ,GACpDK,EJkBS,SAAyBnP,EAAS0O,EAAUE,EAAczM,GACvE,IAAIiN,EAAmC,oBAAbV,EAlB5B,SAA4B1O,GAC1B,IAAIpB,EAAkBgO,GAAkB5G,EAAchG,IAElDqP,EADoB,CAAC,WAAY,SAASzJ,QAAQ,EAAiB5F,GAASiC,WAAa,GACnDtB,EAAcX,GAAWoG,EAAgBpG,GAAWA,EAE9F,OAAKS,EAAU4O,GAKRzQ,EAAgBgI,QAAO,SAAUyG,GACtC,OAAO5M,EAAU4M,IAAmBpI,EAASoI,EAAgBgC,IAAmD,SAAhCtP,EAAYsN,EAC9F,IANS,EAOX,CAK6DiC,CAAmBtP,GAAW,GAAGZ,OAAOsP,GAC/F9P,EAAkB,GAAGQ,OAAOgQ,EAAqB,CAACR,IAClDW,EAAsB3Q,EAAgB,GACtC4Q,EAAe5Q,EAAgBK,QAAO,SAAUwQ,EAASpC,GAC3D,IAAIF,EAAOC,GAA2BpN,EAASqN,EAAgBlL,GAK/D,OAJAsN,EAAQrN,IAAM,EAAI+K,EAAK/K,IAAKqN,EAAQrN,KACpCqN,EAAQnR,MAAQ,EAAI6O,EAAK7O,MAAOmR,EAAQnR,OACxCmR,EAAQpR,OAAS,EAAI8O,EAAK9O,OAAQoR,EAAQpR,QAC1CoR,EAAQlR,KAAO,EAAI4O,EAAK5O,KAAMkR,EAAQlR,MAC/BkR,CACT,GAAGrC,GAA2BpN,EAASuP,EAAqBpN,IAK5D,OAJAqN,EAAalL,MAAQkL,EAAalR,MAAQkR,EAAajR,KACvDiR,EAAahL,OAASgL,EAAanR,OAASmR,EAAapN,IACzDoN,EAAa7K,EAAI6K,EAAajR,KAC9BiR,EAAa3K,EAAI2K,EAAapN,IACvBoN,CACT,CInC2BE,CAAgBjP,EAAUT,GAAWA,EAAUA,EAAQ2P,gBAAkB9J,EAAmBzE,EAAME,SAASxC,QAAS4P,EAAUE,EAAczM,GACjKyN,EAAsB7L,EAAsB3C,EAAME,SAASvC,WAC3DuI,EAAgB2G,GAAe,CACjClP,UAAW6Q,EACX5P,QAASmJ,EACThH,SAAU,WACVhD,UAAWA,IAET0Q,EAAmB3C,GAAiBzP,OAAOkE,OAAO,CAAC,EAAGwH,EAAY7B,IAClEwI,EAAoBhB,IAAmBhQ,EAAS+Q,EAAmBD,EAGnEG,EAAkB,CACpB3N,IAAK+M,EAAmB/M,IAAM0N,EAAkB1N,IAAM6E,EAAc7E,IACpE/D,OAAQyR,EAAkBzR,OAAS8Q,EAAmB9Q,OAAS4I,EAAc5I,OAC7EE,KAAM4Q,EAAmB5Q,KAAOuR,EAAkBvR,KAAO0I,EAAc1I,KACvED,MAAOwR,EAAkBxR,MAAQ6Q,EAAmB7Q,MAAQ2I,EAAc3I,OAExE0R,EAAa5O,EAAMmG,cAAckB,OAErC,GAAIqG,IAAmBhQ,GAAUkR,EAAY,CAC3C,IAAIvH,EAASuH,EAAW7Q,GACxB1B,OAAO4D,KAAK0O,GAAiBxO,SAAQ,SAAUhE,GAC7C,IAAI0S,EAAW,CAAC3R,EAAOD,GAAQuH,QAAQrI,IAAQ,EAAI,GAAK,EACpDkK,EAAO,CAAC,EAAKpJ,GAAQuH,QAAQrI,IAAQ,EAAI,IAAM,IACnDwS,EAAgBxS,IAAQkL,EAAOhB,GAAQwI,CACzC,GACF,CAEA,OAAOF,CACT,CCyEA,UACEhP,KAAM,OACNC,SAAS,EACTC,MAAO,OACPC,GA5HF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KAEhB,IAAIK,EAAMmG,cAAcxG,GAAMmP,MAA9B,CAoCA,IAhCA,IAAIC,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAqCA,EACpDG,EAA8BtO,EAAQuO,mBACtC9I,EAAUzF,EAAQyF,QAClB+G,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtB0B,EAAwBxO,EAAQyO,eAChCA,OAA2C,IAA1BD,GAA0CA,EAC3DE,EAAwB1O,EAAQ0O,sBAChCC,EAAqBzP,EAAMc,QAAQ/C,UACnCqI,EAAgB9E,EAAiBmO,GAEjCJ,EAAqBD,IADHhJ,IAAkBqJ,GACqCF,EAjC/E,SAAuCxR,GACrC,GAAIuD,EAAiBvD,KAAeX,EAClC,MAAO,GAGT,IAAIsS,EAAoBnF,GAAqBxM,GAC7C,MAAO,CAAC2M,GAA8B3M,GAAY2R,EAAmBhF,GAA8BgF,GACrG,CA0B6IC,CAA8BF,GAA3E,CAAClF,GAAqBkF,KAChHG,EAAa,CAACH,GAAoBzR,OAAOqR,GAAoBxR,QAAO,SAAUC,EAAKC,GACrF,OAAOD,EAAIE,OAAOsD,EAAiBvD,KAAeX,ECvCvC,SAA8B4C,EAAOc,QAClC,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACX/C,EAAYmP,EAASnP,UACrBuP,EAAWJ,EAASI,SACpBE,EAAeN,EAASM,aACxBjH,EAAU2G,EAAS3G,QACnBgJ,EAAiBrC,EAASqC,eAC1BM,EAAwB3C,EAASsC,sBACjCA,OAAkD,IAA1BK,EAAmC,EAAgBA,EAC3E7H,EAAYL,EAAa5J,GACzB6R,EAAa5H,EAAYuH,EAAiB3R,EAAsBA,EAAoB4H,QAAO,SAAUzH,GACvG,OAAO4J,EAAa5J,KAAeiK,CACrC,IAAK3K,EACDyS,EAAoBF,EAAWpK,QAAO,SAAUzH,GAClD,OAAOyR,EAAsBhL,QAAQzG,IAAc,CACrD,IAEiC,IAA7B+R,EAAkBC,SACpBD,EAAoBF,GAItB,IAAII,EAAYF,EAAkBjS,QAAO,SAAUC,EAAKC,GAOtD,OANAD,EAAIC,GAAakP,GAAejN,EAAO,CACrCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,IACRjF,EAAiBvD,IACbD,CACT,GAAG,CAAC,GACJ,OAAOzB,OAAO4D,KAAK+P,GAAWC,MAAK,SAAUC,EAAGC,GAC9C,OAAOH,EAAUE,GAAKF,EAAUG,EAClC,GACF,CDC6DC,CAAqBpQ,EAAO,CACnFjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTgJ,eAAgBA,EAChBC,sBAAuBA,IACpBzR,EACP,GAAG,IACCsS,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzB4S,EAAY,IAAIC,IAChBC,GAAqB,EACrBC,EAAwBb,EAAW,GAE9Bc,EAAI,EAAGA,EAAId,EAAWG,OAAQW,IAAK,CAC1C,IAAI3S,EAAY6R,EAAWc,GAEvBC,EAAiBrP,EAAiBvD,GAElC6S,EAAmBjJ,EAAa5J,KAAeT,EAC/CuT,EAAa,CAAC,EAAK5T,GAAQuH,QAAQmM,IAAmB,EACtDrK,EAAMuK,EAAa,QAAU,SAC7B1F,EAAW8B,GAAejN,EAAO,CACnCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdI,YAAaA,EACbrH,QAASA,IAEPuK,EAAoBD,EAAaD,EAAmB1T,EAAQC,EAAOyT,EAAmB3T,EAAS,EAE/FoT,EAAc/J,GAAOyB,EAAWzB,KAClCwK,EAAoBvG,GAAqBuG,IAG3C,IAAIC,EAAmBxG,GAAqBuG,GACxCE,EAAS,GAUb,GARIhC,GACFgC,EAAOC,KAAK9F,EAASwF,IAAmB,GAGtCxB,GACF6B,EAAOC,KAAK9F,EAAS2F,IAAsB,EAAG3F,EAAS4F,IAAqB,GAG1EC,EAAOE,OAAM,SAAUC,GACzB,OAAOA,CACT,IAAI,CACFV,EAAwB1S,EACxByS,GAAqB,EACrB,KACF,CAEAF,EAAUc,IAAIrT,EAAWiT,EAC3B,CAEA,GAAIR,EAqBF,IAnBA,IAEIa,EAAQ,SAAeC,GACzB,IAAIC,EAAmB3B,EAAW4B,MAAK,SAAUzT,GAC/C,IAAIiT,EAASV,EAAU9T,IAAIuB,GAE3B,GAAIiT,EACF,OAAOA,EAAOS,MAAM,EAAGH,GAAIJ,OAAM,SAAUC,GACzC,OAAOA,CACT,GAEJ,IAEA,GAAII,EAEF,OADAd,EAAwBc,EACjB,OAEX,EAESD,EAnBY/B,EAAiB,EAAI,EAmBZ+B,EAAK,GAGpB,UAFFD,EAAMC,GADmBA,KAOpCtR,EAAMjC,YAAc0S,IACtBzQ,EAAMmG,cAAcxG,GAAMmP,OAAQ,EAClC9O,EAAMjC,UAAY0S,EAClBzQ,EAAM0R,OAAQ,EA5GhB,CA8GF,EAQEhK,iBAAkB,CAAC,UACnBgC,KAAM,CACJoF,OAAO,IE7IX,SAAS6C,GAAexG,EAAUY,EAAM6F,GAQtC,YAPyB,IAArBA,IACFA,EAAmB,CACjBrO,EAAG,EACHE,EAAG,IAIA,CACLzC,IAAKmK,EAASnK,IAAM+K,EAAK3I,OAASwO,EAAiBnO,EACnDvG,MAAOiO,EAASjO,MAAQ6O,EAAK7I,MAAQ0O,EAAiBrO,EACtDtG,OAAQkO,EAASlO,OAAS8O,EAAK3I,OAASwO,EAAiBnO,EACzDtG,KAAMgO,EAAShO,KAAO4O,EAAK7I,MAAQ0O,EAAiBrO,EAExD,CAEA,SAASsO,GAAsB1G,GAC7B,MAAO,CAAC,EAAKjO,EAAOD,EAAQE,GAAM2U,MAAK,SAAUC,GAC/C,OAAO5G,EAAS4G,IAAS,CAC3B,GACF,CA+BA,UACEpS,KAAM,OACNC,SAAS,EACTC,MAAO,OACP6H,iBAAkB,CAAC,mBACnB5H,GAlCF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZ0Q,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBkU,EAAmB5R,EAAMmG,cAAc6L,gBACvCC,EAAoBhF,GAAejN,EAAO,CAC5C0N,eAAgB,cAEdwE,EAAoBjF,GAAejN,EAAO,CAC5C4N,aAAa,IAEXuE,EAA2BR,GAAeM,EAAmB5B,GAC7D+B,EAAsBT,GAAeO,EAAmBnK,EAAY6J,GACpES,EAAoBR,GAAsBM,GAC1CG,EAAmBT,GAAsBO,GAC7CpS,EAAMmG,cAAcxG,GAAQ,CAC1BwS,yBAA0BA,EAC1BC,oBAAqBA,EACrBC,kBAAmBA,EACnBC,iBAAkBA,GAEpBtS,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,+BAAgC2U,EAChC,sBAAuBC,GAE3B,GCJA,IACE3S,KAAM,SACNC,SAAS,EACTC,MAAO,OACPwB,SAAU,CAAC,iBACXvB,GA5BF,SAAgBa,GACd,IAAIX,EAAQW,EAAMX,MACdc,EAAUH,EAAMG,QAChBnB,EAAOgB,EAAMhB,KACb4S,EAAkBzR,EAAQuG,OAC1BA,OAA6B,IAApBkL,EAA6B,CAAC,EAAG,GAAKA,EAC/C7I,EAAO,EAAW7L,QAAO,SAAUC,EAAKC,GAE1C,OADAD,EAAIC,GA5BD,SAAiCA,EAAWyI,EAAOa,GACxD,IAAIjB,EAAgB9E,EAAiBvD,GACjCyU,EAAiB,CAACrV,EAAM,GAAKqH,QAAQ4B,IAAkB,GAAK,EAAI,EAEhErG,EAAyB,mBAAXsH,EAAwBA,EAAOhL,OAAOkE,OAAO,CAAC,EAAGiG,EAAO,CACxEzI,UAAWA,KACPsJ,EACFoL,EAAW1S,EAAK,GAChB2S,EAAW3S,EAAK,GAIpB,OAFA0S,EAAWA,GAAY,EACvBC,GAAYA,GAAY,GAAKF,EACtB,CAACrV,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAAI,CACjD7C,EAAGmP,EACHjP,EAAGgP,GACD,CACFlP,EAAGkP,EACHhP,EAAGiP,EAEP,CASqBC,CAAwB5U,EAAWiC,EAAMwG,MAAOa,GAC1DvJ,CACT,GAAG,CAAC,GACA8U,EAAwBlJ,EAAK1J,EAAMjC,WACnCwF,EAAIqP,EAAsBrP,EAC1BE,EAAImP,EAAsBnP,EAEW,MAArCzD,EAAMmG,cAAcD,gBACtBlG,EAAMmG,cAAcD,cAAc3C,GAAKA,EACvCvD,EAAMmG,cAAcD,cAAczC,GAAKA,GAGzCzD,EAAMmG,cAAcxG,GAAQ+J,CAC9B,GC1BA,IACE/J,KAAM,gBACNC,SAAS,EACTC,MAAO,OACPC,GApBF,SAAuBC,GACrB,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KAKhBK,EAAMmG,cAAcxG,GAAQkN,GAAe,CACzClP,UAAWqC,EAAMwG,MAAM7I,UACvBiB,QAASoB,EAAMwG,MAAM9I,OACrBqD,SAAU,WACVhD,UAAWiC,EAAMjC,WAErB,EAQE2L,KAAM,CAAC,GCgHT,IACE/J,KAAM,kBACNC,SAAS,EACTC,MAAO,OACPC,GA/HF,SAAyBC,GACvB,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KACZoP,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAsCA,EACrD3B,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtBrH,EAAUzF,EAAQyF,QAClBsM,EAAkB/R,EAAQgS,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAwBjS,EAAQkS,aAChCA,OAAyC,IAA1BD,EAAmC,EAAIA,EACtD5H,EAAW8B,GAAejN,EAAO,CACnCsN,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTqH,YAAaA,IAEXxH,EAAgB9E,EAAiBtB,EAAMjC,WACvCiK,EAAYL,EAAa3H,EAAMjC,WAC/BkV,GAAmBjL,EACnBgF,EAAWtH,EAAyBU,GACpC8I,ECrCY,MDqCSlC,ECrCH,IAAM,IDsCxB9G,EAAgBlG,EAAMmG,cAAcD,cACpCmK,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBwV,EAA4C,mBAAjBF,EAA8BA,EAAa3W,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CACvGzI,UAAWiC,EAAMjC,aACbiV,EACFG,EAA2D,iBAAtBD,EAAiC,CACxElG,SAAUkG,EACVhE,QAASgE,GACP7W,OAAOkE,OAAO,CAChByM,SAAU,EACVkC,QAAS,GACRgE,GACCE,EAAsBpT,EAAMmG,cAAckB,OAASrH,EAAMmG,cAAckB,OAAOrH,EAAMjC,WAAa,KACjG2L,EAAO,CACTnG,EAAG,EACHE,EAAG,GAGL,GAAKyC,EAAL,CAIA,GAAI8I,EAAe,CACjB,IAAIqE,EAEAC,EAAwB,MAAbtG,EAAmB,EAAM7P,EACpCoW,EAAuB,MAAbvG,EAAmB/P,EAASC,EACtCoJ,EAAmB,MAAb0G,EAAmB,SAAW,QACpC3F,EAASnB,EAAc8G,GACvBtL,EAAM2F,EAAS8D,EAASmI,GACxB7R,EAAM4F,EAAS8D,EAASoI,GACxBC,EAAWV,GAAU/K,EAAWzB,GAAO,EAAI,EAC3CmN,EAASzL,IAAc1K,EAAQ+S,EAAc/J,GAAOyB,EAAWzB,GAC/DoN,EAAS1L,IAAc1K,GAASyK,EAAWzB,IAAQ+J,EAAc/J,GAGjEL,EAAejG,EAAME,SAASgB,MAC9BwF,EAAYoM,GAAU7M,EAAetC,EAAcsC,GAAgB,CACrE/C,MAAO,EACPE,OAAQ,GAENuQ,GAAqB3T,EAAMmG,cAAc,oBAAsBnG,EAAMmG,cAAc,oBAAoBI,QxBhFtG,CACLvF,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GwB6EFyW,GAAkBD,GAAmBL,GACrCO,GAAkBF,GAAmBJ,GAMrCO,GAAWnO,EAAO,EAAG0K,EAAc/J,GAAMI,EAAUJ,IACnDyN,GAAYd,EAAkB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWF,GAAkBT,EAA4BnG,SAAWyG,EAASK,GAAWF,GAAkBT,EAA4BnG,SACxMgH,GAAYf,GAAmB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWD,GAAkBV,EAA4BnG,SAAW0G,EAASI,GAAWD,GAAkBV,EAA4BnG,SACzMjG,GAAoB/G,EAAME,SAASgB,OAAS8D,EAAgBhF,EAAME,SAASgB,OAC3E+S,GAAelN,GAAiC,MAAbiG,EAAmBjG,GAAkBsF,WAAa,EAAItF,GAAkBuF,YAAc,EAAI,EAC7H4H,GAAwH,OAAjGb,EAA+C,MAAvBD,OAA8B,EAASA,EAAoBpG,IAAqBqG,EAAwB,EAEvJc,GAAY9M,EAAS2M,GAAYE,GACjCE,GAAkBzO,EAAOmN,EAAS,EAAQpR,EAF9B2F,EAAS0M,GAAYG,GAAsBD,IAEKvS,EAAK2F,EAAQyL,EAAS,EAAQrR,EAAK0S,IAAa1S,GAChHyE,EAAc8G,GAAYoH,GAC1B1K,EAAKsD,GAAYoH,GAAkB/M,CACrC,CAEA,GAAI8H,EAAc,CAChB,IAAIkF,GAEAC,GAAyB,MAAbtH,EAAmB,EAAM7P,EAErCoX,GAAwB,MAAbvH,EAAmB/P,EAASC,EAEvCsX,GAAUtO,EAAcgJ,GAExBuF,GAAmB,MAAZvF,EAAkB,SAAW,QAEpCwF,GAAOF,GAAUrJ,EAASmJ,IAE1BK,GAAOH,GAAUrJ,EAASoJ,IAE1BK,IAAuD,IAAxC,CAAC,EAAKzX,GAAMqH,QAAQ4B,GAEnCyO,GAAyH,OAAjGR,GAAgD,MAAvBjB,OAA8B,EAASA,EAAoBlE,IAAoBmF,GAAyB,EAEzJS,GAAaF,GAAeF,GAAOF,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAEzI6F,GAAaH,GAAeJ,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAAUyF,GAE5IK,GAAmBlC,GAAU8B,G1BzH9B,SAAwBlT,EAAK1E,EAAOyE,GACzC,IAAIwT,EAAItP,EAAOjE,EAAK1E,EAAOyE,GAC3B,OAAOwT,EAAIxT,EAAMA,EAAMwT,CACzB,C0BsHoDC,CAAeJ,GAAYN,GAASO,IAAcpP,EAAOmN,EAASgC,GAAaJ,GAAMF,GAAS1B,EAASiC,GAAaJ,IAEpKzO,EAAcgJ,GAAW8F,GACzBtL,EAAKwF,GAAW8F,GAAmBR,EACrC,CAEAxU,EAAMmG,cAAcxG,GAAQ+J,CAvE5B,CAwEF,EAQEhC,iBAAkB,CAAC,WE1HN,SAASyN,GAAiBC,EAAyBrQ,EAAcsD,QAC9D,IAAZA,IACFA,GAAU,GAGZ,ICnBoCrJ,ECJOJ,EFuBvCyW,EAA0B9V,EAAcwF,GACxCuQ,EAAuB/V,EAAcwF,IAf3C,SAAyBnG,GACvB,IAAImN,EAAOnN,EAAQ+D,wBACfI,EAASpB,EAAMoK,EAAK7I,OAAStE,EAAQqE,aAAe,EACpDD,EAASrB,EAAMoK,EAAK3I,QAAUxE,EAAQuE,cAAgB,EAC1D,OAAkB,IAAXJ,GAA2B,IAAXC,CACzB,CAU4DuS,CAAgBxQ,GACtEJ,EAAkBF,EAAmBM,GACrCgH,EAAOpJ,EAAsByS,EAAyBE,EAAsBjN,GAC5EyB,EAAS,CACXc,WAAY,EACZE,UAAW,GAET7C,EAAU,CACZ1E,EAAG,EACHE,EAAG,GAkBL,OAfI4R,IAA4BA,IAA4BhN,MACxB,SAA9B1J,EAAYoG,IAChBkG,GAAetG,MACbmF,GCnCgC9K,EDmCT+F,KClCdhG,EAAUC,IAAUO,EAAcP,GCJxC,CACL4L,YAFyChM,EDQbI,GCNR4L,WACpBE,UAAWlM,EAAQkM,WDGZH,GAAgB3L,IDoCnBO,EAAcwF,KAChBkD,EAAUtF,EAAsBoC,GAAc,IACtCxB,GAAKwB,EAAauH,WAC1BrE,EAAQxE,GAAKsB,EAAasH,WACjB1H,IACTsD,EAAQ1E,EAAIyH,GAAoBrG,KAI7B,CACLpB,EAAGwI,EAAK5O,KAAO2M,EAAOc,WAAa3C,EAAQ1E,EAC3CE,EAAGsI,EAAK/K,IAAM8I,EAAOgB,UAAY7C,EAAQxE,EACzCP,MAAO6I,EAAK7I,MACZE,OAAQ2I,EAAK3I,OAEjB,CGvDA,SAASoS,GAAMC,GACb,IAAItT,EAAM,IAAIoO,IACVmF,EAAU,IAAIC,IACdC,EAAS,GAKb,SAAS3F,EAAK4F,GACZH,EAAQI,IAAID,EAASlW,MACN,GAAG3B,OAAO6X,EAASxU,UAAY,GAAIwU,EAASnO,kBAAoB,IACtEvH,SAAQ,SAAU4V,GACzB,IAAKL,EAAQM,IAAID,GAAM,CACrB,IAAIE,EAAc9T,EAAI3F,IAAIuZ,GAEtBE,GACFhG,EAAKgG,EAET,CACF,IACAL,EAAO3E,KAAK4E,EACd,CAQA,OAzBAJ,EAAUtV,SAAQ,SAAU0V,GAC1B1T,EAAIiP,IAAIyE,EAASlW,KAAMkW,EACzB,IAiBAJ,EAAUtV,SAAQ,SAAU0V,GACrBH,EAAQM,IAAIH,EAASlW,OAExBsQ,EAAK4F,EAET,IACOD,CACT,CCvBA,IAAIM,GAAkB,CACpBnY,UAAW,SACX0X,UAAW,GACX1U,SAAU,YAGZ,SAASoV,KACP,IAAK,IAAI1B,EAAO2B,UAAUrG,OAAQsG,EAAO,IAAIpU,MAAMwS,GAAO6B,EAAO,EAAGA,EAAO7B,EAAM6B,IAC/ED,EAAKC,GAAQF,UAAUE,GAGzB,OAAQD,EAAKvE,MAAK,SAAUlT,GAC1B,QAASA,GAAoD,mBAAlCA,EAAQ+D,sBACrC,GACF,CAEO,SAAS4T,GAAgBC,QACL,IAArBA,IACFA,EAAmB,CAAC,GAGtB,IAAIC,EAAoBD,EACpBE,EAAwBD,EAAkBE,iBAC1CA,OAA6C,IAA1BD,EAAmC,GAAKA,EAC3DE,EAAyBH,EAAkBI,eAC3CA,OAA4C,IAA3BD,EAAoCV,GAAkBU,EAC3E,OAAO,SAAsBjZ,EAAWD,EAAQoD,QAC9B,IAAZA,IACFA,EAAU+V,GAGZ,ICxC6B/W,EAC3BgX,EDuCE9W,EAAQ,CACVjC,UAAW,SACXgZ,iBAAkB,GAClBjW,QAASzE,OAAOkE,OAAO,CAAC,EAAG2V,GAAiBW,GAC5C1Q,cAAe,CAAC,EAChBjG,SAAU,CACRvC,UAAWA,EACXD,OAAQA,GAEV4C,WAAY,CAAC,EACbD,OAAQ,CAAC,GAEP2W,EAAmB,GACnBC,GAAc,EACdrN,EAAW,CACb5J,MAAOA,EACPkX,WAAY,SAAoBC,GAC9B,IAAIrW,EAAsC,mBAArBqW,EAAkCA,EAAiBnX,EAAMc,SAAWqW,EACzFC,IACApX,EAAMc,QAAUzE,OAAOkE,OAAO,CAAC,EAAGsW,EAAgB7W,EAAMc,QAASA,GACjEd,EAAMiK,cAAgB,CACpBtM,UAAW0B,EAAU1B,GAAa6N,GAAkB7N,GAAaA,EAAU4Q,eAAiB/C,GAAkB7N,EAAU4Q,gBAAkB,GAC1I7Q,OAAQ8N,GAAkB9N,IAI5B,IElE4B+X,EAC9B4B,EFiEMN,EDhCG,SAAwBtB,GAErC,IAAIsB,EAAmBvB,GAAMC,GAE7B,OAAO/W,EAAeb,QAAO,SAAUC,EAAK+B,GAC1C,OAAO/B,EAAIE,OAAO+Y,EAAiBvR,QAAO,SAAUqQ,GAClD,OAAOA,EAAShW,QAAUA,CAC5B,IACF,GAAG,GACL,CCuB+ByX,EElEK7B,EFkEsB,GAAGzX,OAAO2Y,EAAkB3W,EAAMc,QAAQ2U,WEjE9F4B,EAAS5B,EAAU5X,QAAO,SAAUwZ,EAAQE,GAC9C,IAAIC,EAAWH,EAAOE,EAAQ5X,MAK9B,OAJA0X,EAAOE,EAAQ5X,MAAQ6X,EAAWnb,OAAOkE,OAAO,CAAC,EAAGiX,EAAUD,EAAS,CACrEzW,QAASzE,OAAOkE,OAAO,CAAC,EAAGiX,EAAS1W,QAASyW,EAAQzW,SACrD4I,KAAMrN,OAAOkE,OAAO,CAAC,EAAGiX,EAAS9N,KAAM6N,EAAQ7N,QAC5C6N,EACEF,CACT,GAAG,CAAC,GAEGhb,OAAO4D,KAAKoX,GAAQlV,KAAI,SAAUhG,GACvC,OAAOkb,EAAOlb,EAChB,MF4DM,OAJA6D,EAAM+W,iBAAmBA,EAAiBvR,QAAO,SAAUiS,GACzD,OAAOA,EAAE7X,OACX,IA+FFI,EAAM+W,iBAAiB5W,SAAQ,SAAUJ,GACvC,IAAIJ,EAAOI,EAAKJ,KACZ+X,EAAe3X,EAAKe,QACpBA,OAA2B,IAAjB4W,EAA0B,CAAC,EAAIA,EACzChX,EAASX,EAAKW,OAElB,GAAsB,mBAAXA,EAAuB,CAChC,IAAIiX,EAAYjX,EAAO,CACrBV,MAAOA,EACPL,KAAMA,EACNiK,SAAUA,EACV9I,QAASA,IAKXkW,EAAiB/F,KAAK0G,GAFT,WAAmB,EAGlC,CACF,IA/GS/N,EAASQ,QAClB,EAMAwN,YAAa,WACX,IAAIX,EAAJ,CAIA,IAAIY,EAAkB7X,EAAME,SACxBvC,EAAYka,EAAgBla,UAC5BD,EAASma,EAAgBna,OAG7B,GAAKyY,GAAiBxY,EAAWD,GAAjC,CAKAsC,EAAMwG,MAAQ,CACZ7I,UAAWwX,GAAiBxX,EAAWqH,EAAgBtH,GAAoC,UAA3BsC,EAAMc,QAAQC,UAC9ErD,OAAQiG,EAAcjG,IAOxBsC,EAAM0R,OAAQ,EACd1R,EAAMjC,UAAYiC,EAAMc,QAAQ/C,UAKhCiC,EAAM+W,iBAAiB5W,SAAQ,SAAU0V,GACvC,OAAO7V,EAAMmG,cAAc0P,EAASlW,MAAQtD,OAAOkE,OAAO,CAAC,EAAGsV,EAASnM,KACzE,IAEA,IAAK,IAAIoO,EAAQ,EAAGA,EAAQ9X,EAAM+W,iBAAiBhH,OAAQ+H,IACzD,IAAoB,IAAhB9X,EAAM0R,MAAV,CAMA,IAAIqG,EAAwB/X,EAAM+W,iBAAiBe,GAC/ChY,EAAKiY,EAAsBjY,GAC3BkY,EAAyBD,EAAsBjX,QAC/CoM,OAAsC,IAA3B8K,EAAoC,CAAC,EAAIA,EACpDrY,EAAOoY,EAAsBpY,KAEf,mBAAPG,IACTE,EAAQF,EAAG,CACTE,MAAOA,EACPc,QAASoM,EACTvN,KAAMA,EACNiK,SAAUA,KACN5J,EAdR,MAHEA,EAAM0R,OAAQ,EACdoG,GAAS,CAzBb,CATA,CAqDF,EAGA1N,QC1I2BtK,ED0IV,WACf,OAAO,IAAImY,SAAQ,SAAUC,GAC3BtO,EAASgO,cACTM,EAAQlY,EACV,GACF,EC7IG,WAUL,OATK8W,IACHA,EAAU,IAAImB,SAAQ,SAAUC,GAC9BD,QAAQC,UAAUC,MAAK,WACrBrB,OAAUsB,EACVF,EAAQpY,IACV,GACF,KAGKgX,CACT,GDmIIuB,QAAS,WACPjB,IACAH,GAAc,CAChB,GAGF,IAAKd,GAAiBxY,EAAWD,GAC/B,OAAOkM,EAmCT,SAASwN,IACPJ,EAAiB7W,SAAQ,SAAUL,GACjC,OAAOA,GACT,IACAkX,EAAmB,EACrB,CAEA,OAvCApN,EAASsN,WAAWpW,GAASqX,MAAK,SAAUnY,IACrCiX,GAAenW,EAAQwX,eAC1BxX,EAAQwX,cAActY,EAE1B,IAmCO4J,CACT,CACF,CACO,IAAI2O,GAA4BhC,KGzLnC,GAA4BA,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,EAAa,GAAQ,GAAM,GAAiB,EAAO,MCJrH,GAA4BjC,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,KCatE,MAAMC,GAAa,IAAIlI,IACjBmI,GAAO,CACX,GAAAtH,CAAIxS,EAASzC,EAAKyN,GACX6O,GAAWzC,IAAIpX,IAClB6Z,GAAWrH,IAAIxS,EAAS,IAAI2R,KAE9B,MAAMoI,EAAcF,GAAWjc,IAAIoC,GAI9B+Z,EAAY3C,IAAI7Z,IAA6B,IAArBwc,EAAYC,KAKzCD,EAAYvH,IAAIjV,EAAKyN,GAHnBiP,QAAQC,MAAM,+EAA+E7W,MAAM8W,KAAKJ,EAAY1Y,QAAQ,MAIhI,EACAzD,IAAG,CAACoC,EAASzC,IACPsc,GAAWzC,IAAIpX,IACV6Z,GAAWjc,IAAIoC,GAASpC,IAAIL,IAE9B,KAET,MAAA6c,CAAOpa,EAASzC,GACd,IAAKsc,GAAWzC,IAAIpX,GAClB,OAEF,MAAM+Z,EAAcF,GAAWjc,IAAIoC,GACnC+Z,EAAYM,OAAO9c,GAGM,IAArBwc,EAAYC,MACdH,GAAWQ,OAAOra,EAEtB,GAYIsa,GAAiB,gBAOjBC,GAAgBC,IAChBA,GAAYna,OAAOoa,KAAOpa,OAAOoa,IAAIC,SAEvCF,EAAWA,EAAS5O,QAAQ,iBAAiB,CAAC+O,EAAOC,IAAO,IAAIH,IAAIC,OAAOE,QAEtEJ,GA4CHK,GAAuB7a,IAC3BA,EAAQ8a,cAAc,IAAIC,MAAMT,IAAgB,EAE5C,GAAYU,MACXA,GAA4B,iBAAXA,UAGO,IAAlBA,EAAOC,SAChBD,EAASA,EAAO,SAEgB,IAApBA,EAAOE,UAEjBC,GAAaH,GAEb,GAAUA,GACLA,EAAOC,OAASD,EAAO,GAAKA,EAEf,iBAAXA,GAAuBA,EAAO7J,OAAS,EACzCrL,SAAS+C,cAAc0R,GAAcS,IAEvC,KAEHI,GAAYpb,IAChB,IAAK,GAAUA,IAAgD,IAApCA,EAAQqb,iBAAiBlK,OAClD,OAAO,EAET,MAAMmK,EAAgF,YAA7D5V,iBAAiB1F,GAASub,iBAAiB,cAE9DC,EAAgBxb,EAAQyb,QAAQ,uBACtC,IAAKD,EACH,OAAOF,EAET,GAAIE,IAAkBxb,EAAS,CAC7B,MAAM0b,EAAU1b,EAAQyb,QAAQ,WAChC,GAAIC,GAAWA,EAAQlW,aAAegW,EACpC,OAAO,EAET,GAAgB,OAAZE,EACF,OAAO,CAEX,CACA,OAAOJ,CAAgB,EAEnBK,GAAa3b,IACZA,GAAWA,EAAQkb,WAAaU,KAAKC,gBAGtC7b,EAAQ8b,UAAU7W,SAAS,mBAGC,IAArBjF,EAAQ+b,SACV/b,EAAQ+b,SAEV/b,EAAQgc,aAAa,aAAoD,UAArChc,EAAQic,aAAa,aAE5DC,GAAiBlc,IACrB,IAAK8F,SAASC,gBAAgBoW,aAC5B,OAAO,KAIT,GAAmC,mBAAxBnc,EAAQqF,YAA4B,CAC7C,MAAM+W,EAAOpc,EAAQqF,cACrB,OAAO+W,aAAgBtb,WAAasb,EAAO,IAC7C,CACA,OAAIpc,aAAmBc,WACdd,EAIJA,EAAQwF,WAGN0W,GAAelc,EAAQwF,YAFrB,IAEgC,EAErC6W,GAAO,OAUPC,GAAStc,IACbA,EAAQuE,YAAY,EAEhBgY,GAAY,IACZlc,OAAOmc,SAAW1W,SAAS6G,KAAKqP,aAAa,qBACxC3b,OAAOmc,OAET,KAEHC,GAA4B,GAgB5BC,GAAQ,IAAuC,QAAjC5W,SAASC,gBAAgB4W,IACvCC,GAAqBC,IAhBAC,QAiBN,KACjB,MAAMC,EAAIR,KAEV,GAAIQ,EAAG,CACL,MAAMhc,EAAO8b,EAAOG,KACdC,EAAqBF,EAAE7b,GAAGH,GAChCgc,EAAE7b,GAAGH,GAAQ8b,EAAOK,gBACpBH,EAAE7b,GAAGH,GAAMoc,YAAcN,EACzBE,EAAE7b,GAAGH,GAAMqc,WAAa,KACtBL,EAAE7b,GAAGH,GAAQkc,EACNJ,EAAOK,gBAElB,GA5B0B,YAAxBpX,SAASuX,YAENZ,GAA0BtL,QAC7BrL,SAASyF,iBAAiB,oBAAoB,KAC5C,IAAK,MAAMuR,KAAYL,GACrBK,GACF,IAGJL,GAA0BpK,KAAKyK,IAE/BA,GAkBA,EAEEQ,GAAU,CAACC,EAAkB9F,EAAO,GAAI+F,EAAeD,IACxB,mBAArBA,EAAkCA,KAAoB9F,GAAQ+F,EAExEC,GAAyB,CAACX,EAAUY,EAAmBC,GAAoB,KAC/E,IAAKA,EAEH,YADAL,GAAQR,GAGV,MACMc,EA/JiC5d,KACvC,IAAKA,EACH,OAAO,EAIT,IAAI,mBACF6d,EAAkB,gBAClBC,GACEzd,OAAOqF,iBAAiB1F,GAC5B,MAAM+d,EAA0BC,OAAOC,WAAWJ,GAC5CK,EAAuBF,OAAOC,WAAWH,GAG/C,OAAKC,GAA4BG,GAKjCL,EAAqBA,EAAmBlb,MAAM,KAAK,GACnDmb,EAAkBA,EAAgBnb,MAAM,KAAK,GAtDf,KAuDtBqb,OAAOC,WAAWJ,GAAsBG,OAAOC,WAAWH,KANzD,CAMoG,EA0IpFK,CAAiCT,GADlC,EAExB,IAAIU,GAAS,EACb,MAAMC,EAAU,EACdrR,aAEIA,IAAW0Q,IAGfU,GAAS,EACTV,EAAkBjS,oBAAoB6O,GAAgB+D,GACtDf,GAAQR,GAAS,EAEnBY,EAAkBnS,iBAAiB+O,GAAgB+D,GACnDC,YAAW,KACJF,GACHvD,GAAqB6C,EACvB,GACCE,EAAiB,EAYhBW,GAAuB,CAAC1R,EAAM2R,EAAeC,EAAeC,KAChE,MAAMC,EAAa9R,EAAKsE,OACxB,IAAI+H,EAAQrM,EAAKjH,QAAQ4Y,GAIzB,OAAe,IAAXtF,GACMuF,GAAiBC,EAAiB7R,EAAK8R,EAAa,GAAK9R,EAAK,IAExEqM,GAASuF,EAAgB,GAAK,EAC1BC,IACFxF,GAASA,EAAQyF,GAAcA,GAE1B9R,EAAKjK,KAAKC,IAAI,EAAGD,KAAKE,IAAIoW,EAAOyF,EAAa,KAAI,EAerDC,GAAiB,qBACjBC,GAAiB,OACjBC,GAAgB,SAChBC,GAAgB,CAAC,EACvB,IAAIC,GAAW,EACf,MAAMC,GAAe,CACnBC,WAAY,YACZC,WAAY,YAERC,GAAe,IAAIrI,IAAI,CAAC,QAAS,WAAY,UAAW,YAAa,cAAe,aAAc,iBAAkB,YAAa,WAAY,YAAa,cAAe,YAAa,UAAW,WAAY,QAAS,oBAAqB,aAAc,YAAa,WAAY,cAAe,cAAe,cAAe,YAAa,eAAgB,gBAAiB,eAAgB,gBAAiB,aAAc,QAAS,OAAQ,SAAU,QAAS,SAAU,SAAU,UAAW,WAAY,OAAQ,SAAU,eAAgB,SAAU,OAAQ,mBAAoB,mBAAoB,QAAS,QAAS,WAM/lB,SAASsI,GAAarf,EAASsf,GAC7B,OAAOA,GAAO,GAAGA,MAAQN,QAAgBhf,EAAQgf,UAAYA,IAC/D,CACA,SAASO,GAAiBvf,GACxB,MAAMsf,EAAMD,GAAarf,GAGzB,OAFAA,EAAQgf,SAAWM,EACnBP,GAAcO,GAAOP,GAAcO,IAAQ,CAAC,EACrCP,GAAcO,EACvB,CAiCA,SAASE,GAAYC,EAAQC,EAAUC,EAAqB,MAC1D,OAAOliB,OAAOmiB,OAAOH,GAAQ7M,MAAKiN,GAASA,EAAMH,WAAaA,GAAYG,EAAMF,qBAAuBA,GACzG,CACA,SAASG,GAAoBC,EAAmB1B,EAAS2B,GACvD,MAAMC,EAAiC,iBAAZ5B,EAErBqB,EAAWO,EAAcD,EAAqB3B,GAAW2B,EAC/D,IAAIE,EAAYC,GAAaJ,GAI7B,OAHKX,GAAahI,IAAI8I,KACpBA,EAAYH,GAEP,CAACE,EAAaP,EAAUQ,EACjC,CACA,SAASE,GAAWpgB,EAAS+f,EAAmB1B,EAAS2B,EAAoBK,GAC3E,GAAiC,iBAAtBN,IAAmC/f,EAC5C,OAEF,IAAKigB,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GAIzF,GAAID,KAAqBd,GAAc,CACrC,MAAMqB,EAAepf,GACZ,SAAU2e,GACf,IAAKA,EAAMU,eAAiBV,EAAMU,gBAAkBV,EAAMW,iBAAmBX,EAAMW,eAAevb,SAAS4a,EAAMU,eAC/G,OAAOrf,EAAGjD,KAAKwiB,KAAMZ,EAEzB,EAEFH,EAAWY,EAAaZ,EAC1B,CACA,MAAMD,EAASF,GAAiBvf,GAC1B0gB,EAAWjB,EAAOS,KAAeT,EAAOS,GAAa,CAAC,GACtDS,EAAmBnB,GAAYkB,EAAUhB,EAAUO,EAAc5B,EAAU,MACjF,GAAIsC,EAEF,YADAA,EAAiBN,OAASM,EAAiBN,QAAUA,GAGvD,MAAMf,EAAMD,GAAaK,EAAUK,EAAkBnU,QAAQgT,GAAgB,KACvE1d,EAAK+e,EA5Db,SAAoCjgB,EAASwa,EAAUtZ,GACrD,OAAO,SAASmd,EAAQwB,GACtB,MAAMe,EAAc5gB,EAAQ6gB,iBAAiBrG,GAC7C,IAAK,IAAI,OACPxN,GACE6S,EAAO7S,GAAUA,IAAWyT,KAAMzT,EAASA,EAAOxH,WACpD,IAAK,MAAMsb,KAAcF,EACvB,GAAIE,IAAe9T,EASnB,OANA+T,GAAWlB,EAAO,CAChBW,eAAgBxT,IAEdqR,EAAQgC,QACVW,GAAaC,IAAIjhB,EAAS6f,EAAMqB,KAAM1G,EAAUtZ,GAE3CA,EAAGigB,MAAMnU,EAAQ,CAAC6S,GAG/B,CACF,CAwC2BuB,CAA2BphB,EAASqe,EAASqB,GAvExE,SAA0B1f,EAASkB,GACjC,OAAO,SAASmd,EAAQwB,GAOtB,OANAkB,GAAWlB,EAAO,CAChBW,eAAgBxgB,IAEdqe,EAAQgC,QACVW,GAAaC,IAAIjhB,EAAS6f,EAAMqB,KAAMhgB,GAEjCA,EAAGigB,MAAMnhB,EAAS,CAAC6f,GAC5B,CACF,CA6DoFwB,CAAiBrhB,EAAS0f,GAC5Gxe,EAAGye,mBAAqBM,EAAc5B,EAAU,KAChDnd,EAAGwe,SAAWA,EACdxe,EAAGmf,OAASA,EACZnf,EAAG8d,SAAWM,EACdoB,EAASpB,GAAOpe,EAChBlB,EAAQuL,iBAAiB2U,EAAWhf,EAAI+e,EAC1C,CACA,SAASqB,GAActhB,EAASyf,EAAQS,EAAW7B,EAASsB,GAC1D,MAAMze,EAAKse,GAAYC,EAAOS,GAAY7B,EAASsB,GAC9Cze,IAGLlB,EAAQyL,oBAAoByU,EAAWhf,EAAIqgB,QAAQ5B,WAC5CF,EAAOS,GAAWhf,EAAG8d,UAC9B,CACA,SAASwC,GAAyBxhB,EAASyf,EAAQS,EAAWuB,GAC5D,MAAMC,EAAoBjC,EAAOS,IAAc,CAAC,EAChD,IAAK,MAAOyB,EAAY9B,KAAUpiB,OAAOmkB,QAAQF,GAC3CC,EAAWE,SAASJ,IACtBH,GAActhB,EAASyf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAGtE,CACA,SAASQ,GAAaN,GAGpB,OADAA,EAAQA,EAAMjU,QAAQiT,GAAgB,IAC/BI,GAAaY,IAAUA,CAChC,CACA,MAAMmB,GAAe,CACnB,EAAAc,CAAG9hB,EAAS6f,EAAOxB,EAAS2B,GAC1BI,GAAWpgB,EAAS6f,EAAOxB,EAAS2B,GAAoB,EAC1D,EACA,GAAA+B,CAAI/hB,EAAS6f,EAAOxB,EAAS2B,GAC3BI,GAAWpgB,EAAS6f,EAAOxB,EAAS2B,GAAoB,EAC1D,EACA,GAAAiB,CAAIjhB,EAAS+f,EAAmB1B,EAAS2B,GACvC,GAAiC,iBAAtBD,IAAmC/f,EAC5C,OAEF,MAAOigB,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GACrFgC,EAAc9B,IAAcH,EAC5BN,EAASF,GAAiBvf,GAC1B0hB,EAAoBjC,EAAOS,IAAc,CAAC,EAC1C+B,EAAclC,EAAkBmC,WAAW,KACjD,QAAwB,IAAbxC,EAAX,CAQA,GAAIuC,EACF,IAAK,MAAME,KAAgB1kB,OAAO4D,KAAKoe,GACrC+B,GAAyBxhB,EAASyf,EAAQ0C,EAAcpC,EAAkBlN,MAAM,IAGpF,IAAK,MAAOuP,EAAavC,KAAUpiB,OAAOmkB,QAAQF,GAAoB,CACpE,MAAMC,EAAaS,EAAYxW,QAAQkT,GAAe,IACjDkD,IAAejC,EAAkB8B,SAASF,IAC7CL,GAActhB,EAASyf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAEpE,CAXA,KAPA,CAEE,IAAKliB,OAAO4D,KAAKqgB,GAAmBvQ,OAClC,OAEFmQ,GAActhB,EAASyf,EAAQS,EAAWR,EAAUO,EAAc5B,EAAU,KAE9E,CAYF,EACA,OAAAgE,CAAQriB,EAAS6f,EAAOpI,GACtB,GAAqB,iBAAVoI,IAAuB7f,EAChC,OAAO,KAET,MAAM+c,EAAIR,KAGV,IAAI+F,EAAc,KACdC,GAAU,EACVC,GAAiB,EACjBC,GAAmB,EAJH5C,IADFM,GAAaN,IAMZ9C,IACjBuF,EAAcvF,EAAEhC,MAAM8E,EAAOpI,GAC7BsF,EAAE/c,GAASqiB,QAAQC,GACnBC,GAAWD,EAAYI,uBACvBF,GAAkBF,EAAYK,gCAC9BF,EAAmBH,EAAYM,sBAEjC,MAAMC,EAAM9B,GAAW,IAAIhG,MAAM8E,EAAO,CACtC0C,UACAO,YAAY,IACVrL,GAUJ,OATIgL,GACFI,EAAIE,iBAEFP,GACFxiB,EAAQ8a,cAAc+H,GAEpBA,EAAIJ,kBAAoBH,GAC1BA,EAAYS,iBAEPF,CACT,GAEF,SAAS9B,GAAWljB,EAAKmlB,EAAO,CAAC,GAC/B,IAAK,MAAOzlB,EAAKa,KAAUX,OAAOmkB,QAAQoB,GACxC,IACEnlB,EAAIN,GAAOa,CACb,CAAE,MAAO6kB,GACPxlB,OAAOC,eAAeG,EAAKN,EAAK,CAC9B2lB,cAAc,EACdtlB,IAAG,IACMQ,GAGb,CAEF,OAAOP,CACT,CASA,SAASslB,GAAc/kB,GACrB,GAAc,SAAVA,EACF,OAAO,EAET,GAAc,UAAVA,EACF,OAAO,EAET,GAAIA,IAAU4f,OAAO5f,GAAOkC,WAC1B,OAAO0d,OAAO5f,GAEhB,GAAc,KAAVA,GAA0B,SAAVA,EAClB,OAAO,KAET,GAAqB,iBAAVA,EACT,OAAOA,EAET,IACE,OAAOglB,KAAKC,MAAMC,mBAAmBllB,GACvC,CAAE,MAAO6kB,GACP,OAAO7kB,CACT,CACF,CACA,SAASmlB,GAAiBhmB,GACxB,OAAOA,EAAIqO,QAAQ,UAAU4X,GAAO,IAAIA,EAAItjB,iBAC9C,CACA,MAAMujB,GAAc,CAClB,gBAAAC,CAAiB1jB,EAASzC,EAAKa,GAC7B4B,EAAQ6B,aAAa,WAAW0hB,GAAiBhmB,KAAQa,EAC3D,EACA,mBAAAulB,CAAoB3jB,EAASzC,GAC3ByC,EAAQ4B,gBAAgB,WAAW2hB,GAAiBhmB,KACtD,EACA,iBAAAqmB,CAAkB5jB,GAChB,IAAKA,EACH,MAAO,CAAC,EAEV,MAAM0B,EAAa,CAAC,EACdmiB,EAASpmB,OAAO4D,KAAKrB,EAAQ8jB,SAASld,QAAOrJ,GAAOA,EAAI2kB,WAAW,QAAU3kB,EAAI2kB,WAAW,cAClG,IAAK,MAAM3kB,KAAOsmB,EAAQ,CACxB,IAAIE,EAAUxmB,EAAIqO,QAAQ,MAAO,IACjCmY,EAAUA,EAAQC,OAAO,GAAG9jB,cAAgB6jB,EAAQlR,MAAM,EAAGkR,EAAQ5S,QACrEzP,EAAWqiB,GAAWZ,GAAcnjB,EAAQ8jB,QAAQvmB,GACtD,CACA,OAAOmE,CACT,EACAuiB,iBAAgB,CAACjkB,EAASzC,IACjB4lB,GAAcnjB,EAAQic,aAAa,WAAWsH,GAAiBhmB,QAgB1E,MAAM2mB,GAEJ,kBAAWC,GACT,MAAO,CAAC,CACV,CACA,sBAAWC,GACT,MAAO,CAAC,CACV,CACA,eAAWpH,GACT,MAAM,IAAIqH,MAAM,sEAClB,CACA,UAAAC,CAAWC,GAIT,OAHAA,EAAS9D,KAAK+D,gBAAgBD,GAC9BA,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CACA,iBAAAE,CAAkBF,GAChB,OAAOA,CACT,CACA,eAAAC,CAAgBD,EAAQvkB,GACtB,MAAM2kB,EAAa,GAAU3kB,GAAWyjB,GAAYQ,iBAAiBjkB,EAAS,UAAY,CAAC,EAE3F,MAAO,IACFygB,KAAKmE,YAAYT,WACM,iBAAfQ,EAA0BA,EAAa,CAAC,KAC/C,GAAU3kB,GAAWyjB,GAAYG,kBAAkB5jB,GAAW,CAAC,KAC7C,iBAAXukB,EAAsBA,EAAS,CAAC,EAE/C,CACA,gBAAAG,CAAiBH,EAAQM,EAAcpE,KAAKmE,YAAYR,aACtD,IAAK,MAAO7hB,EAAUuiB,KAAkBrnB,OAAOmkB,QAAQiD,GAAc,CACnE,MAAMzmB,EAAQmmB,EAAOhiB,GACfwiB,EAAY,GAAU3mB,GAAS,UAhiBrC4c,OADSA,EAiiB+C5c,GA/hBnD,GAAG4c,IAELvd,OAAOM,UAAUuC,SAASrC,KAAK+c,GAAQL,MAAM,eAAe,GAAGza,cA8hBlE,IAAK,IAAI8kB,OAAOF,GAAehhB,KAAKihB,GAClC,MAAM,IAAIE,UAAU,GAAGxE,KAAKmE,YAAY5H,KAAKkI,0BAA0B3iB,qBAA4BwiB,yBAAiCD,MAExI,CAriBW9J,KAsiBb,EAqBF,MAAMmK,WAAsBjB,GAC1B,WAAAU,CAAY5kB,EAASukB,GACnBa,SACAplB,EAAUmb,GAAWnb,MAIrBygB,KAAK4E,SAAWrlB,EAChBygB,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/BzK,GAAKtH,IAAIiO,KAAK4E,SAAU5E,KAAKmE,YAAYW,SAAU9E,MACrD,CAGA,OAAA+E,GACE1L,GAAKM,OAAOqG,KAAK4E,SAAU5E,KAAKmE,YAAYW,UAC5CvE,GAAaC,IAAIR,KAAK4E,SAAU5E,KAAKmE,YAAYa,WACjD,IAAK,MAAMC,KAAgBjoB,OAAOkoB,oBAAoBlF,MACpDA,KAAKiF,GAAgB,IAEzB,CACA,cAAAE,CAAe9I,EAAU9c,EAAS6lB,GAAa,GAC7CpI,GAAuBX,EAAU9c,EAAS6lB,EAC5C,CACA,UAAAvB,CAAWC,GAIT,OAHAA,EAAS9D,KAAK+D,gBAAgBD,EAAQ9D,KAAK4E,UAC3Cd,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CAGA,kBAAOuB,CAAY9lB,GACjB,OAAO8Z,GAAKlc,IAAIud,GAAWnb,GAAUygB,KAAK8E,SAC5C,CACA,0BAAOQ,CAAoB/lB,EAASukB,EAAS,CAAC,GAC5C,OAAO9D,KAAKqF,YAAY9lB,IAAY,IAAIygB,KAAKzgB,EAA2B,iBAAXukB,EAAsBA,EAAS,KAC9F,CACA,kBAAWyB,GACT,MA5CY,OA6Cd,CACA,mBAAWT,GACT,MAAO,MAAM9E,KAAKzD,MACpB,CACA,oBAAWyI,GACT,MAAO,IAAIhF,KAAK8E,UAClB,CACA,gBAAOU,CAAUllB,GACf,MAAO,GAAGA,IAAO0f,KAAKgF,WACxB,EAUF,MAAMS,GAAclmB,IAClB,IAAIwa,EAAWxa,EAAQic,aAAa,kBACpC,IAAKzB,GAAyB,MAAbA,EAAkB,CACjC,IAAI2L,EAAgBnmB,EAAQic,aAAa,QAMzC,IAAKkK,IAAkBA,EAActE,SAAS,OAASsE,EAAcjE,WAAW,KAC9E,OAAO,KAILiE,EAActE,SAAS,OAASsE,EAAcjE,WAAW,OAC3DiE,EAAgB,IAAIA,EAAcxjB,MAAM,KAAK,MAE/C6X,EAAW2L,GAAmC,MAAlBA,EAAwBA,EAAcC,OAAS,IAC7E,CACA,OAAO5L,EAAWA,EAAS7X,MAAM,KAAKY,KAAI8iB,GAAO9L,GAAc8L,KAAM1iB,KAAK,KAAO,IAAI,EAEjF2iB,GAAiB,CACrB1T,KAAI,CAAC4H,EAAUxa,EAAU8F,SAASC,kBACzB,GAAG3G,UAAUsB,QAAQ3C,UAAU8iB,iBAAiB5iB,KAAK+B,EAASwa,IAEvE+L,QAAO,CAAC/L,EAAUxa,EAAU8F,SAASC,kBAC5BrF,QAAQ3C,UAAU8K,cAAc5K,KAAK+B,EAASwa,GAEvDgM,SAAQ,CAACxmB,EAASwa,IACT,GAAGpb,UAAUY,EAAQwmB,UAAU5f,QAAOzB,GAASA,EAAMshB,QAAQjM,KAEtE,OAAAkM,CAAQ1mB,EAASwa,GACf,MAAMkM,EAAU,GAChB,IAAIC,EAAW3mB,EAAQwF,WAAWiW,QAAQjB,GAC1C,KAAOmM,GACLD,EAAQrU,KAAKsU,GACbA,EAAWA,EAASnhB,WAAWiW,QAAQjB,GAEzC,OAAOkM,CACT,EACA,IAAAE,CAAK5mB,EAASwa,GACZ,IAAIqM,EAAW7mB,EAAQ8mB,uBACvB,KAAOD,GAAU,CACf,GAAIA,EAASJ,QAAQjM,GACnB,MAAO,CAACqM,GAEVA,EAAWA,EAASC,sBACtB,CACA,MAAO,EACT,EAEA,IAAAxhB,CAAKtF,EAASwa,GACZ,IAAIlV,EAAOtF,EAAQ+mB,mBACnB,KAAOzhB,GAAM,CACX,GAAIA,EAAKmhB,QAAQjM,GACf,MAAO,CAAClV,GAEVA,EAAOA,EAAKyhB,kBACd,CACA,MAAO,EACT,EACA,iBAAAC,CAAkBhnB,GAChB,MAAMinB,EAAa,CAAC,IAAK,SAAU,QAAS,WAAY,SAAU,UAAW,aAAc,4BAA4B1jB,KAAIiX,GAAY,GAAGA,2BAAiC7W,KAAK,KAChL,OAAO8c,KAAK7N,KAAKqU,EAAYjnB,GAAS4G,QAAOsgB,IAAOvL,GAAWuL,IAAO9L,GAAU8L,IAClF,EACA,sBAAAC,CAAuBnnB,GACrB,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAIwa,GACK8L,GAAeC,QAAQ/L,GAAYA,EAErC,IACT,EACA,sBAAA4M,CAAuBpnB,GACrB,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAOwa,EAAW8L,GAAeC,QAAQ/L,GAAY,IACvD,EACA,+BAAA6M,CAAgCrnB,GAC9B,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAOwa,EAAW8L,GAAe1T,KAAK4H,GAAY,EACpD,GAUI8M,GAAuB,CAACC,EAAWC,EAAS,UAChD,MAAMC,EAAa,gBAAgBF,EAAU9B,YACvC1kB,EAAOwmB,EAAUvK,KACvBgE,GAAac,GAAGhc,SAAU2hB,EAAY,qBAAqB1mB,OAAU,SAAU8e,GAI7E,GAHI,CAAC,IAAK,QAAQgC,SAASpB,KAAKiH,UAC9B7H,EAAMkD,iBAEJpH,GAAW8E,MACb,OAEF,MAAMzT,EAASsZ,GAAec,uBAAuB3G,OAASA,KAAKhF,QAAQ,IAAI1a,KAC9DwmB,EAAUxB,oBAAoB/Y,GAGtCwa,IACX,GAAE,EAiBEG,GAAc,YACdC,GAAc,QAAQD,KACtBE,GAAe,SAASF,KAQ9B,MAAMG,WAAc3C,GAElB,eAAWnI,GACT,MAfW,OAgBb,CAGA,KAAA+K,GAEE,GADmB/G,GAAaqB,QAAQ5B,KAAK4E,SAAUuC,IACxCnF,iBACb,OAEFhC,KAAK4E,SAASvJ,UAAU1B,OAlBF,QAmBtB,MAAMyL,EAAapF,KAAK4E,SAASvJ,UAAU7W,SApBrB,QAqBtBwb,KAAKmF,gBAAe,IAAMnF,KAAKuH,mBAAmBvH,KAAK4E,SAAUQ,EACnE,CAGA,eAAAmC,GACEvH,KAAK4E,SAASjL,SACd4G,GAAaqB,QAAQ5B,KAAK4E,SAAUwC,IACpCpH,KAAK+E,SACP,CAGA,sBAAOtI,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOgd,GAAM/B,oBAAoBtF,MACvC,GAAsB,iBAAX8D,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KAJb,CAKF,GACF,EAOF6G,GAAqBQ,GAAO,SAM5BlL,GAAmBkL,IAcnB,MAKMI,GAAyB,4BAO/B,MAAMC,WAAehD,GAEnB,eAAWnI,GACT,MAfW,QAgBb,CAGA,MAAAoL,GAEE3H,KAAK4E,SAASxjB,aAAa,eAAgB4e,KAAK4E,SAASvJ,UAAUsM,OAjB3C,UAkB1B,CAGA,sBAAOlL,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOqd,GAAOpC,oBAAoBtF,MACzB,WAAX8D,GACFzZ,EAAKyZ,IAET,GACF,EAOFvD,GAAac,GAAGhc,SAjCe,2BAiCmBoiB,IAAwBrI,IACxEA,EAAMkD,iBACN,MAAMsF,EAASxI,EAAM7S,OAAOyO,QAAQyM,IACvBC,GAAOpC,oBAAoBsC,GACnCD,QAAQ,IAOfxL,GAAmBuL,IAcnB,MACMG,GAAc,YACdC,GAAmB,aAAaD,KAChCE,GAAkB,YAAYF,KAC9BG,GAAiB,WAAWH,KAC5BI,GAAoB,cAAcJ,KAClCK,GAAkB,YAAYL,KAK9BM,GAAY,CAChBC,YAAa,KACbC,aAAc,KACdC,cAAe,MAEXC,GAAgB,CACpBH,YAAa,kBACbC,aAAc,kBACdC,cAAe,mBAOjB,MAAME,WAAc/E,GAClB,WAAAU,CAAY5kB,EAASukB,GACnBa,QACA3E,KAAK4E,SAAWrlB,EACXA,GAAYipB,GAAMC,gBAGvBzI,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAK0I,QAAU,EACf1I,KAAK2I,sBAAwB7H,QAAQlhB,OAAOgpB,cAC5C5I,KAAK6I,cACP,CAGA,kBAAWnF,GACT,OAAOyE,EACT,CACA,sBAAWxE,GACT,OAAO4E,EACT,CACA,eAAWhM,GACT,MA/CW,OAgDb,CAGA,OAAAwI,GACExE,GAAaC,IAAIR,KAAK4E,SAAUiD,GAClC,CAGA,MAAAiB,CAAO1J,GACAY,KAAK2I,sBAIN3I,KAAK+I,wBAAwB3J,KAC/BY,KAAK0I,QAAUtJ,EAAM4J,SAJrBhJ,KAAK0I,QAAUtJ,EAAM6J,QAAQ,GAAGD,OAMpC,CACA,IAAAE,CAAK9J,GACCY,KAAK+I,wBAAwB3J,KAC/BY,KAAK0I,QAAUtJ,EAAM4J,QAAUhJ,KAAK0I,SAEtC1I,KAAKmJ,eACLtM,GAAQmD,KAAK6E,QAAQuD,YACvB,CACA,KAAAgB,CAAMhK,GACJY,KAAK0I,QAAUtJ,EAAM6J,SAAW7J,EAAM6J,QAAQvY,OAAS,EAAI,EAAI0O,EAAM6J,QAAQ,GAAGD,QAAUhJ,KAAK0I,OACjG,CACA,YAAAS,GACE,MAAME,EAAYlnB,KAAKoC,IAAIyb,KAAK0I,SAChC,GAAIW,GAnEgB,GAoElB,OAEF,MAAM/b,EAAY+b,EAAYrJ,KAAK0I,QACnC1I,KAAK0I,QAAU,EACVpb,GAGLuP,GAAQvP,EAAY,EAAI0S,KAAK6E,QAAQyD,cAAgBtI,KAAK6E,QAAQwD,aACpE,CACA,WAAAQ,GACM7I,KAAK2I,uBACPpI,GAAac,GAAGrB,KAAK4E,SAAUqD,IAAmB7I,GAASY,KAAK8I,OAAO1J,KACvEmB,GAAac,GAAGrB,KAAK4E,SAAUsD,IAAiB9I,GAASY,KAAKkJ,KAAK9J,KACnEY,KAAK4E,SAASvJ,UAAU5E,IAlFG,mBAoF3B8J,GAAac,GAAGrB,KAAK4E,SAAUkD,IAAkB1I,GAASY,KAAK8I,OAAO1J,KACtEmB,GAAac,GAAGrB,KAAK4E,SAAUmD,IAAiB3I,GAASY,KAAKoJ,MAAMhK,KACpEmB,GAAac,GAAGrB,KAAK4E,SAAUoD,IAAgB5I,GAASY,KAAKkJ,KAAK9J,KAEtE,CACA,uBAAA2J,CAAwB3J,GACtB,OAAOY,KAAK2I,wBA3FS,QA2FiBvJ,EAAMkK,aA5FrB,UA4FyDlK,EAAMkK,YACxF,CAGA,kBAAOb,GACL,MAAO,iBAAkBpjB,SAASC,iBAAmB7C,UAAU8mB,eAAiB,CAClF,EAeF,MAEMC,GAAc,eACdC,GAAiB,YACjBC,GAAmB,YACnBC,GAAoB,aAGpBC,GAAa,OACbC,GAAa,OACbC,GAAiB,OACjBC,GAAkB,QAClBC,GAAc,QAAQR,KACtBS,GAAa,OAAOT,KACpBU,GAAkB,UAAUV,KAC5BW,GAAqB,aAAaX,KAClCY,GAAqB,aAAaZ,KAClCa,GAAmB,YAAYb,KAC/Bc,GAAwB,OAAOd,KAAcC,KAC7Cc,GAAyB,QAAQf,KAAcC,KAC/Ce,GAAsB,WACtBC,GAAsB,SAMtBC,GAAkB,UAClBC,GAAgB,iBAChBC,GAAuBF,GAAkBC,GAKzCE,GAAmB,CACvB,CAACnB,IAAmBK,GACpB,CAACJ,IAAoBG,IAEjBgB,GAAY,CAChBC,SAAU,IACVC,UAAU,EACVC,MAAO,QACPC,MAAM,EACNC,OAAO,EACPC,MAAM,GAEFC,GAAgB,CACpBN,SAAU,mBAEVC,SAAU,UACVC,MAAO,mBACPC,KAAM,mBACNC,MAAO,UACPC,KAAM,WAOR,MAAME,WAAiB5G,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKuL,UAAY,KACjBvL,KAAKwL,eAAiB,KACtBxL,KAAKyL,YAAa,EAClBzL,KAAK0L,aAAe,KACpB1L,KAAK2L,aAAe,KACpB3L,KAAK4L,mBAAqB/F,GAAeC,QArCjB,uBAqC8C9F,KAAK4E,UAC3E5E,KAAK6L,qBACD7L,KAAK6E,QAAQqG,OAASV,IACxBxK,KAAK8L,OAET,CAGA,kBAAWpI,GACT,OAAOoH,EACT,CACA,sBAAWnH,GACT,OAAO0H,EACT,CACA,eAAW9O,GACT,MAnFW,UAoFb,CAGA,IAAA1X,GACEmb,KAAK+L,OAAOnC,GACd,CACA,eAAAoC,IAIO3mB,SAAS4mB,QAAUtR,GAAUqF,KAAK4E,WACrC5E,KAAKnb,MAET,CACA,IAAAshB,GACEnG,KAAK+L,OAAOlC,GACd,CACA,KAAAoB,GACMjL,KAAKyL,YACPrR,GAAqB4F,KAAK4E,UAE5B5E,KAAKkM,gBACP,CACA,KAAAJ,GACE9L,KAAKkM,iBACLlM,KAAKmM,kBACLnM,KAAKuL,UAAYa,aAAY,IAAMpM,KAAKgM,mBAAmBhM,KAAK6E,QAAQkG,SAC1E,CACA,iBAAAsB,GACOrM,KAAK6E,QAAQqG,OAGdlL,KAAKyL,WACPlL,GAAae,IAAItB,KAAK4E,SAAUqF,IAAY,IAAMjK,KAAK8L,UAGzD9L,KAAK8L,QACP,CACA,EAAAQ,CAAG7T,GACD,MAAM8T,EAAQvM,KAAKwM,YACnB,GAAI/T,EAAQ8T,EAAM7b,OAAS,GAAK+H,EAAQ,EACtC,OAEF,GAAIuH,KAAKyL,WAEP,YADAlL,GAAae,IAAItB,KAAK4E,SAAUqF,IAAY,IAAMjK,KAAKsM,GAAG7T,KAG5D,MAAMgU,EAAczM,KAAK0M,cAAc1M,KAAK2M,cAC5C,GAAIF,IAAgBhU,EAClB,OAEF,MAAMtC,EAAQsC,EAAQgU,EAAc7C,GAAaC,GACjD7J,KAAK+L,OAAO5V,EAAOoW,EAAM9T,GAC3B,CACA,OAAAsM,GACM/E,KAAK2L,cACP3L,KAAK2L,aAAa5G,UAEpBJ,MAAMI,SACR,CAGA,iBAAAf,CAAkBF,GAEhB,OADAA,EAAO8I,gBAAkB9I,EAAOiH,SACzBjH,CACT,CACA,kBAAA+H,GACM7L,KAAK6E,QAAQmG,UACfzK,GAAac,GAAGrB,KAAK4E,SAAUsF,IAAiB9K,GAASY,KAAK6M,SAASzN,KAE9C,UAAvBY,KAAK6E,QAAQoG,QACf1K,GAAac,GAAGrB,KAAK4E,SAAUuF,IAAoB,IAAMnK,KAAKiL,UAC9D1K,GAAac,GAAGrB,KAAK4E,SAAUwF,IAAoB,IAAMpK,KAAKqM,uBAE5DrM,KAAK6E,QAAQsG,OAAS3C,GAAMC,eAC9BzI,KAAK8M,yBAET,CACA,uBAAAA,GACE,IAAK,MAAMC,KAAOlH,GAAe1T,KArIX,qBAqImC6N,KAAK4E,UAC5DrE,GAAac,GAAG0L,EAAK1C,IAAkBjL,GAASA,EAAMkD,mBAExD,MAmBM0K,EAAc,CAClB3E,aAAc,IAAMrI,KAAK+L,OAAO/L,KAAKiN,kBAAkBnD,KACvDxB,cAAe,IAAMtI,KAAK+L,OAAO/L,KAAKiN,kBAAkBlD,KACxD3B,YAtBkB,KACS,UAAvBpI,KAAK6E,QAAQoG,QAYjBjL,KAAKiL,QACDjL,KAAK0L,cACPwB,aAAalN,KAAK0L,cAEpB1L,KAAK0L,aAAe7N,YAAW,IAAMmC,KAAKqM,qBAjLjB,IAiL+DrM,KAAK6E,QAAQkG,UAAS,GAOhH/K,KAAK2L,aAAe,IAAInD,GAAMxI,KAAK4E,SAAUoI,EAC/C,CACA,QAAAH,CAASzN,GACP,GAAI,kBAAkB/b,KAAK+b,EAAM7S,OAAO0a,SACtC,OAEF,MAAM3Z,EAAYud,GAAiBzL,EAAMtiB,KACrCwQ,IACF8R,EAAMkD,iBACNtC,KAAK+L,OAAO/L,KAAKiN,kBAAkB3f,IAEvC,CACA,aAAAof,CAAcntB,GACZ,OAAOygB,KAAKwM,YAAYrnB,QAAQ5F,EAClC,CACA,0BAAA4tB,CAA2B1U,GACzB,IAAKuH,KAAK4L,mBACR,OAEF,MAAMwB,EAAkBvH,GAAeC,QAAQ4E,GAAiB1K,KAAK4L,oBACrEwB,EAAgB/R,UAAU1B,OAAO8Q,IACjC2C,EAAgBjsB,gBAAgB,gBAChC,MAAMksB,EAAqBxH,GAAeC,QAAQ,sBAAsBrN,MAAWuH,KAAK4L,oBACpFyB,IACFA,EAAmBhS,UAAU5E,IAAIgU,IACjC4C,EAAmBjsB,aAAa,eAAgB,QAEpD,CACA,eAAA+qB,GACE,MAAM5sB,EAAUygB,KAAKwL,gBAAkBxL,KAAK2M,aAC5C,IAAKptB,EACH,OAEF,MAAM+tB,EAAkB/P,OAAOgQ,SAAShuB,EAAQic,aAAa,oBAAqB,IAClFwE,KAAK6E,QAAQkG,SAAWuC,GAAmBtN,KAAK6E,QAAQ+H,eAC1D,CACA,MAAAb,CAAO5V,EAAO5W,EAAU,MACtB,GAAIygB,KAAKyL,WACP,OAEF,MAAM1N,EAAgBiC,KAAK2M,aACrBa,EAASrX,IAAUyT,GACnB6D,EAAcluB,GAAWue,GAAqBkC,KAAKwM,YAAazO,EAAeyP,EAAQxN,KAAK6E,QAAQuG,MAC1G,GAAIqC,IAAgB1P,EAClB,OAEF,MAAM2P,EAAmB1N,KAAK0M,cAAce,GACtCE,EAAenI,GACZjF,GAAaqB,QAAQ5B,KAAK4E,SAAUY,EAAW,CACpD1F,cAAe2N,EACfngB,UAAW0S,KAAK4N,kBAAkBzX,GAClCuD,KAAMsG,KAAK0M,cAAc3O,GACzBuO,GAAIoB,IAIR,GADmBC,EAAa3D,IACjBhI,iBACb,OAEF,IAAKjE,IAAkB0P,EAGrB,OAEF,MAAMI,EAAY/M,QAAQd,KAAKuL,WAC/BvL,KAAKiL,QACLjL,KAAKyL,YAAa,EAClBzL,KAAKmN,2BAA2BO,GAChC1N,KAAKwL,eAAiBiC,EACtB,MAAMK,EAAuBN,EA3OR,sBADF,oBA6ObO,EAAiBP,EA3OH,qBACA,qBA2OpBC,EAAYpS,UAAU5E,IAAIsX,GAC1BlS,GAAO4R,GACP1P,EAAc1C,UAAU5E,IAAIqX,GAC5BL,EAAYpS,UAAU5E,IAAIqX,GAQ1B9N,KAAKmF,gBAPoB,KACvBsI,EAAYpS,UAAU1B,OAAOmU,EAAsBC,GACnDN,EAAYpS,UAAU5E,IAAIgU,IAC1B1M,EAAc1C,UAAU1B,OAAO8Q,GAAqBsD,EAAgBD,GACpE9N,KAAKyL,YAAa,EAClBkC,EAAa1D,GAAW,GAEYlM,EAAeiC,KAAKgO,eACtDH,GACF7N,KAAK8L,OAET,CACA,WAAAkC,GACE,OAAOhO,KAAK4E,SAASvJ,UAAU7W,SAhQV,QAiQvB,CACA,UAAAmoB,GACE,OAAO9G,GAAeC,QAAQ8E,GAAsB5K,KAAK4E,SAC3D,CACA,SAAA4H,GACE,OAAO3G,GAAe1T,KAAKwY,GAAe3K,KAAK4E,SACjD,CACA,cAAAsH,GACMlM,KAAKuL,YACP0C,cAAcjO,KAAKuL,WACnBvL,KAAKuL,UAAY,KAErB,CACA,iBAAA0B,CAAkB3f,GAChB,OAAI2O,KACK3O,IAAcwc,GAAiBD,GAAaD,GAE9Ctc,IAAcwc,GAAiBF,GAAaC,EACrD,CACA,iBAAA+D,CAAkBzX,GAChB,OAAI8F,KACK9F,IAAU0T,GAAaC,GAAiBC,GAE1C5T,IAAU0T,GAAaE,GAAkBD,EAClD,CAGA,sBAAOrN,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOihB,GAAShG,oBAAoBtF,KAAM8D,GAChD,GAAsB,iBAAXA,GAIX,GAAsB,iBAAXA,EAAqB,CAC9B,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IACP,OAREzZ,EAAKiiB,GAAGxI,EASZ,GACF,EAOFvD,GAAac,GAAGhc,SAAUklB,GAvSE,uCAuS2C,SAAUnL,GAC/E,MAAM7S,EAASsZ,GAAec,uBAAuB3G,MACrD,IAAKzT,IAAWA,EAAO8O,UAAU7W,SAASgmB,IACxC,OAEFpL,EAAMkD,iBACN,MAAM4L,EAAW5C,GAAShG,oBAAoB/Y,GACxC4hB,EAAanO,KAAKxE,aAAa,oBACrC,OAAI2S,GACFD,EAAS5B,GAAG6B,QACZD,EAAS7B,qBAGyC,SAAhDrJ,GAAYQ,iBAAiBxD,KAAM,UACrCkO,EAASrpB,YACTqpB,EAAS7B,sBAGX6B,EAAS/H,YACT+H,EAAS7B,oBACX,IACA9L,GAAac,GAAGzhB,OAAQ0qB,IAAuB,KAC7C,MAAM8D,EAAYvI,GAAe1T,KA5TR,6BA6TzB,IAAK,MAAM+b,KAAYE,EACrB9C,GAAShG,oBAAoB4I,EAC/B,IAOF/R,GAAmBmP,IAcnB,MAEM+C,GAAc,eAEdC,GAAe,OAAOD,KACtBE,GAAgB,QAAQF,KACxBG,GAAe,OAAOH,KACtBI,GAAiB,SAASJ,KAC1BK,GAAyB,QAAQL,cACjCM,GAAoB,OACpBC,GAAsB,WACtBC,GAAwB,aAExBC,GAA6B,WAAWF,OAAwBA,KAKhEG,GAAyB,8BACzBC,GAAY,CAChBvqB,OAAQ,KACRkjB,QAAQ,GAEJsH,GAAgB,CACpBxqB,OAAQ,iBACRkjB,OAAQ,WAOV,MAAMuH,WAAiBxK,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKmP,kBAAmB,EACxBnP,KAAKoP,cAAgB,GACrB,MAAMC,EAAaxJ,GAAe1T,KAAK4c,IACvC,IAAK,MAAMO,KAAQD,EAAY,CAC7B,MAAMtV,EAAW8L,GAAea,uBAAuB4I,GACjDC,EAAgB1J,GAAe1T,KAAK4H,GAAU5T,QAAOqpB,GAAgBA,IAAiBxP,KAAK4E,WAChF,OAAb7K,GAAqBwV,EAAc7e,QACrCsP,KAAKoP,cAAcxd,KAAK0d,EAE5B,CACAtP,KAAKyP,sBACAzP,KAAK6E,QAAQpgB,QAChBub,KAAK0P,0BAA0B1P,KAAKoP,cAAepP,KAAK2P,YAEtD3P,KAAK6E,QAAQ8C,QACf3H,KAAK2H,QAET,CAGA,kBAAWjE,GACT,OAAOsL,EACT,CACA,sBAAWrL,GACT,OAAOsL,EACT,CACA,eAAW1S,GACT,MA9DW,UA+Db,CAGA,MAAAoL,GACM3H,KAAK2P,WACP3P,KAAK4P,OAEL5P,KAAK6P,MAET,CACA,IAAAA,GACE,GAAI7P,KAAKmP,kBAAoBnP,KAAK2P,WAChC,OAEF,IAAIG,EAAiB,GAQrB,GALI9P,KAAK6E,QAAQpgB,SACfqrB,EAAiB9P,KAAK+P,uBAhEH,wCAgE4C5pB,QAAO5G,GAAWA,IAAYygB,KAAK4E,WAAU9hB,KAAIvD,GAAW2vB,GAAS5J,oBAAoB/lB,EAAS,CAC/JooB,QAAQ,OAGRmI,EAAepf,QAAUof,EAAe,GAAGX,iBAC7C,OAGF,GADmB5O,GAAaqB,QAAQ5B,KAAK4E,SAAU0J,IACxCtM,iBACb,OAEF,IAAK,MAAMgO,KAAkBF,EAC3BE,EAAeJ,OAEjB,MAAMK,EAAYjQ,KAAKkQ,gBACvBlQ,KAAK4E,SAASvJ,UAAU1B,OAAOiV,IAC/B5O,KAAK4E,SAASvJ,UAAU5E,IAAIoY,IAC5B7O,KAAK4E,SAAS7jB,MAAMkvB,GAAa,EACjCjQ,KAAK0P,0BAA0B1P,KAAKoP,eAAe,GACnDpP,KAAKmP,kBAAmB,EACxB,MAQMgB,EAAa,SADUF,EAAU,GAAGxL,cAAgBwL,EAAU7d,MAAM,KAE1E4N,KAAKmF,gBATY,KACfnF,KAAKmP,kBAAmB,EACxBnP,KAAK4E,SAASvJ,UAAU1B,OAAOkV,IAC/B7O,KAAK4E,SAASvJ,UAAU5E,IAAImY,GAAqBD,IACjD3O,KAAK4E,SAAS7jB,MAAMkvB,GAAa,GACjC1P,GAAaqB,QAAQ5B,KAAK4E,SAAU2J,GAAc,GAItBvO,KAAK4E,UAAU,GAC7C5E,KAAK4E,SAAS7jB,MAAMkvB,GAAa,GAAGjQ,KAAK4E,SAASuL,MACpD,CACA,IAAAP,GACE,GAAI5P,KAAKmP,mBAAqBnP,KAAK2P,WACjC,OAGF,GADmBpP,GAAaqB,QAAQ5B,KAAK4E,SAAU4J,IACxCxM,iBACb,OAEF,MAAMiO,EAAYjQ,KAAKkQ,gBACvBlQ,KAAK4E,SAAS7jB,MAAMkvB,GAAa,GAAGjQ,KAAK4E,SAASthB,wBAAwB2sB,OAC1EpU,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIoY,IAC5B7O,KAAK4E,SAASvJ,UAAU1B,OAAOiV,GAAqBD,IACpD,IAAK,MAAM/M,KAAW5B,KAAKoP,cAAe,CACxC,MAAM7vB,EAAUsmB,GAAec,uBAAuB/E,GAClDriB,IAAYygB,KAAK2P,SAASpwB,IAC5BygB,KAAK0P,0BAA0B,CAAC9N,IAAU,EAE9C,CACA5B,KAAKmP,kBAAmB,EAOxBnP,KAAK4E,SAAS7jB,MAAMkvB,GAAa,GACjCjQ,KAAKmF,gBAPY,KACfnF,KAAKmP,kBAAmB,EACxBnP,KAAK4E,SAASvJ,UAAU1B,OAAOkV,IAC/B7O,KAAK4E,SAASvJ,UAAU5E,IAAImY,IAC5BrO,GAAaqB,QAAQ5B,KAAK4E,SAAU6J,GAAe,GAGvBzO,KAAK4E,UAAU,EAC/C,CACA,QAAA+K,CAASpwB,EAAUygB,KAAK4E,UACtB,OAAOrlB,EAAQ8b,UAAU7W,SAASmqB,GACpC,CAGA,iBAAA3K,CAAkBF,GAGhB,OAFAA,EAAO6D,OAAS7G,QAAQgD,EAAO6D,QAC/B7D,EAAOrf,OAASiW,GAAWoJ,EAAOrf,QAC3Bqf,CACT,CACA,aAAAoM,GACE,OAAOlQ,KAAK4E,SAASvJ,UAAU7W,SA3IL,uBAChB,QACC,QA0Ib,CACA,mBAAAirB,GACE,IAAKzP,KAAK6E,QAAQpgB,OAChB,OAEF,MAAMshB,EAAW/F,KAAK+P,uBAAuBhB,IAC7C,IAAK,MAAMxvB,KAAWwmB,EAAU,CAC9B,MAAMqK,EAAWvK,GAAec,uBAAuBpnB,GACnD6wB,GACFpQ,KAAK0P,0BAA0B,CAACnwB,GAAUygB,KAAK2P,SAASS,GAE5D,CACF,CACA,sBAAAL,CAAuBhW,GACrB,MAAMgM,EAAWF,GAAe1T,KAAK2c,GAA4B9O,KAAK6E,QAAQpgB,QAE9E,OAAOohB,GAAe1T,KAAK4H,EAAUiG,KAAK6E,QAAQpgB,QAAQ0B,QAAO5G,IAAYwmB,EAAS3E,SAAS7hB,IACjG,CACA,yBAAAmwB,CAA0BW,EAAcC,GACtC,GAAKD,EAAa3f,OAGlB,IAAK,MAAMnR,KAAW8wB,EACpB9wB,EAAQ8b,UAAUsM,OArKK,aAqKyB2I,GAChD/wB,EAAQ6B,aAAa,gBAAiBkvB,EAE1C,CAGA,sBAAO7T,CAAgBqH,GACrB,MAAMe,EAAU,CAAC,EAIjB,MAHsB,iBAAXf,GAAuB,YAAYzgB,KAAKygB,KACjDe,EAAQ8C,QAAS,GAEZ3H,KAAKwH,MAAK,WACf,MAAMnd,EAAO6kB,GAAS5J,oBAAoBtF,KAAM6E,GAChD,GAAsB,iBAAXf,EAAqB,CAC9B,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IACP,CACF,GACF,EAOFvD,GAAac,GAAGhc,SAAUqpB,GAAwBK,IAAwB,SAAU3P,IAErD,MAAzBA,EAAM7S,OAAO0a,SAAmB7H,EAAMW,gBAAmD,MAAjCX,EAAMW,eAAekH,UAC/E7H,EAAMkD,iBAER,IAAK,MAAM/iB,KAAWsmB,GAAee,gCAAgC5G,MACnEkP,GAAS5J,oBAAoB/lB,EAAS,CACpCooB,QAAQ,IACPA,QAEP,IAMAxL,GAAmB+S,IAcnB,MAAMqB,GAAS,WAETC,GAAc,eACdC,GAAiB,YAGjBC,GAAiB,UACjBC,GAAmB,YAGnBC,GAAe,OAAOJ,KACtBK,GAAiB,SAASL,KAC1BM,GAAe,OAAON,KACtBO,GAAgB,QAAQP,KACxBQ,GAAyB,QAAQR,KAAcC,KAC/CQ,GAAyB,UAAUT,KAAcC,KACjDS,GAAuB,QAAQV,KAAcC,KAC7CU,GAAoB,OAMpBC,GAAyB,4DACzBC,GAA6B,GAAGD,MAA0BD,KAC1DG,GAAgB,iBAIhBC,GAAgBtV,KAAU,UAAY,YACtCuV,GAAmBvV,KAAU,YAAc,UAC3CwV,GAAmBxV,KAAU,aAAe,eAC5CyV,GAAsBzV,KAAU,eAAiB,aACjD0V,GAAkB1V,KAAU,aAAe,cAC3C2V,GAAiB3V,KAAU,cAAgB,aAG3C4V,GAAY,CAChBC,WAAW,EACX7jB,SAAU,kBACV8jB,QAAS,UACT/pB,OAAQ,CAAC,EAAG,GACZgqB,aAAc,KACd1zB,UAAW,UAEP2zB,GAAgB,CACpBH,UAAW,mBACX7jB,SAAU,mBACV8jB,QAAS,SACT/pB,OAAQ,0BACRgqB,aAAc,yBACd1zB,UAAW,2BAOb,MAAM4zB,WAAiBxN,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKmS,QAAU,KACfnS,KAAKoS,QAAUpS,KAAK4E,SAAS7f,WAE7Bib,KAAKqS,MAAQxM,GAAehhB,KAAKmb,KAAK4E,SAAU0M,IAAe,IAAMzL,GAAeM,KAAKnG,KAAK4E,SAAU0M,IAAe,IAAMzL,GAAeC,QAAQwL,GAAetR,KAAKoS,SACxKpS,KAAKsS,UAAYtS,KAAKuS,eACxB,CAGA,kBAAW7O,GACT,OAAOmO,EACT,CACA,sBAAWlO,GACT,OAAOsO,EACT,CACA,eAAW1V,GACT,OAAOgU,EACT,CAGA,MAAA5I,GACE,OAAO3H,KAAK2P,WAAa3P,KAAK4P,OAAS5P,KAAK6P,MAC9C,CACA,IAAAA,GACE,GAAI3U,GAAW8E,KAAK4E,WAAa5E,KAAK2P,WACpC,OAEF,MAAM7P,EAAgB,CACpBA,cAAeE,KAAK4E,UAGtB,IADkBrE,GAAaqB,QAAQ5B,KAAK4E,SAAUkM,GAAchR,GACtDkC,iBAAd,CASA,GANAhC,KAAKwS,gBAMD,iBAAkBntB,SAASC,kBAAoB0a,KAAKoS,QAAQpX,QAzExC,eA0EtB,IAAK,MAAMzb,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK6Z,UAC/CxF,GAAac,GAAG9hB,EAAS,YAAaqc,IAG1CoE,KAAK4E,SAAS6N,QACdzS,KAAK4E,SAASxjB,aAAa,iBAAiB,GAC5C4e,KAAKqS,MAAMhX,UAAU5E,IAAI0a,IACzBnR,KAAK4E,SAASvJ,UAAU5E,IAAI0a,IAC5B5Q,GAAaqB,QAAQ5B,KAAK4E,SAAUmM,GAAejR,EAhBnD,CAiBF,CACA,IAAA8P,GACE,GAAI1U,GAAW8E,KAAK4E,YAAc5E,KAAK2P,WACrC,OAEF,MAAM7P,EAAgB,CACpBA,cAAeE,KAAK4E,UAEtB5E,KAAK0S,cAAc5S,EACrB,CACA,OAAAiF,GACM/E,KAAKmS,SACPnS,KAAKmS,QAAQnZ,UAEf2L,MAAMI,SACR,CACA,MAAAha,GACEiV,KAAKsS,UAAYtS,KAAKuS,gBAClBvS,KAAKmS,SACPnS,KAAKmS,QAAQpnB,QAEjB,CAGA,aAAA2nB,CAAc5S,GAEZ,IADkBS,GAAaqB,QAAQ5B,KAAK4E,SAAUgM,GAAc9Q,GACtDkC,iBAAd,CAMA,GAAI,iBAAkB3c,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK6Z,UAC/CxF,GAAaC,IAAIjhB,EAAS,YAAaqc,IAGvCoE,KAAKmS,SACPnS,KAAKmS,QAAQnZ,UAEfgH,KAAKqS,MAAMhX,UAAU1B,OAAOwX,IAC5BnR,KAAK4E,SAASvJ,UAAU1B,OAAOwX,IAC/BnR,KAAK4E,SAASxjB,aAAa,gBAAiB,SAC5C4hB,GAAYE,oBAAoBlD,KAAKqS,MAAO,UAC5C9R,GAAaqB,QAAQ5B,KAAK4E,SAAUiM,GAAgB/Q,EAhBpD,CAiBF,CACA,UAAA+D,CAAWC,GAET,GAAgC,iBADhCA,EAASa,MAAMd,WAAWC,IACRxlB,YAA2B,GAAUwlB,EAAOxlB,YAAgE,mBAA3CwlB,EAAOxlB,UAAUgF,sBAElG,MAAM,IAAIkhB,UAAU,GAAG+L,GAAO9L,+GAEhC,OAAOX,CACT,CACA,aAAA0O,GACE,QAAsB,IAAX,EACT,MAAM,IAAIhO,UAAU,gEAEtB,IAAImO,EAAmB3S,KAAK4E,SACG,WAA3B5E,KAAK6E,QAAQvmB,UACfq0B,EAAmB3S,KAAKoS,QACf,GAAUpS,KAAK6E,QAAQvmB,WAChCq0B,EAAmBjY,GAAWsF,KAAK6E,QAAQvmB,WACA,iBAA3B0hB,KAAK6E,QAAQvmB,YAC7Bq0B,EAAmB3S,KAAK6E,QAAQvmB,WAElC,MAAM0zB,EAAehS,KAAK4S,mBAC1B5S,KAAKmS,QAAU,GAAoBQ,EAAkB3S,KAAKqS,MAAOL,EACnE,CACA,QAAArC,GACE,OAAO3P,KAAKqS,MAAMhX,UAAU7W,SAAS2sB,GACvC,CACA,aAAA0B,GACE,MAAMC,EAAiB9S,KAAKoS,QAC5B,GAAIU,EAAezX,UAAU7W,SArKN,WAsKrB,OAAOmtB,GAET,GAAImB,EAAezX,UAAU7W,SAvKJ,aAwKvB,OAAOotB,GAET,GAAIkB,EAAezX,UAAU7W,SAzKA,iBA0K3B,MA5JsB,MA8JxB,GAAIsuB,EAAezX,UAAU7W,SA3KE,mBA4K7B,MA9JyB,SAkK3B,MAAMuuB,EAAkF,QAA1E9tB,iBAAiB+a,KAAKqS,OAAOvX,iBAAiB,iBAAiB6K,OAC7E,OAAImN,EAAezX,UAAU7W,SArLP,UAsLbuuB,EAAQvB,GAAmBD,GAE7BwB,EAAQrB,GAAsBD,EACvC,CACA,aAAAc,GACE,OAAkD,OAA3CvS,KAAK4E,SAAS5J,QAnLD,UAoLtB,CACA,UAAAgY,GACE,MAAM,OACJhrB,GACEgY,KAAK6E,QACT,MAAsB,iBAAX7c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAAS4f,OAAOgQ,SAAS5vB,EAAO,MAEzC,mBAAXqK,EACFirB,GAAcjrB,EAAOirB,EAAYjT,KAAK4E,UAExC5c,CACT,CACA,gBAAA4qB,GACE,MAAMM,EAAwB,CAC5Bx0B,UAAWshB,KAAK6S,gBAChBzc,UAAW,CAAC,CACV9V,KAAM,kBACNmB,QAAS,CACPwM,SAAU+R,KAAK6E,QAAQ5W,WAExB,CACD3N,KAAM,SACNmB,QAAS,CACPuG,OAAQgY,KAAKgT,iBAanB,OAPIhT,KAAKsS,WAAsC,WAAzBtS,KAAK6E,QAAQkN,WACjC/O,GAAYC,iBAAiBjD,KAAKqS,MAAO,SAAU,UACnDa,EAAsB9c,UAAY,CAAC,CACjC9V,KAAM,cACNC,SAAS,KAGN,IACF2yB,KACArW,GAAQmD,KAAK6E,QAAQmN,aAAc,CAACkB,IAE3C,CACA,eAAAC,EAAgB,IACdr2B,EAAG,OACHyP,IAEA,MAAMggB,EAAQ1G,GAAe1T,KAhOF,8DAgO+B6N,KAAKqS,OAAOlsB,QAAO5G,GAAWob,GAAUpb,KAC7FgtB,EAAM7b,QAMXoN,GAAqByO,EAAOhgB,EAAQzP,IAAQ6zB,IAAmBpE,EAAMnL,SAAS7U,IAASkmB,OACzF,CAGA,sBAAOhW,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAO6nB,GAAS5M,oBAAoBtF,KAAM8D,GAChD,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,CACA,iBAAOsP,CAAWhU,GAChB,GA5QuB,IA4QnBA,EAAMwI,QAAgD,UAAfxI,EAAMqB,MA/QnC,QA+QuDrB,EAAMtiB,IACzE,OAEF,MAAMu2B,EAAcxN,GAAe1T,KAAKkf,IACxC,IAAK,MAAM1J,KAAU0L,EAAa,CAChC,MAAMC,EAAUpB,GAAS7M,YAAYsC,GACrC,IAAK2L,IAAyC,IAA9BA,EAAQzO,QAAQiN,UAC9B,SAEF,MAAMyB,EAAenU,EAAMmU,eACrBC,EAAeD,EAAanS,SAASkS,EAAQjB,OACnD,GAAIkB,EAAanS,SAASkS,EAAQ1O,WAA2C,WAA9B0O,EAAQzO,QAAQiN,YAA2B0B,GAA8C,YAA9BF,EAAQzO,QAAQiN,WAA2B0B,EACnJ,SAIF,GAAIF,EAAQjB,MAAM7tB,SAAS4a,EAAM7S,UAA2B,UAAf6S,EAAMqB,MA/RvC,QA+R2DrB,EAAMtiB,KAAqB,qCAAqCuG,KAAK+b,EAAM7S,OAAO0a,UACvJ,SAEF,MAAMnH,EAAgB,CACpBA,cAAewT,EAAQ1O,UAEN,UAAfxF,EAAMqB,OACRX,EAAckH,WAAa5H,GAE7BkU,EAAQZ,cAAc5S,EACxB,CACF,CACA,4BAAO2T,CAAsBrU,GAI3B,MAAMsU,EAAU,kBAAkBrwB,KAAK+b,EAAM7S,OAAO0a,SAC9C0M,EAjTW,WAiTKvU,EAAMtiB,IACtB82B,EAAkB,CAAClD,GAAgBC,IAAkBvP,SAAShC,EAAMtiB,KAC1E,IAAK82B,IAAoBD,EACvB,OAEF,GAAID,IAAYC,EACd,OAEFvU,EAAMkD,iBAGN,MAAMuR,EAAkB7T,KAAKgG,QAAQoL,IAA0BpR,KAAO6F,GAAeM,KAAKnG,KAAMoR,IAAwB,IAAMvL,GAAehhB,KAAKmb,KAAMoR,IAAwB,IAAMvL,GAAeC,QAAQsL,GAAwBhS,EAAMW,eAAehb,YACpPwF,EAAW2nB,GAAS5M,oBAAoBuO,GAC9C,GAAID,EAIF,OAHAxU,EAAM0U,kBACNvpB,EAASslB,YACTtlB,EAAS4oB,gBAAgB/T,GAGvB7U,EAASolB,aAEXvQ,EAAM0U,kBACNvpB,EAASqlB,OACTiE,EAAgBpB,QAEpB,EAOFlS,GAAac,GAAGhc,SAAU4rB,GAAwBG,GAAwBc,GAASuB,uBACnFlT,GAAac,GAAGhc,SAAU4rB,GAAwBK,GAAeY,GAASuB,uBAC1ElT,GAAac,GAAGhc,SAAU2rB,GAAwBkB,GAASkB,YAC3D7S,GAAac,GAAGhc,SAAU6rB,GAAsBgB,GAASkB,YACzD7S,GAAac,GAAGhc,SAAU2rB,GAAwBI,IAAwB,SAAUhS,GAClFA,EAAMkD,iBACN4P,GAAS5M,oBAAoBtF,MAAM2H,QACrC,IAMAxL,GAAmB+V,IAcnB,MAAM6B,GAAS,WAETC,GAAoB,OACpBC,GAAkB,gBAAgBF,KAClCG,GAAY,CAChBC,UAAW,iBACXC,cAAe,KACfhP,YAAY,EACZzK,WAAW,EAEX0Z,YAAa,QAETC,GAAgB,CACpBH,UAAW,SACXC,cAAe,kBACfhP,WAAY,UACZzK,UAAW,UACX0Z,YAAa,oBAOf,MAAME,WAAiB9Q,GACrB,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKwU,aAAc,EACnBxU,KAAK4E,SAAW,IAClB,CAGA,kBAAWlB,GACT,OAAOwQ,EACT,CACA,sBAAWvQ,GACT,OAAO2Q,EACT,CACA,eAAW/X,GACT,OAAOwX,EACT,CAGA,IAAAlE,CAAKxT,GACH,IAAK2D,KAAK6E,QAAQlK,UAEhB,YADAkC,GAAQR,GAGV2D,KAAKyU,UACL,MAAMl1B,EAAUygB,KAAK0U,cACjB1U,KAAK6E,QAAQO,YACfvJ,GAAOtc,GAETA,EAAQ8b,UAAU5E,IAAIud,IACtBhU,KAAK2U,mBAAkB,KACrB9X,GAAQR,EAAS,GAErB,CACA,IAAAuT,CAAKvT,GACE2D,KAAK6E,QAAQlK,WAIlBqF,KAAK0U,cAAcrZ,UAAU1B,OAAOqa,IACpChU,KAAK2U,mBAAkB,KACrB3U,KAAK+E,UACLlI,GAAQR,EAAS,KANjBQ,GAAQR,EAQZ,CACA,OAAA0I,GACO/E,KAAKwU,cAGVjU,GAAaC,IAAIR,KAAK4E,SAAUqP,IAChCjU,KAAK4E,SAASjL,SACdqG,KAAKwU,aAAc,EACrB,CAGA,WAAAE,GACE,IAAK1U,KAAK4E,SAAU,CAClB,MAAMgQ,EAAWvvB,SAASwvB,cAAc,OACxCD,EAAST,UAAYnU,KAAK6E,QAAQsP,UAC9BnU,KAAK6E,QAAQO,YACfwP,EAASvZ,UAAU5E,IApFD,QAsFpBuJ,KAAK4E,SAAWgQ,CAClB,CACA,OAAO5U,KAAK4E,QACd,CACA,iBAAAZ,CAAkBF,GAGhB,OADAA,EAAOuQ,YAAc3Z,GAAWoJ,EAAOuQ,aAChCvQ,CACT,CACA,OAAA2Q,GACE,GAAIzU,KAAKwU,YACP,OAEF,MAAMj1B,EAAUygB,KAAK0U,cACrB1U,KAAK6E,QAAQwP,YAAYS,OAAOv1B,GAChCghB,GAAac,GAAG9hB,EAAS00B,IAAiB,KACxCpX,GAAQmD,KAAK6E,QAAQuP,cAAc,IAErCpU,KAAKwU,aAAc,CACrB,CACA,iBAAAG,CAAkBtY,GAChBW,GAAuBX,EAAU2D,KAAK0U,cAAe1U,KAAK6E,QAAQO,WACpE,EAeF,MAEM2P,GAAc,gBACdC,GAAkB,UAAUD,KAC5BE,GAAoB,cAAcF,KAGlCG,GAAmB,WACnBC,GAAY,CAChBC,WAAW,EACXC,YAAa,MAETC,GAAgB,CACpBF,UAAW,UACXC,YAAa,WAOf,MAAME,WAAkB9R,GACtB,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKwV,WAAY,EACjBxV,KAAKyV,qBAAuB,IAC9B,CAGA,kBAAW/R,GACT,OAAOyR,EACT,CACA,sBAAWxR,GACT,OAAO2R,EACT,CACA,eAAW/Y,GACT,MArCW,WAsCb,CAGA,QAAAmZ,GACM1V,KAAKwV,YAGLxV,KAAK6E,QAAQuQ,WACfpV,KAAK6E,QAAQwQ,YAAY5C,QAE3BlS,GAAaC,IAAInb,SAAU0vB,IAC3BxU,GAAac,GAAGhc,SAAU2vB,IAAiB5V,GAASY,KAAK2V,eAAevW,KACxEmB,GAAac,GAAGhc,SAAU4vB,IAAmB7V,GAASY,KAAK4V,eAAexW,KAC1EY,KAAKwV,WAAY,EACnB,CACA,UAAAK,GACO7V,KAAKwV,YAGVxV,KAAKwV,WAAY,EACjBjV,GAAaC,IAAInb,SAAU0vB,IAC7B,CAGA,cAAAY,CAAevW,GACb,MAAM,YACJiW,GACErV,KAAK6E,QACT,GAAIzF,EAAM7S,SAAWlH,UAAY+Z,EAAM7S,SAAW8oB,GAAeA,EAAY7wB,SAAS4a,EAAM7S,QAC1F,OAEF,MAAM1L,EAAWglB,GAAeU,kBAAkB8O,GAC1B,IAApBx0B,EAAS6P,OACX2kB,EAAY5C,QACHzS,KAAKyV,uBAAyBP,GACvCr0B,EAASA,EAAS6P,OAAS,GAAG+hB,QAE9B5xB,EAAS,GAAG4xB,OAEhB,CACA,cAAAmD,CAAexW,GAzED,QA0ERA,EAAMtiB,MAGVkjB,KAAKyV,qBAAuBrW,EAAM0W,SAAWZ,GA5EzB,UA6EtB,EAeF,MAAMa,GAAyB,oDACzBC,GAA0B,cAC1BC,GAAmB,gBACnBC,GAAkB,eAMxB,MAAMC,GACJ,WAAAhS,GACEnE,KAAK4E,SAAWvf,SAAS6G,IAC3B,CAGA,QAAAkqB,GAEE,MAAMC,EAAgBhxB,SAASC,gBAAgBuC,YAC/C,OAAO1F,KAAKoC,IAAI3E,OAAO02B,WAAaD,EACtC,CACA,IAAAzG,GACE,MAAM/rB,EAAQmc,KAAKoW,WACnBpW,KAAKuW,mBAELvW,KAAKwW,sBAAsBxW,KAAK4E,SAAUqR,IAAkBQ,GAAmBA,EAAkB5yB,IAEjGmc,KAAKwW,sBAAsBT,GAAwBE,IAAkBQ,GAAmBA,EAAkB5yB,IAC1Gmc,KAAKwW,sBAAsBR,GAAyBE,IAAiBO,GAAmBA,EAAkB5yB,GAC5G,CACA,KAAAwO,GACE2N,KAAK0W,wBAAwB1W,KAAK4E,SAAU,YAC5C5E,KAAK0W,wBAAwB1W,KAAK4E,SAAUqR,IAC5CjW,KAAK0W,wBAAwBX,GAAwBE,IACrDjW,KAAK0W,wBAAwBV,GAAyBE,GACxD,CACA,aAAAS,GACE,OAAO3W,KAAKoW,WAAa,CAC3B,CAGA,gBAAAG,GACEvW,KAAK4W,sBAAsB5W,KAAK4E,SAAU,YAC1C5E,KAAK4E,SAAS7jB,MAAM+K,SAAW,QACjC,CACA,qBAAA0qB,CAAsBzc,EAAU8c,EAAexa,GAC7C,MAAMya,EAAiB9W,KAAKoW,WAS5BpW,KAAK+W,2BAA2Bhd,GARHxa,IAC3B,GAAIA,IAAYygB,KAAK4E,UAAYhlB,OAAO02B,WAAa/2B,EAAQsI,YAAcivB,EACzE,OAEF9W,KAAK4W,sBAAsBr3B,EAASs3B,GACpC,MAAMJ,EAAkB72B,OAAOqF,iBAAiB1F,GAASub,iBAAiB+b,GAC1Et3B,EAAQwB,MAAMi2B,YAAYH,EAAe,GAAGxa,EAASkB,OAAOC,WAAWiZ,QAAsB,GAGjG,CACA,qBAAAG,CAAsBr3B,EAASs3B,GAC7B,MAAMI,EAAc13B,EAAQwB,MAAM+Z,iBAAiB+b,GAC/CI,GACFjU,GAAYC,iBAAiB1jB,EAASs3B,EAAeI,EAEzD,CACA,uBAAAP,CAAwB3c,EAAU8c,GAWhC7W,KAAK+W,2BAA2Bhd,GAVHxa,IAC3B,MAAM5B,EAAQqlB,GAAYQ,iBAAiBjkB,EAASs3B,GAEtC,OAAVl5B,GAIJqlB,GAAYE,oBAAoB3jB,EAASs3B,GACzCt3B,EAAQwB,MAAMi2B,YAAYH,EAAel5B,IAJvC4B,EAAQwB,MAAMm2B,eAAeL,EAIgB,GAGnD,CACA,0BAAAE,CAA2Bhd,EAAUod,GACnC,GAAI,GAAUpd,GACZod,EAASpd,QAGX,IAAK,MAAM6L,KAAOC,GAAe1T,KAAK4H,EAAUiG,KAAK4E,UACnDuS,EAASvR,EAEb,EAeF,MAEMwR,GAAc,YAGdC,GAAe,OAAOD,KACtBE,GAAyB,gBAAgBF,KACzCG,GAAiB,SAASH,KAC1BI,GAAe,OAAOJ,KACtBK,GAAgB,QAAQL,KACxBM,GAAiB,SAASN,KAC1BO,GAAsB,gBAAgBP,KACtCQ,GAA0B,oBAAoBR,KAC9CS,GAA0B,kBAAkBT,KAC5CU,GAAyB,QAAQV,cACjCW,GAAkB,aAElBC,GAAoB,OACpBC,GAAoB,eAKpBC,GAAY,CAChBtD,UAAU,EACVnC,OAAO,EACPzH,UAAU,GAENmN,GAAgB,CACpBvD,SAAU,mBACVnC,MAAO,UACPzH,SAAU,WAOZ,MAAMoN,WAAc1T,GAClB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKqY,QAAUxS,GAAeC,QArBV,gBAqBmC9F,KAAK4E,UAC5D5E,KAAKsY,UAAYtY,KAAKuY,sBACtBvY,KAAKwY,WAAaxY,KAAKyY,uBACvBzY,KAAK2P,UAAW,EAChB3P,KAAKmP,kBAAmB,EACxBnP,KAAK0Y,WAAa,IAAIvC,GACtBnW,KAAK6L,oBACP,CAGA,kBAAWnI,GACT,OAAOwU,EACT,CACA,sBAAWvU,GACT,OAAOwU,EACT,CACA,eAAW5b,GACT,MA1DW,OA2Db,CAGA,MAAAoL,CAAO7H,GACL,OAAOE,KAAK2P,SAAW3P,KAAK4P,OAAS5P,KAAK6P,KAAK/P,EACjD,CACA,IAAA+P,CAAK/P,GACCE,KAAK2P,UAAY3P,KAAKmP,kBAGR5O,GAAaqB,QAAQ5B,KAAK4E,SAAU4S,GAAc,CAClE1X,kBAEYkC,mBAGdhC,KAAK2P,UAAW,EAChB3P,KAAKmP,kBAAmB,EACxBnP,KAAK0Y,WAAW9I,OAChBvqB,SAAS6G,KAAKmP,UAAU5E,IAAIshB,IAC5B/X,KAAK2Y,gBACL3Y,KAAKsY,UAAUzI,MAAK,IAAM7P,KAAK4Y,aAAa9Y,KAC9C,CACA,IAAA8P,GACO5P,KAAK2P,WAAY3P,KAAKmP,mBAGT5O,GAAaqB,QAAQ5B,KAAK4E,SAAUyS,IACxCrV,mBAGdhC,KAAK2P,UAAW,EAChB3P,KAAKmP,kBAAmB,EACxBnP,KAAKwY,WAAW3C,aAChB7V,KAAK4E,SAASvJ,UAAU1B,OAAOqe,IAC/BhY,KAAKmF,gBAAe,IAAMnF,KAAK6Y,cAAc7Y,KAAK4E,SAAU5E,KAAKgO,gBACnE,CACA,OAAAjJ,GACExE,GAAaC,IAAI5gB,OAAQw3B,IACzB7W,GAAaC,IAAIR,KAAKqY,QAASjB,IAC/BpX,KAAKsY,UAAUvT,UACf/E,KAAKwY,WAAW3C,aAChBlR,MAAMI,SACR,CACA,YAAA+T,GACE9Y,KAAK2Y,eACP,CAGA,mBAAAJ,GACE,OAAO,IAAIhE,GAAS,CAClB5Z,UAAWmG,QAAQd,KAAK6E,QAAQ+P,UAEhCxP,WAAYpF,KAAKgO,eAErB,CACA,oBAAAyK,GACE,OAAO,IAAIlD,GAAU,CACnBF,YAAarV,KAAK4E,UAEtB,CACA,YAAAgU,CAAa9Y,GAENza,SAAS6G,KAAK1H,SAASwb,KAAK4E,WAC/Bvf,SAAS6G,KAAK4oB,OAAO9U,KAAK4E,UAE5B5E,KAAK4E,SAAS7jB,MAAMgxB,QAAU,QAC9B/R,KAAK4E,SAASzjB,gBAAgB,eAC9B6e,KAAK4E,SAASxjB,aAAa,cAAc,GACzC4e,KAAK4E,SAASxjB,aAAa,OAAQ,UACnC4e,KAAK4E,SAASnZ,UAAY,EAC1B,MAAMstB,EAAYlT,GAAeC,QA7GT,cA6GsC9F,KAAKqY,SAC/DU,IACFA,EAAUttB,UAAY,GAExBoQ,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIuhB,IAU5BhY,KAAKmF,gBATsB,KACrBnF,KAAK6E,QAAQ4N,OACfzS,KAAKwY,WAAW9C,WAElB1V,KAAKmP,kBAAmB,EACxB5O,GAAaqB,QAAQ5B,KAAK4E,SAAU6S,GAAe,CACjD3X,iBACA,GAEoCE,KAAKqY,QAASrY,KAAKgO,cAC7D,CACA,kBAAAnC,GACEtL,GAAac,GAAGrB,KAAK4E,SAAUiT,IAAyBzY,IAhJvC,WAiJXA,EAAMtiB,MAGNkjB,KAAK6E,QAAQmG,SACfhL,KAAK4P,OAGP5P,KAAKgZ,6BAA4B,IAEnCzY,GAAac,GAAGzhB,OAAQ83B,IAAgB,KAClC1X,KAAK2P,WAAa3P,KAAKmP,kBACzBnP,KAAK2Y,eACP,IAEFpY,GAAac,GAAGrB,KAAK4E,SAAUgT,IAAyBxY,IAEtDmB,GAAae,IAAItB,KAAK4E,SAAU+S,IAAqBsB,IAC/CjZ,KAAK4E,WAAaxF,EAAM7S,QAAUyT,KAAK4E,WAAaqU,EAAO1sB,SAGjC,WAA1ByT,KAAK6E,QAAQ+P,SAIb5U,KAAK6E,QAAQ+P,UACf5U,KAAK4P,OAJL5P,KAAKgZ,6BAKP,GACA,GAEN,CACA,UAAAH,GACE7Y,KAAK4E,SAAS7jB,MAAMgxB,QAAU,OAC9B/R,KAAK4E,SAASxjB,aAAa,eAAe,GAC1C4e,KAAK4E,SAASzjB,gBAAgB,cAC9B6e,KAAK4E,SAASzjB,gBAAgB,QAC9B6e,KAAKmP,kBAAmB,EACxBnP,KAAKsY,UAAU1I,MAAK,KAClBvqB,SAAS6G,KAAKmP,UAAU1B,OAAOoe,IAC/B/X,KAAKkZ,oBACLlZ,KAAK0Y,WAAWrmB,QAChBkO,GAAaqB,QAAQ5B,KAAK4E,SAAU2S,GAAe,GAEvD,CACA,WAAAvJ,GACE,OAAOhO,KAAK4E,SAASvJ,UAAU7W,SAjLT,OAkLxB,CACA,0BAAAw0B,GAEE,GADkBzY,GAAaqB,QAAQ5B,KAAK4E,SAAU0S,IACxCtV,iBACZ,OAEF,MAAMmX,EAAqBnZ,KAAK4E,SAASvX,aAAehI,SAASC,gBAAgBsC,aAC3EwxB,EAAmBpZ,KAAK4E,SAAS7jB,MAAMiL,UAEpB,WAArBotB,GAAiCpZ,KAAK4E,SAASvJ,UAAU7W,SAASyzB,MAGjEkB,IACHnZ,KAAK4E,SAAS7jB,MAAMiL,UAAY,UAElCgU,KAAK4E,SAASvJ,UAAU5E,IAAIwhB,IAC5BjY,KAAKmF,gBAAe,KAClBnF,KAAK4E,SAASvJ,UAAU1B,OAAOse,IAC/BjY,KAAKmF,gBAAe,KAClBnF,KAAK4E,SAAS7jB,MAAMiL,UAAYotB,CAAgB,GAC/CpZ,KAAKqY,QAAQ,GACfrY,KAAKqY,SACRrY,KAAK4E,SAAS6N,QAChB,CAMA,aAAAkG,GACE,MAAMQ,EAAqBnZ,KAAK4E,SAASvX,aAAehI,SAASC,gBAAgBsC,aAC3EkvB,EAAiB9W,KAAK0Y,WAAWtC,WACjCiD,EAAoBvC,EAAiB,EAC3C,GAAIuC,IAAsBF,EAAoB,CAC5C,MAAMr3B,EAAWma,KAAU,cAAgB,eAC3C+D,KAAK4E,SAAS7jB,MAAMe,GAAY,GAAGg1B,KACrC,CACA,IAAKuC,GAAqBF,EAAoB,CAC5C,MAAMr3B,EAAWma,KAAU,eAAiB,cAC5C+D,KAAK4E,SAAS7jB,MAAMe,GAAY,GAAGg1B,KACrC,CACF,CACA,iBAAAoC,GACElZ,KAAK4E,SAAS7jB,MAAMu4B,YAAc,GAClCtZ,KAAK4E,SAAS7jB,MAAMw4B,aAAe,EACrC,CAGA,sBAAO9c,CAAgBqH,EAAQhE,GAC7B,OAAOE,KAAKwH,MAAK,WACf,MAAMnd,EAAO+tB,GAAM9S,oBAAoBtF,KAAM8D,GAC7C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQhE,EAJb,CAKF,GACF,EAOFS,GAAac,GAAGhc,SAAUyyB,GA9OK,4BA8O2C,SAAU1Y,GAClF,MAAM7S,EAASsZ,GAAec,uBAAuB3G,MACjD,CAAC,IAAK,QAAQoB,SAASpB,KAAKiH,UAC9B7H,EAAMkD,iBAER/B,GAAae,IAAI/U,EAAQirB,IAAcgC,IACjCA,EAAUxX,kBAIdzB,GAAae,IAAI/U,EAAQgrB,IAAgB,KACnC5c,GAAUqF,OACZA,KAAKyS,OACP,GACA,IAIJ,MAAMgH,EAAc5T,GAAeC,QAnQb,eAoQlB2T,GACFrB,GAAM/S,YAAYoU,GAAa7J,OAEpBwI,GAAM9S,oBAAoB/Y,GAClCob,OAAO3H,KACd,IACA6G,GAAqBuR,IAMrBjc,GAAmBic,IAcnB,MAEMsB,GAAc,gBACdC,GAAiB,YACjBC,GAAwB,OAAOF,KAAcC,KAE7CE,GAAoB,OACpBC,GAAuB,UACvBC,GAAoB,SAEpBC,GAAgB,kBAChBC,GAAe,OAAOP,KACtBQ,GAAgB,QAAQR,KACxBS,GAAe,OAAOT,KACtBU,GAAuB,gBAAgBV,KACvCW,GAAiB,SAASX,KAC1BY,GAAe,SAASZ,KACxBa,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAwB,kBAAkBd,KAE1Ce,GAAY,CAChB7F,UAAU,EACV5J,UAAU,EACVvgB,QAAQ,GAEJiwB,GAAgB,CACpB9F,SAAU,mBACV5J,SAAU,UACVvgB,OAAQ,WAOV,MAAMkwB,WAAkBjW,GACtB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAK2P,UAAW,EAChB3P,KAAKsY,UAAYtY,KAAKuY,sBACtBvY,KAAKwY,WAAaxY,KAAKyY,uBACvBzY,KAAK6L,oBACP,CAGA,kBAAWnI,GACT,OAAO+W,EACT,CACA,sBAAW9W,GACT,OAAO+W,EACT,CACA,eAAWne,GACT,MApDW,WAqDb,CAGA,MAAAoL,CAAO7H,GACL,OAAOE,KAAK2P,SAAW3P,KAAK4P,OAAS5P,KAAK6P,KAAK/P,EACjD,CACA,IAAA+P,CAAK/P,GACCE,KAAK2P,UAGSpP,GAAaqB,QAAQ5B,KAAK4E,SAAUqV,GAAc,CAClEna,kBAEYkC,mBAGdhC,KAAK2P,UAAW,EAChB3P,KAAKsY,UAAUzI,OACV7P,KAAK6E,QAAQpa,SAChB,IAAI0rB,IAAkBvG,OAExB5P,KAAK4E,SAASxjB,aAAa,cAAc,GACzC4e,KAAK4E,SAASxjB,aAAa,OAAQ,UACnC4e,KAAK4E,SAASvJ,UAAU5E,IAAIqjB,IAW5B9Z,KAAKmF,gBAVoB,KAClBnF,KAAK6E,QAAQpa,SAAUuV,KAAK6E,QAAQ+P,UACvC5U,KAAKwY,WAAW9C,WAElB1V,KAAK4E,SAASvJ,UAAU5E,IAAIojB,IAC5B7Z,KAAK4E,SAASvJ,UAAU1B,OAAOmgB,IAC/BvZ,GAAaqB,QAAQ5B,KAAK4E,SAAUsV,GAAe,CACjDpa,iBACA,GAEkCE,KAAK4E,UAAU,GACvD,CACA,IAAAgL,GACO5P,KAAK2P,WAGQpP,GAAaqB,QAAQ5B,KAAK4E,SAAUuV,IACxCnY,mBAGdhC,KAAKwY,WAAW3C,aAChB7V,KAAK4E,SAASgW,OACd5a,KAAK2P,UAAW,EAChB3P,KAAK4E,SAASvJ,UAAU5E,IAAIsjB,IAC5B/Z,KAAKsY,UAAU1I,OAUf5P,KAAKmF,gBAToB,KACvBnF,KAAK4E,SAASvJ,UAAU1B,OAAOkgB,GAAmBE,IAClD/Z,KAAK4E,SAASzjB,gBAAgB,cAC9B6e,KAAK4E,SAASzjB,gBAAgB,QACzB6e,KAAK6E,QAAQpa,SAChB,IAAI0rB,IAAkB9jB,QAExBkO,GAAaqB,QAAQ5B,KAAK4E,SAAUyV,GAAe,GAEfra,KAAK4E,UAAU,IACvD,CACA,OAAAG,GACE/E,KAAKsY,UAAUvT,UACf/E,KAAKwY,WAAW3C,aAChBlR,MAAMI,SACR,CAGA,mBAAAwT,GACE,MASM5d,EAAYmG,QAAQd,KAAK6E,QAAQ+P,UACvC,OAAO,IAAIL,GAAS,CAClBJ,UA3HsB,qBA4HtBxZ,YACAyK,YAAY,EACZiP,YAAarU,KAAK4E,SAAS7f,WAC3BqvB,cAAezZ,EAfK,KACU,WAA1BqF,KAAK6E,QAAQ+P,SAIjB5U,KAAK4P,OAHHrP,GAAaqB,QAAQ5B,KAAK4E,SAAUwV,GAG3B,EAUgC,MAE/C,CACA,oBAAA3B,GACE,OAAO,IAAIlD,GAAU,CACnBF,YAAarV,KAAK4E,UAEtB,CACA,kBAAAiH,GACEtL,GAAac,GAAGrB,KAAK4E,SAAU4V,IAAuBpb,IA5IvC,WA6ITA,EAAMtiB,MAGNkjB,KAAK6E,QAAQmG,SACfhL,KAAK4P,OAGPrP,GAAaqB,QAAQ5B,KAAK4E,SAAUwV,IAAqB,GAE7D,CAGA,sBAAO3d,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOswB,GAAUrV,oBAAoBtF,KAAM8D,GACjD,GAAsB,iBAAXA,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KAJb,CAKF,GACF,EAOFO,GAAac,GAAGhc,SAAUk1B,GA7JK,gCA6J2C,SAAUnb,GAClF,MAAM7S,EAASsZ,GAAec,uBAAuB3G,MAIrD,GAHI,CAAC,IAAK,QAAQoB,SAASpB,KAAKiH,UAC9B7H,EAAMkD,iBAEJpH,GAAW8E,MACb,OAEFO,GAAae,IAAI/U,EAAQ8tB,IAAgB,KAEnC1f,GAAUqF,OACZA,KAAKyS,OACP,IAIF,MAAMgH,EAAc5T,GAAeC,QAAQkU,IACvCP,GAAeA,IAAgBltB,GACjCouB,GAAUtV,YAAYoU,GAAa7J,OAExB+K,GAAUrV,oBAAoB/Y,GACtCob,OAAO3H,KACd,IACAO,GAAac,GAAGzhB,OAAQg6B,IAAuB,KAC7C,IAAK,MAAM7f,KAAY8L,GAAe1T,KAAK6nB,IACzCW,GAAUrV,oBAAoBvL,GAAU8V,MAC1C,IAEFtP,GAAac,GAAGzhB,OAAQ06B,IAAc,KACpC,IAAK,MAAM/6B,KAAWsmB,GAAe1T,KAAK,gDACG,UAAvClN,iBAAiB1F,GAASiC,UAC5Bm5B,GAAUrV,oBAAoB/lB,GAASqwB,MAE3C,IAEF/I,GAAqB8T,IAMrBxe,GAAmBwe,IAUnB,MACME,GAAmB,CAEvB,IAAK,CAAC,QAAS,MAAO,KAAM,OAAQ,OAHP,kBAI7BhqB,EAAG,CAAC,SAAU,OAAQ,QAAS,OAC/BiqB,KAAM,GACNhqB,EAAG,GACHiqB,GAAI,GACJC,IAAK,GACLC,KAAM,GACNC,GAAI,GACJC,IAAK,GACLC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJxqB,EAAG,GACH0b,IAAK,CAAC,MAAO,SAAU,MAAO,QAAS,QAAS,UAChD+O,GAAI,GACJC,GAAI,GACJC,EAAG,GACHC,IAAK,GACLC,EAAG,GACHC,MAAO,GACPC,KAAM,GACNC,IAAK,GACLC,IAAK,GACLC,OAAQ,GACRC,EAAG,GACHC,GAAI,IAIAC,GAAgB,IAAIpmB,IAAI,CAAC,aAAc,OAAQ,OAAQ,WAAY,WAAY,SAAU,MAAO,eAShGqmB,GAAmB,0DACnBC,GAAmB,CAAC76B,EAAW86B,KACnC,MAAMC,EAAgB/6B,EAAUvC,SAASC,cACzC,OAAIo9B,EAAqBzb,SAAS0b,IAC5BJ,GAAc/lB,IAAImmB,IACbhc,QAAQ6b,GAAiBt5B,KAAKtB,EAAUg7B,YAM5CF,EAAqB12B,QAAO62B,GAAkBA,aAA0BzY,SAAQ9R,MAAKwqB,GAASA,EAAM55B,KAAKy5B,IAAe,EA0C3HI,GAAY,CAChBC,UAAWtC,GACXuC,QAAS,CAAC,EAEVC,WAAY,GACZxwB,MAAM,EACNywB,UAAU,EACVC,WAAY,KACZC,SAAU,eAENC,GAAgB,CACpBN,UAAW,SACXC,QAAS,SACTC,WAAY,oBACZxwB,KAAM,UACNywB,SAAU,UACVC,WAAY,kBACZC,SAAU,UAENE,GAAqB,CACzBC,MAAO,iCACP5jB,SAAU,oBAOZ,MAAM6jB,WAAwBna,GAC5B,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,EACjC,CAGA,kBAAWJ,GACT,OAAOwZ,EACT,CACA,sBAAWvZ,GACT,OAAO8Z,EACT,CACA,eAAWlhB,GACT,MA3CW,iBA4Cb,CAGA,UAAAshB,GACE,OAAO7gC,OAAOmiB,OAAOa,KAAK6E,QAAQuY,SAASt6B,KAAIghB,GAAU9D,KAAK8d,yBAAyBha,KAAS3d,OAAO2a,QACzG,CACA,UAAAid,GACE,OAAO/d,KAAK6d,aAAantB,OAAS,CACpC,CACA,aAAAstB,CAAcZ,GAMZ,OALApd,KAAKie,cAAcb,GACnBpd,KAAK6E,QAAQuY,QAAU,IAClBpd,KAAK6E,QAAQuY,WACbA,GAEEpd,IACT,CACA,MAAAke,GACE,MAAMC,EAAkB94B,SAASwvB,cAAc,OAC/CsJ,EAAgBC,UAAYpe,KAAKqe,eAAere,KAAK6E,QAAQ2Y,UAC7D,IAAK,MAAOzjB,EAAUukB,KAASthC,OAAOmkB,QAAQnB,KAAK6E,QAAQuY,SACzDpd,KAAKue,YAAYJ,EAAiBG,EAAMvkB,GAE1C,MAAMyjB,EAAWW,EAAgBpY,SAAS,GACpCsX,EAAard,KAAK8d,yBAAyB9d,KAAK6E,QAAQwY,YAI9D,OAHIA,GACFG,EAASniB,UAAU5E,OAAO4mB,EAAWn7B,MAAM,MAEtCs7B,CACT,CAGA,gBAAAvZ,CAAiBH,GACfa,MAAMV,iBAAiBH,GACvB9D,KAAKie,cAAcna,EAAOsZ,QAC5B,CACA,aAAAa,CAAcO,GACZ,IAAK,MAAOzkB,EAAUqjB,KAAYpgC,OAAOmkB,QAAQqd,GAC/C7Z,MAAMV,iBAAiB,CACrBlK,WACA4jB,MAAOP,GACNM,GAEP,CACA,WAAAa,CAAYf,EAAUJ,EAASrjB,GAC7B,MAAM0kB,EAAkB5Y,GAAeC,QAAQ/L,EAAUyjB,GACpDiB,KAGLrB,EAAUpd,KAAK8d,yBAAyBV,IAKpC,GAAUA,GACZpd,KAAK0e,sBAAsBhkB,GAAW0iB,GAAUqB,GAG9Cze,KAAK6E,QAAQhY,KACf4xB,EAAgBL,UAAYpe,KAAKqe,eAAejB,GAGlDqB,EAAgBE,YAAcvB,EAX5BqB,EAAgB9kB,SAYpB,CACA,cAAA0kB,CAAeG,GACb,OAAOxe,KAAK6E,QAAQyY,SApJxB,SAAsBsB,EAAYzB,EAAW0B,GAC3C,IAAKD,EAAWluB,OACd,OAAOkuB,EAET,GAAIC,GAAgD,mBAArBA,EAC7B,OAAOA,EAAiBD,GAE1B,MACME,GADY,IAAIl/B,OAAOm/B,WACKC,gBAAgBJ,EAAY,aACxD/9B,EAAW,GAAGlC,UAAUmgC,EAAgB5yB,KAAKkU,iBAAiB,MACpE,IAAK,MAAM7gB,KAAWsB,EAAU,CAC9B,MAAMo+B,EAAc1/B,EAAQC,SAASC,cACrC,IAAKzC,OAAO4D,KAAKu8B,GAAW/b,SAAS6d,GAAc,CACjD1/B,EAAQoa,SACR,QACF,CACA,MAAMulB,EAAgB,GAAGvgC,UAAUY,EAAQ0B,YACrCk+B,EAAoB,GAAGxgC,OAAOw+B,EAAU,MAAQ,GAAIA,EAAU8B,IAAgB,IACpF,IAAK,MAAMl9B,KAAam9B,EACjBtC,GAAiB76B,EAAWo9B,IAC/B5/B,EAAQ4B,gBAAgBY,EAAUvC,SAGxC,CACA,OAAOs/B,EAAgB5yB,KAAKkyB,SAC9B,CA2HmCgB,CAAaZ,EAAKxe,KAAK6E,QAAQsY,UAAWnd,KAAK6E,QAAQ0Y,YAAciB,CACtG,CACA,wBAAAV,CAAyBU,GACvB,OAAO3hB,GAAQ2hB,EAAK,CAACxe,MACvB,CACA,qBAAA0e,CAAsBn/B,EAASk/B,GAC7B,GAAIze,KAAK6E,QAAQhY,KAGf,OAFA4xB,EAAgBL,UAAY,QAC5BK,EAAgB3J,OAAOv1B,GAGzBk/B,EAAgBE,YAAcp/B,EAAQo/B,WACxC,EAeF,MACMU,GAAwB,IAAI/oB,IAAI,CAAC,WAAY,YAAa,eAC1DgpB,GAAoB,OAEpBC,GAAoB,OACpBC,GAAyB,iBACzBC,GAAiB,SACjBC,GAAmB,gBACnBC,GAAgB,QAChBC,GAAgB,QAahBC,GAAgB,CACpBC,KAAM,OACNC,IAAK,MACLC,MAAO/jB,KAAU,OAAS,QAC1BgkB,OAAQ,SACRC,KAAMjkB,KAAU,QAAU,QAEtBkkB,GAAY,CAChBhD,UAAWtC,GACXuF,WAAW,EACXnyB,SAAU,kBACVoyB,WAAW,EACXC,YAAa,GACbC,MAAO,EACPvwB,mBAAoB,CAAC,MAAO,QAAS,SAAU,QAC/CnD,MAAM,EACN7E,OAAQ,CAAC,EAAG,GACZtJ,UAAW,MACXszB,aAAc,KACdsL,UAAU,EACVC,WAAY,KACZxjB,UAAU,EACVyjB,SAAU,+GACVgD,MAAO,GACP5e,QAAS,eAEL6e,GAAgB,CACpBtD,UAAW,SACXiD,UAAW,UACXnyB,SAAU,mBACVoyB,UAAW,2BACXC,YAAa,oBACbC,MAAO,kBACPvwB,mBAAoB,QACpBnD,KAAM,UACN7E,OAAQ,0BACRtJ,UAAW,oBACXszB,aAAc,yBACdsL,SAAU,UACVC,WAAY,kBACZxjB,SAAU,mBACVyjB,SAAU,SACVgD,MAAO,4BACP5e,QAAS,UAOX,MAAM8e,WAAgBhc,GACpB,WAAAP,CAAY5kB,EAASukB,GACnB,QAAsB,IAAX,EACT,MAAM,IAAIU,UAAU,+DAEtBG,MAAMplB,EAASukB,GAGf9D,KAAK2gB,YAAa,EAClB3gB,KAAK4gB,SAAW,EAChB5gB,KAAK6gB,WAAa,KAClB7gB,KAAK8gB,eAAiB,CAAC,EACvB9gB,KAAKmS,QAAU,KACfnS,KAAK+gB,iBAAmB,KACxB/gB,KAAKghB,YAAc,KAGnBhhB,KAAKihB,IAAM,KACXjhB,KAAKkhB,gBACAlhB,KAAK6E,QAAQ9K,UAChBiG,KAAKmhB,WAET,CAGA,kBAAWzd,GACT,OAAOyc,EACT,CACA,sBAAWxc,GACT,OAAO8c,EACT,CACA,eAAWlkB,GACT,MAxGW,SAyGb,CAGA,MAAA6kB,GACEphB,KAAK2gB,YAAa,CACpB,CACA,OAAAU,GACErhB,KAAK2gB,YAAa,CACpB,CACA,aAAAW,GACEthB,KAAK2gB,YAAc3gB,KAAK2gB,UAC1B,CACA,MAAAhZ,GACO3H,KAAK2gB,aAGV3gB,KAAK8gB,eAAeS,OAASvhB,KAAK8gB,eAAeS,MAC7CvhB,KAAK2P,WACP3P,KAAKwhB,SAGPxhB,KAAKyhB,SACP,CACA,OAAA1c,GACEmI,aAAalN,KAAK4gB,UAClBrgB,GAAaC,IAAIR,KAAK4E,SAAS5J,QAAQykB,IAAiBC,GAAkB1f,KAAK0hB,mBAC3E1hB,KAAK4E,SAASpJ,aAAa,2BAC7BwE,KAAK4E,SAASxjB,aAAa,QAAS4e,KAAK4E,SAASpJ,aAAa,2BAEjEwE,KAAK2hB,iBACLhd,MAAMI,SACR,CACA,IAAA8K,GACE,GAAoC,SAAhC7P,KAAK4E,SAAS7jB,MAAMgxB,QACtB,MAAM,IAAInO,MAAM,uCAElB,IAAM5D,KAAK4hB,mBAAoB5hB,KAAK2gB,WAClC,OAEF,MAAMnH,EAAYjZ,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAlItD,SAoIXqc,GADapmB,GAAeuE,KAAK4E,WACL5E,KAAK4E,SAAS9kB,cAAcwF,iBAAiBd,SAASwb,KAAK4E,UAC7F,GAAI4U,EAAUxX,mBAAqB6f,EACjC,OAIF7hB,KAAK2hB,iBACL,MAAMV,EAAMjhB,KAAK8hB,iBACjB9hB,KAAK4E,SAASxjB,aAAa,mBAAoB6/B,EAAIzlB,aAAa,OAChE,MAAM,UACJ6kB,GACErgB,KAAK6E,QAYT,GAXK7E,KAAK4E,SAAS9kB,cAAcwF,gBAAgBd,SAASwb,KAAKihB,OAC7DZ,EAAUvL,OAAOmM,GACjB1gB,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAhJpC,cAkJnBxF,KAAKmS,QAAUnS,KAAKwS,cAAcyO,GAClCA,EAAI5lB,UAAU5E,IAAI8oB,IAMd,iBAAkBl6B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK6Z,UAC/CxF,GAAac,GAAG9hB,EAAS,YAAaqc,IAU1CoE,KAAKmF,gBAPY,KACf5E,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAhKrC,WAiKQ,IAApBxF,KAAK6gB,YACP7gB,KAAKwhB,SAEPxhB,KAAK6gB,YAAa,CAAK,GAEK7gB,KAAKihB,IAAKjhB,KAAKgO,cAC/C,CACA,IAAA4B,GACE,GAAK5P,KAAK2P,aAGQpP,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UA/KtD,SAgLHxD,iBAAd,CAQA,GALYhC,KAAK8hB,iBACbzmB,UAAU1B,OAAO4lB,IAIjB,iBAAkBl6B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK6Z,UAC/CxF,GAAaC,IAAIjhB,EAAS,YAAaqc,IAG3CoE,KAAK8gB,eAA4B,OAAI,EACrC9gB,KAAK8gB,eAAelB,KAAiB,EACrC5f,KAAK8gB,eAAenB,KAAiB,EACrC3f,KAAK6gB,WAAa,KAYlB7gB,KAAKmF,gBAVY,KACXnF,KAAK+hB,yBAGJ/hB,KAAK6gB,YACR7gB,KAAK2hB,iBAEP3hB,KAAK4E,SAASzjB,gBAAgB,oBAC9Bof,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAzMpC,WAyM8D,GAEnDxF,KAAKihB,IAAKjhB,KAAKgO,cA1B7C,CA2BF,CACA,MAAAjjB,GACMiV,KAAKmS,SACPnS,KAAKmS,QAAQpnB,QAEjB,CAGA,cAAA62B,GACE,OAAO9gB,QAAQd,KAAKgiB,YACtB,CACA,cAAAF,GAIE,OAHK9hB,KAAKihB,MACRjhB,KAAKihB,IAAMjhB,KAAKiiB,kBAAkBjiB,KAAKghB,aAAehhB,KAAKkiB,2BAEtDliB,KAAKihB,GACd,CACA,iBAAAgB,CAAkB7E,GAChB,MAAM6D,EAAMjhB,KAAKmiB,oBAAoB/E,GAASc,SAG9C,IAAK+C,EACH,OAAO,KAETA,EAAI5lB,UAAU1B,OAAO2lB,GAAmBC,IAExC0B,EAAI5lB,UAAU5E,IAAI,MAAMuJ,KAAKmE,YAAY5H,aACzC,MAAM6lB,EAvuGKC,KACb,GACEA,GAAUlgC,KAAKmgC,MA/BH,IA+BSngC,KAAKogC,gBACnBl9B,SAASm9B,eAAeH,IACjC,OAAOA,CAAM,EAmuGGI,CAAOziB,KAAKmE,YAAY5H,MAAM1c,WAK5C,OAJAohC,EAAI7/B,aAAa,KAAMghC,GACnBpiB,KAAKgO,eACPiT,EAAI5lB,UAAU5E,IAAI6oB,IAEb2B,CACT,CACA,UAAAyB,CAAWtF,GACTpd,KAAKghB,YAAc5D,EACfpd,KAAK2P,aACP3P,KAAK2hB,iBACL3hB,KAAK6P,OAET,CACA,mBAAAsS,CAAoB/E,GAYlB,OAXIpd,KAAK+gB,iBACP/gB,KAAK+gB,iBAAiB/C,cAAcZ,GAEpCpd,KAAK+gB,iBAAmB,IAAInD,GAAgB,IACvC5d,KAAK6E,QAGRuY,UACAC,WAAYrd,KAAK8d,yBAAyB9d,KAAK6E,QAAQyb,eAGpDtgB,KAAK+gB,gBACd,CACA,sBAAAmB,GACE,MAAO,CACL,CAAC1C,IAAyBxf,KAAKgiB,YAEnC,CACA,SAAAA,GACE,OAAOhiB,KAAK8d,yBAAyB9d,KAAK6E,QAAQ2b,QAAUxgB,KAAK4E,SAASpJ,aAAa,yBACzF,CAGA,4BAAAmnB,CAA6BvjB,GAC3B,OAAOY,KAAKmE,YAAYmB,oBAAoBlG,EAAMW,eAAgBC,KAAK4iB,qBACzE,CACA,WAAA5U,GACE,OAAOhO,KAAK6E,QAAQub,WAAapgB,KAAKihB,KAAOjhB,KAAKihB,IAAI5lB,UAAU7W,SAAS86B,GAC3E,CACA,QAAA3P,GACE,OAAO3P,KAAKihB,KAAOjhB,KAAKihB,IAAI5lB,UAAU7W,SAAS+6B,GACjD,CACA,aAAA/M,CAAcyO,GACZ,MAAMviC,EAAYme,GAAQmD,KAAK6E,QAAQnmB,UAAW,CAACshB,KAAMihB,EAAKjhB,KAAK4E,WAC7Die,EAAahD,GAAcnhC,EAAU+lB,eAC3C,OAAO,GAAoBzE,KAAK4E,SAAUqc,EAAKjhB,KAAK4S,iBAAiBiQ,GACvE,CACA,UAAA7P,GACE,MAAM,OACJhrB,GACEgY,KAAK6E,QACT,MAAsB,iBAAX7c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAAS4f,OAAOgQ,SAAS5vB,EAAO,MAEzC,mBAAXqK,EACFirB,GAAcjrB,EAAOirB,EAAYjT,KAAK4E,UAExC5c,CACT,CACA,wBAAA81B,CAAyBU,GACvB,OAAO3hB,GAAQ2hB,EAAK,CAACxe,KAAK4E,UAC5B,CACA,gBAAAgO,CAAiBiQ,GACf,MAAM3P,EAAwB,CAC5Bx0B,UAAWmkC,EACXzsB,UAAW,CAAC,CACV9V,KAAM,OACNmB,QAAS,CACPuO,mBAAoBgQ,KAAK6E,QAAQ7U,qBAElC,CACD1P,KAAM,SACNmB,QAAS,CACPuG,OAAQgY,KAAKgT,eAEd,CACD1yB,KAAM,kBACNmB,QAAS,CACPwM,SAAU+R,KAAK6E,QAAQ5W,WAExB,CACD3N,KAAM,QACNmB,QAAS,CACPlC,QAAS,IAAIygB,KAAKmE,YAAY5H,eAE/B,CACDjc,KAAM,kBACNC,SAAS,EACTC,MAAO,aACPC,GAAI4J,IAGF2V,KAAK8hB,iBAAiB1gC,aAAa,wBAAyBiJ,EAAK1J,MAAMjC,UAAU,KAIvF,MAAO,IACFw0B,KACArW,GAAQmD,KAAK6E,QAAQmN,aAAc,CAACkB,IAE3C,CACA,aAAAgO,GACE,MAAM4B,EAAW9iB,KAAK6E,QAAQjD,QAAQ1f,MAAM,KAC5C,IAAK,MAAM0f,KAAWkhB,EACpB,GAAgB,UAAZlhB,EACFrB,GAAac,GAAGrB,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAjVlC,SAiV4DxF,KAAK6E,QAAQ9K,UAAUqF,IAC/EY,KAAK2iB,6BAA6BvjB,GAC1CuI,QAAQ,SAEb,GA3VU,WA2VN/F,EAA4B,CACrC,MAAMmhB,EAAUnhB,IAAY+d,GAAgB3f,KAAKmE,YAAYqB,UAnV5C,cAmV0ExF,KAAKmE,YAAYqB,UArV5F,WAsVVwd,EAAWphB,IAAY+d,GAAgB3f,KAAKmE,YAAYqB,UAnV7C,cAmV2ExF,KAAKmE,YAAYqB,UArV5F,YAsVjBjF,GAAac,GAAGrB,KAAK4E,SAAUme,EAAS/iB,KAAK6E,QAAQ9K,UAAUqF,IAC7D,MAAMkU,EAAUtT,KAAK2iB,6BAA6BvjB,GAClDkU,EAAQwN,eAA8B,YAAf1hB,EAAMqB,KAAqBmf,GAAgBD,KAAiB,EACnFrM,EAAQmO,QAAQ,IAElBlhB,GAAac,GAAGrB,KAAK4E,SAAUoe,EAAUhjB,KAAK6E,QAAQ9K,UAAUqF,IAC9D,MAAMkU,EAAUtT,KAAK2iB,6BAA6BvjB,GAClDkU,EAAQwN,eAA8B,aAAf1hB,EAAMqB,KAAsBmf,GAAgBD,IAAiBrM,EAAQ1O,SAASpgB,SAAS4a,EAAMU,eACpHwT,EAAQkO,QAAQ,GAEpB,CAEFxhB,KAAK0hB,kBAAoB,KACnB1hB,KAAK4E,UACP5E,KAAK4P,MACP,EAEFrP,GAAac,GAAGrB,KAAK4E,SAAS5J,QAAQykB,IAAiBC,GAAkB1f,KAAK0hB,kBAChF,CACA,SAAAP,GACE,MAAMX,EAAQxgB,KAAK4E,SAASpJ,aAAa,SACpCglB,IAGAxgB,KAAK4E,SAASpJ,aAAa,eAAkBwE,KAAK4E,SAAS+Z,YAAYhZ,QAC1E3F,KAAK4E,SAASxjB,aAAa,aAAco/B,GAE3CxgB,KAAK4E,SAASxjB,aAAa,yBAA0Bo/B,GACrDxgB,KAAK4E,SAASzjB,gBAAgB,SAChC,CACA,MAAAsgC,GACMzhB,KAAK2P,YAAc3P,KAAK6gB,WAC1B7gB,KAAK6gB,YAAa,GAGpB7gB,KAAK6gB,YAAa,EAClB7gB,KAAKijB,aAAY,KACXjjB,KAAK6gB,YACP7gB,KAAK6P,MACP,GACC7P,KAAK6E,QAAQ0b,MAAM1Q,MACxB,CACA,MAAA2R,GACMxhB,KAAK+hB,yBAGT/hB,KAAK6gB,YAAa,EAClB7gB,KAAKijB,aAAY,KACVjjB,KAAK6gB,YACR7gB,KAAK4P,MACP,GACC5P,KAAK6E,QAAQ0b,MAAM3Q,MACxB,CACA,WAAAqT,CAAYrlB,EAASslB,GACnBhW,aAAalN,KAAK4gB,UAClB5gB,KAAK4gB,SAAW/iB,WAAWD,EAASslB,EACtC,CACA,oBAAAnB,GACE,OAAO/kC,OAAOmiB,OAAOa,KAAK8gB,gBAAgB1f,UAAS,EACrD,CACA,UAAAyC,CAAWC,GACT,MAAMqf,EAAiBngB,GAAYG,kBAAkBnD,KAAK4E,UAC1D,IAAK,MAAMwe,KAAiBpmC,OAAO4D,KAAKuiC,GAClC9D,GAAsB1oB,IAAIysB,WACrBD,EAAeC,GAU1B,OAPAtf,EAAS,IACJqf,KACmB,iBAAXrf,GAAuBA,EAASA,EAAS,CAAC,GAEvDA,EAAS9D,KAAK+D,gBAAgBD,GAC9BA,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CACA,iBAAAE,CAAkBF,GAchB,OAbAA,EAAOuc,WAAiC,IAArBvc,EAAOuc,UAAsBh7B,SAAS6G,KAAOwO,GAAWoJ,EAAOuc,WACtD,iBAAjBvc,EAAOyc,QAChBzc,EAAOyc,MAAQ,CACb1Q,KAAM/L,EAAOyc,MACb3Q,KAAM9L,EAAOyc,QAGW,iBAAjBzc,EAAO0c,QAChB1c,EAAO0c,MAAQ1c,EAAO0c,MAAM3gC,YAEA,iBAAnBikB,EAAOsZ,UAChBtZ,EAAOsZ,QAAUtZ,EAAOsZ,QAAQv9B,YAE3BikB,CACT,CACA,kBAAA8e,GACE,MAAM9e,EAAS,CAAC,EAChB,IAAK,MAAOhnB,EAAKa,KAAUX,OAAOmkB,QAAQnB,KAAK6E,SACzC7E,KAAKmE,YAAYT,QAAQ5mB,KAASa,IACpCmmB,EAAOhnB,GAAOa,GASlB,OANAmmB,EAAO/J,UAAW,EAClB+J,EAAOlC,QAAU,SAKVkC,CACT,CACA,cAAA6d,GACM3hB,KAAKmS,UACPnS,KAAKmS,QAAQnZ,UACbgH,KAAKmS,QAAU,MAEbnS,KAAKihB,MACPjhB,KAAKihB,IAAItnB,SACTqG,KAAKihB,IAAM,KAEf,CAGA,sBAAOxkB,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOq2B,GAAQpb,oBAAoBtF,KAAM8D,GAC/C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOF3H,GAAmBukB,IAcnB,MACM2C,GAAiB,kBACjBC,GAAmB,gBACnBC,GAAY,IACb7C,GAAQhd,QACX0Z,QAAS,GACTp1B,OAAQ,CAAC,EAAG,GACZtJ,UAAW,QACX8+B,SAAU,8IACV5b,QAAS,SAEL4hB,GAAgB,IACjB9C,GAAQ/c,YACXyZ,QAAS,kCAOX,MAAMqG,WAAgB/C,GAEpB,kBAAWhd,GACT,OAAO6f,EACT,CACA,sBAAW5f,GACT,OAAO6f,EACT,CACA,eAAWjnB,GACT,MA7BW,SA8Bb,CAGA,cAAAqlB,GACE,OAAO5hB,KAAKgiB,aAAehiB,KAAK0jB,aAClC,CAGA,sBAAAxB,GACE,MAAO,CACL,CAACmB,IAAiBrjB,KAAKgiB,YACvB,CAACsB,IAAmBtjB,KAAK0jB,cAE7B,CACA,WAAAA,GACE,OAAO1jB,KAAK8d,yBAAyB9d,KAAK6E,QAAQuY,QACpD,CAGA,sBAAO3gB,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOo5B,GAAQne,oBAAoBtF,KAAM8D,GAC/C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOF3H,GAAmBsnB,IAcnB,MAEME,GAAc,gBAEdC,GAAiB,WAAWD,KAC5BE,GAAc,QAAQF,KACtBG,GAAwB,OAAOH,cAE/BI,GAAsB,SAEtBC,GAAwB,SAExBC,GAAqB,YAGrBC,GAAsB,GAAGD,mBAA+CA,uBAGxEE,GAAY,CAChBn8B,OAAQ,KAERo8B,WAAY,eACZC,cAAc,EACd93B,OAAQ,KACR+3B,UAAW,CAAC,GAAK,GAAK,IAElBC,GAAgB,CACpBv8B,OAAQ,gBAERo8B,WAAY,SACZC,aAAc,UACd93B,OAAQ,UACR+3B,UAAW,SAOb,MAAME,WAAkB9f,GACtB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GAGf9D,KAAKykB,aAAe,IAAIvzB,IACxB8O,KAAK0kB,oBAAsB,IAAIxzB,IAC/B8O,KAAK2kB,aAA6D,YAA9C1/B,iBAAiB+a,KAAK4E,UAAU5Y,UAA0B,KAAOgU,KAAK4E,SAC1F5E,KAAK4kB,cAAgB,KACrB5kB,KAAK6kB,UAAY,KACjB7kB,KAAK8kB,oBAAsB,CACzBC,gBAAiB,EACjBC,gBAAiB,GAEnBhlB,KAAKilB,SACP,CAGA,kBAAWvhB,GACT,OAAOygB,EACT,CACA,sBAAWxgB,GACT,OAAO4gB,EACT,CACA,eAAWhoB,GACT,MAhEW,WAiEb,CAGA,OAAA0oB,GACEjlB,KAAKklB,mCACLllB,KAAKmlB,2BACDnlB,KAAK6kB,UACP7kB,KAAK6kB,UAAUO,aAEfplB,KAAK6kB,UAAY7kB,KAAKqlB,kBAExB,IAAK,MAAMC,KAAWtlB,KAAK0kB,oBAAoBvlB,SAC7Ca,KAAK6kB,UAAUU,QAAQD,EAE3B,CACA,OAAAvgB,GACE/E,KAAK6kB,UAAUO,aACfzgB,MAAMI,SACR,CAGA,iBAAAf,CAAkBF,GAShB,OAPAA,EAAOvX,OAASmO,GAAWoJ,EAAOvX,SAAWlH,SAAS6G,KAGtD4X,EAAOsgB,WAAatgB,EAAO9b,OAAS,GAAG8b,EAAO9b,oBAAsB8b,EAAOsgB,WAC3C,iBAArBtgB,EAAOwgB,YAChBxgB,EAAOwgB,UAAYxgB,EAAOwgB,UAAUpiC,MAAM,KAAKY,KAAInF,GAAS4f,OAAOC,WAAW7f,MAEzEmmB,CACT,CACA,wBAAAqhB,GACOnlB,KAAK6E,QAAQwf,eAKlB9jB,GAAaC,IAAIR,KAAK6E,QAAQtY,OAAQs3B,IACtCtjB,GAAac,GAAGrB,KAAK6E,QAAQtY,OAAQs3B,GAAaG,IAAuB5kB,IACvE,MAAMomB,EAAoBxlB,KAAK0kB,oBAAoBvnC,IAAIiiB,EAAM7S,OAAOtB,MACpE,GAAIu6B,EAAmB,CACrBpmB,EAAMkD,iBACN,MAAM3G,EAAOqE,KAAK2kB,cAAgB/kC,OAC5BmE,EAASyhC,EAAkBnhC,UAAY2b,KAAK4E,SAASvgB,UAC3D,GAAIsX,EAAK8pB,SAKP,YAJA9pB,EAAK8pB,SAAS,CACZ9jC,IAAKoC,EACL2hC,SAAU,WAMd/pB,EAAKlQ,UAAY1H,CACnB,KAEJ,CACA,eAAAshC,GACE,MAAM5jC,EAAU,CACdka,KAAMqE,KAAK2kB,aACXL,UAAWtkB,KAAK6E,QAAQyf,UACxBF,WAAYpkB,KAAK6E,QAAQuf,YAE3B,OAAO,IAAIuB,sBAAqBxkB,GAAWnB,KAAK4lB,kBAAkBzkB,IAAU1f,EAC9E,CAGA,iBAAAmkC,CAAkBzkB,GAChB,MAAM0kB,EAAgBlI,GAAS3d,KAAKykB,aAAatnC,IAAI,IAAIwgC,EAAMpxB,OAAO4N,MAChEub,EAAWiI,IACf3d,KAAK8kB,oBAAoBC,gBAAkBpH,EAAMpxB,OAAOlI,UACxD2b,KAAK8lB,SAASD,EAAclI,GAAO,EAE/BqH,GAAmBhlB,KAAK2kB,cAAgBt/B,SAASC,iBAAiBmG,UAClEs6B,EAAkBf,GAAmBhlB,KAAK8kB,oBAAoBE,gBACpEhlB,KAAK8kB,oBAAoBE,gBAAkBA,EAC3C,IAAK,MAAMrH,KAASxc,EAAS,CAC3B,IAAKwc,EAAMqI,eAAgB,CACzBhmB,KAAK4kB,cAAgB,KACrB5kB,KAAKimB,kBAAkBJ,EAAclI,IACrC,QACF,CACA,MAAMuI,EAA2BvI,EAAMpxB,OAAOlI,WAAa2b,KAAK8kB,oBAAoBC,gBAEpF,GAAIgB,GAAmBG,GAGrB,GAFAxQ,EAASiI,IAEJqH,EACH,YAMCe,GAAoBG,GACvBxQ,EAASiI,EAEb,CACF,CACA,gCAAAuH,GACEllB,KAAKykB,aAAe,IAAIvzB,IACxB8O,KAAK0kB,oBAAsB,IAAIxzB,IAC/B,MAAMi1B,EAActgB,GAAe1T,KAAK6xB,GAAuBhkB,KAAK6E,QAAQtY,QAC5E,IAAK,MAAM65B,KAAUD,EAAa,CAEhC,IAAKC,EAAOn7B,MAAQiQ,GAAWkrB,GAC7B,SAEF,MAAMZ,EAAoB3f,GAAeC,QAAQugB,UAAUD,EAAOn7B,MAAO+U,KAAK4E,UAG1EjK,GAAU6qB,KACZxlB,KAAKykB,aAAa1yB,IAAIs0B,UAAUD,EAAOn7B,MAAOm7B,GAC9CpmB,KAAK0kB,oBAAoB3yB,IAAIq0B,EAAOn7B,KAAMu6B,GAE9C,CACF,CACA,QAAAM,CAASv5B,GACHyT,KAAK4kB,gBAAkBr4B,IAG3ByT,KAAKimB,kBAAkBjmB,KAAK6E,QAAQtY,QACpCyT,KAAK4kB,cAAgBr4B,EACrBA,EAAO8O,UAAU5E,IAAIstB,IACrB/jB,KAAKsmB,iBAAiB/5B,GACtBgU,GAAaqB,QAAQ5B,KAAK4E,SAAUgf,GAAgB,CAClD9jB,cAAevT,IAEnB,CACA,gBAAA+5B,CAAiB/5B,GAEf,GAAIA,EAAO8O,UAAU7W,SA9LQ,iBA+L3BqhB,GAAeC,QArLc,mBAqLsBvZ,EAAOyO,QAtLtC,cAsLkEK,UAAU5E,IAAIstB,SAGtG,IAAK,MAAMwC,KAAa1gB,GAAeI,QAAQ1Z,EA9LnB,qBAiM1B,IAAK,MAAMxJ,KAAQ8iB,GAAeM,KAAKogB,EAAWrC,IAChDnhC,EAAKsY,UAAU5E,IAAIstB,GAGzB,CACA,iBAAAkC,CAAkBxhC,GAChBA,EAAO4W,UAAU1B,OAAOoqB,IACxB,MAAMyC,EAAc3gB,GAAe1T,KAAK,GAAG6xB,MAAyBD,KAAuBt/B,GAC3F,IAAK,MAAM9E,KAAQ6mC,EACjB7mC,EAAK0b,UAAU1B,OAAOoqB,GAE1B,CAGA,sBAAOtnB,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAOm6B,GAAUlf,oBAAoBtF,KAAM8D,GACjD,GAAsB,iBAAXA,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOFvD,GAAac,GAAGzhB,OAAQkkC,IAAuB,KAC7C,IAAK,MAAM2C,KAAO5gB,GAAe1T,KApOT,0BAqOtBqyB,GAAUlf,oBAAoBmhB,EAChC,IAOFtqB,GAAmBqoB,IAcnB,MAEMkC,GAAc,UACdC,GAAe,OAAOD,KACtBE,GAAiB,SAASF,KAC1BG,GAAe,OAAOH,KACtBI,GAAgB,QAAQJ,KACxBK,GAAuB,QAAQL,KAC/BM,GAAgB,UAAUN,KAC1BO,GAAsB,OAAOP,KAC7BQ,GAAiB,YACjBC,GAAkB,aAClBC,GAAe,UACfC,GAAiB,YACjBC,GAAW,OACXC,GAAU,MACVC,GAAoB,SACpBC,GAAoB,OACpBC,GAAoB,OAEpBC,GAA2B,mBAE3BC,GAA+B,QAAQD,MAIvCE,GAAuB,2EACvBC,GAAsB,YAFOF,uBAAiDA,mBAA6CA,OAE/EC,KAC5CE,GAA8B,IAAIP,8BAA6CA,+BAA8CA,4BAMnI,MAAMQ,WAAYtjB,GAChB,WAAAP,CAAY5kB,GACVolB,MAAMplB,GACNygB,KAAKoS,QAAUpS,KAAK4E,SAAS5J,QAdN,uCAelBgF,KAAKoS,UAOVpS,KAAKioB,sBAAsBjoB,KAAKoS,QAASpS,KAAKkoB,gBAC9C3nB,GAAac,GAAGrB,KAAK4E,SAAUoiB,IAAe5nB,GAASY,KAAK6M,SAASzN,KACvE,CAGA,eAAW7C,GACT,MAnDW,KAoDb,CAGA,IAAAsT,GAEE,MAAMsY,EAAYnoB,KAAK4E,SACvB,GAAI5E,KAAKooB,cAAcD,GACrB,OAIF,MAAME,EAASroB,KAAKsoB,iBACdC,EAAYF,EAAS9nB,GAAaqB,QAAQymB,EAAQ1B,GAAc,CACpE7mB,cAAeqoB,IACZ,KACa5nB,GAAaqB,QAAQumB,EAAWtB,GAAc,CAC9D/mB,cAAeuoB,IAEHrmB,kBAAoBumB,GAAaA,EAAUvmB,mBAGzDhC,KAAKwoB,YAAYH,EAAQF,GACzBnoB,KAAKyoB,UAAUN,EAAWE,GAC5B,CAGA,SAAAI,CAAUlpC,EAASmpC,GACZnpC,IAGLA,EAAQ8b,UAAU5E,IAAI+wB,IACtBxnB,KAAKyoB,UAAU5iB,GAAec,uBAAuBpnB,IAcrDygB,KAAKmF,gBAZY,KACsB,QAAjC5lB,EAAQic,aAAa,SAIzBjc,EAAQ4B,gBAAgB,YACxB5B,EAAQ6B,aAAa,iBAAiB,GACtC4e,KAAK2oB,gBAAgBppC,GAAS,GAC9BghB,GAAaqB,QAAQriB,EAASunC,GAAe,CAC3ChnB,cAAe4oB,KAPfnpC,EAAQ8b,UAAU5E,IAAIixB,GAQtB,GAE0BnoC,EAASA,EAAQ8b,UAAU7W,SAASijC,KACpE,CACA,WAAAe,CAAYjpC,EAASmpC,GACdnpC,IAGLA,EAAQ8b,UAAU1B,OAAO6tB,IACzBjoC,EAAQq7B,OACR5a,KAAKwoB,YAAY3iB,GAAec,uBAAuBpnB,IAcvDygB,KAAKmF,gBAZY,KACsB,QAAjC5lB,EAAQic,aAAa,SAIzBjc,EAAQ6B,aAAa,iBAAiB,GACtC7B,EAAQ6B,aAAa,WAAY,MACjC4e,KAAK2oB,gBAAgBppC,GAAS,GAC9BghB,GAAaqB,QAAQriB,EAASqnC,GAAgB,CAC5C9mB,cAAe4oB,KAPfnpC,EAAQ8b,UAAU1B,OAAO+tB,GAQzB,GAE0BnoC,EAASA,EAAQ8b,UAAU7W,SAASijC,KACpE,CACA,QAAA5a,CAASzN,GACP,IAAK,CAAC8nB,GAAgBC,GAAiBC,GAAcC,GAAgBC,GAAUC,IAASnmB,SAAShC,EAAMtiB,KACrG,OAEFsiB,EAAM0U,kBACN1U,EAAMkD,iBACN,MAAMyD,EAAW/F,KAAKkoB,eAAe/hC,QAAO5G,IAAY2b,GAAW3b,KACnE,IAAIqpC,EACJ,GAAI,CAACtB,GAAUC,IAASnmB,SAAShC,EAAMtiB,KACrC8rC,EAAoB7iB,EAAS3G,EAAMtiB,MAAQwqC,GAAW,EAAIvhB,EAASrV,OAAS,OACvE,CACL,MAAM8c,EAAS,CAAC2Z,GAAiBE,IAAgBjmB,SAAShC,EAAMtiB,KAChE8rC,EAAoB9qB,GAAqBiI,EAAU3G,EAAM7S,OAAQihB,GAAQ,EAC3E,CACIob,IACFA,EAAkBnW,MAAM,CACtBoW,eAAe,IAEjBb,GAAI1iB,oBAAoBsjB,GAAmB/Y,OAE/C,CACA,YAAAqY,GAEE,OAAOriB,GAAe1T,KAAK21B,GAAqB9nB,KAAKoS,QACvD,CACA,cAAAkW,GACE,OAAOtoB,KAAKkoB,eAAe/1B,MAAKzN,GAASsb,KAAKooB,cAAc1jC,MAAW,IACzE,CACA,qBAAAujC,CAAsBxjC,EAAQshB,GAC5B/F,KAAK8oB,yBAAyBrkC,EAAQ,OAAQ,WAC9C,IAAK,MAAMC,KAASqhB,EAClB/F,KAAK+oB,6BAA6BrkC,EAEtC,CACA,4BAAAqkC,CAA6BrkC,GAC3BA,EAAQsb,KAAKgpB,iBAAiBtkC,GAC9B,MAAMukC,EAAWjpB,KAAKooB,cAAc1jC,GAC9BwkC,EAAYlpB,KAAKmpB,iBAAiBzkC,GACxCA,EAAMtD,aAAa,gBAAiB6nC,GAChCC,IAAcxkC,GAChBsb,KAAK8oB,yBAAyBI,EAAW,OAAQ,gBAE9CD,GACHvkC,EAAMtD,aAAa,WAAY,MAEjC4e,KAAK8oB,yBAAyBpkC,EAAO,OAAQ,OAG7Csb,KAAKopB,mCAAmC1kC,EAC1C,CACA,kCAAA0kC,CAAmC1kC,GACjC,MAAM6H,EAASsZ,GAAec,uBAAuBjiB,GAChD6H,IAGLyT,KAAK8oB,yBAAyBv8B,EAAQ,OAAQ,YAC1C7H,EAAMyV,IACR6F,KAAK8oB,yBAAyBv8B,EAAQ,kBAAmB,GAAG7H,EAAMyV,MAEtE,CACA,eAAAwuB,CAAgBppC,EAAS8pC,GACvB,MAAMH,EAAYlpB,KAAKmpB,iBAAiB5pC,GACxC,IAAK2pC,EAAU7tB,UAAU7W,SApKN,YAqKjB,OAEF,MAAMmjB,EAAS,CAAC5N,EAAUoa,KACxB,MAAM50B,EAAUsmB,GAAeC,QAAQ/L,EAAUmvB,GAC7C3pC,GACFA,EAAQ8b,UAAUsM,OAAOwM,EAAWkV,EACtC,EAEF1hB,EAAOggB,GAA0BH,IACjC7f,EA5K2B,iBA4KI+f,IAC/BwB,EAAU9nC,aAAa,gBAAiBioC,EAC1C,CACA,wBAAAP,CAAyBvpC,EAASwC,EAAWpE,GACtC4B,EAAQgc,aAAaxZ,IACxBxC,EAAQ6B,aAAaW,EAAWpE,EAEpC,CACA,aAAAyqC,CAAc9Y,GACZ,OAAOA,EAAKjU,UAAU7W,SAASgjC,GACjC,CAGA,gBAAAwB,CAAiB1Z,GACf,OAAOA,EAAKtJ,QAAQ8hB,IAAuBxY,EAAOzJ,GAAeC,QAAQgiB,GAAqBxY,EAChG,CAGA,gBAAA6Z,CAAiB7Z,GACf,OAAOA,EAAKtU,QA5LO,gCA4LoBsU,CACzC,CAGA,sBAAO7S,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAO29B,GAAI1iB,oBAAoBtF,MACrC,GAAsB,iBAAX8D,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOFvD,GAAac,GAAGhc,SAAU0hC,GAAsBc,IAAsB,SAAUzoB,GAC1E,CAAC,IAAK,QAAQgC,SAASpB,KAAKiH,UAC9B7H,EAAMkD,iBAEJpH,GAAW8E,OAGfgoB,GAAI1iB,oBAAoBtF,MAAM6P,MAChC,IAKAtP,GAAac,GAAGzhB,OAAQqnC,IAAqB,KAC3C,IAAK,MAAM1nC,KAAWsmB,GAAe1T,KAAK41B,IACxCC,GAAI1iB,oBAAoB/lB,EAC1B,IAMF4c,GAAmB6rB,IAcnB,MAEMhjB,GAAY,YACZskB,GAAkB,YAAYtkB,KAC9BukB,GAAiB,WAAWvkB,KAC5BwkB,GAAgB,UAAUxkB,KAC1BykB,GAAiB,WAAWzkB,KAC5B0kB,GAAa,OAAO1kB,KACpB2kB,GAAe,SAAS3kB,KACxB4kB,GAAa,OAAO5kB,KACpB6kB,GAAc,QAAQ7kB,KAEtB8kB,GAAkB,OAClBC,GAAkB,OAClBC,GAAqB,UACrBrmB,GAAc,CAClByc,UAAW,UACX6J,SAAU,UACV1J,MAAO,UAEH7c,GAAU,CACd0c,WAAW,EACX6J,UAAU,EACV1J,MAAO,KAOT,MAAM2J,WAAcxlB,GAClB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAK4gB,SAAW,KAChB5gB,KAAKmqB,sBAAuB,EAC5BnqB,KAAKoqB,yBAA0B,EAC/BpqB,KAAKkhB,eACP,CAGA,kBAAWxd,GACT,OAAOA,EACT,CACA,sBAAWC,GACT,OAAOA,EACT,CACA,eAAWpH,GACT,MA/CS,OAgDX,CAGA,IAAAsT,GACoBtP,GAAaqB,QAAQ5B,KAAK4E,SAAUglB,IACxC5nB,mBAGdhC,KAAKqqB,gBACDrqB,KAAK6E,QAAQub,WACfpgB,KAAK4E,SAASvJ,UAAU5E,IA/CN,QAsDpBuJ,KAAK4E,SAASvJ,UAAU1B,OAAOmwB,IAC/BjuB,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIszB,GAAiBC,IAC7ChqB,KAAKmF,gBARY,KACfnF,KAAK4E,SAASvJ,UAAU1B,OAAOqwB,IAC/BzpB,GAAaqB,QAAQ5B,KAAK4E,SAAUilB,IACpC7pB,KAAKsqB,oBAAoB,GAKGtqB,KAAK4E,SAAU5E,KAAK6E,QAAQub,WAC5D,CACA,IAAAxQ,GACO5P,KAAKuqB,YAGQhqB,GAAaqB,QAAQ5B,KAAK4E,SAAU8kB,IACxC1nB,mBAQdhC,KAAK4E,SAASvJ,UAAU5E,IAAIuzB,IAC5BhqB,KAAKmF,gBANY,KACfnF,KAAK4E,SAASvJ,UAAU5E,IAAIqzB,IAC5B9pB,KAAK4E,SAASvJ,UAAU1B,OAAOqwB,GAAoBD,IACnDxpB,GAAaqB,QAAQ5B,KAAK4E,SAAU+kB,GAAa,GAGrB3pB,KAAK4E,SAAU5E,KAAK6E,QAAQub,YAC5D,CACA,OAAArb,GACE/E,KAAKqqB,gBACDrqB,KAAKuqB,WACPvqB,KAAK4E,SAASvJ,UAAU1B,OAAOowB,IAEjCplB,MAAMI,SACR,CACA,OAAAwlB,GACE,OAAOvqB,KAAK4E,SAASvJ,UAAU7W,SAASulC,GAC1C,CAIA,kBAAAO,GACOtqB,KAAK6E,QAAQolB,WAGdjqB,KAAKmqB,sBAAwBnqB,KAAKoqB,0BAGtCpqB,KAAK4gB,SAAW/iB,YAAW,KACzBmC,KAAK4P,MAAM,GACV5P,KAAK6E,QAAQ0b,QAClB,CACA,cAAAiK,CAAeprB,EAAOqrB,GACpB,OAAQrrB,EAAMqB,MACZ,IAAK,YACL,IAAK,WAEDT,KAAKmqB,qBAAuBM,EAC5B,MAEJ,IAAK,UACL,IAAK,WAEDzqB,KAAKoqB,wBAA0BK,EAIrC,GAAIA,EAEF,YADAzqB,KAAKqqB,gBAGP,MAAM5c,EAAcrO,EAAMU,cACtBE,KAAK4E,WAAa6I,GAAezN,KAAK4E,SAASpgB,SAASipB,IAG5DzN,KAAKsqB,oBACP,CACA,aAAApJ,GACE3gB,GAAac,GAAGrB,KAAK4E,SAAU0kB,IAAiBlqB,GAASY,KAAKwqB,eAAeprB,GAAO,KACpFmB,GAAac,GAAGrB,KAAK4E,SAAU2kB,IAAgBnqB,GAASY,KAAKwqB,eAAeprB,GAAO,KACnFmB,GAAac,GAAGrB,KAAK4E,SAAU4kB,IAAepqB,GAASY,KAAKwqB,eAAeprB,GAAO,KAClFmB,GAAac,GAAGrB,KAAK4E,SAAU6kB,IAAgBrqB,GAASY,KAAKwqB,eAAeprB,GAAO,IACrF,CACA,aAAAirB,GACEnd,aAAalN,KAAK4gB,UAClB5gB,KAAK4gB,SAAW,IAClB,CAGA,sBAAOnkB,CAAgBqH,GACrB,OAAO9D,KAAKwH,MAAK,WACf,MAAMnd,EAAO6/B,GAAM5kB,oBAAoBtF,KAAM8D,GAC7C,GAAsB,iBAAXA,EAAqB,CAC9B,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KACf,CACF,GACF,ECr0IK,SAAS0qB,GAAcruB,GACD,WAAvBhX,SAASuX,WAAyBP,IACjChX,SAASyF,iBAAiB,mBAAoBuR,EACrD,CDy0IAwK,GAAqBqjB,IAMrB/tB,GAAmB+tB,IEpyInBQ,IAzCA,WAC2B,GAAGt4B,MAAM5U,KAChC6H,SAAS+a,iBAAiB,+BAETtd,KAAI,SAAU6nC,GAC/B,OAAO,IAAI,GAAkBA,EAAkB,CAC7CpK,MAAO,CAAE1Q,KAAM,IAAKD,KAAM,MAE9B,GACF,IAiCA8a,IA5BA,WACYrlC,SAASm9B,eAAe,mBAC9B13B,iBAAiB,SAAS,WAC5BzF,SAAS6G,KAAKT,UAAY,EAC1BpG,SAASC,gBAAgBmG,UAAY,CACvC,GACF,IAuBAi/B,IArBA,WACE,IAAIE,EAAMvlC,SAASm9B,eAAe,mBAC9BqI,EAASxlC,SACVylC,uBAAuB,aAAa,GACpCxnC,wBACH1D,OAAOkL,iBAAiB,UAAU,WAC5BkV,KAAK+qB,UAAY/qB,KAAKgrB,SAAWhrB,KAAKgrB,QAAUH,EAAOjtC,OACzDgtC,EAAI7pC,MAAMgxB,QAAU,QAEpB6Y,EAAI7pC,MAAMgxB,QAAU,OAEtB/R,KAAK+qB,UAAY/qB,KAAKgrB,OACxB,GACF,IAUAprC,OAAOqrC,UAAY","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.3 (https://getbootstrap.com/)\n * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.3';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null;\n }\n return selector ? selector.split(',').map(sel => parseSelector(sel)).join(',') : null;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n dd: [],\n div: [],\n dl: [],\n dt: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both