> 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/robot-tipo-segway/fase-1-estabilizacion.md).

# Fase 1: Estabilización

Llegó la parte divertida, ¡comienza el entrenamiento del robot!

## Objetivos de la Fase 1

El resultado visible de esta fase es que el robot sea capaz de mantenerse en equilibrio. La Fase 1 está pensada sobre todo como una fase de aprendizaje del propio framework y del flujo de entrenamiento.

En esta fase busco entender cómo se estructura un entrenamiento en Isaac Lab y para qué sirven cada uno de los ficheros que lo componen.

En concreto, los objetivos de esta fase son:

* Entender cómo se define un entorno de entrenamiento (`env`) y qué responsabilidades tiene.
* Comprender la función del fichero de configuración del entorno (`env config`) y cómo se separa la lógica del código de los parámetros ajustables.
* Identificar qué partes del entrenamiento se ejecutan en cada paso: observaciones, acciones, recompensas, resets y terminaciones.
* Entender cómo y cuándo se inicializa y se reinicia el robot durante el entrenamiento.

El comportamiento del robot en esta fase es simple a propósito. La idea no es optimizar nada ni obtener un resultado espectacular, sino tener un entorno lo más controlado posible que permita centrarse en entender cómo funciona el entrenamiento por dentro.

## Archivos de definición del entrenamiento

De momento, vamos a tocar 3 archivos:

* **`simplerobot_env.py`** \
  Define el entorno de entrenamiento: Es decir, cómo se simula el robot, qué observa, qué acciones aplica y cómo se calculan recompensas, resets y terminaciones.
* **`simplerobot_cfg_env.py`**\
  Configura el entorno: número de entornos paralelos, duración del episodio, decimation, espacios de observación y acción, recompensas y cómo se instancian y distribuyen los robots.
* **`rsl_rl_ppo_cfg.py`** \
  Define cómo se entrena la política: arquitectura de la red, frecuencia y duración del entrenamiento, tamaños de batch, tasas de aprendizaje y parámetros internos del algoritmo.

En resumen, **`env`** define **qué pasa**. `cfg_env` define en qué condiciones pasa. **`rsl_rl_ppo_cfg`** define **cómo se aprende de lo que pasa**.

### simplerobot\_env.py

Archivo ubicado en la ruta `/home/angellm/SimpleRobot/source/SimpleRobot/SimpleRobot/tasks/direct/simplerobot/`

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

{% code title="simplerobot\_env.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

from __future__ import annotations

import torch
from collections.abc import Sequence

import isaaclab.sim as sim_utils
from isaaclab.assets import Articulation
from isaaclab.envs import DirectRLEnv
from isaaclab.sim.spawners.from_files import GroundPlaneCfg, spawn_ground_plane

from .simplerobot_env_cfg import SimplerobotEnvCfg


class SimplerobotEnv(DirectRLEnv):
    cfg: SimplerobotEnvCfg

    def __init__(self, cfg: SimplerobotEnvCfg, render_mode: str | None = None, **kwargs):
        super().__init__(cfg, render_mode, **kwargs) # this super call will invoke _setup_scene()

        self.dof_idx, _ = self.robot.find_joints(self.cfg.dof_names) # get the indices of the controlled dofs


    def _setup_scene(self):
        self.robot = Articulation(self.cfg.robot_cfg)
        # add ground plane
        spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
        # clone and replicate
        self.scene.clone_environments(copy_from_source=False) # copy_from_source=False will use instanceable references for better performance. 
        # add articulation to scene
        self.scene.articulations["robot"] = self.robot
        # add lights
        light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
        light_cfg.func("/World/Light", light_cfg)

        # setting aside useful variables for later
        self.commands = torch.zeros((self.cfg.scene.num_envs, 3)).cuda()                     # initialize commands buffer


    # Both _pre_physics_step and _apply_action are not called every simulation step, but only at the steps when actions are applied (according to the decimation factor).
    # F.g., if decimation=2, these methods are called every 2 simulation steps: _pre_physics_step -> _apply_action -> physics step -> physics step -> _pre_physics_step -> _apply_action -> physics step -> physics step -> ...
    def _pre_physics_step(self, actions: torch.Tensor) -> None:
        # This method is called before the physics step. We store the actions to be applied later in _apply_action()
        self.actions = actions.clone() # Copy the actions and store them for use in _apply_action(). It acts as a buffer between the policy and the physics step.
        self.actions = self.actions * self.cfg.actions_scale # scale the actions to reasonable values

    def _apply_action(self) -> None:
        # This method is called after the _pre_physics_step() and before the physics step. Here we apply the stored actions to the robot.
        self.robot.set_joint_velocity_target(self.actions, joint_ids=self.dof_idx) # set the wheel velocities according to the actions
    
    def _get_observations(self) -> dict:
        self.projected_gravity = self.robot.data.projected_gravity_b # Shape (N,3)
        self.angular_velocity = self.robot.data.root_ang_vel_b # Shape (N,3)
        self.wheel_vel = self.robot.data.joint_vel #Shape (N, num_joints)

        obs = torch.cat(
            [
                self.projected_gravity,
                self.angular_velocity,
                self.wheel_vel,
            ],
            dim=-1,
        )

        observations = {"policy": obs}
        return observations

    def _get_rewards(self) -> torch.Tensor:

        # --- Inclinación ---
        # projected_gravity_b ≈ [0, 0, -1] cuando está vertical
        tilt_error = self.projected_gravity[:, 0]**2 # Solo nos importa el eje X (adelante/atrás)
        upright_reward = torch.exp(-5.0 * tilt_error)

        # --- Velocidad angular (evitar oscilaciones) ---
        ang_vel_penalty = torch.sum(self.angular_velocity ** 2, dim=1)

        # --- Alive ---
        alive_reward = (~self.reset_buf).float()

        # --- Reward final ---
        reward = (
            2.0 * upright_reward
            + 0.2 * alive_reward
            - 0.5 * ang_vel_penalty
        )

        return reward

    def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
        time_out = self.episode_length_buf >= self.max_episode_length - 1 # If the episode length buffer exceeds the max length, we time out

        # Si el robot se inclina más de ~50° en el eje X o Y, se considera que ha caído
        fallen = torch.any(torch.abs(self.projected_gravity[:, :2]) > 0.8727, dim=1)

        return fallen, time_out

    def _reset_idx(self, env_ids: Sequence[int] | None):
        if env_ids is None:
            env_ids = self.robot._ALL_INDICES
        super()._reset_idx(env_ids)

        default_root_state = self.robot.data.default_root_state[env_ids] # get the default root state (position and orientation in World frame)
        default_root_state[:, :3] += self.scene.env_origins[env_ids]     # offset the position according to the environment origin
        default_root_state[:, 2] += 0.0  # SUBIR ROBOT

        # pick new commands for reset envs and normalize them just like in the setup
        self.commands[env_ids] = torch.zeros((len(env_ids), 3)).cuda()

        self.robot.write_root_state_to_sim(default_root_state, env_ids)  # reset the root state of the robot
```

{% endcode %}

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

```python
def __init__(self, cfg: SimplerobotEnvCfg, render_mode: str | None = None, **kwargs):
    super().__init__(cfg, render_mode, **kwargs) # this super call will invoke _setup_scene()
    self.dof_idx, _ = self.robot.find_joints(self.cfg.dof_names) # get the indices of the controlled dofs
```

Esta función se ejecuta una vez al iniciar el entrenamiento, inicializa el entorno, lanza `_setup_scene()` vía `super()` y guarda los índices de las articulaciones en `dof_idx`.

#### \_setup\_scene

{% code expandable="true" %}

```python
def _setup_scene(self):
    self.robot = Articulation(self.cfg.robot_cfg)
    # add ground plane
    spawn_ground_plane(prim_path="/World/ground", cfg=GroundPlaneCfg())
    # clone and replicate
    self.scene.clone_environments(copy_from_source=False) # copy_from_source=False will use instanceable references for better performance. 
    # add articulation to scene
    self.scene.articulations["robot"] = self.robot
    # add lights
    light_cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.75, 0.75, 0.75))
    light_cfg.func("/World/Light", light_cfg)
    # setting aside useful variables for later
    self.commands = torch.zeros((self.cfg.scene.num_envs, 3)).cuda()  # initialize commands buffer
```

{% endcode %}

**`_setup_scene`** construye la escena de simulación: crea el robot, añade el plano de suelo, clona los entornos, registra el robot en la escena y configura la iluminación. Por último, inicializa con ceros el buffer para los comandos.

#### \_pre\_physics\_step

```python
def _pre_physics_step(self, actions: torch.Tensor) -> None:
    # This method is called before the physics step. We store the actions to be applied later in _apply_action()
    self.actions = actions.clone() # Copy the actions and store them for use in _apply_action(). It acts as a buffer between the policy and the physics step.
    self.actions = self.actions * self.cfg.actions_scale # scale the actions to reasonable values
```

**`_pre_physics_step`** se ejecuta antes del paso de físicas cuando se aplican acciones. Guarda las acciones de la política y las escala, dejándolas preparadas para aplicarlas al robot.

#### \_apply\_action

```python
def _apply_action(self) -> None:
    # This method is called after the _pre_physics_step() and before the physics step. Here we apply the stored actions to the robot.
    self.robot.set_joint_velocity_target(self.actions, joint_ids=self.dof_idx) # set the wheel velocities according to the actions
```

**`_apply_action`** aplica al robot las acciones almacenadas, enviándolas como objetivos de velocidad a las articulaciones controladas antes del paso de físicas.

#### \_get\_observations

{% code expandable="true" %}

```python
def _get_observations(self) -> dict:
    self.projected_gravity = self.robot.data.projected_gravity_b # Shape (N,3)
    self.angular_velocity = self.robot.data.root_ang_vel_b # Shape (N,3)
    self.wheel_vel = self.robot.data.joint_vel #Shape (N, num_joints)

    obs = torch.cat(
        [
            self.projected_gravity,
            self.angular_velocity,
            self.wheel_vel,
        ],
        dim=-1,
    )

    observations = {"policy": obs}
    return observations
```

{% endcode %}

**`_get_observations`** recopila el estado actual del robot desde la simulación y construye el vector de observaciones que se pasa a la política en cada paso de entrenamiento.

Para esta fase he escogido las siguientes observaciones:

* **Gravedad proyectada (`projected_gravity`)**: indica la dirección de la gravedad en el marco del robot y permite conocer su inclinación (equivalente a un acelerómetro).
* **Velocidad angular (`angular_velocity`)**: indica cómo de rápido está rotando el robot y permite detectar oscilaciones e inestabilidad (equivalente a un giroscopio).
* **Velocidad de las ruedas (`wheel_vel`)**: indica la velocidad actual de las articulaciones motrices y permite saber qué está haciendo realmente el robot (equivalente a encoders).

{% hint style="info" %}
En un robot real, estas observaciones vendrían de sensores como IMUs o encoders. En este proyecto utilizo directamente los datos que proporciona la simulación para simplificar el entrenamiento y centrarme en entender Isaac Lab.
{% endhint %}

En la página \[Datos accesibles desde self.robot.data]\() recopilo algunos de los datos del robot proporcionados por la simulación.

#### \_get\_rewards

{% code expandable="true" %}

```python
def _get_rewards(self) -> torch.Tensor:
    # --- Inclinación ---
    # projected_gravity_b ≈ [0, 0, -1] cuando está vertical
    tilt_error = self.projected_gravity[:, 0]**2 # Solo nos importa el eje X (adelante/atrás)
    upright_reward = torch.exp(-5.0 * tilt_error)

    # --- Velocidad angular (evitar oscilaciones) ---
    ang_vel_penalty = torch.sum(self.angular_velocity ** 2, dim=1)

    # --- Alive ---
    alive_reward = (~self.reset_buf).float()

    # --- Reward final ---
    reward = (
        2.0 * upright_reward
        + 0.2 * alive_reward
        - 0.5 * ang_vel_penalty
    )

    return reward
```

{% endcode %}

**`_get_rewards`** calcula la recompensa en cada paso de entrenamiento a partir del estado actual del robot, premiando la estabilidad y penalizando comportamientos indeseados.

* **Inclinación (`upright_reward`)**:\
  Premia que el robot se mantenga erguido. Se basa en la gravedad proyectada y penaliza la inclinación hacia delante o atrás.&#x20;
* **Velocidad angular (`ang_vel_penalty`)**:\
  Penaliza rotaciones rápidas del cuerpo para evitar oscilaciones y movimientos bruscos.
* **Alive (`alive_reward`)**:\
  Recompensa que el robot siga activo y no haya sido reseteado en ese paso.

Los pesos de la recompensa controlan qué comportamientos se priorizan durante el entrenamiento. Aunque podrían definirse en el fichero de configuración del entorno (`env_cfg`), de momento los he hardcodeado aquí.

En esta fase de entrenamiento le he dado un valor alto al peso de la recompensa por mantenerse erguido, ya que el objetivo principal es aprender a estabilizar el robot. La penalización por velocidad angular es menor, pero suficiente para reducir oscilaciones y movimientos bruscos sin impedir correcciones rápidas. La recompensa por seguir vivo no es muy grande y sirve únicamente para evitar soluciones en las que el episodio termina lo antes posible.

#### \_get\_dones

{% code expandable="true" %}

```python
def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
    time_out = self.episode_length_buf >= self.max_episode_length - 1 # If the episode length buffer exceeds the max length, we time out
    # Si el robot se inclina más de ~50° en el eje X o Y, se considera que ha caído
    fallen = torch.any(torch.abs(self.projected_gravity[:, :2]) > 0.8727, dim=1)
    return fallen, time_out
```

{% endcode %}

**`_get_dones`** determina cuándo un episodio debe terminar, ya sea porque el robot ha caído o porque se ha alcanzado la duración máxima del episodio.

Por simplicidad, he supuesto que el robot se ha caído cuando se ha inclinado más de 50 grados en el eje X o Y.

#### \_reset\_idx

{% code expandable="true" %}

```python
def _reset_idx(self, env_ids: Sequence[int] | None):
    if env_ids is None:
        env_ids = self.robot._ALL_INDICES
    super()._reset_idx(env_ids)

    default_root_state = self.robot.data.default_root_state[env_ids] # get the default root state (position and orientation in World frame)
    default_root_state[:, :3] += self.scene.env_origins[env_ids]     # offset the position according to the environment origin
    default_root_state[:, 2] += 0.0  # SUBIR ROBOT

    # pick new commands for reset envs and normalize them just like in the setup
    self.commands[env_ids] = torch.zeros((len(env_ids), 3)).cuda()
    
    self.robot.write_root_state_to_sim(default_root_state, env_ids)  # reset the root state of the robot
```

{% endcode %}

**`_reset_idx`** selecciona los entornos a reiniciar, restaura el estado inicial del robot, ajusta su posición según el origen de cada entorno, reinicializa los comandos y escribe el estado reiniciado en la simulación. Esto se hace antes de comenzar un nuevo episodio.

### simplerobot\_env\_cfg.py

Archivo ubicado en la ruta `/home/angellm/SimpleRobot/source/SimpleRobot/SimpleRobot/tasks/direct/simplerobot/`

Este fichero define las condiciones bajo las que se ejecuta el entorno de entrenamiento. Aquí no se implementa lógica, sino que se ajustan parámetros que controlan cómo de rápido avanza la simulación, cuánto dura un episodio, qué tamaño tienen los espacios y cómo se instancian los entornos.

El objetivo es poder modificar el comportamiento del entrenamiento sin tocar el código del entorno.

{% code title="simplerobot\_env\_cfg.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

from isaaclab_assets.robots.simplerobot import SIMPLE_ROBOT_CFG

from isaaclab.assets import ArticulationCfg
from isaaclab.envs import DirectRLEnvCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.sim import SimulationCfg
from isaaclab.utils import configclass


@configclass
class SimplerobotEnvCfg(DirectRLEnvCfg):
    # env
    decimation = 2
    episode_length_s = 20
    actions_scale = 0.25

    # - spaces definition
    action_space = 2  # two wheel velocities: [left_wheel_velocity, right_wheel_velocity]
    observation_space = 3 + 3 + 2 # gravity vector (3), angular velocity (3), wheel velocities (2)
    state_space = 0

    # simulation
    sim: SimulationCfg = SimulationCfg(dt=1 / 120, render_interval=decimation)

    # robot(s)
    robot_cfg: ArticulationCfg = SIMPLE_ROBOT_CFG.replace(prim_path="/World/envs/env_.*/Robot")

     # scene
    # 400 environments in a grid, spaced by 4 meters. Each env has its own physics scene so interactions are independent.
    scene: InteractiveSceneCfg = InteractiveSceneCfg(num_envs=400, env_spacing=4.0, replicate_physics=True) 
    dof_names = ["left_joint", "right_joint"] # as this configuration file defines topology, the names of the dofs should be specified here
```

{% endcode %}

* **decimation**: define cada cuántos pasos de simulación se aplican nuevas acciones de la política.
* **episode\_length\_s**: define la duración máxima de cada episodio en segundos.
* **actions\_scale**: factor de escala aplicado a las acciones antes de enviarlas al robot.
* **action\_space**: tamaño del vector de acciones generado por la política.
* **observation\_space**: tamaño del vector de observaciones que recibe la política.
* **num\_envs**: número de entornos que se ejecutan en paralelo durante el entrenamiento.
* **dof\_names**: lista de articulaciones del robot que están controladas por la política. Lo utilizo en el `__init__` del  `env`.

### rsl\_rl\_ppo\_cfg.py

Archivo ubicado en la ruta `/home/angellm/SimpleRobot/source/SimpleRobot/SimpleRobot/tasks/direct/simplerobot/agents`

Del contenido de este archivo sólamente cambié el nombre del experimento y el número de iteraciones:

```python
max_iterations = 1000
experiment_name = "simplerobot_direct"
```

{% code title="rsl\_rl\_ppo\_cfg.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

from isaaclab.utils import configclass

from isaaclab_rl.rsl_rl import RslRlOnPolicyRunnerCfg, RslRlPpoActorCriticCfg, RslRlPpoAlgorithmCfg


@configclass
class PPORunnerCfg(RslRlOnPolicyRunnerCfg):
    num_steps_per_env = 16
    max_iterations = 1000
    save_interval = 50
    experiment_name = "simplerobot_direct"
    policy = RslRlPpoActorCriticCfg(
        init_noise_std=1.0,
        actor_obs_normalization=False,
        critic_obs_normalization=False,
        actor_hidden_dims=[32, 32],
        critic_hidden_dims=[32, 32],
        activation="elu",
    )
    algorithm = RslRlPpoAlgorithmCfg(
        value_loss_coef=1.0,
        use_clipped_value_loss=True,
        clip_param=0.2,
        entropy_coef=0.005,
        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,
    )
```

{% endcode %}

## Entrenamiento de la Fase 1

Una vez configurado el entrenamiento, toca ejecutarlo.

Primero, tengo que saber el nombre de la Task. Para averiguarlo ejecuto el comando `~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/list_envs.py`  que me devuelve la respuesta:

```bash
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|                                                                            Available Environments in Isaac Lab                                                                             |
+--------+--------------------------------+---------------------------------------------------------------------+----------------------------------------------------------------------------+
| S. No. | Task Name                      | Entry Point                                                         | Config                                                                     |
+--------+--------------------------------+---------------------------------------------------------------------+----------------------------------------------------------------------------+
|   1    | Template-Simplerobot-Direct-v0 | SimpleRobot.tasks.direct.simplerobot.simplerobot_env:SimplerobotEnv | SimpleRobot.tasks.direct.simplerobot.simplerobot_env_cfg:SimplerobotEnvCfg |
+--------+--------------------------------+---------------------------------------------------------------------+----------------------------------------------------------------------------+

```

Con lo que ya se que la tarea se llama **`Template-Simplerobot-Direct-v0`** . Sabiendo esto, puedo ejecutar el entrenamiento con el comando `~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/train.py --task=Template-Simplerobot-Direct-v0`

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2FUKcBFPMyoEAo0kFQLGKI%2Fit0.gif?alt=media&amp;token=6ca28812-ee19-4419-98cb-be2aac32e846" alt=""><figcaption></figcaption></figure>

Si todo va bien y no he metido la pata con el código, se ejecutará una ventana de Isaac Sim donde spawnearán 400 robots un poco torpes que se caerán hacia delante y hacia detrás.

Al principio, los robots no sabrán cómo mantenerse erguidos. Iteración a iteración irán aprendiendo a través de las observaciones cuales son las consecuencias de sus acciones e irán ajustando su algoritmo de control. A medida que vayan transcurriendo las iteraciones, veremos como hay más y más robots que aprenden a no caerse. Esto me pasó aproximadamente a las 100 iteraciones de comenzar el entrenamiento.

{% hint style="danger" %}
Importante: Lo que se ve en pantalla durante el entrenamiento no es un comportamiento “final”. El ruido y la exploración propios del aprendizaje pueden generar movimientos erráticos que desaparecen al ejecutar la simulación en play. Al ejecutar la simulación en play, el comportamiento suele ser mucho más estable y representativo.
{% endhint %}

## Evaluación del entrenamiento de la Fase 1

Para ver la simulación de la politica entrenada utilizo el comando `~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/play.py --task=Template-Simplerobot-Direct-v0`

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2FePjNg6VewIsmdd4fq1p3%2Fit200.gif?alt=media&amp;token=5cbf7f1f-ca13-4045-8cd2-c00febb8077a" alt=""><figcaption></figcaption></figure>

Ahora podemos ver una simulación donde todos los robots siguen la política entrenada y mantienen el equilibrio.

Se puede apreciar un pequeño movimiento de los robots en su eje local X, pero esto es totalmente posible, ya que en ningún momento le hemos dicho que no se mueva, únicamente le hemos pedido que se mantenga erguido y aguante vivo el máximo tiempo posible. Teniendo eso en cuenta, la política ha llegado a una solución donde el robot cumple ambos requisitos.

Por lo tanto, podemos decir que dado que se cumplen el objetivo principal, el entrenamiento de la Fase 1 ha sido un éxito.
