> For the complete documentation index, see [llms.txt](https://angellm.gitbook.io/isaac-sim-and-lab/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://angellm.gitbook.io/isaac-sim-and-lab/proyectos/manipulacion-con-brazo-robotico/challenge-lift.md).

# Challenge: Lift

{% hint style="info" %}
Para abordar este challenge he usado [la implementación oficial de la tarea Lift de Isaac Lab](https://github.com/isaac-sim/IsaacLab/tree/main/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift) para estudiarlo y tomarlo de referencia. He mirado cómo está programado (estructura de recompensas, observaciones, frames, configuración de la red...)\
Una vez comprendido el ejemplo, lo he adaptado al UR10 + Robotiq 2F-140 del tutorial del Reach.
{% endhint %}

## Cambios en `skrl_ppo_cfg.yaml`

Primero dejo el contenido completo del archivo y luego vamos analizando funcion a función:

{% code title="skrl\_ppo\_cfg.yaml" expandable="true" %}

```yaml
seed: 42


# Models are instantiated using skrl's model instantiator utility
# https://skrl.readthedocs.io/en/latest/api/utils/model_instantiators.html
models:
  separate: False
  policy:  # see gaussian_model parameters
    class: GaussianMixin
    clip_actions: False
    clip_log_std: True
    min_log_std: -20.0
    max_log_std: 2.0
    initial_log_std: 0.0
    network:
      - name: net
        input: OBSERVATIONS
        layers: [256, 128, 64]
        activations: elu
    output: ACTIONS
  value:  # see deterministic_model parameters
    class: DeterministicMixin
    clip_actions: False
    network:
      - name: net
        input: OBSERVATIONS
        layers: [256, 128, 64]
        activations: elu
    output: ONE


# Rollout memory
# https://skrl.readthedocs.io/en/latest/api/memories/random.html
memory:
  class: RandomMemory
  memory_size: -1  # automatically determined (same as agent:rollouts)


# PPO agent configuration (field names are from PPO_DEFAULT_CONFIG)
# https://skrl.readthedocs.io/en/latest/api/agents/ppo.html
agent:
  class: PPO
  rollouts: 24
  learning_epochs: 8
  mini_batches: 4
  discount_factor: 0.99
  lambda: 0.95
  learning_rate: 1.0e-04
  learning_rate_scheduler: KLAdaptiveLR
  learning_rate_scheduler_kwargs:
    kl_threshold: 0.01
  state_preprocessor: RunningStandardScaler
  state_preprocessor_kwargs: null
  value_preprocessor: RunningStandardScaler
  value_preprocessor_kwargs: null
  random_timesteps: 0
  learning_starts: 0
  grad_norm_clip: 1.0
  ratio_clip: 0.2
  value_clip: 0.2
  clip_predicted_values: True
  entropy_loss_scale: 0.001
  value_loss_scale: 2.0
  kl_threshold: 0.0
  rewards_shaper_scale: 0.01
  time_limit_bootstrap: False
  # logging and checkpoint
  experiment:
    directory: "manipulation_ur10"
    experiment_name: ""
    write_interval: auto
    checkpoint_interval: auto


# Sequential trainer
# https://skrl.readthedocs.io/en/latest/api/trainers/sequential.html
trainer:
  class: SequentialTrainer
  timesteps: 36000
  environment_info: log
```

{% endcode %}

### Aumento del tamaño de la red

**`layers: [256, 128, 64]`**

En el ejemplo de Reach, la red solo necesita aprender a mover las articulaciones en función de la posición actual y la posición objetivo. Para eso, una red neuronal pequeña de dos capas es más que suficiente.

En cambio, para coger y levantar un cubo, la red tiene que aprender 4 comportamientos encadenados: acercarse al cubo, cerrar la pinza, levantar el cubo y llevarlo a la pose objetivo. Cada uno de estos comportamientos necesita una estrategia diferente en función de las observaciones que ve el agente.\
Una red pequeña como la del Reach no tiene suficiente capacidad para representar los cuatro comportamientos a la vez sin que uno interfiera con otro. \
Aumentar la red permite darle más espacio para aprender comportamientos más complejos y variados.

### Aumento de `learning_epochs`

**`learning_epochs: 8`**

En el aprendizaje por refuerzo, el agente interactúa con el entorno durante un número fijo de pasos, acumulando observaciones, acciones y recompensas. Ese bloque de datos se llama **rollout**, y es lo que la red usa para actualizar sus pesos antes de descartarlo. El número de `learning_epochs` determina cuántas pasadas de entrenamiento se hacen sobre ese mismo rollout antes de tirarlo.

En el caso del robot que levanta un cubo, los momentos clave del lote (el instante en que el robot cierra la pinza sobre el cubo, el momento en que lo levanta por encima del umbral…) son relativamente raros: la mayor parte del rollout son pasos de aproximación más o menos repetitivos. Con solo 5 epochs, esos momentos clave se aprovechan poco; subiendo a 8, la red tiene más pasadas para extraer información antes de que el lote quede obsoleto.

### Disminución de `learning_rate`

**`learning_rate: 1.0e-04`**

El **learning rate** controla **cuánto cambia la red en cada actualización**. Un valor alto significa cambios grandes y rápidos mientras que uno bajo indica cambios pequeños y graduales.

En Reach, ir rápido no es un problema: si la red cambia mucho de una vez, lo peor que puede pasar es que la política retroceda un poco y se corrija en el siguiente paso. \
En Lift, el riesgo es mayor: si el agente ya ha aprendido a cerrar la pinza y una actualización agresiva redistribuye los pesos de la red para mejorar el comportamiento de la aproximación, puede romper accidentalmente lo que ya sabía hacer.&#x20;

Bajando el learning rate, cada actualización da pasos más pequeños y seguros, preservando las habilidades que ya ha aprendido mientras se siguen mejorando las demás.

### Disminución de `entropy_loss_scale`

**`entropy_loss_scale: 0.001`**

La entropía mide cuánto varía la política en sus decisiones, cuánto improvisa el agente. El algoritmo de RL PPO añade un bonus proporcional para incentivar la exploración, así si la política es muy determinista, se penaliza para que siga probando cosas nuevas.

Esto es útil al principio del entrenamiento, cuando el agente no sabe nada y necesita explorar para descubrir qué funciona. Pero tiene un efecto secundario: si la entropía es demasiado alta, el agente sigue experimentando incluso cuando ya ha aprendido algo bueno, pudiendo desaprender habilidades ya aprendidas por querer explorar variantes.

Bajando el coeficiente de entropía, reducimos ese incentivo a explorar. La política se vuelve progresivamente más consistente: en cuanto descubre que cerrar la pinza en el momento correcto funciona, empieza a repetir ese comportamiento en lugar de seguir variándolo.

### Aumento de `value_loss_scale`

**`value_loss_scale: 2.0`**

PPO tiene dos redes: el **actor** (que decide qué acción tomar) y el **crítico** (que trata de predecir la recompensa que se espera obtener desde el estado actual). El crítico se usa para calcular la ventaja de cada acción: `A = recompensa_real - valor_estimado`. Si la acción devolvió una recompensa mejor de la esperada, la ventaja es positiva y se refuerza; si fue peor, se penaliza.

En Lift, la recompensa tiene varios componentes (aproximación, agarre, transporte) con pesos muy distintos y condiciones de activación (umbrales de altura). Esto hace que el crítico tenga que hacer estimaciones mucho más complejas que en Reach. Si el crítico no es preciso, las ventajas son ruidosas y la señal de entrenamiento del actor se degrada.

Subiendo `value_loss_scale` simplemente le decimos a PPO que priorice más el entrenamiento del crítico en cada actualización, para que aprenda a estimar los retornos con suficiente precisión.

### Disminución de `rewards_shaper_scale`

**`rewards_shaper_scale: 0.01`**

El `reward_shaper_scale` es un multiplicador global que se aplica a la recompensa total antes de pasársela a PPO. En la nueva configuración de recompensas (que veremos más adelante) los pesos son bastante altos (15 para el agarre, 16 para el transporte). Si dejaramos `rewards_shaper_scale=1.0`, las recompensas acumuladas en un episodio llegarían a ser números muy grandes (del orden de decenas), lo que dificulta el entrenamiento porque las redes neuronales funcionan mucho mejor cuando los valores que manejan están en rangos pequeños (típicamente entre -1 y 1, o -10 y 10).

Bajando el `reward_shaper_scale` a `0.01` se multiplica toda la recompensa por ese factor, manteniéndola en una escala manejable. Es simplemente una forma cómoda de ajustar la magnitud global sin tener que recalcular a mano todos los weight individuales.

### Aumento de `timesteps`

**`timesteps: 36000`**

En Reach, el agente tiene una sola cosa que aprender y lo hace en 24k pasos. En Lift, tiene que descubrir en secuencia cómo acercarse al cubo, cómo cerrarlo en el momento correcto, cómo levantarlo sin que se caiga y cómo llevarlo al objetivo. Cada una de esas fases tarda un tiempo en consolidarse, por lo que el entrenamiento total necesita más pasos para que todas puedan ser aprendidas.

## Cambios en MDP

### Creación de `mdp/observations.py`

{% code title="observations.py" expandable="true" %}

```python
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

from typing import TYPE_CHECKING

import torch

from isaaclab.assets import RigidObject
from isaaclab.managers import SceneEntityCfg
from isaaclab.utils.math import subtract_frame_transforms

if TYPE_CHECKING:
    from isaaclab.envs import ManagerBasedRLEnv


def object_position_in_robot_root_frame(
    env: ManagerBasedRLEnv,
    robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
    object_cfg: SceneEntityCfg = SceneEntityCfg("object"),
) -> torch.Tensor:
    """The position of the object in the robot's root frame."""
    robot: RigidObject = env.scene[robot_cfg.name]
    object: RigidObject = env.scene[object_cfg.name]
    object_pos_w = object.data.root_pos_w[:, :3]
    object_pos_b, _ = subtract_frame_transforms(robot.data.root_pos_w, robot.data.root_quat_w, object_pos_w)
    return object_pos_b
```

{% endcode %}

Se ha creado un archivo nuevo para meter la función de observación `object_position_in_robot_root_frame`. La función devuelve la posición del cubo con respecto a la base del robot, de esta manera cada entorno conoce la posición de su cubo.

### Cambios en `mdp/rewards.py`&#x20;

{% code title="rewards.py" expandable="true" %}

```python
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

from __future__ import annotations

from typing import TYPE_CHECKING

import torch

from isaaclab.assets import RigidObject
from isaaclab.managers import SceneEntityCfg
from isaaclab.sensors import FrameTransformer
from isaaclab.utils.math import combine_frame_transforms

if TYPE_CHECKING:
    from isaaclab.envs import ManagerBasedRLEnv


def object_is_lifted(
    env: ManagerBasedRLEnv, minimal_height: float, object_cfg: SceneEntityCfg = SceneEntityCfg("object")
) -> torch.Tensor:
    """Reward the agent for lifting the object above the minimal height."""
    object: RigidObject = env.scene[object_cfg.name]
    return torch.where(object.data.root_pos_w[:, 2] > minimal_height, 1.0, 0.0)


def object_ee_distance(
    env: ManagerBasedRLEnv,
    std: float,
    object_cfg: SceneEntityCfg = SceneEntityCfg("object"),
    ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame"),
) -> torch.Tensor:
    """Reward the agent for reaching the object using tanh-kernel."""
    # extract the used quantities (to enable type-hinting)
    object: RigidObject = env.scene[object_cfg.name]
    ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name]
    # Target object position: (num_envs, 3)
    cube_pos_w = object.data.root_pos_w
    # End-effector position: (num_envs, 3)
    ee_w = ee_frame.data.target_pos_w[..., 0, :]
    # Distance of the end-effector to the object: (num_envs,)
    object_ee_distance = torch.norm(cube_pos_w - ee_w, dim=1)

    return 1 - torch.tanh(object_ee_distance / std)


def object_goal_distance(
    env: ManagerBasedRLEnv,
    std: float,
    minimal_height: float,
    command_name: str,
    robot_cfg: SceneEntityCfg = SceneEntityCfg("robot"),
    object_cfg: SceneEntityCfg = SceneEntityCfg("object"),
) -> torch.Tensor:
    """Reward the agent for tracking the goal pose using tanh-kernel."""
    # extract the used quantities (to enable type-hinting)
    robot: RigidObject = env.scene[robot_cfg.name]
    object: RigidObject = env.scene[object_cfg.name]
    command = env.command_manager.get_command(command_name)
    # compute the desired position in the world frame
    des_pos_b = command[:, :3]
    des_pos_w, _ = combine_frame_transforms(robot.data.root_pos_w, robot.data.root_quat_w, des_pos_b)
    # distance of the end-effector to the object: (num_envs,)
    distance = torch.norm(des_pos_w - object.data.root_pos_w, dim=1)
    # rewarded if the object is lifted above the threshold
    return (object.data.root_pos_w[:, 2] > minimal_height) * (1 - torch.tanh(distance / std))
```

{% endcode %}

Se han deprecado las funciones que teníamos para Reach y se han añadido tres nuevas funciones que encapsulan las distintas fases de la tarea de Lift.

#### Recompensa **`object_is_lifted`**

```python
def object_is_lifted(
    env: ManagerBasedRLEnv, minimal_height: float, object_cfg: SceneEntityCfg = SceneEntityCfg("object")
) -> torch.Tensor:
    """Reward the agent for lifting the object above the minimal height."""
    object: RigidObject = env.scene[object_cfg.name]
    return torch.where(object.data.root_pos_w[:, 2] > minimal_height, 1.0, 0.0)
```

Recompensa **binaria** (0 o 1) por levantar el cubo por encima de una altura mínima. Es el hito más claro de la tarea ya que indica que el cubo ya no está apoyado, sino agarrado y elevado.

#### Recompensa `object_ee_distance`

```python
def object_ee_distance(
    env: ManagerBasedRLEnv,
    std: float,
    object_cfg: SceneEntityCfg = SceneEntityCfg("object"),
    ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame"),
) -> torch.Tensor:
    """Reward the agent for reaching the object using tanh-kernel."""
    object: RigidObject = env.scene[object_cfg.name]
    ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name]
    cube_pos_w = object.data.root_pos_w
    ee_w = ee_frame.data.target_pos_w[..., 0, :]
    object_ee_distance = torch.norm(cube_pos_w - ee_w, dim=1)
    return 1 - torch.tanh(object_ee_distance / std)
```

Recompensa **continua** por la proximidad de la pinza al cubo, usando `tanh` para acotarla en el rango `[0, 1]`. Esta recompensa es la que guía al agente durante la fase de aproximación: aunque todavía no haya agarrado el cubo, recibe señal positiva por acercarse.

#### Recompensa **`object_goal_distance`**

```python
def object_ee_distance(
    env: ManagerBasedRLEnv,
    std: float,
    object_cfg: SceneEntityCfg = SceneEntityCfg("object"),
    ee_frame_cfg: SceneEntityCfg = SceneEntityCfg("ee_frame"),
) -> torch.Tensor:
    """Reward the agent for reaching the object using tanh-kernel."""
    object: RigidObject = env.scene[object_cfg.name]
    ee_frame: FrameTransformer = env.scene[ee_frame_cfg.name]
    cube_pos_w = object.data.root_pos_w
    ee_w = ee_frame.data.target_pos_w[..., 0, :]
    object_ee_distance = torch.norm(cube_pos_w - ee_w, dim=1)
    return 1 - torch.tanh(object_ee_distance / std)
```

Recompensa **condicionada** por acercar el cubo a la posición objetivo (goal). Sólo se entrega la recompensa cuando el cubo ya ha sido levantado por encima de la altura mínima del umbral. Si no lo hiciésemos, el agente podría intentar obtener esta recompensa arrastrando el cubo por el suelo. \
El producto por la máscara `(object_z > minimal_height)` fuerza el orden correcto de fases: primero levantar, después transportar.

{% hint style="info" %}
**El efecto del parámetro `std`**

Las dos últimas funciones usan kernels de la forma `1 - tanh(d / std)`, donde `d` es una distancia. El parámetro `std` controla **lo "ancho" que es el kernel** y, en la práctica, determina a qué distancias el agente recibe señal aprovechable:

* **`std` grande** (p.ej. `0.3`): el `tanh` tarda en saturar. La recompensa va de `1.0` (cubo encima del goal) decayendo suavemente hasta cero, y a 10-20 cm de distancia el agente todavía recibe una señal claramente positiva. Esto es ideal para la **fase gruesa** del aprendizaje: el agente recibe gradiente útil aunque todavía esté lejos del objetivo.
* **`std` pequeño** (p.ej. `0.05`): el `tanh` satura muy rápido. La recompensa cae prácticamente a cero salvo cuando el cubo está **muy cerca** del goal (pocos centímetros). Esto es ideal para la **fase fina**: una vez que el agente ya sabe llegar "más o menos", esta recompensa premia con fuerza pulir los últimos centímetros y mantenerse en el objetivo.

Esta parametrización es justo lo que nos permite, desde el `env_cfg`, instanciar **dos términos de recompensa con la misma función pero con `std` distinto** (`object_goal_tracking` con `std=0.3` y `object_goal_tracking_fine_grained` con `std=0.05`), cubriendo ambos regímenes simultáneamente.

{% endhint %}

## Cambios en `reach_env_cfg.py`

{% code expandable="true" %}

```python
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

import math
import carb

NUCLEUS_ASSET_ROOT_DIR = carb.settings.get_settings().get("/persistent/isaac/asset_root/cloud")
"""Path to the root directory on the Nucleus Server."""

NVIDIA_NUCLEUS_DIR = f"{NUCLEUS_ASSET_ROOT_DIR}/NVIDIA"
"""Path to the root directory on the NVIDIA Nucleus Server."""

ISAAC_NUCLEUS_DIR = f"{NUCLEUS_ASSET_ROOT_DIR}/Isaac"
"""Path to the ``Isaac`` directory on the NVIDIA Nucleus Server."""

import isaaclab.sim as sim_utils
from isaaclab.assets import AssetBaseCfg
from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.managers import ActionTermCfg as ActionTerm
from isaaclab.managers import CurriculumTermCfg as CurrTerm
from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import RewardTermCfg as RewTerm
from isaaclab.managers import SceneEntityCfg
from isaaclab.managers import TerminationTermCfg as DoneTerm
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.utils import configclass
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
from isaaclab.sim.spawners.from_files.from_files_cfg import GroundPlaneCfg

from . import mdp

##
# Pre-defined configs
##

from .ur_gripper import UR_GRIPPER_CFG  # isort:skip

##
# Scene definition
##

@configclass
class ReachSceneCfg(InteractiveSceneCfg):
    """Configuration for a scene."""

    # world
    ground = AssetBaseCfg(
        prim_path="/World/ground",
        spawn=sim_utils.GroundPlaneCfg(),
        init_state=AssetBaseCfg.InitialStateCfg(pos=(0.0, 0.0, -1.05)),
    )

    # robot
    robot = UR_GRIPPER_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
    
    # lights
    dome_light = AssetBaseCfg(
        prim_path="/World/DomeLight",
        spawn=sim_utils.DomeLightCfg(color=(0.9, 0.9, 0.9), intensity=5000.0),
    )

    table = AssetBaseCfg(
        prim_path="{ENV_REGEX_NS}/Table",
        spawn=sim_utils.UsdFileCfg(
            usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Mounts/SeattleLabTable/table_instanceable.usd",
        ),
        init_state=AssetBaseCfg.InitialStateCfg(pos=(0.55, 0.0, 0.0), rot=(0.70711, 0.0, 0.0, 0.70711)),
    )

    # plane
    plane = AssetBaseCfg(
        prim_path="/World/GroundPlane",
        init_state=AssetBaseCfg.InitialStateCfg(pos=[0, 0, -1.05]),
        spawn=GroundPlaneCfg(),
    )

##
# MDP settings
##

@configclass
class ActionsCfg:
    """Action specifications for the MDP."""

    arm_action: ActionTerm = mdp.JointPositionActionCfg(
        asset_name="robot", 
        joint_names=["shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint", "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"], 
        scale=.5, 
        use_default_offset=True, 
        debug_vis=True
    )

@configclass
class CommandsCfg:
    """Command terms for the MDP."""

    ee_pose = mdp.UniformPoseCommandCfg(
        asset_name="robot",
        body_name="ee_link", # This is the body in the USD file
        resampling_time_range=(4.0, 4.0),
        debug_vis=True,
        # These are essentially ranges of poses that can be commanded for the end of the robot during training
        ranges=mdp.UniformPoseCommandCfg.Ranges(
            pos_x=(0.35, 0.65),
            pos_y=(-0.2, 0.2),
            pos_z=(0.15, 0.5),
            roll=(0.0, 0.0),
            pitch=(math.pi / 2, math.pi / 2),
            yaw=(-3.14, 3.14),
        ),
    )

@configclass
class ObservationsCfg:
    """Observation specifications for the MDP."""

    @configclass
    class PolicyCfg(ObsGroup):
        """Observations for policy group."""

        # observation terms (order preserved)
        joint_pos = ObsTerm(func=mdp.joint_pos_rel, noise=Unoise(n_min=-0.01, n_max=0.01))
        joint_vel = ObsTerm(func=mdp.joint_vel_rel, noise=Unoise(n_min=-0.01, n_max=0.01))
        pose_command = ObsTerm(func=mdp.generated_commands, params={"command_name": "ee_pose"})
        actions = ObsTerm(func=mdp.last_action)

        def __post_init__(self):
            self.enable_corruption = True
            self.concatenate_terms = True

    # observation groups
    policy: PolicyCfg = PolicyCfg()

@configclass
class EventCfg:
    """Configuration for events."""

    reset_robot_joints = EventTerm(
        func=mdp.reset_joints_by_scale,
        mode="reset",
        params={
            "position_range": (0.75, 1.25),
            "velocity_range": (0.0, 0.0),
        },
    )

@configclass
class RewardsCfg:
    """Reward terms for the MDP."""

    # task terms
    end_effector_position_tracking = RewTerm(
        func=mdp.position_command_error,
        weight=-0.2,
        params={"asset_cfg": SceneEntityCfg("robot", body_names=["ee_link"]), "command_name": "ee_pose"},
    )
    end_effector_position_tracking_fine_grained = RewTerm(
        func=mdp.position_command_error_tanh,
        weight=0.1,
        params={"asset_cfg": SceneEntityCfg("robot", body_names=["ee_link"]), "std": 0.1, "command_name": "ee_pose"},
    )

    # action penalty
    action_rate = RewTerm(func=mdp.action_rate_l2, weight=-0.0001)
    joint_vel = RewTerm(
        func=mdp.joint_vel_l2,
        weight=-0.0001,
        params={"asset_cfg": SceneEntityCfg("robot")},
    )

@configclass
class TerminationsCfg:
    """Termination terms for the MDP."""

    time_out = DoneTerm(func=mdp.time_out, time_out=True)

@configclass
class CurriculumCfg:
    """Curriculum terms for the MDP."""

    action_rate = CurrTerm(
        func=mdp.modify_reward_weight, params={"term_name": "action_rate", "weight": -0.005, "num_steps": 4500}
    )

    joint_vel = CurrTerm(
        func=mdp.modify_reward_weight, params={"term_name": "joint_vel", "weight": -0.001, "num_steps": 4500}
    )

##
# Environment configuration
##

@configclass
class ReachEnvCfg(ManagerBasedRLEnvCfg):
    """Configuration for the reach end-effector pose tracking environment."""

    # Scene settings - how many robots, how far apart?
    scene = ReachSceneCfg(num_envs=2000, env_spacing=2.5)
    # Basic settings
    observations = ObservationsCfg()
    actions = ActionsCfg()
    commands: CommandsCfg = CommandsCfg()
    # MDP settings
    rewards = RewardsCfg()
    terminations = TerminationsCfg()
    events = EventCfg()
    curriculum = CurriculumCfg()

    def __post_init__(self):
        """Post initialization."""
        # general settings
        self.decimation = 2
        self.sim.render_interval = self.decimation
        self.episode_length_s = 3.0
        self.viewer.eye = (3.5, 3.5, 3.5)
        # simulation settings
        self.sim.dt = 1.0 / 60.0

@configclass
class ReachEnvCfg_PLAY(ReachEnvCfg):
    def __post_init__(self):
        # post init of parent
        super().__post_init__()
        # make a smaller scene for play
        self.scene.num_envs = 50
        self.scene.env_spacing = 2.5
        # disable randomization for play
        self.observations.policy.enable_corruption = False
```

{% endcode %}

Este es el archivo donde se han hecho más cambios. Vamos por bloques.

### Scene

Se han añadido dos nuevos elementos a la escena:

**El cubo (object)**

```python
    object = RigidObjectCfg(
        prim_path="{ENV_REGEX_NS}/Object",
        init_state=RigidObjectCfg.InitialStateCfg(pos=[0.9, 0, 0.055], rot=[1, 0, 0, 0]),
        spawn=UsdFileCfg(
            usd_path=f"{ISAAC_NUCLEUS_DIR}/Props/Blocks/DexCube/dex_cube_instanceable.usd",
            rigid_props=RigidBodyPropertiesCfg(
                solver_position_iteration_count=16,
                solver_velocity_iteration_count=1,
                max_angular_velocity=1000.0,
                max_linear_velocity=1000.0,
                max_depenetration_velocity=5.0,
                disable_gravity=False,
            ),
        ),
    )
```

Lo añadimos como `RigidObjectCfg` y no como `AssetBaseCfg` porque queremos que interactúe físicamente con la pinza. El `solver_position_iteration_count` tiene un valor superior al habitual para ayudar a que los contactos entre la pinza y el cubo se resuelvan con más precisión.

**El frame de la pinza (ee\_frame)**

```python
ee_frame = FrameTransformerCfg(
        prim_path="{ENV_REGEX_NS}/Robot/ur10/base_link",
        debug_vis=False,
        visualizer_cfg=FRAME_MARKER_CFG.replace(prim_path="/Visuals/FrameTransformer"),
        target_frames=[
            FrameTransformerCfg.FrameCfg(
                prim_path="{ENV_REGEX_NS}/Robot/Robotiq_2F_140_physics_edit/robotiq_base_link",
                name="end_effector",
                offset=OffsetCfg(
                    pos=[0.0, 0.0, 0.16],
                ),
            ),
        ],
    )
```

Agregamos este frame porque el frame `robotiq_base_link` está físicamente situado en el cuerpo de la pinza (en la base, donde se atornilla al brazo), no entre los dedos. Si usásemos ese frame directamente como punto de agarre, el agente intentaría llevar la muñeca de la pinza al cubo, dejando los dedos siempre por delante del objeto y haciendo el agarre imposible.

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2FNrEsIAHOkGUedgQAmHfj%2Fimage.png?alt=media&amp;token=32ebe711-845a-4623-9c71-67d87937a94a" alt=""><figcaption></figcaption></figure>

Para resolver esto, definimos un `FrameTransformer` que toma `robotiq_base_link` y le aplica un offset de 16 cm en su eje Z local, situando el end-effector virtual en el punto entre los dedos, que es donde nos interesa que se coloque el cubo. Las recompensas de proximidad (`object_ee_distance`) miden distancia respecto a este frame, no respecto a la muñeca.

### Actions

Además de la acción del brazo que ya teníamos, hay que añadir una acción para la pinza:

```python
    gripper_action: ActionTerm = mdp.BinaryJointPositionActionCfg(
        asset_name="robot",
        joint_names=["finger_joint"],
        open_command_expr={"finger_joint": 0.0},        # 0°  – abierto
        close_command_expr={"finger_joint": 0.7854},    # 45° – cerrado
    )
```

Es una acción **binaria** (abrir/cerrar), así el agente solo necesita decidir si cierra o abre la pinza, no calcular el ángulo de la articulación. Esto facilita el aprendizaje, ya que reduce mucho el espacio de acciones efectivo.

### Commands

Hemos cambiado el comando de pose del end-effector por un comando de **pose objetivo del cubo**:

```python
    object_pose = mdp.UniformPoseCommandCfg(
        asset_name="robot",
        body_name="robotiq_base_link",  # will be set by agent env cfg
        resampling_time_range=(5.0, 5.0),
        debug_vis=True,
        ranges=mdp.UniformPoseCommandCfg.Ranges(
            pos_x=(0.8, 1.0), pos_y=(-0.25, 0.25), pos_z=(0.25, 0.5), roll=(0.0, 0.0), pitch=(0.0, 0.0), yaw=(0.0, 0.0)
        ),
    )
```

Ahora lo que comandamos no es dónde queremos que esté el end-effector, sino dónde queremos que esté el cubo una vez levantado. El rango de Z (0.25 a 0.5 metros) está puesto a propósito por encima del suelo para forzar al agente a levantar el cubo.

### Observaciones

```python
        object_position = ObsTerm(func=mdp.object_position_in_robot_root_frame)
        target_object_position = ObsTerm(func=mdp.generated_commands, params={"command_name": "object_pose"})
```

Se ha agregado la **posición del cubo respecto al robot** a las observaciones y se ha cambiado la observación de la pose del end-effector por la **pose objetivo del objeto**.

### Events

Cuando se resetea el entorno, además de la pose del robot, también **reseteamos la posición del cubo**, randomizando su posición dentro de un rango:

```python
    reset_object_position = EventTerm(
        func=mdp.reset_root_state_uniform,
        mode="reset",
        params={
            "pose_range": {"x": (-0.1, 0.1), "y": (-0.25, 0.25), "z": (0.0, 0.0)},
            "velocity_range": {},
            "asset_cfg": SceneEntityCfg("object", body_names="Object"),
        },
    )
```

La randomización es muy importante. Si entrenásemos siempre con el cubo en exactamente la misma posición, la política aprendería una secuencia de movimientos memorizada en lugar de una verdadera política reactiva que use las observaciones del cubo. \
Al variar la posición inicial unos centímetros, obligamos al agente a **utilizar realmente la observación de la posición del cubo** para decidir su movimiento.

### Rewards

Las **penalizaciones** (`action_rate`, `joint_vel`) se dejan como estaban. Lo que cambian son las recompensas positivas:

<pre class="language-python"><code class="lang-python"><strong>    reaching_object = RewTerm(func=mdp.object_ee_distance, params={"std": 0.1}, weight=1.0)
</strong>
    lifting_object = RewTerm(func=mdp.object_is_lifted, params={"minimal_height": 0.06}, weight=15.0)

    object_goal_tracking = RewTerm(
        func=mdp.object_goal_distance,
        params={"std": 0.3, "minimal_height": 0.06, "command_name": "object_pose"},
        weight=16.0,
    )

    object_goal_tracking_fine_grained = RewTerm(
        func=mdp.object_goal_distance,
        params={"std": 0.05, "minimal_height": 0.06, "command_name": "object_pose"},
        weight=5.0,
    )
</code></pre>

La elección de pesos no es arbitraria: refleja **el orden de importancia que queremos que el agente perciba**, y es lo que hace que la tarea sea aprendible en lugar de quedarse atascada en un óptimo local.

* **`reaching_object` (peso 1.0)**: la recompensa más pequeña y más densa. Está siempre activa y guía al agente desde el primer paso hacia el cubo. Sirve como señal de orientación básica: aunque el agente todavía no sepa nada, recibe retroalimentación continua que le empuja a acercarse al cubo.
* **`lifting_object` (peso 15.0):** cuando el agente consigue levantar el cubo recibe un premio comparativamente enorme. Esto es lo que asegura que, una vez descubierto el agarre, el agente lo consolide en lugar de abandonarlo para seguir optimizando la recompensa de aproximación.
* **`object_goal_tracking` (peso 16.0)**: la recompensa más alta de todas, pero solo activa cuando el cubo está levantado. Es la que empuja a la política a llevar el cubo hasta la posición objetivo una vez aprendido el agarre. Usa un `std=0.3` amplio para que la señal llegue al agente incluso cuando el cubo todavía está lejos de la pose objetivo.
* [**`object_goal_tracking_fine_grained`**](#user-content-fn-1)[^1] **(peso 5.0):** usa exactamente la misma función que la anterior pero con un `std=0.05` mucho más estrecho, de modo que solo se activa cuando el cubo está realmente cerca del objetivo. Su función es que la política afine los últimos centímetros y se mantenga estable en lugar de oscilar alrededor de la pose objetivo.

La regla general es que los pesos crecen a medida que avanza la tarea, de forma que cada nueva fase resulte más atractiva para el agente que la anterior. Si la recompensa de aproximación fuese mayor que la del agarre, el agente podría conformarse con quedarse cerca del cubo sin llegar a agarrarlo nunca.

### Terminations

Añadimos una nueva condición de terminación: si el cubo se cae por debajo de cierta altura, terminamos el episodio:

```python
    object_dropping = DoneTerm(
        func=mdp.root_height_below_minimum, params={"minimum_height": -0.05, "asset_cfg": SceneEntityCfg("object")}
    )
```

Cortar el episodio en cuanto el cubo se cae de la mesa evita que el agente acumule pasos inútiles tras un fallo, y le ayuda a aprender más rápido que tirar el cubo tiene consecuencias.

### Curriculum

Hemos subido a 10000 steps el `num_steps` en el que se aumentan los pesos de las penalizaciones de `action_rate` y `joint_vel`:

```python
    action_rate = CurrTerm(
        func=mdp.modify_reward_weight, params={"term_name": "action_rate", "weight": -1e-1, "num_steps": 10000}
    )

    joint_vel = CurrTerm(
        func=mdp.modify_reward_weight, params={"term_name": "joint_vel", "weight": -1e-1, "num_steps": 10000}
    )
```

La idea con esto es permitir que el agente explore libremente al principio (con penalizaciones suaves) y endurecer las restricciones más tarde (cuando ya sabe hacer la tarea, queremos que la haga sin movimientos bruscos ni velocidades altas).

En Reach se subian los pesos a partir de los 4500 pasos. En Lift necesitamos darle al agente mucho más margen para descubrir las habilidades de la tarea (aproximarse, agarrar, levantar) antes de penalizarle por moverse de más. Si aumentasemos las penalizaciones demasiado pronto, el agente se quedaría quieto para evitar la penalización y nunca descubriría nuevos comportamientos.

### \_\_post\_init\_\_

Se han hecho unos ajustes&#x20;

```python
    def __post_init__(self):
        """Post initialization."""
        # general settings
        self.decimation = 2
        self.sim.render_interval = self.decimation
        self.episode_length_s = 5.0
        self.viewer.eye = (3.5, 3.5, 3.5)
        # simulation settings
        self.sim.dt = 0.01

        self.sim.physx.bounce_threshold_velocity = 0.2
        self.sim.physx.bounce_threshold_velocity = 0.01
        self.sim.physx.gpu_found_lost_aggregate_pairs_capacity = 1024 * 1024 * 4
        self.sim.physx.gpu_total_aggregate_pairs_capacity = 16 * 1024
        self.sim.physx.friction_correlation_distance = 0.00625
```

* **`episode_length_s` ha pasado de 3 a 5 segundos**. La tarea de Lift necesita encadenar aproximación + agarre + elevación + transporte, y completar esta secuencia en 3 segundos es complicado, sobre todo al inicio. Con 5 segundos por episodio el agente tiene el tiempo necesario para completar la secuencia completa.
* **`self.sim.dt = 0.01`**. Es el paso de integración de la simulación física en segundos. Lo hemos reducido para aumentar la precisión de la simulación, lo que importa especialmente en los contactos entre la pinza y el cubo: con un paso de tiempo demasiado grande, el cubo puede atravesar los dedos de la pinza y escaparse antes de que el simulador resuelva la colisión.
* **Parámetros de PhysX**. Se han aplicado ajustes finos sobre el motor de física para mejorar la estabilidad y precisión en escenarios con muchos contactos simultáneos. En nuestro caso tenemos 4096 entornos en paralelo con interacciones continuas entre la pinza y el cubo, por lo que este ajuste es conveniente.

## Cambios en `ur_conf.py`

{% hint style="info" %}
Para los cambios realizados en este archivo se ha tomado como referencia los parámetros del archivo de configuración [`universal_robots.py`](https://github.com/isaac-sim/IsaacLab/blob/main/source/isaaclab_assets/isaaclab_assets/robots/universal_robots.py) disponible en los archivos de assets de Isaac Lab.
{% endhint %}

{% code title="ur\_conf.py" expandable="true" %}

```python

# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Configuration for the Universal Robots.
Reference: https://github.com/ros-industrial/universal_robot
"""

from pathlib import Path
import isaaclab.sim as sim_utils
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.assets.articulation import ArticulationCfg

PROJECT_DIR = Path(__file__).resolve().parents[6]
UR_USD_PATH = PROJECT_DIR / "assets" / "UR-with-gripper.usd"

UR_GRIPPER_CFG = ArticulationCfg(

    # Where is the USD file for this robot?
    spawn=sim_utils.UsdFileCfg(       
        usd_path=str(UR_USD_PATH),
            activate_contact_sensors=False,
            rigid_props=sim_utils.RigidBodyPropertiesCfg(
                rigid_body_enabled=True,
                max_linear_velocity=1000.0,
                max_angular_velocity=1000.0,
                max_depenetration_velocity=5.0,
            ),
            articulation_props=sim_utils.ArticulationRootPropertiesCfg(
                enabled_self_collisions=True, 
                solver_position_iteration_count=8, 
                solver_velocity_iteration_count=0
            ),
        ),
    # What is its initial position of the robot, and its joints?
        init_state=ArticulationCfg.InitialStateCfg(
            joint_pos={
                "shoulder_pan_joint": 0.0,
                "shoulder_lift_joint": -1.5708,
                "elbow_joint": 1.5708,
                "wrist_1_joint": -1.5708,
                "wrist_2_joint": -1.5708,
                "wrist_3_joint": 0.0,

                "finger_joint": 0.0,
                ".*_inner_finger_joint": 0.0,
                ".*_inner_finger_pad_joint": 0.0,
                ".*_outer_.*_joint": 0.0,
            },
        ),
    # What parts of the robot move, and how stiff / damped are they?
        actuators={
            "arm": ImplicitActuatorCfg(
                joint_names_expr=["shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint", "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"], 
                effort_limit_sim=87.0,
                stiffness=800.0,
                damping=40.0,
            ),

            "gripper": ImplicitActuatorCfg(
                joint_names_expr=["finger_joint"],
                effort_limit_sim=200.0,
                velocity_limit_sim=2.0,
                stiffness=500.0,
                damping=50.0,
                friction=0.0,
                armature=0.0,
            ),

            # the auxiliary actuator joint for gripper
            "gripper_aux": ImplicitActuatorCfg(
                joint_names_expr=[".*_inner_finger_joint"],
                effort_limit_sim=1.0,
                velocity_limit_sim=1.0,
                stiffness=0.2,
                damping=0.001,
                friction=0.0,
                armature=0.0,
            ),

            # the passive joints for gripper
            "gripper_passive": ImplicitActuatorCfg(
                joint_names_expr=[".*_inner_finger_pad_joint", ".*_outer_finger_joint", "right_outer_knuckle_joint"],
                effort_limit_sim=1.0,
                velocity_limit_sim=1.0,
                stiffness=0.0,
                damping=0.0,
                friction=0.0,
                armature=0.0,
            )
        }
)

```

{% endcode %}

Se han modificado 3 cosas en al configuración del UR10:

#### Pose articular inicial

```python
        init_state=ArticulationCfg.InitialStateCfg(
            joint_pos={
                "shoulder_pan_joint": 0.0,
                "shoulder_lift_joint": -1.5708,
                "elbow_joint": 1.5708,
                "wrist_1_joint": -1.5708,
                "wrist_2_joint": -1.5708,
                "wrist_3_joint": 0.0,

                "finger_joint": 0.0,
                ".*_inner_finger_joint": 0.0,
                ".*_inner_finger_pad_joint": 0.0,
                ".*_outer_.*_joint": 0.0,
            },
        ),
```

La pose inicial del brazo se ha cambiado para que el robot empiece en una configuración natural para coger un objeto del suelo. Partir de una pose mejor reduce drásticamente el tiempo que el agente tarda en aprender.

Además, hemos añadido los valores iniciales de todas las articulaciones de la pinza Robotiq 2F-140 (las del dedo principal y todas las auxiliares/pasivas). Si no las inicializamos explícitamente, Isaac Lab puede dejarlas en valores indefinidos y el primer step de la simulación puede partir de una pinza abierta de forma rara.

#### Actuadores para la pinza

Este es el cambio más importante (y menos obvio) de este archivo. La pinza Robotiq 2F-140 es un mecanismo de paralelogramo con muchas articulaciones acopladas mecánicamente, y necesita tres grupos de actuadores distintos en Isaac Lab para que la simulación se comporte correctamente:

```python
            "gripper": ImplicitActuatorCfg(
                joint_names_expr=["finger_joint"],
                effort_limit_sim=200.0,
                velocity_limit_sim=2.0,
                stiffness=500.0,
                damping=50.0,
                friction=0.0,
                armature=0.0,
            ),

            # the auxiliary actuator joint for gripper
            "gripper_aux": ImplicitActuatorCfg(
                joint_names_expr=[".*_inner_finger_joint"],
                effort_limit_sim=1.0,
                velocity_limit_sim=1.0,
                stiffness=0.2,
                damping=0.001,
                friction=0.0,
                armature=0.0,
            ),

            # the passive joints for gripper
            "gripper_passive": ImplicitActuatorCfg(
                joint_names_expr=[".*_inner_finger_pad_joint", ".*_outer_finger_joint", "right_outer_knuckle_joint"],
                effort_limit_sim=1.0,
                velocity_limit_sim=1.0,
                stiffness=0.0,
                damping=0.0,
                friction=0.0,
                armature=0.0,
            )
```

* **`gripper`**: el actuador principal, el que comanda la política mediante la acción de cierre. Tiene rigidez y damping altos para que la pinza siga la consigna con fuerza.
* **`gripper_aux`**: los actuadores auxiliares, encargados de mantener los dedos paralelos durante el cierre. En el mecanismo real trabajan con muy poca fuerza, de ahí que reciban valores de rigidez y límite de esfuerzo muy bajos.
* **`gripper_passive`**: las articulaciones pasivas, en el mecanismo real no tienen motor y su posición queda determinada por la geometría del paralelogramo. En Isaac Lab hay que declararlas explícitamente, pero con rigidez y damping a cero para que se muevan libremente arrastradas por el resto del mecanismo.

{% hint style="warning" %}
**¿Por qué es tan importante esto?** \
Si solo declarásemos el actuador principal y no los auxiliares y pasivos, Isaac Lab dejaría esas articulaciones en su modo por defecto, lo que en la práctica hace que la pinza se cierre de forma "rota": los dedos pierden el paralelismo, se atraviesan entre sí, o el cubo se escapa por huecos que no deberían existir. El resultado es una pinza que nunca consigue agarrar bien el cubo, y por tanto un entrenamiento que no converge nunca.
{% endhint %}

## Resultado del entrenamiento de Lift:

Realizados los cambios comentados en las secciones anteriores, esta es la visualización de la política obtenida tras un entrenamiento de 36000 iteraciones:

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2FPCeEuMIB3yGA5HnIoZT5%2F2026-05-17%2020-24-31.gif?alt=media&amp;token=969d4f9a-a8b0-4573-9a60-404dd522670e" alt=""><figcaption></figcaption></figure>

Todos los archivos del proyecto Lift están disponibles en el [tag "Lift" de mi repositorio isaaclab\_projects](https://github.com/AngelLM/isaaclab_projects/tree/Lift/).

[^1]:
