From 3c55db4b82ebf57a989675fb3334aefc83b8b438 Mon Sep 17 00:00:00 2001 From: Michael Noseworthy Date: Mon, 16 Dec 2024 20:41:01 -0500 Subject: [PATCH] Adds Factory contact-rich manipulation tasks to IsaacLab (#1520) # Description This MR adds new tasks for contact-rich manipulation based on the Factory line of work. Tasks include peg insertion, gear meshing, and nut threading. ## Type of change - New feature (non-breaking change which adds functionality) - This change requires a documentation update ## Screenshots peg_insertion gear_meshing nut_threading ## Checklist - [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] I have updated the changelog and the corresponding version in the extension's `config/extension.toml` file - [x] I have added my name to the `CONTRIBUTORS.md` or my name already exists there --------- Signed-off-by: Kelly Guo Signed-off-by: Kelly Guo Co-authored-by: Iretiayo Akinola Co-authored-by: Kelly Guo Co-authored-by: Kelly Guo --- CONTRIBUTORS.md | 3 + .../_static/tasks/factory/gear_mesh.jpg | Bin 0 -> 47279 bytes .../_static/tasks/factory/nut_thread.jpg | Bin 0 -> 44543 bytes .../_static/tasks/factory/peg_insert.jpg | Bin 0 -> 43938 bytes docs/source/overview/environments.rst | 45 + .../config/extension.toml | 2 +- .../omni.isaac.lab_tasks/docs/CHANGELOG.rst | 14 + .../lab_tasks/direct/factory/__init__.py | 44 + .../direct/factory/agents/__init__.py | 4 + .../factory/agents/rl_games_ppo_cfg.yaml | 118 +++ .../direct/factory/factory_control.py | 196 ++++ .../lab_tasks/direct/factory/factory_env.py | 880 ++++++++++++++++++ .../direct/factory/factory_env_cfg.py | 208 +++++ .../direct/factory/factory_tasks_cfg.py | 448 +++++++++ source/standalone/workflows/rl_games/play.py | 2 +- tools/per_test_timeouts.py | 2 +- 16 files changed, 1963 insertions(+), 3 deletions(-) create mode 100644 docs/source/_static/tasks/factory/gear_mesh.jpg create mode 100644 docs/source/_static/tasks/factory/nut_thread.jpg create mode 100644 docs/source/_static/tasks/factory/peg_insert.jpg create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/__init__.py create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/__init__.py create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_control.py create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env.py create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env_cfg.py create mode 100644 source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_tasks_cfg.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bd48566556..5ae174987b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -50,6 +50,7 @@ Guidelines for modifications: * Haoran Zhou * HoJin Jeon * Hongwei Xiong +* Iretiayo Akinola * Jan Kerner * Jean Tampon * Jia Lin Yuan @@ -63,6 +64,7 @@ Guidelines for modifications: * Lorenz Wellhausen * Masoud Moghani * Michael Gussert +* Michael Noseworthy * Muhong Guo * Nuralem Abizov * Oyindamola Omotuyi @@ -91,3 +93,4 @@ Guidelines for modifications: * Gavriel State * Hammad Mazhar * Marco Hutter +* Yashraj Narang diff --git a/docs/source/_static/tasks/factory/gear_mesh.jpg b/docs/source/_static/tasks/factory/gear_mesh.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af26e5818a4febde88a33656f9e821e17f27b186 GIT binary patch literal 47279 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 Kt0qMcKQ@|@-Ct4lf|c#gA%afCq3sp zhr)wy=Shj<5ZJ6XN>3>zn~N$v)EZA?6n5!tXL$=825ccDpDF^|+B7JrmjkqZtjWcX zhG;F>>B`qZr@fYZL;yOcBajX~#8DQNAQ6;SU=J~cV~= zO|;E#a=z0E91!E-L`%0#Mr99f^O(6gOaby?&WaI~CD~PCw(rT2m?-yPbfDLs;@sH( z05@qoPa_ZLE`7EB>7d53woNu>E}fjpa`2JM+zx0oFK(ok*S6D8LaoX`Mi2SRjQm9a zEH0w3RhP08E`6Zqzu!<_f=j!Lr_`;jqcCIIhydKGjJr!6KT^D!>MdpMT^N&Y%0NC| zR2FY_Ij=3ng^i*hU+Y(q$nVGkfFbU0)pXra8etzWLKfxk#%L@_94%_r@Q9uW`k;_; zkA`#fpe3UAP$-6ZEy4S_ucXx!nSj0Zm)a-h31O19W*n=54<7=t8$ zd(SEe#d&Lc52&3|aRhwa#}*48GeETDPTE6frApSz_vLUnz4Q`^qIY$U6#yLea4M1W zph(x3VWNji;)@^#mH2u5C?TpumH{BUxwn!-kML-f-28dsg2Gx|M__JEtkGNJkYo$` z&;;DZEW}XyyO|H&S2*{9$j}l~c#y|oHP;Mp)+kgfbN3I{f)c}JrOnzTlH1^9@s4NR z*PQ~2d+U^zYfFDtKmUr3tEM11sUAi>JZu72Ntsfh9ka$^fua@w{K%DpZF0%8bi=>>(?|I z8eW|pjkJ*6-wBppLMB{bcy*!yY_`)p_7>{hoM5`3kC`8>6=vQmYk-9sI-R?i=1=PwA8W{wK@KP{H0EoR03hU0uNXdE^Z

0t)cPRO6u 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 Y%|%tzZe_5&k{GyLXE~8UKRToT*>aaU^8f$< literal 0 HcmV?d00001 diff --git a/docs/source/_static/tasks/factory/nut_thread.jpg b/docs/source/_static/tasks/factory/nut_thread.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a3155e6c47edf1691669afecf0381cd83cf4026 GIT binary patch literal 44543 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 Kyp~*=5C7SvXJuCa literal 0 HcmV?d00001 diff --git a/docs/source/_static/tasks/factory/peg_insert.jpg b/docs/source/_static/tasks/factory/peg_insert.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7aaa2c145081a4e3583f7bb2919aeba156f42acb GIT binary patch literal 43938 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 D%@p2w literal 0 HcmV?d00001 diff --git a/docs/source/overview/environments.rst b/docs/source/overview/environments.rst index 95ace6c61a..6559bec4d8 100644 --- a/docs/source/overview/environments.rst +++ b/docs/source/overview/environments.rst @@ -152,6 +152,39 @@ for the lift-cube environment: .. |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 `__ +Contact-rich Manipulation +~~~~~~~~~~~~ + +Environments based on contact-rich manipulation tasks such as peg insertion, gear meshing and nut-bolt fastening. + +These tasks share the same task configurations and control options. You can switch between them by specifying the task name. +For example: + +* |factory-peg-link|: Peg insertion with the Franka arm +* |factory-gear-link|: Gear meshing with the Franka arm +* |factory-nut-link|: Nut-Bolt fastening with the Franka arm + +.. table:: + :widths: 33 37 30 + + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | World | Environment ID | Description | + +====================+=========================+=============================================================================+ + | |factory-peg| | |factory-peg-link| | Insert peg into the socket with the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |factory-gear| | |factory-gear-link| | Insert and mesh gear into the base with other gears, using the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + | |factory-nut| | |factory-nut-link| | Thread the nut onto the first 2 threads of the bolt, using the Franka robot | + +--------------------+-------------------------+-----------------------------------------------------------------------------+ + +.. |factory-peg| image:: ../_static/tasks/factory/peg_insert.jpg +.. |factory-gear| image:: ../_static/tasks/factory/gear_mesh.jpg +.. |factory-nut| image:: ../_static/tasks/factory/nut_thread.jpg + +.. |factory-peg-link| replace:: `Isaac-Factory-PegInsert-Direct-v0 `__ +.. |factory-gear-link| replace:: `Isaac-Factory-GearMesh-Direct-v0 `__ +.. |factory-nut-link| replace:: `Isaac-Factory-NutThread-Direct-v0 `__ + Locomotion ~~~~~~~~~~ @@ -369,6 +402,18 @@ Comprehensive List of Environments - - Manager Based - **rl_games** (PPO), **rsl_rl** (PPO), **skrl** (PPO), **sb3** (PPO) + * - Isaac-Factory-GearMesh-Direct-v0 + - + - Direct + - **rl_games** (PPO) + * - Isaac-Factory-NutThread-Direct-v0 + - + - Direct + - **rl_games** (PPO) + * - Isaac-Factory-PegInsert-Direct-v0 + - + - Direct + - **rl_games** (PPO) * - Isaac-Franka-Cabinet-Direct-v0 - - Direct diff --git a/source/extensions/omni.isaac.lab_tasks/config/extension.toml b/source/extensions/omni.isaac.lab_tasks/config/extension.toml index accf06c5c5..e82e19521f 100644 --- a/source/extensions/omni.isaac.lab_tasks/config/extension.toml +++ b/source/extensions/omni.isaac.lab_tasks/config/extension.toml @@ -1,7 +1,7 @@ [package] # Note: Semantic Versioning is used: https://semver.org/ -version = "0.10.15" +version = "0.10.16" # Description title = "Isaac Lab Environments" diff --git a/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst b/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst index 2e10dc6dbd..69a4c04aea 100644 --- a/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst +++ b/source/extensions/omni.isaac.lab_tasks/docs/CHANGELOG.rst @@ -1,11 +1,23 @@ Changelog --------- +0.10.16 (2024-12-16) +~~~~~~~~~~~~~~~~~~~~ + +Added +^^^^^ + +* Added ``Factory-Direct-v0`` environment as a direct RL env that + implements contact-rich manipulation tasks including peg insertion, + gear meshing, and nut threading. + + 0.10.15 (2024-12-16) ~~~~~~~~~~~~~~~~~~~~ Added ^^^^^ + * Added ``Isaac-Reach-Franka-OSC-v0`` and ``Isaac-Reach-Franka-OSC-Play-v0`` variations of the manager based reach environment that uses :class:`omni.isaac.lab.envs.mdp.actions.OperationalSpaceControllerAction`. @@ -20,6 +32,7 @@ Added * Added ``Isaac-Stack-Cube-Franka-IK-Rel-v0`` and ``Isaac-Stack-Cube-Instance-Randomize-Franka-IK-Rel-v0`` environments as manager-based RL envs that implement a three cube stacking task. + 0.10.13 (2024-10-30) ~~~~~~~~~~~~~~~~~~~~ @@ -49,6 +62,7 @@ Added * Added feature extracted observation cartpole examples. + 0.10.10 (2024-10-25) ~~~~~~~~~~~~~~~~~~~~ diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/__init__.py new file mode 100644 index 0000000000..c19c96f708 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/__init__.py @@ -0,0 +1,44 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import gymnasium as gym + +from . import agents +from .factory_env import FactoryEnv +from .factory_env_cfg import FactoryTaskGearMeshCfg, FactoryTaskNutThreadCfg, FactoryTaskPegInsertCfg + +## +# Register Gym environments. +## + +gym.register( + id="Isaac-Factory-PegInsert-Direct-v0", + entry_point="omni.isaac.lab_tasks.direct.factory:FactoryEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": FactoryTaskPegInsertCfg, + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Factory-GearMesh-Direct-v0", + entry_point="omni.isaac.lab_tasks.direct.factory:FactoryEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": FactoryTaskGearMeshCfg, + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, +) + +gym.register( + id="Isaac-Factory-NutThread-Direct-v0", + entry_point="omni.isaac.lab_tasks.direct.factory:FactoryEnv", + disable_env_checker=True, + kwargs={ + "env_cfg_entry_point": FactoryTaskNutThreadCfg, + "rl_games_cfg_entry_point": f"{agents.__name__}:rl_games_ppo_cfg.yaml", + }, +) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/__init__.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/__init__.py new file mode 100644 index 0000000000..c3ee657052 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml new file mode 100644 index 0000000000..5494199846 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/agents/rl_games_ppo_cfg.yaml @@ -0,0 +1,118 @@ +params: + seed: 0 + algo: + name: a2c_continuous + + env: + clip_actions: 1.0 + + model: + name: continuous_a2c_logstd + + network: + name: actor_critic + separate: False + + space: + continuous: + mu_activation: None + sigma_activation: None + mu_init: + name: default + sigma_init: + name: const_initializer + val: 0 + fixed_sigma: False + mlp: + units: [512, 128, 64] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + rnn: + name: lstm + units: 1024 + layers: 2 + before_mlp: True + concat_input: True + layer_norm: True + + load_checkpoint: False + load_path: "" + + config: + name: Factory + device: cuda:0 + full_experiment_name: test + env_name: rlgpu + multi_gpu: False + ppo: True + mixed_precision: True + normalize_input: True + normalize_value: True + value_bootstrap: True + num_actors: 128 + reward_shaper: + scale_value: 1.0 + normalize_advantage: True + gamma: 0.995 + tau: 0.95 + learning_rate: 1.0e-4 + lr_schedule: adaptive + schedule_type: standard + kl_threshold: 0.008 + score_to_win: 20000 + max_epochs: 200 + save_best_after: 10 + save_frequency: 100 + print_stats: True + grad_norm: 1.0 + entropy_coef: 0.0 # 0.0001 # 0.0 + truncate_grads: True + e_clip: 0.2 + horizon_length: 128 + minibatch_size: 512 # batch size = num_envs * horizon_length; minibatch_size = batch_size / num_minibatches + mini_epochs: 4 + critic_coef: 2 + clip_value: True + seq_length: 128 + bounds_loss_coef: 0.0001 + + central_value_config: + minibatch_size: 512 + mini_epochs: 4 + learning_rate: 1e-4 + lr_schedule: adaptive + kl_threshold: 0.008 + clip_value: True + normalize_input: True + truncate_grads: True + + network: + name: actor_critic + central_value: True + + mlp: + units: [512, 128, 64] + activation: elu + d2rl: False + + initializer: + name: default + regularizer: + name: None + + rnn: + name: lstm + units: 1024 + layers: 2 + before_mlp: True + concat_input: True + layer_norm: True + + player: + deterministic: False diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_control.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_control.py new file mode 100644 index 0000000000..33efc41ca0 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_control.py @@ -0,0 +1,196 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Factory: control module. + +Imported by base, environment, and task classes. Not directly executed. +""" + +import math +import torch + +import omni.isaac.core.utils.torch as torch_utils + +from omni.isaac.lab.utils.math import axis_angle_from_quat + + +def compute_dof_torque( + cfg, + dof_pos, + dof_vel, + fingertip_midpoint_pos, + fingertip_midpoint_quat, + fingertip_midpoint_linvel, + fingertip_midpoint_angvel, + jacobian, + arm_mass_matrix, + ctrl_target_fingertip_midpoint_pos, + ctrl_target_fingertip_midpoint_quat, + task_prop_gains, + task_deriv_gains, + device, +): + """Compute Franka DOF torque to move fingertips towards target pose.""" + # References: + # 1) https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf + # 2) Modern Robotics + + num_envs = cfg.scene.num_envs + dof_torque = torch.zeros((num_envs, dof_pos.shape[1]), device=device) + task_wrench = torch.zeros((num_envs, 6), device=device) + + pos_error, axis_angle_error = get_pose_error( + fingertip_midpoint_pos=fingertip_midpoint_pos, + fingertip_midpoint_quat=fingertip_midpoint_quat, + ctrl_target_fingertip_midpoint_pos=ctrl_target_fingertip_midpoint_pos, + ctrl_target_fingertip_midpoint_quat=ctrl_target_fingertip_midpoint_quat, + jacobian_type="geometric", + rot_error_type="axis_angle", + ) + delta_fingertip_pose = torch.cat((pos_error, axis_angle_error), dim=1) + + # Set tau = k_p * task_pos_error - k_d * task_vel_error (building towards eq. 3.96-3.98) + task_wrench_motion = _apply_task_space_gains( + delta_fingertip_pose=delta_fingertip_pose, + fingertip_midpoint_linvel=fingertip_midpoint_linvel, + fingertip_midpoint_angvel=fingertip_midpoint_angvel, + task_prop_gains=task_prop_gains, + task_deriv_gains=task_deriv_gains, + ) + task_wrench += task_wrench_motion + + # Set tau = J^T * tau, i.e., map tau into joint space as desired + jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) + dof_torque[:, 0:7] = (jacobian_T @ task_wrench.unsqueeze(-1)).squeeze(-1) + + # adapted from https://gitlab-master.nvidia.com/carbon-gym/carbgym/-/blob/b4bbc66f4e31b1a1bee61dbaafc0766bbfbf0f58/python/examples/franka_cube_ik_osc.py#L70-78 + # roboticsproceedings.org/rss07/p31.pdf + + # useful tensors + arm_mass_matrix_inv = torch.inverse(arm_mass_matrix) + jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) + arm_mass_matrix_task = torch.inverse( + jacobian @ torch.inverse(arm_mass_matrix) @ jacobian_T + ) # ETH eq. 3.86; geometric Jacobian is assumed + j_eef_inv = arm_mass_matrix_task @ jacobian @ arm_mass_matrix_inv + default_dof_pos_tensor = torch.tensor(cfg.ctrl.default_dof_pos_tensor, device=device).repeat((num_envs, 1)) + # nullspace computation + distance_to_default_dof_pos = default_dof_pos_tensor - dof_pos[:, :7] + distance_to_default_dof_pos = (distance_to_default_dof_pos + math.pi) % ( + 2 * math.pi + ) - math.pi # normalize to [-pi, pi] + u_null = cfg.ctrl.kd_null * -dof_vel[:, :7] + cfg.ctrl.kp_null * distance_to_default_dof_pos + u_null = arm_mass_matrix @ u_null.unsqueeze(-1) + torque_null = (torch.eye(7, device=device).unsqueeze(0) - torch.transpose(jacobian, 1, 2) @ j_eef_inv) @ u_null + dof_torque[:, 0:7] += torque_null.squeeze(-1) + + # TODO: Verify it's okay to no longer do gripper control here. + dof_torque = torch.clamp(dof_torque, min=-100.0, max=100.0) + return dof_torque, task_wrench + + +def get_pose_error( + fingertip_midpoint_pos, + fingertip_midpoint_quat, + ctrl_target_fingertip_midpoint_pos, + ctrl_target_fingertip_midpoint_quat, + jacobian_type, + rot_error_type, +): + """Compute task-space error between target Franka fingertip pose and current pose.""" + # Reference: https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf + + # Compute pos error + pos_error = ctrl_target_fingertip_midpoint_pos - fingertip_midpoint_pos + + # Compute rot error + if jacobian_type == "geometric": # See example 2.9.8; note use of J_g and transformation between rotation vectors + # Compute quat error (i.e., difference quat) + # Reference: https://personal.utdallas.edu/~sxb027100/dock/quat.html + + # Check for shortest path using quaternion dot product. + quat_dot = (ctrl_target_fingertip_midpoint_quat * fingertip_midpoint_quat).sum(dim=1, keepdim=True) + ctrl_target_fingertip_midpoint_quat = torch.where( + quat_dot.expand(-1, 4) >= 0, ctrl_target_fingertip_midpoint_quat, -ctrl_target_fingertip_midpoint_quat + ) + + fingertip_midpoint_quat_norm = torch_utils.quat_mul( + fingertip_midpoint_quat, torch_utils.quat_conjugate(fingertip_midpoint_quat) + )[ + :, 0 + ] # scalar component + fingertip_midpoint_quat_inv = torch_utils.quat_conjugate( + fingertip_midpoint_quat + ) / fingertip_midpoint_quat_norm.unsqueeze(-1) + quat_error = torch_utils.quat_mul(ctrl_target_fingertip_midpoint_quat, fingertip_midpoint_quat_inv) + + # Convert to axis-angle error + axis_angle_error = axis_angle_from_quat(quat_error) + + if rot_error_type == "quat": + return pos_error, quat_error + elif rot_error_type == "axis_angle": + return pos_error, axis_angle_error + + +def _get_delta_dof_pos(delta_pose, ik_method, jacobian, device): + """Get delta Franka DOF position from delta pose using specified IK method.""" + # References: + # 1) https://www.cs.cmu.edu/~15464-s13/lectures/lecture6/iksurvey.pdf + # 2) https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2018/RD_HS2018script.pdf (p. 47) + + if ik_method == "pinv": # Jacobian pseudoinverse + k_val = 1.0 + jacobian_pinv = torch.linalg.pinv(jacobian) + delta_dof_pos = k_val * jacobian_pinv @ delta_pose.unsqueeze(-1) + delta_dof_pos = delta_dof_pos.squeeze(-1) + + elif ik_method == "trans": # Jacobian transpose + k_val = 1.0 + jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) + delta_dof_pos = k_val * jacobian_T @ delta_pose.unsqueeze(-1) + delta_dof_pos = delta_dof_pos.squeeze(-1) + + elif ik_method == "dls": # damped least squares (Levenberg-Marquardt) + lambda_val = 0.1 # 0.1 + jacobian_T = torch.transpose(jacobian, dim0=1, dim1=2) + lambda_matrix = (lambda_val**2) * torch.eye(n=jacobian.shape[1], device=device) + delta_dof_pos = jacobian_T @ torch.inverse(jacobian @ jacobian_T + lambda_matrix) @ delta_pose.unsqueeze(-1) + delta_dof_pos = delta_dof_pos.squeeze(-1) + + elif ik_method == "svd": # adaptive SVD + k_val = 1.0 + U, S, Vh = torch.linalg.svd(jacobian) + S_inv = 1.0 / S + min_singular_value = 1.0e-5 + 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_dof_pos = k_val * jacobian_pinv @ delta_pose.unsqueeze(-1) + delta_dof_pos = delta_dof_pos.squeeze(-1) + + return delta_dof_pos + + +def _apply_task_space_gains( + delta_fingertip_pose, fingertip_midpoint_linvel, fingertip_midpoint_angvel, task_prop_gains, task_deriv_gains +): + """Interpret PD gains as task-space gains. Apply to task-space error.""" + + task_wrench = torch.zeros_like(delta_fingertip_pose) + + # Apply gains to lin error components + lin_error = delta_fingertip_pose[:, 0:3] + task_wrench[:, 0:3] = task_prop_gains[:, 0:3] * lin_error + task_deriv_gains[:, 0:3] * ( + 0.0 - fingertip_midpoint_linvel + ) + + # Apply gains to rot error components + rot_error = delta_fingertip_pose[:, 3:6] + task_wrench[:, 3:6] = task_prop_gains[:, 3:6] * rot_error + task_deriv_gains[:, 3:6] * ( + 0.0 - fingertip_midpoint_angvel + ) + return task_wrench diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env.py new file mode 100644 index 0000000000..df7850d32c --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env.py @@ -0,0 +1,880 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import numpy as np +import torch + +import carb +import omni.isaac.core.utils.torch as torch_utils + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import Articulation +from omni.isaac.lab.envs import DirectRLEnv +from omni.isaac.lab.sim.spawners.from_files import GroundPlaneCfg, spawn_ground_plane +from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR +from omni.isaac.lab.utils.math import axis_angle_from_quat + +from . import factory_control as fc +from .factory_env_cfg import OBS_DIM_CFG, STATE_DIM_CFG, FactoryEnvCfg + + +class FactoryEnv(DirectRLEnv): + cfg: FactoryEnvCfg + + def __init__(self, cfg: FactoryEnvCfg, render_mode: str | None = None, **kwargs): + # Update number of obs/states + cfg.observation_space = sum([OBS_DIM_CFG[obs] for obs in cfg.obs_order]) + cfg.state_space = sum([STATE_DIM_CFG[state] for state in cfg.state_order]) + cfg.observation_space += cfg.action_space + cfg.state_space += cfg.action_space + self.cfg_task = cfg.task + + super().__init__(cfg, render_mode, **kwargs) + + self._set_body_inertias() + self._init_tensors() + self._set_default_dynamics_parameters() + self._compute_intermediate_values(dt=self.physics_dt) + + def _set_body_inertias(self): + """Note: this is to account for the asset_options.armature parameter in IGE.""" + inertias = self._robot.root_physx_view.get_inertias() + offset = torch.zeros_like(inertias) + offset[:, :, [0, 4, 8]] += 0.01 + new_inertias = inertias + offset + self._robot.root_physx_view.set_inertias(new_inertias, torch.arange(self.num_envs)) + + def _set_default_dynamics_parameters(self): + """Set parameters defining dynamic interactions.""" + self.default_gains = torch.tensor(self.cfg.ctrl.default_task_prop_gains, device=self.device).repeat( + (self.num_envs, 1) + ) + + self.pos_threshold = torch.tensor(self.cfg.ctrl.pos_action_threshold, device=self.device).repeat( + (self.num_envs, 1) + ) + self.rot_threshold = torch.tensor(self.cfg.ctrl.rot_action_threshold, device=self.device).repeat( + (self.num_envs, 1) + ) + + # Set masses and frictions. + self._set_friction(self._held_asset, self.cfg_task.held_asset_cfg.friction) + self._set_friction(self._fixed_asset, self.cfg_task.fixed_asset_cfg.friction) + self._set_friction(self._robot, self.cfg_task.robot_cfg.friction) + + def _set_friction(self, asset, value): + """Update material properties for a given asset.""" + materials = asset.root_physx_view.get_material_properties() + materials[..., 0] = value # Static friction. + materials[..., 1] = value # Dynamic friction. + env_ids = torch.arange(self.scene.num_envs, device="cpu") + asset.root_physx_view.set_material_properties(materials, env_ids) + + def _init_tensors(self): + """Initialize tensors once.""" + self.identity_quat = ( + torch.tensor([1.0, 0.0, 0.0, 0.0], device=self.device).unsqueeze(0).repeat(self.num_envs, 1) + ) + + # Control targets. + self.ctrl_target_joint_pos = torch.zeros((self.num_envs, self._robot.num_joints), device=self.device) + self.ctrl_target_fingertip_midpoint_pos = torch.zeros((self.num_envs, 3), device=self.device) + self.ctrl_target_fingertip_midpoint_quat = torch.zeros((self.num_envs, 4), device=self.device) + + # Fixed asset. + self.fixed_pos_action_frame = torch.zeros((self.num_envs, 3), device=self.device) + self.fixed_pos_obs_frame = torch.zeros((self.num_envs, 3), device=self.device) + self.init_fixed_pos_obs_noise = torch.zeros((self.num_envs, 3), device=self.device) + + # Held asset + held_base_x_offset = 0.0 + if self.cfg_task.name == "peg_insert": + held_base_z_offset = 0.0 + elif self.cfg_task.name == "gear_mesh": + gear_base_offset = self._get_target_gear_base_offset() + held_base_x_offset = gear_base_offset[0] + held_base_z_offset = gear_base_offset[2] + elif self.cfg_task.name == "nut_thread": + held_base_z_offset = self.cfg_task.fixed_asset_cfg.base_height + else: + raise NotImplementedError("Task not implemented") + + self.held_base_pos_local = torch.tensor([0.0, 0.0, 0.0], device=self.device).repeat((self.num_envs, 1)) + self.held_base_pos_local[:, 0] = held_base_x_offset + self.held_base_pos_local[:, 2] = held_base_z_offset + self.held_base_quat_local = self.identity_quat.clone().detach() + + self.held_base_pos = torch.zeros_like(self.held_base_pos_local) + self.held_base_quat = self.identity_quat.clone().detach() + + # Computer body indices. + self.left_finger_body_idx = self._robot.body_names.index("panda_leftfinger") + self.right_finger_body_idx = self._robot.body_names.index("panda_rightfinger") + self.fingertip_body_idx = self._robot.body_names.index("panda_fingertip_centered") + + # Tensors for finite-differencing. + self.last_update_timestamp = 0.0 # Note: This is for finite differencing body velocities. + self.prev_fingertip_pos = torch.zeros((self.num_envs, 3), device=self.device) + self.prev_fingertip_quat = self.identity_quat.clone() + self.prev_joint_pos = torch.zeros((self.num_envs, 7), device=self.device) + + # Keypoint tensors. + self.target_held_base_pos = torch.zeros((self.num_envs, 3), device=self.device) + self.target_held_base_quat = self.identity_quat.clone().detach() + + offsets = self._get_keypoint_offsets(self.cfg_task.num_keypoints) + self.keypoint_offsets = offsets * self.cfg_task.keypoint_scale + self.keypoints_held = torch.zeros((self.num_envs, self.cfg_task.num_keypoints, 3), device=self.device) + self.keypoints_fixed = torch.zeros_like(self.keypoints_held, device=self.device) + + # Used to compute target poses. + self.fixed_success_pos_local = torch.zeros((self.num_envs, 3), device=self.device) + if self.cfg_task.name == "peg_insert": + self.fixed_success_pos_local[:, 2] = 0.0 + elif self.cfg_task.name == "gear_mesh": + gear_base_offset = self._get_target_gear_base_offset() + self.fixed_success_pos_local[:, 0] = gear_base_offset[0] + self.fixed_success_pos_local[:, 2] = gear_base_offset[2] + elif self.cfg_task.name == "nut_thread": + head_height = self.cfg_task.fixed_asset_cfg.base_height + shank_length = self.cfg_task.fixed_asset_cfg.height + thread_pitch = self.cfg_task.fixed_asset_cfg.thread_pitch + self.fixed_success_pos_local[:, 2] = head_height + shank_length - thread_pitch * 1.5 + else: + raise NotImplementedError("Task not implemented") + + self.ep_succeeded = torch.zeros((self.num_envs,), dtype=torch.long, device=self.device) + self.ep_success_times = torch.zeros((self.num_envs,), dtype=torch.long, device=self.device) + + def _get_keypoint_offsets(self, num_keypoints): + """Get uniformly-spaced keypoints along a line of unit length, centered at 0.""" + keypoint_offsets = torch.zeros((num_keypoints, 3), device=self.device) + keypoint_offsets[:, -1] = torch.linspace(0.0, 1.0, num_keypoints, device=self.device) - 0.5 + + return keypoint_offsets + + def _setup_scene(self): + """Initialize simulation scene.""" + spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg(), translation=(0.0, 0.0, -0.4)) + + # spawn a usd file of a table into the scene + cfg = sim_utils.UsdFileCfg(usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd") + cfg.func( + "/World/envs/env_.*/Table", cfg, translation=(0.55, 0.0, 0.0), orientation=(0.70711, 0.0, 0.0, 0.70711) + ) + + self._robot = Articulation(self.cfg.robot) + self._fixed_asset = Articulation(self.cfg_task.fixed_asset) + self._held_asset = Articulation(self.cfg_task.held_asset) + if self.cfg_task.name == "gear_mesh": + self._small_gear_asset = Articulation(self.cfg_task.small_gear_cfg) + self._large_gear_asset = Articulation(self.cfg_task.large_gear_cfg) + + self.scene.clone_environments(copy_from_source=False) + self.scene.filter_collisions() + + self.scene.articulations["robot"] = self._robot + self.scene.articulations["fixed_asset"] = self._fixed_asset + self.scene.articulations["held_asset"] = self._held_asset + if self.cfg_task.name == "gear_mesh": + self.scene.articulations["small_gear"] = self._small_gear_asset + self.scene.articulations["large_gear"] = self._large_gear_asset + + # add lights + light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75)) + light_cfg.func("/World/Light", light_cfg) + + def _compute_intermediate_values(self, dt): + """Get values computed from raw tensors. This includes adding noise.""" + # TODO: A lot of these can probably only be set once? + self.fixed_pos = self._fixed_asset.data.root_pos_w - self.scene.env_origins + self.fixed_quat = self._fixed_asset.data.root_quat_w + + self.held_pos = self._held_asset.data.root_pos_w - self.scene.env_origins + self.held_quat = self._held_asset.data.root_quat_w + + self.fingertip_midpoint_pos = self._robot.data.body_pos_w[:, self.fingertip_body_idx] - self.scene.env_origins + self.fingertip_midpoint_quat = self._robot.data.body_quat_w[:, self.fingertip_body_idx] + self.fingertip_midpoint_linvel = self._robot.data.body_lin_vel_w[:, self.fingertip_body_idx] + self.fingertip_midpoint_angvel = self._robot.data.body_ang_vel_w[:, self.fingertip_body_idx] + + jacobians = self._robot.root_physx_view.get_jacobians() + + self.left_finger_jacobian = jacobians[:, self.left_finger_body_idx - 1, 0:6, 0:7] + self.right_finger_jacobian = jacobians[:, self.right_finger_body_idx - 1, 0:6, 0:7] + self.fingertip_midpoint_jacobian = (self.left_finger_jacobian + self.right_finger_jacobian) * 0.5 + self.arm_mass_matrix = self._robot.root_physx_view.get_mass_matrices()[:, 0:7, 0:7] + self.joint_pos = self._robot.data.joint_pos.clone() + self.joint_vel = self._robot.data.joint_vel.clone() + + # Finite-differencing results in more reliable velocity estimates. + self.ee_linvel_fd = (self.fingertip_midpoint_pos - self.prev_fingertip_pos) / dt + self.prev_fingertip_pos = self.fingertip_midpoint_pos.clone() + + # Add state differences if velocity isn't being added. + rot_diff_quat = torch_utils.quat_mul( + self.fingertip_midpoint_quat, torch_utils.quat_conjugate(self.prev_fingertip_quat) + ) + rot_diff_quat *= torch.sign(rot_diff_quat[:, 0]).unsqueeze(-1) + rot_diff_aa = axis_angle_from_quat(rot_diff_quat) + self.ee_angvel_fd = rot_diff_aa / dt + self.prev_fingertip_quat = self.fingertip_midpoint_quat.clone() + + joint_diff = self.joint_pos[:, 0:7] - self.prev_joint_pos + self.joint_vel_fd = joint_diff / dt + self.prev_joint_pos = self.joint_pos[:, 0:7].clone() + + # Keypoint tensors. + self.held_base_quat[:], self.held_base_pos[:] = torch_utils.tf_combine( + self.held_quat, self.held_pos, self.held_base_quat_local, self.held_base_pos_local + ) + self.target_held_base_quat[:], self.target_held_base_pos[:] = torch_utils.tf_combine( + self.fixed_quat, self.fixed_pos, self.identity_quat, self.fixed_success_pos_local + ) + + # Compute pos of keypoints on held asset, and fixed asset in world frame + for idx, keypoint_offset in enumerate(self.keypoint_offsets): + self.keypoints_held[:, idx] = torch_utils.tf_combine( + self.held_base_quat, self.held_base_pos, self.identity_quat, keypoint_offset.repeat(self.num_envs, 1) + )[1] + self.keypoints_fixed[:, idx] = torch_utils.tf_combine( + self.target_held_base_quat, + self.target_held_base_pos, + self.identity_quat, + keypoint_offset.repeat(self.num_envs, 1), + )[1] + + self.keypoint_dist = torch.norm(self.keypoints_held - self.keypoints_fixed, p=2, dim=-1).mean(-1) + self.last_update_timestamp = self._robot._data._sim_timestamp + + def _get_observations(self): + """Get actor/critic inputs using asymmetric critic.""" + noisy_fixed_pos = self.fixed_pos_obs_frame + self.init_fixed_pos_obs_noise + + prev_actions = self.actions.clone() + + obs_dict = { + "fingertip_pos": self.fingertip_midpoint_pos, + "fingertip_pos_rel_fixed": self.fingertip_midpoint_pos - noisy_fixed_pos, + "fingertip_quat": self.fingertip_midpoint_quat, + "ee_linvel": self.ee_linvel_fd, + "ee_angvel": self.ee_angvel_fd, + "prev_actions": prev_actions, + } + + state_dict = { + "fingertip_pos": self.fingertip_midpoint_pos, + "fingertip_pos_rel_fixed": self.fingertip_midpoint_pos - self.fixed_pos_obs_frame, + "fingertip_quat": self.fingertip_midpoint_quat, + "ee_linvel": self.fingertip_midpoint_linvel, + "ee_angvel": self.fingertip_midpoint_angvel, + "joint_pos": self.joint_pos[:, 0:7], + "held_pos": self.held_pos, + "held_pos_rel_fixed": self.held_pos - self.fixed_pos_obs_frame, + "held_quat": self.held_quat, + "fixed_pos": self.fixed_pos, + "fixed_quat": self.fixed_quat, + "task_prop_gains": self.task_prop_gains, + "pos_threshold": self.pos_threshold, + "rot_threshold": self.rot_threshold, + "prev_actions": prev_actions, + } + obs_tensors = [obs_dict[obs_name] for obs_name in self.cfg.obs_order + ["prev_actions"]] + obs_tensors = torch.cat(obs_tensors, dim=-1) + state_tensors = [state_dict[state_name] for state_name in self.cfg.state_order + ["prev_actions"]] + state_tensors = torch.cat(state_tensors, dim=-1) + return {"policy": obs_tensors, "critic": state_tensors} + + def _reset_buffers(self, env_ids): + """Reset buffers.""" + self.ep_succeeded[env_ids] = 0 + + def _pre_physics_step(self, action): + """Apply policy actions with smoothing.""" + env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) + if len(env_ids) > 0: + self._reset_buffers(env_ids) + + self.actions = ( + self.cfg.ctrl.ema_factor * action.clone().to(self.device) + (1 - self.cfg.ctrl.ema_factor) * self.actions + ) + + def close_gripper_in_place(self): + """Keep gripper in current position as gripper closes.""" + actions = torch.zeros((self.num_envs, 6), device=self.device) + ctrl_target_gripper_dof_pos = 0.0 + + # Interpret actions as target pos displacements and set pos target + pos_actions = actions[:, 0:3] * self.pos_threshold + self.ctrl_target_fingertip_midpoint_pos = self.fingertip_midpoint_pos + pos_actions + + # Interpret actions as target rot (axis-angle) displacements + rot_actions = actions[:, 3:6] + + # Convert to quat and set rot target + angle = torch.norm(rot_actions, p=2, dim=-1) + axis = rot_actions / angle.unsqueeze(-1) + + rot_actions_quat = torch_utils.quat_from_angle_axis(angle, axis) + + rot_actions_quat = torch.where( + angle.unsqueeze(-1).repeat(1, 4) > 1.0e-6, + rot_actions_quat, + torch.tensor([1.0, 0.0, 0.0, 0.0], device=self.device).repeat(self.num_envs, 1), + ) + self.ctrl_target_fingertip_midpoint_quat = torch_utils.quat_mul(rot_actions_quat, self.fingertip_midpoint_quat) + + target_euler_xyz = torch.stack(torch_utils.get_euler_xyz(self.ctrl_target_fingertip_midpoint_quat), dim=1) + target_euler_xyz[:, 0] = 3.14159 + target_euler_xyz[:, 1] = 0.0 + + self.ctrl_target_fingertip_midpoint_quat = torch_utils.quat_from_euler_xyz( + roll=target_euler_xyz[:, 0], pitch=target_euler_xyz[:, 1], yaw=target_euler_xyz[:, 2] + ) + + self.ctrl_target_gripper_dof_pos = ctrl_target_gripper_dof_pos + self.generate_ctrl_signals() + + def _apply_action(self): + """Apply actions for policy as delta targets from current position.""" + # Get current yaw for success checking. + _, _, curr_yaw = torch_utils.get_euler_xyz(self.fingertip_midpoint_quat) + self.curr_yaw = torch.where(curr_yaw > np.deg2rad(235), curr_yaw - 2 * np.pi, curr_yaw) + + # Note: We use finite-differenced velocities for control and observations. + # Check if we need to re-compute velocities within the decimation loop. + if self.last_update_timestamp < self._robot._data._sim_timestamp: + self._compute_intermediate_values(dt=self.physics_dt) + + # Interpret actions as target pos displacements and set pos target + pos_actions = self.actions[:, 0:3] * self.pos_threshold + + # Interpret actions as target rot (axis-angle) displacements + rot_actions = self.actions[:, 3:6] + if self.cfg_task.unidirectional_rot: + rot_actions[:, 2] = -(rot_actions[:, 2] + 1.0) * 0.5 # [-1, 0] + rot_actions = rot_actions * self.rot_threshold + + self.ctrl_target_fingertip_midpoint_pos = self.fingertip_midpoint_pos + pos_actions + # To speed up learning, never allow the policy to move more than 5cm away from the base. + delta_pos = self.ctrl_target_fingertip_midpoint_pos - self.fixed_pos_action_frame + pos_error_clipped = torch.clip( + delta_pos, -self.cfg.ctrl.pos_action_bounds[0], self.cfg.ctrl.pos_action_bounds[1] + ) + self.ctrl_target_fingertip_midpoint_pos = self.fixed_pos_action_frame + pos_error_clipped + + # Convert to quat and set rot target + angle = torch.norm(rot_actions, p=2, dim=-1) + axis = rot_actions / angle.unsqueeze(-1) + + rot_actions_quat = torch_utils.quat_from_angle_axis(angle, axis) + rot_actions_quat = torch.where( + angle.unsqueeze(-1).repeat(1, 4) > 1e-6, + rot_actions_quat, + torch.tensor([1.0, 0.0, 0.0, 0.0], device=self.device).repeat(self.num_envs, 1), + ) + self.ctrl_target_fingertip_midpoint_quat = torch_utils.quat_mul(rot_actions_quat, self.fingertip_midpoint_quat) + + target_euler_xyz = torch.stack(torch_utils.get_euler_xyz(self.ctrl_target_fingertip_midpoint_quat), dim=1) + target_euler_xyz[:, 0] = 3.14159 # Restrict actions to be upright. + target_euler_xyz[:, 1] = 0.0 + + self.ctrl_target_fingertip_midpoint_quat = torch_utils.quat_from_euler_xyz( + roll=target_euler_xyz[:, 0], pitch=target_euler_xyz[:, 1], yaw=target_euler_xyz[:, 2] + ) + + self.ctrl_target_gripper_dof_pos = 0.0 + self.generate_ctrl_signals() + + def _set_gains(self, prop_gains, rot_deriv_scale=1.0): + """Set robot gains using critical damping.""" + self.task_prop_gains = prop_gains + self.task_deriv_gains = 2 * torch.sqrt(prop_gains) + self.task_deriv_gains[:, 3:6] /= rot_deriv_scale + + def generate_ctrl_signals(self): + """Get Jacobian. Set Franka DOF position targets (fingers) or DOF torques (arm).""" + self.joint_torque, self.applied_wrench = fc.compute_dof_torque( + cfg=self.cfg, + dof_pos=self.joint_pos, + dof_vel=self.joint_vel, # _fd, + fingertip_midpoint_pos=self.fingertip_midpoint_pos, + fingertip_midpoint_quat=self.fingertip_midpoint_quat, + fingertip_midpoint_linvel=self.ee_linvel_fd, + fingertip_midpoint_angvel=self.ee_angvel_fd, + jacobian=self.fingertip_midpoint_jacobian, + arm_mass_matrix=self.arm_mass_matrix, + ctrl_target_fingertip_midpoint_pos=self.ctrl_target_fingertip_midpoint_pos, + ctrl_target_fingertip_midpoint_quat=self.ctrl_target_fingertip_midpoint_quat, + task_prop_gains=self.task_prop_gains, + task_deriv_gains=self.task_deriv_gains, + device=self.device, + ) + + # set target for gripper joints to use physx's PD controller + self.ctrl_target_joint_pos[:, 7:9] = self.ctrl_target_gripper_dof_pos + self.joint_torque[:, 7:9] = 0.0 + + self._robot.set_joint_position_target(self.ctrl_target_joint_pos) + self._robot.set_joint_effort_target(self.joint_torque) + + def _get_dones(self): + """Update intermediate values used for rewards and observations.""" + self._compute_intermediate_values(dt=self.physics_dt) + time_out = self.episode_length_buf >= self.max_episode_length - 1 + return time_out, time_out + + def _get_curr_successes(self, success_threshold, check_rot=False): + """Get success mask at current timestep.""" + curr_successes = torch.zeros((self.num_envs,), dtype=torch.bool, device=self.device) + + xy_dist = torch.linalg.vector_norm(self.target_held_base_pos[:, 0:2] - self.held_base_pos[:, 0:2], dim=1) + z_disp = self.held_base_pos[:, 2] - self.target_held_base_pos[:, 2] + + is_centered = torch.where(xy_dist < 0.0025, torch.ones_like(curr_successes), torch.zeros_like(curr_successes)) + # Height threshold to target + fixed_cfg = self.cfg_task.fixed_asset_cfg + if self.cfg_task.name == "peg_insert" or self.cfg_task.name == "gear_mesh": + height_threshold = fixed_cfg.height * success_threshold + elif self.cfg_task.name == "nut_thread": + height_threshold = fixed_cfg.thread_pitch * success_threshold + else: + raise NotImplementedError("Task not implemented") + is_close_or_below = torch.where( + z_disp < height_threshold, torch.ones_like(curr_successes), torch.zeros_like(curr_successes) + ) + curr_successes = torch.logical_and(is_centered, is_close_or_below) + + if check_rot: + is_rotated = self.curr_yaw < self.cfg_task.ee_success_yaw + curr_successes = torch.logical_and(curr_successes, is_rotated) + + return curr_successes + + def _get_rewards(self): + """Update rewards and compute success statistics.""" + # Get successful and failed envs at current timestep + check_rot = self.cfg_task.name == "nut_thread" + curr_successes = self._get_curr_successes( + success_threshold=self.cfg_task.success_threshold, check_rot=check_rot + ) + + rew_buf = self._update_rew_buf(curr_successes) + + # Only log episode success rates at the end of an episode. + if torch.any(self.reset_buf): + self.extras["successes"] = torch.count_nonzero(curr_successes) / self.num_envs + + # Get the time at which an episode first succeeds. + first_success = torch.logical_and(curr_successes, torch.logical_not(self.ep_succeeded)) + self.ep_succeeded[curr_successes] = 1 + + first_success_ids = first_success.nonzero(as_tuple=False).squeeze(-1) + self.ep_success_times[first_success_ids] = self.episode_length_buf[first_success_ids] + nonzero_success_ids = self.ep_success_times.nonzero(as_tuple=False).squeeze(-1) + + if len(nonzero_success_ids) > 0: # Only log for successful episodes. + success_times = self.ep_success_times[nonzero_success_ids].sum() / len(nonzero_success_ids) + self.extras["success_times"] = success_times + + self.prev_actions = self.actions.clone() + return rew_buf + + def _update_rew_buf(self, curr_successes): + """Compute reward at current timestep.""" + rew_dict = {} + + # Keypoint rewards. + def squashing_fn(x, a, b): + return 1 / (torch.exp(a * x) + b + torch.exp(-a * x)) + + a0, b0 = self.cfg_task.keypoint_coef_baseline + rew_dict["kp_baseline"] = squashing_fn(self.keypoint_dist, a0, b0) + # a1, b1 = 25, 2 + a1, b1 = self.cfg_task.keypoint_coef_coarse + rew_dict["kp_coarse"] = squashing_fn(self.keypoint_dist, a1, b1) + a2, b2 = self.cfg_task.keypoint_coef_fine + # a2, b2 = 300, 0 + rew_dict["kp_fine"] = squashing_fn(self.keypoint_dist, a2, b2) + + # Action penalties. + rew_dict["action_penalty"] = torch.norm(self.actions, p=2) + rew_dict["action_grad_penalty"] = torch.norm(self.actions - self.prev_actions, p=2, dim=-1) + rew_dict["curr_engaged"] = ( + self._get_curr_successes(success_threshold=self.cfg_task.engage_threshold, check_rot=False).clone().float() + ) + rew_dict["curr_successes"] = curr_successes.clone().float() + + rew_buf = ( + rew_dict["kp_coarse"] + + rew_dict["kp_baseline"] + + rew_dict["kp_fine"] + - rew_dict["action_penalty"] * self.cfg_task.action_penalty_scale + - rew_dict["action_grad_penalty"] * self.cfg_task.action_grad_penalty_scale + + rew_dict["curr_engaged"] + + rew_dict["curr_successes"] + ) + + for rew_name, rew in rew_dict.items(): + self.extras[f"logs_rew_{rew_name}"] = rew.mean() + + return rew_buf + + def _reset_idx(self, env_ids): + """ + We assume all envs will always be reset at the same time. + """ + super()._reset_idx(env_ids) + + self._set_assets_to_default_pose(env_ids) + self._set_franka_to_default_pose(joints=self.cfg.ctrl.reset_joints, env_ids=env_ids) + self.step_sim_no_action() + + self.randomize_initial_state(env_ids) + + def _get_target_gear_base_offset(self): + """Get offset of target gear from the gear base asset.""" + target_gear = self.cfg_task.target_gear + if target_gear == "gear_large": + gear_base_offset = self.cfg_task.fixed_asset_cfg.large_gear_base_offset + elif target_gear == "gear_medium": + gear_base_offset = self.cfg_task.fixed_asset_cfg.medium_gear_base_offset + elif target_gear == "gear_small": + gear_base_offset = self.cfg_task.fixed_asset_cfg.small_gear_base_offset + else: + raise ValueError(f"{target_gear} not valid in this context!") + return gear_base_offset + + def _set_assets_to_default_pose(self, env_ids): + """Move assets to default pose before randomization.""" + held_state = self._held_asset.data.default_root_state.clone()[env_ids] + held_state[:, 0:3] += self.scene.env_origins[env_ids] + held_state[:, 7:] = 0.0 + self._held_asset.write_root_state_to_sim(held_state, env_ids=env_ids) + self._held_asset.reset() + + fixed_state = self._fixed_asset.data.default_root_state.clone()[env_ids] + fixed_state[:, 0:3] += self.scene.env_origins[env_ids] + fixed_state[:, 7:] = 0.0 + self._fixed_asset.write_root_state_to_sim(fixed_state, env_ids=env_ids) + self._fixed_asset.reset() + + def set_pos_inverse_kinematics(self, env_ids): + """Set robot joint position using DLS IK.""" + ik_time = 0.0 + while ik_time < 0.25: + # Compute error to target. + pos_error, axis_angle_error = fc.get_pose_error( + fingertip_midpoint_pos=self.fingertip_midpoint_pos[env_ids], + fingertip_midpoint_quat=self.fingertip_midpoint_quat[env_ids], + ctrl_target_fingertip_midpoint_pos=self.ctrl_target_fingertip_midpoint_pos[env_ids], + ctrl_target_fingertip_midpoint_quat=self.ctrl_target_fingertip_midpoint_quat[env_ids], + jacobian_type="geometric", + rot_error_type="axis_angle", + ) + + delta_hand_pose = torch.cat((pos_error, axis_angle_error), dim=-1) + + # Solve DLS problem. + delta_dof_pos = fc._get_delta_dof_pos( + delta_pose=delta_hand_pose, + ik_method="dls", + jacobian=self.fingertip_midpoint_jacobian[env_ids], + device=self.device, + ) + self.joint_pos[env_ids, 0:7] += delta_dof_pos[:, 0:7] + self.joint_vel[env_ids, :] = torch.zeros_like(self.joint_pos[env_ids,]) + + self.ctrl_target_joint_pos[env_ids, 0:7] = self.joint_pos[env_ids, 0:7] + # Update dof state. + self._robot.write_joint_state_to_sim(self.joint_pos, self.joint_vel) + self._robot.set_joint_position_target(self.ctrl_target_joint_pos) + + # Simulate and update tensors. + self.step_sim_no_action() + ik_time += self.physics_dt + + return pos_error, axis_angle_error + + def get_handheld_asset_relative_pose(self): + """Get default relative pose between help asset and fingertip.""" + if self.cfg_task.name == "peg_insert": + held_asset_relative_pos = torch.zeros_like(self.held_base_pos_local) + held_asset_relative_pos[:, 2] = self.cfg_task.held_asset_cfg.height + held_asset_relative_pos[:, 2] -= self.cfg_task.robot_cfg.franka_fingerpad_length + elif self.cfg_task.name == "gear_mesh": + held_asset_relative_pos = torch.zeros_like(self.held_base_pos_local) + gear_base_offset = self._get_target_gear_base_offset() + held_asset_relative_pos[:, 0] += gear_base_offset[0] + held_asset_relative_pos[:, 2] += gear_base_offset[2] + held_asset_relative_pos[:, 2] += self.cfg_task.held_asset_cfg.height / 2.0 * 1.1 + elif self.cfg_task.name == "nut_thread": + held_asset_relative_pos = self.held_base_pos_local + else: + raise NotImplementedError("Task not implemented") + + held_asset_relative_quat = self.identity_quat + if self.cfg_task.name == "nut_thread": + # Rotate along z-axis of frame for default position. + initial_rot_deg = self.cfg_task.held_asset_rot_init + rot_yaw_euler = torch.tensor([0.0, 0.0, initial_rot_deg * np.pi / 180.0], device=self.device).repeat( + self.num_envs, 1 + ) + held_asset_relative_quat = torch_utils.quat_from_euler_xyz( + roll=rot_yaw_euler[:, 0], pitch=rot_yaw_euler[:, 1], yaw=rot_yaw_euler[:, 2] + ) + + return held_asset_relative_pos, held_asset_relative_quat + + def _set_franka_to_default_pose(self, joints, env_ids): + """Return Franka to its default joint position.""" + gripper_width = self.cfg_task.held_asset_cfg.diameter / 2 * 1.25 + joint_pos = self._robot.data.default_joint_pos[env_ids] + joint_pos[:, 7:] = gripper_width # MIMIC + joint_pos[:, :7] = torch.tensor(joints, device=self.device)[None, :] + joint_vel = torch.zeros_like(joint_pos) + joint_effort = torch.zeros_like(joint_pos) + self.ctrl_target_joint_pos[env_ids, :] = joint_pos + print(f"Resetting {len(env_ids)} envs...") + self._robot.set_joint_position_target(self.ctrl_target_joint_pos[env_ids], env_ids=env_ids) + self._robot.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids) + self._robot.reset() + self._robot.set_joint_effort_target(joint_effort, env_ids=env_ids) + + self.step_sim_no_action() + + def step_sim_no_action(self): + """Step the simulation without an action. Used for resets.""" + self.scene.write_data_to_sim() + self.sim.step(render=False) + self.scene.update(dt=self.physics_dt) + self._compute_intermediate_values(dt=self.physics_dt) + + def randomize_initial_state(self, env_ids): + """Randomize initial state and perform any episode-level randomization.""" + # Disable gravity. + physics_sim_view = sim_utils.SimulationContext.instance().physics_sim_view + physics_sim_view.set_gravity(carb.Float3(0.0, 0.0, 0.0)) + + # (1.) Randomize fixed asset pose. + fixed_state = self._fixed_asset.data.default_root_state.clone()[env_ids] + # (1.a.) Position + rand_sample = torch.rand((len(env_ids), 3), dtype=torch.float32, device=self.device) + fixed_pos_init_rand = 2 * (rand_sample - 0.5) # [-1, 1] + fixed_asset_init_pos_rand = torch.tensor( + self.cfg_task.fixed_asset_init_pos_noise, dtype=torch.float32, device=self.device + ) + fixed_pos_init_rand = fixed_pos_init_rand @ torch.diag(fixed_asset_init_pos_rand) + fixed_state[:, 0:3] += fixed_pos_init_rand + self.scene.env_origins[env_ids] + # (1.b.) Orientation + fixed_orn_init_yaw = np.deg2rad(self.cfg_task.fixed_asset_init_orn_deg) + fixed_orn_yaw_range = np.deg2rad(self.cfg_task.fixed_asset_init_orn_range_deg) + rand_sample = torch.rand((len(env_ids), 3), dtype=torch.float32, device=self.device) + fixed_orn_euler = fixed_orn_init_yaw + fixed_orn_yaw_range * rand_sample + fixed_orn_euler[:, 0:2] = 0.0 # Only change yaw. + fixed_orn_quat = torch_utils.quat_from_euler_xyz( + fixed_orn_euler[:, 0], fixed_orn_euler[:, 1], fixed_orn_euler[:, 2] + ) + fixed_state[:, 3:7] = fixed_orn_quat + # (1.c.) Velocity + fixed_state[:, 7:] = 0.0 # vel + # (1.d.) Update values. + self._fixed_asset.write_root_state_to_sim(fixed_state, env_ids=env_ids) + self._fixed_asset.reset() + + # (1.e.) Noisy position observation. + fixed_asset_pos_noise = torch.randn((len(env_ids), 3), dtype=torch.float32, device=self.device) + fixed_asset_pos_rand = torch.tensor(self.cfg.obs_rand.fixed_asset_pos, dtype=torch.float32, device=self.device) + fixed_asset_pos_noise = fixed_asset_pos_noise @ torch.diag(fixed_asset_pos_rand) + self.init_fixed_pos_obs_noise[:] = fixed_asset_pos_noise + + self.step_sim_no_action() + + # Compute the frame on the bolt that would be used as observation: fixed_pos_obs_frame + # For example, the tip of the bolt can be used as the observation frame + fixed_tip_pos_local = torch.zeros_like(self.fixed_pos) + fixed_tip_pos_local[:, 2] += self.cfg_task.fixed_asset_cfg.height + fixed_tip_pos_local[:, 2] += self.cfg_task.fixed_asset_cfg.base_height + if self.cfg_task.name == "gear_mesh": + fixed_tip_pos_local[:, 0] = self._get_target_gear_base_offset()[0] + + _, fixed_tip_pos = torch_utils.tf_combine( + self.fixed_quat, self.fixed_pos, self.identity_quat, fixed_tip_pos_local + ) + self.fixed_pos_obs_frame[:] = fixed_tip_pos + + # (2) Move gripper to randomizes location above fixed asset. Keep trying until IK succeeds. + # (a) get position vector to target + bad_envs = env_ids.clone() + ik_attempt = 0 + + hand_down_quat = torch.zeros((self.num_envs, 4), dtype=torch.float32, device=self.device) + self.hand_down_euler = torch.zeros((self.num_envs, 3), dtype=torch.float32, device=self.device) + while True: + n_bad = bad_envs.shape[0] + + above_fixed_pos = fixed_tip_pos.clone() + above_fixed_pos[:, 2] += self.cfg_task.hand_init_pos[2] + + rand_sample = torch.rand((n_bad, 3), dtype=torch.float32, device=self.device) + above_fixed_pos_rand = 2 * (rand_sample - 0.5) # [-1, 1] + hand_init_pos_rand = torch.tensor(self.cfg_task.hand_init_pos_noise, device=self.device) + above_fixed_pos_rand = above_fixed_pos_rand @ torch.diag(hand_init_pos_rand) + above_fixed_pos[bad_envs] += above_fixed_pos_rand + + # (b) get random orientation facing down + hand_down_euler = ( + torch.tensor(self.cfg_task.hand_init_orn, device=self.device).unsqueeze(0).repeat(n_bad, 1) + ) + + rand_sample = torch.rand((n_bad, 3), dtype=torch.float32, device=self.device) + above_fixed_orn_noise = 2 * (rand_sample - 0.5) # [-1, 1] + hand_init_orn_rand = torch.tensor(self.cfg_task.hand_init_orn_noise, device=self.device) + above_fixed_orn_noise = above_fixed_orn_noise @ torch.diag(hand_init_orn_rand) + hand_down_euler += above_fixed_orn_noise + self.hand_down_euler[bad_envs, ...] = hand_down_euler + hand_down_quat[bad_envs, :] = torch_utils.quat_from_euler_xyz( + roll=hand_down_euler[:, 0], pitch=hand_down_euler[:, 1], yaw=hand_down_euler[:, 2] + ) + + # (c) iterative IK Method + self.ctrl_target_fingertip_midpoint_pos[bad_envs, ...] = above_fixed_pos[bad_envs, ...] + self.ctrl_target_fingertip_midpoint_quat[bad_envs, ...] = hand_down_quat[bad_envs, :] + + pos_error, aa_error = self.set_pos_inverse_kinematics(env_ids=bad_envs) + pos_error = torch.linalg.norm(pos_error, dim=1) > 1e-3 + angle_error = torch.norm(aa_error, dim=1) > 1e-3 + any_error = torch.logical_or(pos_error, angle_error) + bad_envs = bad_envs[any_error.nonzero(as_tuple=False).squeeze(-1)] + + # Check IK succeeded for all envs, otherwise try again for those envs + if bad_envs.shape[0] == 0: + break + + self._set_franka_to_default_pose( + joints=[0.00871, -0.10368, -0.00794, -1.49139, -0.00083, 1.38774, 0.0], env_ids=bad_envs + ) + + ik_attempt += 1 + print(f"IK Attempt: {ik_attempt}\tBad Envs: {bad_envs.shape[0]}") + + self.step_sim_no_action() + + # Add flanking gears after servo (so arm doesn't move them). + if self.cfg_task.name == "gear_mesh" and self.cfg_task.add_flanking_gears: + small_gear_state = self._small_gear_asset.data.default_root_state.clone()[env_ids] + small_gear_state[:, 0:7] = fixed_state[:, 0:7] + small_gear_state[:, 7:] = 0.0 # vel + self._small_gear_asset.write_root_state_to_sim(small_gear_state, env_ids=env_ids) + self._small_gear_asset.reset() + + large_gear_state = self._large_gear_asset.data.default_root_state.clone()[env_ids] + large_gear_state[:, 0:7] = fixed_state[:, 0:7] + large_gear_state[:, 7:] = 0.0 # vel + self._large_gear_asset.write_root_state_to_sim(large_gear_state, env_ids=env_ids) + self._large_gear_asset.reset() + + # (3) Randomize asset-in-gripper location. + # flip gripper z orientation + flip_z_quat = torch.tensor([0.0, 0.0, 1.0, 0.0], device=self.device).unsqueeze(0).repeat(self.num_envs, 1) + fingertip_flipped_quat, fingertip_flipped_pos = torch_utils.tf_combine( + q1=self.fingertip_midpoint_quat, + t1=self.fingertip_midpoint_pos, + q2=flip_z_quat, + t2=torch.zeros_like(self.fingertip_midpoint_pos), + ) + + # get default gripper in asset transform + held_asset_relative_pos, held_asset_relative_quat = self.get_handheld_asset_relative_pose() + asset_in_hand_quat, asset_in_hand_pos = torch_utils.tf_inverse( + held_asset_relative_quat, held_asset_relative_pos + ) + + translated_held_asset_quat, translated_held_asset_pos = torch_utils.tf_combine( + q1=fingertip_flipped_quat, t1=fingertip_flipped_pos, q2=asset_in_hand_quat, t2=asset_in_hand_pos + ) + + # Add asset in hand randomization + rand_sample = torch.rand((self.num_envs, 3), dtype=torch.float32, device=self.device) + self.held_asset_pos_noise = 2 * (rand_sample - 0.5) # [-1, 1] + if self.cfg_task.name == "gear_mesh": + self.held_asset_pos_noise[:, 2] = -rand_sample[:, 2] # [-1, 0] + + held_asset_pos_noise = torch.tensor(self.cfg_task.held_asset_pos_noise, device=self.device) + self.held_asset_pos_noise = self.held_asset_pos_noise @ torch.diag(held_asset_pos_noise) + translated_held_asset_quat, translated_held_asset_pos = torch_utils.tf_combine( + q1=translated_held_asset_quat, + t1=translated_held_asset_pos, + q2=self.identity_quat, + t2=self.held_asset_pos_noise, + ) + + held_state = self._held_asset.data.default_root_state.clone() + held_state[:, 0:3] = translated_held_asset_pos + self.scene.env_origins + held_state[:, 3:7] = translated_held_asset_quat + held_state[:, 7:] = 0.0 + self._held_asset.write_root_state_to_sim(held_state) + self._held_asset.reset() + + # Close hand + # Set gains to use for quick resets. + reset_task_prop_gains = torch.tensor(self.cfg.ctrl.reset_task_prop_gains, device=self.device).repeat( + (self.num_envs, 1) + ) + reset_rot_deriv_scale = self.cfg.ctrl.reset_rot_deriv_scale + self._set_gains(reset_task_prop_gains, reset_rot_deriv_scale) + + self.step_sim_no_action() + + grasp_time = 0.0 + while grasp_time < 0.25: + self.ctrl_target_joint_pos[env_ids, 7:] = 0.0 # Close gripper. + self.ctrl_target_gripper_dof_pos = 0.0 + self.close_gripper_in_place() + self.step_sim_no_action() + grasp_time += self.sim.get_physics_dt() + + self.prev_joint_pos = self.joint_pos[:, 0:7].clone() + self.prev_fingertip_pos = self.fingertip_midpoint_pos.clone() + self.prev_fingertip_quat = self.fingertip_midpoint_quat.clone() + + # Set initial actions to involve no-movement. Needed for EMA/correct penalties. + self.actions = torch.zeros_like(self.actions) + self.prev_actions = torch.zeros_like(self.actions) + # Back out what actions should be for initial state. + # Relative position to bolt tip. + self.fixed_pos_action_frame[:] = self.fixed_pos_obs_frame + self.init_fixed_pos_obs_noise + + pos_actions = self.fingertip_midpoint_pos - self.fixed_pos_action_frame + pos_action_bounds = torch.tensor(self.cfg.ctrl.pos_action_bounds, device=self.device) + pos_actions = pos_actions @ torch.diag(1.0 / pos_action_bounds) + self.actions[:, 0:3] = self.prev_actions[:, 0:3] = pos_actions + + # Relative yaw to bolt. + unrot_180_euler = torch.tensor([-np.pi, 0.0, 0.0], device=self.device).repeat(self.num_envs, 1) + unrot_quat = torch_utils.quat_from_euler_xyz( + roll=unrot_180_euler[:, 0], pitch=unrot_180_euler[:, 1], yaw=unrot_180_euler[:, 2] + ) + + fingertip_quat_rel_bolt = torch_utils.quat_mul(unrot_quat, self.fingertip_midpoint_quat) + fingertip_yaw_bolt = torch_utils.get_euler_xyz(fingertip_quat_rel_bolt)[-1] + fingertip_yaw_bolt = torch.where( + fingertip_yaw_bolt > torch.pi / 2, fingertip_yaw_bolt - 2 * torch.pi, fingertip_yaw_bolt + ) + fingertip_yaw_bolt = torch.where( + fingertip_yaw_bolt < -torch.pi, fingertip_yaw_bolt + 2 * torch.pi, fingertip_yaw_bolt + ) + + yaw_action = (fingertip_yaw_bolt + np.deg2rad(180.0)) / np.deg2rad(270.0) * 2.0 - 1.0 + self.actions[:, 5] = self.prev_actions[:, 5] = yaw_action + + # Zero initial velocity. + self.ee_angvel_fd[:, :] = 0.0 + self.ee_linvel_fd[:, :] = 0.0 + + # Set initial gains for the episode. + self._set_gains(self.default_gains) + + physics_sim_view.set_gravity(carb.Float3(*self.cfg.sim.gravity)) diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env_cfg.py new file mode 100644 index 0000000000..0356e9e434 --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_env_cfg.py @@ -0,0 +1,208 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.actuators.actuator_cfg import ImplicitActuatorCfg +from omni.isaac.lab.assets import ArticulationCfg +from omni.isaac.lab.envs import DirectRLEnvCfg +from omni.isaac.lab.scene import InteractiveSceneCfg +from omni.isaac.lab.sim import PhysxCfg, SimulationCfg +from omni.isaac.lab.sim.spawners.materials.physics_materials_cfg import RigidBodyMaterialCfg +from omni.isaac.lab.utils import configclass + +from .factory_tasks_cfg import ASSET_DIR, FactoryTask, GearMesh, NutThread, PegInsert + +OBS_DIM_CFG = { + "fingertip_pos": 3, + "fingertip_pos_rel_fixed": 3, + "fingertip_quat": 4, + "ee_linvel": 3, + "ee_angvel": 3, +} + +STATE_DIM_CFG = { + "fingertip_pos": 3, + "fingertip_pos_rel_fixed": 3, + "fingertip_quat": 4, + "ee_linvel": 3, + "ee_angvel": 3, + "joint_pos": 7, + "held_pos": 3, + "held_pos_rel_fixed": 3, + "held_quat": 4, + "fixed_pos": 3, + "fixed_quat": 4, + "task_prop_gains": 6, + "ema_factor": 1, + "pos_threshold": 3, + "rot_threshold": 3, +} + + +@configclass +class ObsRandCfg: + fixed_asset_pos = [0.001, 0.001, 0.001] + + +@configclass +class CtrlCfg: + ema_factor = 0.2 + + pos_action_bounds = [0.05, 0.05, 0.05] + rot_action_bounds = [1.0, 1.0, 1.0] + + pos_action_threshold = [0.02, 0.02, 0.02] + rot_action_threshold = [0.097, 0.097, 0.097] + + reset_joints = [1.5178e-03, -1.9651e-01, -1.4364e-03, -1.9761, -2.7717e-04, 1.7796, 7.8556e-01] + reset_task_prop_gains = [300, 300, 300, 20, 20, 20] + reset_rot_deriv_scale = 10.0 + default_task_prop_gains = [100, 100, 100, 30, 30, 30] + + # Null space parameters. + default_dof_pos_tensor = [-1.3003, -0.4015, 1.1791, -2.1493, 0.4001, 1.9425, 0.4754] + kp_null = 10.0 + kd_null = 6.3246 + + +@configclass +class FactoryEnvCfg(DirectRLEnvCfg): + decimation = 8 + action_space = 6 + # num_*: will be overwritten to correspond to obs_order, state_order. + observation_space = 21 + state_space = 72 + obs_order: list = ["fingertip_pos_rel_fixed", "fingertip_quat", "ee_linvel", "ee_angvel"] + state_order: list = [ + "fingertip_pos", + "fingertip_quat", + "ee_linvel", + "ee_angvel", + "joint_pos", + "held_pos", + "held_pos_rel_fixed", + "held_quat", + "fixed_pos", + "fixed_quat", + ] + + task_name: str = "peg_insert" # peg_insert, gear_mesh, nut_thread + task: FactoryTask = FactoryTask() + obs_rand: ObsRandCfg = ObsRandCfg() + ctrl: CtrlCfg = CtrlCfg() + + episode_length_s = 10.0 # Probably need to override. + sim: SimulationCfg = SimulationCfg( + device="cuda:0", + dt=1 / 120, + gravity=(0.0, 0.0, -9.81), + physx=PhysxCfg( + solver_type=1, + max_position_iteration_count=192, # Important to avoid interpenetration. + max_velocity_iteration_count=1, + bounce_threshold_velocity=0.2, + friction_offset_threshold=0.01, + friction_correlation_distance=0.00625, + gpu_max_rigid_contact_count=2**23, + gpu_max_rigid_patch_count=2**23, + gpu_max_num_partitions=1, # Important for stable simulation. + ), + physics_material=RigidBodyMaterialCfg( + static_friction=1.0, + dynamic_friction=1.0, + ), + ) + + scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=128, env_spacing=2.0) + + robot = ArticulationCfg( + prim_path="/World/envs/env_.*/Robot", + spawn=sim_utils.UsdFileCfg( + usd_path=f"{ASSET_DIR}/franka_mimic.usd", + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + ), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + joint_pos={ + "panda_joint1": 0.00871, + "panda_joint2": -0.10368, + "panda_joint3": -0.00794, + "panda_joint4": -1.49139, + "panda_joint5": -0.00083, + "panda_joint6": 1.38774, + "panda_joint7": 0.0, + "panda_finger_joint2": 0.04, + }, + pos=(0.0, 0.0, 0.0), + rot=(1.0, 0.0, 0.0, 0.0), + ), + actuators={ + "panda_arm1": ImplicitActuatorCfg( + joint_names_expr=["panda_joint[1-4]"], + stiffness=0.0, + damping=0.0, + friction=0.0, + armature=0.0, + effort_limit=87, + velocity_limit=124.6, + ), + "panda_arm2": ImplicitActuatorCfg( + joint_names_expr=["panda_joint[5-7]"], + stiffness=0.0, + damping=0.0, + friction=0.0, + armature=0.0, + effort_limit=12, + velocity_limit=149.5, + ), + "panda_hand": ImplicitActuatorCfg( + joint_names_expr=["panda_finger_joint[1-2]"], + effort_limit=40.0, + velocity_limit=0.04, + stiffness=7500.0, + damping=173.0, + friction=0.1, + armature=0.0, + ), + }, + ) + + +@configclass +class FactoryTaskPegInsertCfg(FactoryEnvCfg): + task_name = "peg_insert" + task = PegInsert() + episode_length_s = 10.0 + + +@configclass +class FactoryTaskGearMeshCfg(FactoryEnvCfg): + task_name = "gear_mesh" + task = GearMesh() + episode_length_s = 20.0 + + +@configclass +class FactoryTaskNutThreadCfg(FactoryEnvCfg): + task_name = "nut_thread" + task = NutThread() + episode_length_s = 30.0 diff --git a/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_tasks_cfg.py b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_tasks_cfg.py new file mode 100644 index 0000000000..643cbc2afb --- /dev/null +++ b/source/extensions/omni.isaac.lab_tasks/omni/isaac/lab_tasks/direct/factory/factory_tasks_cfg.py @@ -0,0 +1,448 @@ +# Copyright (c) 2022-2024, The Isaac Lab Project Developers. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +import omni.isaac.lab.sim as sim_utils +from omni.isaac.lab.assets import ArticulationCfg +from omni.isaac.lab.utils import configclass +from omni.isaac.lab.utils.assets import ISAACLAB_NUCLEUS_DIR + +ASSET_DIR = f"{ISAACLAB_NUCLEUS_DIR}/Factory" + + +@configclass +class FixedAssetCfg: + usd_path: str = "" + diameter: float = 0.0 + height: float = 0.0 + base_height: float = 0.0 # Used to compute held asset CoM. + friction: float = 0.75 + mass: float = 0.05 + + +@configclass +class HeldAssetCfg: + usd_path: str = "" + diameter: float = 0.0 # Used for gripper width. + height: float = 0.0 + friction: float = 0.75 + mass: float = 0.05 + + +@configclass +class RobotCfg: + robot_usd: str = "" + franka_fingerpad_length: float = 0.017608 + friction: float = 0.75 + + +@configclass +class FactoryTask: + robot_cfg: RobotCfg = RobotCfg() + name: str = "" + duration_s = 5.0 + + fixed_asset_cfg: FixedAssetCfg = FixedAssetCfg() + held_asset_cfg: HeldAssetCfg = HeldAssetCfg() + asset_size: float = 0.0 + + # Robot + hand_init_pos: list = [0.0, 0.0, 0.015] # Relative to fixed asset tip. + hand_init_pos_noise: list = [0.02, 0.02, 0.01] + hand_init_orn: list = [3.1416, 0, 2.356] + hand_init_orn_noise: list = [0.0, 0.0, 1.57] + + # Action + unidirectional_rot: bool = False + + # Fixed Asset (applies to all tasks) + fixed_asset_init_pos_noise: list = [0.05, 0.05, 0.05] + fixed_asset_init_orn_deg: float = 0.0 + fixed_asset_init_orn_range_deg: float = 360.0 + + # Held Asset (applies to all tasks) + held_asset_pos_noise: list = [0.0, 0.006, 0.003] # noise level of the held asset in gripper + held_asset_rot_init: float = -90.0 + + # Reward + ee_success_yaw: float = 0.0 # nut_thread task only. + action_penalty_scale: float = 0.0 + action_grad_penalty_scale: float = 0.0 + # Reward function details can be found in Appendix B of https://arxiv.org/pdf/2408.04587. + # Multi-scale keypoints are used to capture different phases of the task. + # Each reward passes the keypoint distance, x, through a squashing function: + # r(x) = 1/(exp(-ax) + b + exp(ax)). + # Each list defines [a, b] which control the slope and maximum of the squashing function. + num_keypoints: int = 4 + keypoint_scale: float = 0.15 + keypoint_coef_baseline: list = [5, 4] # General movement towards fixed object. + keypoint_coef_coarse: list = [50, 2] # Movement to align the assets. + keypoint_coef_fine: list = [100, 0] # Smaller distances for threading or last-inch insertion. + # Fixed-asset height fraction for which different bonuses are rewarded (see individual tasks). + success_threshold: float = 0.04 + engage_threshold: float = 0.9 + + +@configclass +class Peg8mm(HeldAssetCfg): + usd_path = f"{ASSET_DIR}/factory_peg_8mm.usd" + diameter = 0.007986 + height = 0.050 + mass = 0.019 + + +@configclass +class Hole8mm(FixedAssetCfg): + usd_path = f"{ASSET_DIR}/factory_hole_8mm.usd" + diameter = 0.0081 + height = 0.025 + base_height = 0.0 + + +@configclass +class PegInsert(FactoryTask): + name = "peg_insert" + fixed_asset_cfg = Hole8mm() + held_asset_cfg = Peg8mm() + asset_size = 8.0 + duration_s = 10.0 + + # Robot + hand_init_pos: list = [0.0, 0.0, 0.047] # Relative to fixed asset tip. + hand_init_pos_noise: list = [0.02, 0.02, 0.01] + hand_init_orn: list = [3.1416, 0.0, 0.0] + hand_init_orn_noise: list = [0.0, 0.0, 0.785] + + # Fixed Asset (applies to all tasks) + fixed_asset_init_pos_noise: list = [0.05, 0.05, 0.05] + fixed_asset_init_orn_deg: float = 0.0 + fixed_asset_init_orn_range_deg: float = 360.0 + + # Held Asset (applies to all tasks) + held_asset_pos_noise: list = [0.003, 0.0, 0.003] # noise level of the held asset in gripper + held_asset_rot_init: float = 0.0 + + # Rewards + keypoint_coef_baseline: list = [5, 4] + keypoint_coef_coarse: list = [50, 2] + keypoint_coef_fine: list = [100, 0] + # Fraction of socket height. + success_threshold: float = 0.04 + engage_threshold: float = 0.9 + + fixed_asset: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/FixedAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=fixed_asset_cfg.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=fixed_asset_cfg.mass), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.6, 0.0, 0.05), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + held_asset: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/HeldAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=held_asset_cfg.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=held_asset_cfg.mass), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.4, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + + +@configclass +class GearBase(FixedAssetCfg): + usd_path = f"{ASSET_DIR}/factory_gear_base.usd" + height = 0.02 + base_height = 0.005 + small_gear_base_offset = [5.075e-2, 0.0, 0.0] + medium_gear_base_offset = [2.025e-2, 0.0, 0.0] + large_gear_base_offset = [-3.025e-2, 0.0, 0.0] + + +@configclass +class MediumGear(HeldAssetCfg): + usd_path = f"{ASSET_DIR}/factory_gear_medium.usd" + diameter = 0.03 # Used for gripper width. + height: float = 0.03 + mass = 0.012 + + +@configclass +class GearMesh(FactoryTask): + name = "gear_mesh" + fixed_asset_cfg = GearBase() + held_asset_cfg = MediumGear() + target_gear = "gear_medium" + duration_s = 20.0 + + small_gear_usd = f"{ASSET_DIR}/factory_gear_small.usd" + large_gear_usd = f"{ASSET_DIR}/factory_gear_large.usd" + + small_gear_cfg: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/SmallGearAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=small_gear_usd, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.019), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.4, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + + large_gear_cfg: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/LargeGearAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=large_gear_usd, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.019), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.4, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + + # Gears Asset + add_flanking_gears = True + add_flanking_gears_prob = 1.0 + + # Robot + hand_init_pos: list = [0.0, 0.0, 0.035] # Relative to fixed asset tip. + hand_init_pos_noise: list = [0.02, 0.02, 0.01] + hand_init_orn: list = [3.1416, 0, 0.0] + hand_init_orn_noise: list = [0.0, 0.0, 0.785] + + # Fixed Asset (applies to all tasks) + fixed_asset_init_pos_noise: list = [0.05, 0.05, 0.05] + fixed_asset_init_orn_deg: float = 0.0 + fixed_asset_init_orn_range_deg: float = 15.0 + + # Held Asset (applies to all tasks) + held_asset_pos_noise: list = [0.003, 0.0, 0.003] # noise level of the held asset in gripper + held_asset_rot_init: float = -90.0 + + keypoint_coef_baseline: list = [5, 4] + keypoint_coef_coarse: list = [50, 2] + keypoint_coef_fine: list = [100, 0] + # Fraction of gear peg height. + success_threshold: float = 0.05 + engage_threshold: float = 0.9 + + fixed_asset: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/FixedAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=fixed_asset_cfg.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=fixed_asset_cfg.mass), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.6, 0.0, 0.05), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + held_asset: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/HeldAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=held_asset_cfg.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=held_asset_cfg.mass), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.4, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + + +@configclass +class NutM16(HeldAssetCfg): + usd_path = f"{ASSET_DIR}/factory_nut_m16.usd" + diameter = 0.024 + height = 0.01 + mass = 0.03 + friction = 0.01 # Additive with the nut means friction is (-0.25 + 0.75)/2 = 0.25 + + +@configclass +class BoltM16(FixedAssetCfg): + usd_path = f"{ASSET_DIR}/factory_bolt_m16.usd" + diameter = 0.024 + height = 0.025 + base_height = 0.01 + thread_pitch = 0.002 + + +@configclass +class NutThread(FactoryTask): + name = "nut_thread" + fixed_asset_cfg = BoltM16() + held_asset_cfg = NutM16() + asset_size = 16.0 + duration_s = 30.0 + + # Robot + hand_init_pos: list = [0.0, 0.0, 0.015] # Relative to fixed asset tip. + hand_init_pos_noise: list = [0.02, 0.02, 0.01] + hand_init_orn: list = [3.1416, 0.0, 1.83] + hand_init_orn_noise: list = [0.0, 0.0, 0.26] + + # Action + unidirectional_rot: bool = True + + # Fixed Asset (applies to all tasks) + fixed_asset_init_pos_noise: list = [0.05, 0.05, 0.05] + fixed_asset_init_orn_deg: float = 120.0 + fixed_asset_init_orn_range_deg: float = 30.0 + + # Held Asset (applies to all tasks) + held_asset_pos_noise: list = [0.0, 0.003, 0.003] # noise level of the held asset in gripper + held_asset_rot_init: float = -90.0 + + # Reward. + ee_success_yaw = 0.0 + keypoint_coef_baseline: list = [100, 2] + keypoint_coef_coarse: list = [500, 2] # 100, 2 + keypoint_coef_fine: list = [1500, 0] # 500, 0 + # Fraction of thread-height. + success_threshold: float = 0.375 + engage_threshold: float = 0.5 + keypoint_scale: float = 0.05 + + fixed_asset: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/FixedAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=fixed_asset_cfg.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=False, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=fixed_asset_cfg.mass), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.6, 0.0, 0.05), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) + held_asset: ArticulationCfg = ArticulationCfg( + prim_path="/World/envs/env_.*/HeldAsset", + spawn=sim_utils.UsdFileCfg( + usd_path=held_asset_cfg.usd_path, + activate_contact_sensors=True, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + max_depenetration_velocity=5.0, + linear_damping=0.0, + angular_damping=0.0, + max_linear_velocity=1000.0, + max_angular_velocity=3666.0, + enable_gyroscopic_forces=True, + solver_position_iteration_count=192, + solver_velocity_iteration_count=1, + max_contact_impulse=1e32, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=held_asset_cfg.mass), + collision_props=sim_utils.CollisionPropertiesCfg(contact_offset=0.005, rest_offset=0.0), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=(0.0, 0.4, 0.1), rot=(1.0, 0.0, 0.0, 0.0), joint_pos={}, joint_vel={} + ), + actuators={}, + ) diff --git a/source/standalone/workflows/rl_games/play.py b/source/standalone/workflows/rl_games/play.py index d16e46b2ef..7aa1456fe0 100644 --- a/source/standalone/workflows/rl_games/play.py +++ b/source/standalone/workflows/rl_games/play.py @@ -155,7 +155,7 @@ def main(): # convert obs to agent format obs = agent.obs_to_torch(obs) # agent stepping - actions = agent.get_action(obs, is_deterministic=True) + actions = agent.get_action(obs, is_deterministic=agent.is_deterministic) # env stepping obs, _, dones, _ = env.step(actions) diff --git a/tools/per_test_timeouts.py b/tools/per_test_timeouts.py index a82b49daac..340abde85c 100644 --- a/tools/per_test_timeouts.py +++ b/tools/per_test_timeouts.py @@ -10,7 +10,7 @@ PER_TEST_TIMEOUTS = { "test_articulation.py": 200, "test_deformable_object.py": 200, - "test_environments.py": 1200, # This test runs through all the environments for 100 steps each + "test_environments.py": 1500, # This test runs through all the environments for 100 steps each "test_environment_determinism.py": 200, # This test runs through many the environments for 100 steps each "test_env_rendering_logic.py": 300, "test_camera.py": 500,