Unverified Commit 55745eb3 authored by Farbod Farshidian's avatar Farbod Farshidian Committed by GitHub

Adds the Spot locomotion environment (#450)

Adds the training task for the Spot robot.  


## Type of change

- New feature

## 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
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have run all the tests with `./isaaclab.sh --test` and they pass
- [ ] 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
parent 4ed26b9c
......@@ -204,7 +204,6 @@ html_css_files = ["custom.css"]
html_theme_options = {
"collapse_navigation": True,
"repository_url": "https://github.com/isaac-sim/IsaacLab",
"announcement": "We have now released v0.3.0! Please use the latest version for the best experience.",
"use_repository_button": True,
"use_issues_button": True,
"use_edit_page_button": True,
......
This diff is collapsed.
......@@ -28,8 +28,10 @@ from .actuator_cfg import (
ActuatorNetLSTMCfg,
ActuatorNetMLPCfg,
DCMotorCfg,
DelayedPDActuatorCfg,
IdealPDActuatorCfg,
ImplicitActuatorCfg,
RemotizedPDActuatorCfg,
)
from .actuator_net import ActuatorNetLSTM, ActuatorNetMLP
from .actuator_pd import DCMotor, IdealPDActuator, ImplicitActuator
from .actuator_pd import DCMotor, DelayedPDActuator, IdealPDActuator, ImplicitActuator, RemotizedPDActuator
......@@ -3,6 +3,7 @@
#
# SPDX-License-Identifier: BSD-3-Clause
import torch
from collections.abc import Iterable
from dataclasses import MISSING
from typing import Literal
......@@ -153,3 +154,42 @@ class ActuatorNetMLPCfg(DCMotorCfg):
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_num_time_lags: int = 0
"""Minimum number of physics time-steps that the actuator command may be delayed."""
max_num_time_lags: int = 0
"""Maximum number of physics time-steps that the actuator command may be delayed."""
num_time_lags: int = 0
"""The number of physics time-steps that the actuator command will be delayed.
Note:
This values cannot be greater than `max_num_time_lags`.
"""
@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` describing 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).
The tensor describes relationship between the joint angle (rad), the transmission ratio (in/out),
and the output torque (N*m).
"""
......@@ -11,10 +11,18 @@ from typing import TYPE_CHECKING
from omni.isaac.core.utils.types import ArticulationActions
from omni.isaac.lab.utils import DelayBuffer, LinearInterpolation
from .actuator_base import ActuatorBase
if TYPE_CHECKING:
from .actuator_cfg import DCMotorCfg, IdealPDActuatorCfg, ImplicitActuatorCfg
from .actuator_cfg import (
DCMotorCfg,
DelayedPDActuatorCfg,
IdealPDActuatorCfg,
ImplicitActuatorCfg,
RemotizedPDActuatorCfg,
)
"""
......@@ -210,3 +218,138 @@ class DCMotor(IdealPDActuator):
# 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 data.
The DelayedPDActuator has configurable minimum and maximum time lag values, which are used to initialize a
DelayBuffer to hold a queue of pending actuator commands. On reset, a value time_lags will be randomly sampled
from the min and max time lag bounds. At every physics step, the most recent actuation value is pushed to the
DelayBuffer, but the final actuation value applied to simulation will be `time_lags` physics steps in the past.
"""
def __init__(
self,
cfg: DelayedPDActuatorCfg,
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,
):
super().__init__(
cfg,
joint_names,
joint_ids,
num_envs,
device,
stiffness,
damping,
armature,
friction,
effort_limit,
velocity_limit,
)
# instantiate the delay buffers
self.positions_delay_buffer = DelayBuffer(cfg.max_num_time_lags, num_envs=num_envs, device=device)
self.velocities_delay_buffer = DelayBuffer(cfg.max_num_time_lags, num_envs=num_envs, device=device)
self.efforts_delay_buffer = DelayBuffer(cfg.max_num_time_lags, num_envs=num_envs, device=device)
# all of the envs
self._ALL_INDICES = torch.arange(num_envs, dtype=torch.long, device=device)
def reset(self, env_ids: Sequence[int]):
super().reset(env_ids)
# number of environments (since env_ids can be a slice)
env_size = self._ALL_INDICES[env_ids].size()
# set a new random delay for environments in env_ids
time_lags = self.positions_delay_buffer.time_lags
time_lags[env_ids] = torch.randint(
low=self.cfg.min_num_time_lags,
high=self.cfg.max_num_time_lags + 1,
size=env_size,
device=self._device,
dtype=torch.int,
)
# set delays
self.positions_delay_buffer.set_time_lag(time_lags)
self.velocities_delay_buffer.set_time_lag(time_lags)
self.efforts_delay_buffer.set_time_lag(time_lags)
# 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.
The torque limits for this actuator are applied by querying a lookup table describing the relationship between
the joint angle and the maximum output torque.
"""
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)
@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]
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
......@@ -6,7 +6,9 @@
"""Sub-package containing utilities for common operations and helper functions."""
from .array import *
from .buffers import *
from .configclass import configclass
from .dict import *
from .linear_interpolation import *
from .string import *
from .timer import Timer
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Sub-module containing different buffers."""
from .circular_buffer import BatchedCircularBuffer
from .delay_buffer import DelayBuffer
# 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
class BatchedCircularBuffer:
"""Circular buffer for storing a history of batched tensor data."""
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 value is one.
batch_size: The batch dimension of the data.
device: Device used for processing.
"""
if max_len < 1:
raise ValueError(f"The buffer size should be greater than zero. However, it is set to {max_len}!")
self._max_len = max_len
self._batch_size = batch_size
self._device = device
self._ALL_INDICES = torch.arange(batch_size, 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 circular buffer for data storage
self._buffer: torch.Tensor | None = None # the data buffer
def reset(self, batch_ids: Sequence[int] | None = None):
"""Reset the circular buffer.
Args:
batch_ids: Elements to reset in the batch dimension.
"""
# resolve all indices
if batch_ids is None:
batch_ids = self._ALL_INDICES
self._num_pushes[batch_ids] = 0
def append(self, data: torch.Tensor):
"""Append the data to the circular buffer.
Args:
data: The data to be appended, where `len(data) == self.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_len, *data.shape), dtype=data.dtype, device=self._device)
# move the head to the next slot
self._pointer = (self._pointer + 1) % self.max_len
# add the new data to the last layer
self._buffer[self._pointer] = data
# increment number of number of pushes
self._num_pushes += 1
def __getitem__(self, key: torch.Tensor) -> torch.Tensor:
"""Get the data from the circular buffer in LIFO fashion.
Args:
key: The index of the data to be retrieved. It can be a single integer or a tensor of integers.
"""
if len(key) != self.batch_size:
raise ValueError(f"The key has length {key.shape[0]} while expecting {self.batch_size}")
if torch.any(self._num_pushes == 0) or self._buffer is None:
raise ValueError("Attempting to get data on an empty circular buffer.")
# 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_len)
# return output
return self._buffer[index_in_buffer, self._ALL_INDICES, :]
"""
Properties.
"""
@property
def batch_size(self) -> int:
"""The batch size in the ring buffer."""
return self._batch_size
@property
def device(self) -> str:
"""Device used for processing."""
return self._device
@property
def max_len(self) -> int:
"""The maximum length of the ring buffer."""
return self._max_len
# 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 .circular_buffer import BatchedCircularBuffer
class DelayBuffer:
"""Provides the functionality to simulate delays for a data stream.
The delay can be set constant, using :meth:`set_time_lag` with an integer input or different per environment, using
the same method with an integer tensor input. If the requested delay is larger than the number of buffered data
points since the last reset, the :meth:`compute` will return the oldest stored data, which is obviously less delayed
than expected. Internally, this class uses a circular buffer for better computation efficiency.
"""
def __init__(self, max_num_histories: int, num_envs: int, device: str):
"""Initialize the Delay Buffer.
By default all the environments will have no delay.
Args:
max_num_histories: The maximum number of time steps that the data will be buffered. It is recommended
to set this value equal to the maximum number of time lags that are expected. The minimum value is zero.
num_envs: Number of articulations in the view.
device: Device used for processing.
"""
self._max_num_histories = max(0, max_num_histories)
# the buffer size: current data plus the history length
self._circular_buffer = BatchedCircularBuffer(self._max_num_histories + 1, num_envs, 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(num_envs, dtype=torch.int, device=device)
def set_time_lag(self, time_lag: int | torch.Tensor):
"""Sets the time lags for each environment.
Args:
time_lag: A single integer will result in a fixed delay across all environments, while a tensor of integers
with the size (num_envs, ) will set a different delay for each environment. This value cannot be larger than
:meth:`max_num_histories`.
"""
# parse requested time_lag
if isinstance(time_lag, int):
self._min_time_lag = time_lag
self._max_time_lag = time_lag
self._time_lags = torch.ones(self.num_envs, dtype=torch.int, device=self.device) * time_lag
elif isinstance(time_lag, torch.Tensor):
if time_lag.size() != torch.Size([
self.num_envs,
]):
raise TypeError(
f"Invalid size for time_lag: {time_lag.size()}. Expected torch.Size([{self.num_envs}])."
)
self._min_time_lag = torch.min(time_lag).item()
self._max_time_lag = torch.max(time_lag).item()
self._time_lags = time_lag.to(dtype=torch.int, device=self.device)
else:
raise TypeError(f"Invalid type for time_lag: {type(time_lag)}. Expected int or Tensor.")
# check that time_lag is feasible
if self._min_time_lag < 0:
raise ValueError("Minimum of `time_lag` cannot be negative!")
if self._max_time_lag > self._max_num_histories:
raise ValueError(f"Maximum of `time_lag` cannot be larger than {self._max_num_histories}!")
def reset(self, env_ids: Sequence[int] | None = None):
"""Reset the delay buffer.
Args:
env_ids: List of environment IDs to reset.
"""
self._circular_buffer.reset(env_ids)
def compute(self, data: torch.Tensor) -> torch.Tensor:
"""Adds the data to buffer and returns a stale version of the data based on :meth:`time_lags`.
If the requested delay is larger than the number of buffered data points since the last reset, the :meth:`compute`
will return the oldest stored data, which is obviously less delayed than expected.
Args:
data: The input data. Shape is ``(num_envs, num_feature)``.
Returns:
The delayed version of the input data. Shape is ``(num_envs, num_feature)``.
"""
# 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()
"""
Properties.
"""
@property
def num_envs(self) -> int:
"""Number of articulations in the view."""
return self._circular_buffer.batch_size
@property
def device(self) -> str:
"""Device used for processing."""
return self._circular_buffer.device
@property
def max_num_histories(self) -> int:
"""Maximum number of time steps that the data can buffered."""
return self._max_num_histories
@property
def min_time_lag(self) -> int:
"""Minimum number of time steps that the data can be delayed. This value cannot be negative."""
return self._min_time_lag
@property
def max_time_lag(self) -> int:
"""Maximum number of time steps that the data can be delayed. This value cannot be greater than :meth:`max_num_histories`."""
return self._max_time_lag
@property
def time_lags(self) -> torch.Tensor:
"""The time lag for each environment. These values are between :meth:`mim_time_lag` and :meth:`max_time_lag`."""
return self._time_lags
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from __future__ import annotations
import torch
class LinearInterpolation:
"""Linearly interpolates a sampled scalar function ``y = f(x)`` where :math:`f: R -> R`.
It assumes that the function's domain, X, is sampled in an ascending order. For the query points out of
the sampling range of X, the class does a zero-order-hold extrapolation based on the boundary values.
"""
def __init__(self, x: torch.Tensor, y: torch.Tensor, device: str):
"""Initialize the Linear Interpolation.
The scalar function maps from real values, x, to real values, y.
Args:
x: An ascending vector of samples from the function's domain.
y: The function's values associated to x.
device: Device used for processing.
"""
# 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("Tensor x and y should have the same size!")
# make sure that x is sorted
if torch.any(self._x[1:] < self._x[:-1]):
raise ValueError("x is not sorted!")
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 interpolation values. 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
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
import torch
import unittest
"""Launch Isaac Sim Simulator first."""
from omni.isaac.lab.app import AppLauncher, run_tests
# launch omniverse app in headless mode
simulation_app = AppLauncher(headless=True).app
from omni.isaac.lab.utils import DelayBuffer
class TestDelayBuffer(unittest.TestCase):
"""Test fixture for checking Delay Buffer utilities in Orbit."""
device: str = "cpu"
num_envs: int = 10
max_num_histories: int = 4
def generate_data(self, length: int) -> torch.Tensor:
for step in range(length):
yield torch.ones((self.num_envs, 1), dtype=int, device=self.device) * step
def test_constant_time_lags(self):
"""Test constant delay."""
const_lag: int = 3
delay_buffer = DelayBuffer(self.max_num_histories, num_envs=self.num_envs, device=self.device)
delay_buffer.set_time_lag(const_lag)
all_data = []
for i, data in enumerate(self.generate_data(20)):
all_data.append(data)
# apply delay
delayed_data = delay_buffer.compute(data)
error = delayed_data - all_data[max(0, i - const_lag)]
self.assertTrue(torch.all(error == 0))
def test_reset(self):
"""Test resetting the last two environments after iteration `reset_itr`."""
const_lag: int = 2
reset_itr = 10
delay_buffer = DelayBuffer(self.max_num_histories, num_envs=self.num_envs, device=self.device)
delay_buffer.set_time_lag(const_lag)
all_data = []
for i, data in enumerate(self.generate_data(20)):
all_data.append(data)
# from 'reset_itr' iteration reset the last and second-to-last environments
if i == reset_itr:
delay_buffer.reset([-2, -1])
# apply delay
delayed_data = delay_buffer.compute(data)
# before 'reset_itr' is is similar to test_constant_time_lags
# after that indices [-2, -1] should be treated separately
if i < reset_itr:
error = delayed_data - all_data[max(0, i - const_lag)]
self.assertTrue(torch.all(error == 0))
else:
# error_regular = delayed_data[:-2] - all_data[max(0, i - const_lag)][:-2]
error2_reset = delayed_data[-2, -1] - all_data[max(reset_itr, i - const_lag)][-2, -1]
# self.assertTrue(torch.all(error_regular == 0))
self.assertTrue(torch.all(error2_reset == 0))
def test_random_time_lags(self):
"""Test random delay."""
max_lag: int = 3
time_lags = torch.randint(low=0, high=max_lag + 1, size=(self.num_envs,), dtype=torch.int, device=self.device)
delay_buffer = DelayBuffer(self.max_num_histories, num_envs=self.num_envs, device=self.device)
delay_buffer.set_time_lag(time_lags)
all_data = []
for i, data in enumerate(self.generate_data(20)):
all_data.append(data)
# apply delay
delayed_data = delay_buffer.compute(data)
true_delayed_index = torch.maximum(i - delay_buffer.time_lags, torch.zeros_like(delay_buffer.time_lags))
true_delayed_index = true_delayed_index.tolist()
for i in range(self.num_envs):
error = delayed_data[i] - all_data[true_delayed_index[i]][i]
self.assertTrue(torch.all(error == 0))
if __name__ == "__main__":
run_tests()
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Configuration for the Boston Dynamics Spot robot."""
from __future__ import annotations
import torch
import omni.isaac.lab.sim as sim_utils
from omni.isaac.lab.actuators import DelayedPDActuatorCfg, RemotizedPDActuatorCfg
from omni.isaac.lab.assets.articulation import ArticulationCfg
from omni.isaac.lab.utils.assets import ISAAC_NUCLEUS_DIR
# Note: This data was collected by the Boston Dynamics AI Institute.
joint_parameter_lookup = torch.tensor([
[-2.792900, -24.776718, 37.165077],
[-2.767442, -26.290108, 39.435162],
[-2.741984, -27.793369, 41.690054],
[-2.716526, -29.285997, 43.928996],
[-2.691068, -30.767536, 46.151304],
[-2.665610, -32.237423, 48.356134],
[-2.640152, -33.695168, 50.542751],
[-2.614694, -35.140221, 52.710331],
[-2.589236, -36.572052, 54.858078],
[-2.563778, -37.990086, 56.985128],
[-2.538320, -39.393730, 59.090595],
[-2.512862, -40.782406, 61.173609],
[-2.487404, -42.155487, 63.233231],
[-2.461946, -43.512371, 65.268557],
[-2.436488, -44.852371, 67.278557],
[-2.411030, -46.174873, 69.262310],
[-2.385572, -47.479156, 71.218735],
[-2.360114, -48.764549, 73.146824],
[-2.334656, -50.030334, 75.045502],
[-2.309198, -51.275761, 76.913641],
[-2.283740, -52.500103, 78.750154],
[-2.258282, -53.702587, 80.553881],
[-2.232824, -54.882442, 82.323664],
[-2.207366, -56.038860, 84.058290],
[-2.181908, -57.171028, 85.756542],
[-2.156450, -58.278133, 87.417200],
[-2.130992, -59.359314, 89.038971],
[-2.105534, -60.413738, 90.620607],
[-2.080076, -61.440529, 92.160793],
[-2.054618, -62.438812, 93.658218],
[-2.029160, -63.407692, 95.111538],
[-2.003702, -64.346268, 96.519402],
[-1.978244, -65.253670, 97.880505],
[-1.952786, -66.128944, 99.193417],
[-1.927328, -66.971176, 100.456764],
[-1.901870, -67.779457, 101.669186],
[-1.876412, -68.552864, 102.829296],
[-1.850954, -69.290451, 103.935677],
[-1.825496, -69.991325, 104.986988],
[-1.800038, -70.654541, 105.981812],
[-1.774580, -71.279190, 106.918785],
[-1.749122, -71.864319, 107.796478],
[-1.723664, -72.409088, 108.613632],
[-1.698206, -72.912567, 109.368851],
[-1.672748, -73.373871, 110.060806],
[-1.647290, -73.792130, 110.688194],
[-1.621832, -74.166512, 111.249767],
[-1.596374, -74.496147, 111.744221],
[-1.570916, -74.780251, 112.170376],
[-1.545458, -75.017998, 112.526997],
[-1.520000, -75.208656, 112.812984],
[-1.494542, -75.351448, 113.027172],
[-1.469084, -75.445686, 113.168530],
[-1.443626, -75.490677, 113.236015],
[-1.418168, -75.485771, 113.228657],
[-1.392710, -75.430344, 113.145515],
[-1.367252, -75.323830, 112.985744],
[-1.341794, -75.165688, 112.748531],
[-1.316336, -74.955406, 112.433109],
[-1.290878, -74.692551, 112.038826],
[-1.265420, -74.376694, 111.565041],
[-1.239962, -74.007477, 111.011215],
[-1.214504, -73.584579, 110.376869],
[-1.189046, -73.107742, 109.661613],
[-1.163588, -72.576752, 108.865128],
[-1.138130, -71.991455, 107.987183],
[-1.112672, -71.351707, 107.027561],
[-1.087214, -70.657486, 105.986229],
[-1.061756, -69.908813, 104.863220],
[-1.036298, -69.105721, 103.658581],
[-1.010840, -68.248337, 102.372505],
[-0.985382, -67.336861, 101.005291],
[-0.959924, -66.371513, 99.557270],
[-0.934466, -65.352615, 98.028923],
[-0.909008, -64.280533, 96.420799],
[-0.883550, -63.155693, 94.733540],
[-0.858092, -61.978588, 92.967882],
[-0.832634, -60.749775, 91.124662],
[-0.807176, -59.469845, 89.204767],
[-0.781718, -58.139503, 87.209255],
[-0.756260, -56.759487, 85.139231],
[-0.730802, -55.330616, 82.995924],
[-0.705344, -53.853729, 80.780594],
[-0.679886, -52.329796, 78.494694],
[-0.654428, -50.759762, 76.139643],
[-0.628970, -49.144699, 73.717049],
[-0.603512, -47.485737, 71.228605],
[-0.578054, -45.784004, 68.676006],
[-0.552596, -44.040764, 66.061146],
[-0.527138, -42.257267, 63.385900],
[-0.501680, -40.434883, 60.652325],
[-0.476222, -38.574947, 57.862421],
[-0.450764, -36.678982, 55.018473],
[-0.425306, -34.748432, 52.122648],
[-0.399848, -32.784836, 49.177254],
[-0.374390, -30.789810, 46.184715],
[-0.348932, -28.764952, 43.147428],
[-0.323474, -26.711969, 40.067954],
[-0.298016, -24.632576, 36.948864],
[-0.272558, -22.528547, 33.792821],
[-0.247100, -20.401667, 30.602500],
])
"""Describes relationship between the joint angle (rad), the transmission ratio (in/out), and the output torque (N*m)
for the knees of the Boston Dynamics Spot robot.
"""
##
# Configuration
##
SPOT_CFG = ArticulationCfg(
spawn=sim_utils.UsdFileCfg(
usd_path=f"{ISAAC_NUCLEUS_DIR}/Robots/BostonDynamics/spot/spot.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
),
),
init_state=ArticulationCfg.InitialStateCfg(
pos=(0.0, 0.0, 0.5),
joint_pos={
"[fh]l_hx": 0.1, # all left hip_x
"[fh]r_hx": -0.1, # all right hip_x
"f[rl]_hy": 0.9, # front hip_y
"h[rl]_hy": 1.1, # hind hip_y
".*_kn": -1.5, # all knees
},
joint_vel={".*": 0.0},
),
actuators={
"spot_hip": DelayedPDActuatorCfg(
joint_names_expr=[".*_h[xy]"],
effort_limit=45.0,
stiffness=60.0,
damping=1.5,
min_num_time_lags=0, # physics time steps (min: 2.0*0=0.0ms)
max_num_time_lags=4, # physics time steps (max: 2.0*4=8.0ms)
),
"spot_knee": RemotizedPDActuatorCfg(
joint_names_expr=[".*_kn"],
joint_parameter_lookup=joint_parameter_lookup,
effort_limit=None, # torque limits are handled based experimental data (:meth:`RemotizedPDActuatorCfg.data`)
stiffness=60.0,
damping=1.5,
min_num_time_lags=0, # physics time steps (min: 2.0*0=0.0ms)
max_num_time_lags=4, # physics time steps (max: 2.0*4=8.0ms)
),
},
)
"""Configuration for the Boston Dynamics Spot robot."""
We would like to acknowledge The AI Institute's efforts in developing the Spot MDP from specifications provided by Boston Dynamics.
They trained, verified, and deployed the resulting policy on the Spot hardware and demonstrated its capability and reliability out in the real world.
The accompanying deployment code and access to Spot's low-level API will be available in the Spot RL Researcher Kit.
We thank The AI Institute for their trailblazing use of and contributions to Isaac Lab and for sharing their code publicly with the community to promote wider use of the Nvidia RL ecosystem.
# 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, flat_env_cfg
##
# Register Gym environments.
##
gym.register(
id="Isaac-Velocity-Flat-Spot-v0",
entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": flat_env_cfg.SpotFlatEnvCfg,
"rsl_rl_cfg_entry_point": agents.rsl_rl_cfg.SpotFlatPPORunnerCfg,
},
)
gym.register(
id="Isaac-Velocity-Flat-Spot-Play-v0",
entry_point="omni.isaac.lab.envs:ManagerBasedRLEnv",
disable_env_checker=True,
kwargs={
"env_cfg_entry_point": flat_env_cfg.SpotFlatEnvCfg_PLAY,
"rsl_rl_cfg_entry_point": agents.rsl_rl_cfg.SpotFlatPPORunnerCfg,
},
)
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
from . import rsl_rl_cfg # noqa: F401, F403
# 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 omni.isaac.lab_tasks.utils.wrappers.rsl_rl import (
RslRlOnPolicyRunnerCfg,
RslRlPpoActorCriticCfg,
RslRlPpoAlgorithmCfg,
)
@configclass
class SpotFlatPPORunnerCfg(RslRlOnPolicyRunnerCfg):
num_steps_per_env = 24
max_iterations = 35000
save_interval = 50
experiment_name = "spot_flat"
empirical_normalization = False
store_code_state = False
policy = RslRlPpoActorCriticCfg(
init_noise_std=1.0,
actor_hidden_dims=[512, 256, 128],
critic_hidden_dims=[512, 256, 128],
activation="elu",
)
algorithm = RslRlPpoAlgorithmCfg(
value_loss_coef=0.5,
use_clipped_value_loss=True,
clip_param=0.2,
entropy_coef=0.0025,
num_learning_epochs=5,
num_mini_batches=4,
learning_rate=1.0e-3,
schedule="adaptive",
gamma=0.99,
lam=0.95,
desired_kl=0.01,
max_grad_norm=1.0,
)
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""This sub-module contains the functions that are specific to the Spot locomotion task."""
from .events import * # noqa: F401, F403
from .rewards import * # noqa: F401, F403
from .terminations import * # noqa: F401, F403
# Copyright (c) 2022-2024, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""This sub-module contains the functions that can be used to enable Spot randomizations.
The functions can be passed to the :class:`omni.isaac.lab.managers.EventTermCfg` object to enable
the randomization introduced by the function.
"""
from __future__ import annotations
import torch
from typing import TYPE_CHECKING
from omni.isaac.lab.assets import Articulation
from omni.isaac.lab.managers import SceneEntityCfg
from omni.isaac.lab.utils.math import sample_uniform
if TYPE_CHECKING:
from omni.isaac.lab.envs import ManagerBasedEnv
def reset_joints_around_default(
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 in the interval around the default position and velocity by the given ranges.
This function samples random values from the given ranges around the default joint positions and velocities.
The ranges are clipped to fit inside the soft joint limits. The sampled 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_min_pos = asset.data.default_joint_pos[env_ids] + position_range[0]
joint_max_pos = asset.data.default_joint_pos[env_ids] + position_range[1]
joint_min_vel = asset.data.default_joint_vel[env_ids] + velocity_range[0]
joint_max_vel = asset.data.default_joint_vel[env_ids] + velocity_range[1]
# clip pos to range
joint_pos_limits = asset.data.soft_joint_pos_limits[env_ids, ...]
joint_min_pos = torch.clamp(joint_min_pos, min=joint_pos_limits[..., 0], max=joint_pos_limits[..., 1])
joint_max_pos = torch.clamp(joint_max_pos, min=joint_pos_limits[..., 0], max=joint_pos_limits[..., 1])
# clip vel to range
joint_vel_abs_limits = asset.data.soft_joint_vel_limits[env_ids]
joint_min_vel = torch.clamp(joint_min_vel, min=-joint_vel_abs_limits, max=joint_vel_abs_limits)
joint_max_vel = torch.clamp(joint_max_vel, min=-joint_vel_abs_limits, max=joint_vel_abs_limits)
# sample these values randomly
joint_pos = sample_uniform(joint_min_pos, joint_max_pos, joint_min_pos.shape, joint_min_pos.device)
joint_vel = sample_uniform(joint_min_vel, joint_max_vel, joint_min_vel.shape, joint_min_vel.device)
# set into the physics simulation
asset.write_joint_state_to_sim(joint_pos, joint_vel, env_ids=env_ids)
# 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 RigidObject
from omni.isaac.lab.managers import SceneEntityCfg
if TYPE_CHECKING:
from omni.isaac.lab.envs import ManagerBasedRLEnv
"""
Terrain size limits.
"""
def terrain_out_of_bounds(
env: ManagerBasedRLEnv, asset_cfg: SceneEntityCfg = SceneEntityCfg("robot"), distance_buffer: float = 3.0
) -> torch.Tensor:
"""Terminate when agents move too close to the edge of the terrain."""
# extract the used quantities (to enable type-hinting)
asset: RigidObject = env.scene[asset_cfg.name]
def get_map_size(env: ManagerBasedRLEnv) -> tuple[float, float]:
grid_width, grid_length = env.scene.terrain.cfg.terrain_generator.size
n_cols = env.scene.terrain.cfg.terrain_generator.num_cols
n_rows = env.scene.terrain.cfg.terrain_generator.num_rows
border_width = env.scene.terrain.cfg.terrain_generator.border_width
length = n_cols * grid_length + 2 * border_width
width = n_rows * grid_width + 2 * border_width
return (width, length)
if env.scene.cfg.terrain.terrain_type == "plane":
return False
elif env.scene.cfg.terrain.terrain_type == "generator":
map_width, map_height = get_map_size(env)
x_out_of_bounds = torch.abs(asset.data.root_pos_w[:, 0]) > 0.5 * map_width - distance_buffer
y_out_of_bounds = torch.abs(asset.data.root_pos_w[:, 1]) > 0.5 * map_height - distance_buffer
return torch.logical_or(x_out_of_bounds, y_out_of_bounds)
else:
raise ValueError("Received unsupported terrain type, must be either 'plane' or 'generator'")
......@@ -76,7 +76,7 @@ class MySceneCfg(InteractiveSceneCfg):
sky_light = AssetBaseCfg(
prim_path="/World/skyLight",
spawn=sim_utils.DomeLightCfg(
intensity=900.0,
intensity=750.0,
texture_file=f"{ISAAC_NUCLEUS_DIR}/Materials/Textures/Skies/PolyHaven/kloofendal_43d_clear_puresky_4k.hdr",
),
)
......
......@@ -44,6 +44,7 @@ from omni.isaac.lab.assets import Articulation
# Pre-defined configs
##
from omni.isaac.lab_assets.anymal import ANYMAL_B_CFG, ANYMAL_C_CFG, ANYMAL_D_CFG # isort:skip
from omni.isaac.lab_assets.spot import SPOT_CFG # isort:skip
from omni.isaac.lab_assets.unitree import UNITREE_A1_CFG, UNITREE_GO1_CFG, UNITREE_GO2_CFG # isort:skip
......@@ -73,7 +74,7 @@ def design_scene() -> tuple[dict, list[list[float]]]:
# Create separate groups called "Origin1", "Origin2", "Origin3"
# Each group will have a mount and a robot on top of it
origins = define_origins(num_origins=6, spacing=1.25)
origins = define_origins(num_origins=7, spacing=1.25)
# Origin 1 with Anymal B
prim_utils.create_prim("/World/Origin1", "Xform", translation=origins[0])
......@@ -105,6 +106,11 @@ def design_scene() -> tuple[dict, list[list[float]]]:
# -- Robot
unitree_go2 = Articulation(UNITREE_GO2_CFG.replace(prim_path="/World/Origin6/Robot"))
# Origin 7 with Boston Dynamics Spot
prim_utils.create_prim("/World/Origin7", "Xform", translation=origins[5])
# -- Robot
spot = Articulation(SPOT_CFG.replace(prim_path="/World/Origin7/Robot"))
# return the scene information
scene_entities = {
"anymal_b": anymal_b,
......@@ -113,6 +119,7 @@ def design_scene() -> tuple[dict, list[list[float]]]:
"unitree_a1": unitree_a1,
"unitree_go1": unitree_go1,
"unitree_go2": unitree_go2,
"spot": spot,
}
return scene_entities, origins
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment