> 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-3-movimiento-avanzado.md).

# Fase 3: Movimiento avanzado

## Objetivos de la Fase 3

En esta fase buscaré que el robot responda bien a comandos de velocidad más avanzados introduciendo un rango de velocidad lineal más amplio (de -1m/s a 1m/s) y una componente de velocidad angular (yaw) para que el robot aprenda a girar. Todo esto sin olvidar lo aprendido en fases anteriores.

Los objetivos por lo tanto serán:

* Que el robot sea capaz de mantenerse en equilibrio estático cuando el comando de movimiento es cero.
* Que el robot sea capaz de desplazarse recto a la velocidad lineal solicitada cuando el comando de velocidad no tiene componente angular.
* Que el robot sea capaz de realizar movimientos curvilineos, adaptando su velocidad linear y angular a la solicitada cuando el comando tiene tanto componente lineal como angular.

## Estrategia de entrenamiento

Al igual que en la fase anterior, vamos a tener que introducir nuevas observaciones para lograr los nuevos objetivos. El vector de acción se mantendrá igual, puesto que seguiremos actuando únicamente sobre la velocidad de cada una de las ruedas del robot.

Como ya vimos, cambiar el espacio de observaciones hace que la política entrenada anteriormente no pueda utilizarse para continuar el entrenamiento de esta nueva fase. Debido a esto, modificaremos las observaciones y entrenaremos de nuevo las fases 1 y 2, para así poder utilizar la política aprendida en la segunda fase para entrenar la tercera.

## Cambios en el espacio de observaciones

### Cambios en `simplerobot_env.py`

He modificado la función `_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[:, :2] # Shape (N,2) pitch y roll
        self.wheel_vel = self.robot.data.joint_vel #Shape (N, num_joints)
        self.vx = self.robot.data.root_lin_vel_b[:, 0:1]   # frontal
        self.yaw_vel = self.robot.data.root_ang_vel_b[:, 2:3]
        self.cmd = self.commands[:,0:2]

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

        observations = {"policy": obs}

        return observations
```

{% endcode %}

1. He cambiado el nombre de `lin_vel_b` por `vx` por conveniencia.
2. He agregado `yaw_vel` al vector de observaciones, que representa la componente de la velocidad angular **yaw** (giro en el eje vertical del robot)
3. He simplificado la observación de `angular_velocity` para que solo contenga los valores de las componentes **pitch** y **roll**, ya que la componente **yaw** la estoy observando con `yaw_vel`&#x20;
4. He modificado la observación del comando `cmd`, ya que ahora tendrá dos componentes: la velocidad lineal y la angular.

### Cambios en `simplerobot_env_cfg.py`&#x20;

Los cambios en el env, nos obligan a cambiar la dimensión de la variable `observation_space` :

```python
    observation_space = 3 + 2 + 2 + 1 + 1 + 2 # gravity vector (3), angular velocity pitch y yaw (2), wheel velocities (2), linear velocity (1), angular yaw (1),  command velocity linear & yaw (2)
```

## Cambios en pesos y tasks

En las fases anteriores he estado modificando manualmente los pesos de las recompensas y penalizaciones y adaptándolas según la fase que estuviese entrenando. Eso funciona, pero hardcodear no suele ser una buena práctica, así que voy a tratar de parametrizar un poco el tema de las fases.

### Migración de los pesos a `simplerobot_env_cfg.py`

Lo primero, es pasar los pesos de las recompensas y penalizaciones al archivo `simplerobot_env_cfg.py`:

{% 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 + 2 + 2 + 1 + 1 + 2 # gravity vector (3), angular velocity pitch y yaw (2), wheel velocities (2), linear velocity (1), angular yaw (1),  command velocity linear & yaw (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
    # 100 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

    # reward weights (defaults / neutral)
    upright_reward_weight = 0.2
    alive_reward_weight = 0.5
    ang_vel_penalty_weight = -0.05
    vel_penalty_weight = -1.0
    no_still_penalty_weight = -1.0
    diff_penalty_weight = -0.2
```

{% endcode %}

Ahora, podremos acceder a estos parámetros desde `simplerobot_env.py` utilizando `robot.cfg.nombre_del_parametro`. Entonces, en mi cálculo del reward puedo poner:

```
# --- Reward final ---
reward = (
    self.cfg.upright_reward_weight * upright_reward
    + self.cfg.ang_vel_penalty_weight * ang_vel_penalty
    + self.cfg.alive_reward_weight * alive_reward
    + self.cfg.vel_penalty_weight * vel_penalty
    + self.cfg.no_still_penalty_weight * no_still_penalty
    + self.cfg.diff_penalty_weight * diff_penalty
)
```

Ahora ya no hace falta tocar el archivo `simplerobot_env.py` para cambiar los pesos de las recompensas y penalizaciones entre fases. Para ello solamente tendremos que modificar el archivo `simplerobot_env_cfg.py`, ajustando los valores de los pesos según la fase en la que estemos. \
Los valores positivos estarán asociados a recompensas y los valores negativos a las penalizaciones. Pondremos un valor de 0.0 para aquellas recompensas o penalizaciones que no queramos utilizar durante el entrenamiento de esa fase en concreto.&#x20;

### Creación de nuevas tasks

He movido los pesos al archivo de configuración, pero eso no evita que tenga que modificar manualmente los pesos entre los entrenamientos de las distintas fases. Además, me resulta un poco complicado recordar qué pesos había utilizado anteriormente y he terminado anotándolo todo en un bloc de notas un poco chapucero, así que voy a intentar hacerlo un poco mejor.

Investigando un poco, he visto que se pueden crear distintas tasks. Esto permite lanzar entrenamientos utilizando configuraciones concretas.

Lo primero es crear una configuración por cada una de las fases que va a tener el entrenamiento. Estas configuraciones van a heredar de la configuración base que ya teníamos definida anteriormente:

{% 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 + 2 + 2 + 1 + 1 + 2 # gravity vector (3), angular velocity pitch y yaw (2), wheel velocities (2), linear velocity (1), angular yaw (1),  command velocity linear & yaw (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
    # 100 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

    phase = 0
    # reward weights (defaults / neutral)
    upright_reward_weight = 0.0
    alive_reward_weight = 0.0
    ang_vel_penalty_weight = 0.0
    vel_penalty_weight = 0.0
    no_still_penalty_weight = 0.0
    diff_penalty_weight = 0.0


@configclass
class SimplerobotEnvCfgPhase1(SimplerobotEnvCfg):
    """Phase 1: stabilization / balance"""
    phase = 1
    upright_reward_weight = 2.0
    alive_reward_weight = 0.2
    ang_vel_penalty_weight = -0.5
    vel_penalty_weight = 0.0
    no_still_penalty_weight = 0.0
    diff_penalty_weight = 0.0

@configclass
class SimplerobotEnvCfgPhase2(SimplerobotEnvCfg):
    """Phase 2: velocity tracking"""
    phase = 2
    upright_reward_weight = 0.2
    alive_reward_weight = 0.2
    ang_vel_penalty_weight = -0.05
    vel_penalty_weight = -2.0
    no_still_penalty_weight = -5.0
    diff_penalty_weight = -0.2
```

{% endcode %}

Como se puede ver, he asignado los pesos que quiero que tengan las recompensas y penalizaciones en cada una de las fases. Además he añadido la variable `phase` que me ayudará posteriormente a parametrizar el entrenamiento en el archivo del env.

Después de crear estas configuraciones, tenemos que crear las tareas. Para ello hay que modificar el archivo `source/SimpleRobot/SimpleRobot/tasks/direct/simplerobot/__init__.py`:

{% code title="**init**.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

import gymnasium as gym

from . import agents

##
# Register Gym environments.
##


gym.register(
    id="Template-Simplerobot-Direct-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfg",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)

gym.register(
    id="Template-Simplerobot-Direct-Phase1-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfgPhase1",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)

gym.register(
    id="Template-Simplerobot-Direct-Phase2-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfgPhase2",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)
```

{% endcode %}

Por defecto solo hay una task registrada, la copiamos y pegamos tantas veces como fases tengamos.

Cada task debe tener un nombre diferente, así que yo he identificado a cada una de ellas con el número de la fase del entrenamiento que quiero que lancen.

Por último, en cada task hay que cambiar el parámetro de configuración `env_cfg_entry_point` para hacerlo coincidir con el nombre de la configuración de la fase creada en `simplerobot_env_cfg.py`

Una vez hecho esto, podrémos lanzar los entrenamientos de la siguiente forma:

Para lanzar el entrenamiento de la Fase 1 con sus pesos de las recompensas y penalizaciones:\
`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/train.py --task=Template-Simplerobot-Direct-Phase1-v0`

Para lanzar el entrenamiento de la Fase 2 con los pesos de las recompensas y penalizaciones de la segunda fase, cargando además la política de un checkpoint de la Fase 1:\
`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/train_mod.py --task=Template-Simplerobot-Direct-Phase2-v0 --load_policy <path_del_checkpoint>`

## Entrenamiento

### Cambios en el `simplerobot_env.py`

{% 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
import random

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)

        self.commands = torch.zeros((self.cfg.scene.num_envs, 2)).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[:, :2] # Shape (N,2) pitch y roll
        self.wheel_vel = self.robot.data.joint_vel #Shape (N, num_joints)
        self.vx = self.robot.data.root_lin_vel_b[:, 0:1]   # frontal
        self.yaw_vel = self.robot.data.root_ang_vel_b[:, 2:3]
        self.cmd = self.commands[:,0:2]

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

        observations = {"policy": obs}

        # print(f"Cmd: ({self.commands[1,0].item():.2f}, {self.commands[1,1].item():.2f}) | Robot: ({self.robot.data.root_lin_vel_b[1,0].item():.2f}, {self.robot.data.root_ang_vel_b[1,2].item():.2f})")
        
        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] ** 2, dim=1) # penaliza pitch y roll, no yaw

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

        # --- Penalización por no moverse a la velocidad del comando ---
        vel_cmd = self.commands[:, 0:1]
        vel_cmd_mask = torch.abs(vel_cmd) > 1e-3
        vel_error = torch.zeros_like(vel_cmd)
        vel_error[vel_cmd_mask] = torch.abs(self.vx - vel_cmd)[vel_cmd_mask]
        vel_penalty = torch.clamp(vel_error / self.cfg.max_lin_vel, 0.0, 1.0).squeeze(-1)

        # --- Penalización por no estar quieto cuando el comando es 0 ---
        still_mask = torch.abs(vel_cmd) <= 1e-3
        no_still_error = torch.zeros_like(vel_cmd)
        no_still_error[still_mask] = torch.abs(self.vx - 0.0)[still_mask]
        no_still_penalty = torch.clamp(no_still_error / self.cfg.max_lin_vel, 0.0, 1.0).squeeze(-1)

        # --- Penalización por diferencia de velocidad entre ruedas (evitar giros) ---
        diff_penalty = (self.wheel_vel[:, 0] - self.wheel_vel[:, 1])**2

        # --- Penalización por no moverse a la velocidad angular del comando ---
        yaw_cmd = self.commands[:, 1:2]
        yaw_cmd_mask = torch.abs(yaw_cmd) > 1e-3
        yaw_error = torch.zeros_like(yaw_cmd)
        yaw_error[yaw_cmd_mask]= torch.abs(self.yaw_vel - yaw_cmd)[yaw_cmd_mask]
        yaw_penalty = torch.clamp(yaw_error / self.cfg.max_yaw_vel, 0.0, 1.0).squeeze(-1)

        # --- Reward final ---
        reward = (
            self.cfg.upright_reward_weight * upright_reward
            + self.cfg.ang_vel_penalty_weight * ang_vel_penalty
            + self.cfg.alive_reward_weight * alive_reward
            + self.cfg.vel_penalty_weight * vel_penalty
            + self.cfg.no_still_penalty_weight * no_still_penalty
            + self.cfg.diff_penalty_weight * diff_penalty
            + self.cfg.yaw_penalty_weight * yaw_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 # for now we only terminate episodes on timeout, forgetting about other termination conditions

    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
        if self.cfg.phase <= 1:
            self.commands[env_ids] = torch.zeros((len(env_ids), 2)).cuda()
        elif self.cfg.phase == 2:
            if random.random() < 0.3:
                self.commands[env_ids] = torch.zeros((len(env_ids), 2)).cuda()
            else:
                self.commands[env_ids, 0] = 0.3
                self.commands[env_ids, 1] = 0
        elif self.cfg.phase == 3:
            num = len(env_ids)
            rnd = torch.rand(num, device=self.device)
            self.commands[env_ids] = 0.0
            # 20% robots quietos
            mask_zero = rnd <= 0.2 # con estos no hay que hacer nada porque ya estan a cero
            # 20% robots solo con velocidad lineal
            mask_lin = (rnd > 0.2) & (rnd <= 0.4)
            self.commands[env_ids[mask_lin], 0] = (torch.rand(mask_lin.sum(), device=self.device) * 2 - 1)
            # 60% robots velocidad lineal y angular (yaw)
            mask_full = rnd > 0.4
            self.commands[env_ids[mask_full], 0] = (torch.rand(mask_full.sum(), device=self.device) * 2 - 1)
            self.commands[env_ids[mask_full], 1] = (torch.rand(mask_full.sum(), device=self.device) * 2 - 1)

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

{% endcode %}

#### Cambios en `_get_observations`

Los cambios realizados en esta función ya han sido comentados anteriormente.

#### Cambios en `_get_rewards`

```python
# --- Penalización por no moverse a la velocidad del comando ---
vel_cmd = self.commands[:, 0:1]
vel_cmd_mask = torch.abs(vel_cmd) > 1e-3
vel_error = torch.zeros_like(vel_cmd)
vel_error[vel_cmd_mask] = torch.abs(self.vx - vel_cmd)[vel_cmd_mask]
vel_penalty = torch.clamp(vel_error / self.cfg.max_lin_vel, 0.0, 1.0).squeeze(-1)
```

He cambiado cómo se calcula la penalización por no moverse a la velocidad indicada por el comando cuando este impone un valor de velocidad lineal. Ahora acoto el valor de la penalización entre 0 y 1, esto hace que sea más sencillo ajustar los pesos de las penalizaciones después.

{% hint style="info" %}
En el aprendizaje por refuerzo, solo debemos aplicar una penalización cuando el comportamiento sea inequívocamente incorrecto. Si puede ser correcto en otro contexto, debemos condicionarlo para no introducir señales de aprendizaje contradictorias.
{% endhint %}

```python
# --- Penalización por no estar quieto cuando el comando es 0 ---
still_mask = torch.abs(vel_cmd) <= 1e-3
no_still_error = torch.zeros_like(vel_cmd)
no_still_error[still_mask] = torch.abs(self.vx - 0.0)[still_mask]
no_still_penalty = torch.clamp(no_still_error / self.cfg.max_lin_vel, 0.0, 1.0).squeeze(-1)
```

De la misma manera, he cambiado el cómo se calcula la penalización por moverse cuando no se recibe un comando de movimiento.

```python
# --- Penalización por no moverse a la velocidad angular del comando ---
yaw_cmd = self.commands[:, 1:2]
yaw_cmd_mask = torch.abs(yaw_cmd) > 1e-3
yaw_error = torch.zeros_like(yaw_cmd)
yaw_error[yaw_cmd_mask]= torch.abs(self.yaw_vel - yaw_cmd)[yaw_cmd_mask]
yaw_penalty = torch.clamp(yaw_error / self.cfg.max_yaw_vel, 0.0, 1.0).squeeze(-1)
```

He agregado una nueva penalización por no girar cuando existe el comando indica una velocidad angular.

```python
# --- Reward final ---
reward = (
    self.cfg.upright_reward_weight * upright_reward
    + self.cfg.ang_vel_penalty_weight * ang_vel_penalty
    + self.cfg.alive_reward_weight * alive_reward
    + self.cfg.vel_penalty_weight * vel_penalty
    + self.cfg.no_still_penalty_weight * no_still_penalty
    + self.cfg.diff_penalty_weight * diff_penalty
    + self.cfg.yaw_penalty_weight * yaw_penalty
)
```

Y por último he ajustado el cálculo del reward final.&#x20;

#### Cambios en `_reset_idx`

```python
# pick new commands for reset envs and normalize them just like in the setup
if self.cfg.phase <= 1:
    self.commands[env_ids] = torch.zeros((len(env_ids), 2)).cuda()
elif self.cfg.phase == 2:
    num = len(env_ids)
    rnd = torch.rand(num, device=self.device)
    self.commands[env_ids] = 0.0
    # 30% robots quietos
    mask_zero = rnd <= 0.3 # con estos no hay que hacer nada porque ya estan a cero
    # 70% robots solo con velocidad lineal
    mask_lin = (rnd > 0.3)
    self.commands[env_ids[mask_lin], 0] = 0.3
elif self.cfg.phase == 3:
    num = len(env_ids)
    rnd = torch.rand(num, device=self.device)
    self.commands[env_ids] = 0.0
    # 20% robots quietos
    mask_zero = rnd <= 0.2 # con estos no hay que hacer nada porque ya estan a cero
    # 20% robots solo con velocidad lineal
    mask_lin = (rnd > 0.2) & (rnd <= 0.4)
    self.commands[env_ids[mask_lin], 0] = (torch.rand(mask_lin.sum(), device=self.device) * 2 - 1)
    # 60% robots velocidad lineal y angular (yaw)
    mask_full = rnd > 0.4
    self.commands[env_ids[mask_full], 0] = (torch.rand(mask_full.sum(), device=self.device) * 2 - 1)
    self.commands[env_ids[mask_full], 1] = (torch.rand(mask_full.sum(), device=self.device) * 2 - 1)
```

He utilizado el parámetro `phase` de la configuración para decidir cómo he de resetear los comandos en función de la fase del entrenamiento que se esté ejecutando.

En la primera fase, todos los robots reciben un comando de velocidad lineal y angular nulo.

En la segunda fase, el 30% de los robots reciben un comando de velocidad lineal y angular nulo. El 70% restante reciben un comando de velocidad lineal de 0.3m/s y una velocidad angular nula.\
He cambiado la manera de asignar los valores respecto al entrenamiento de la Fase 2. Antes no eran el 30% de los robots, si no el 30% de las iteraciones las que tenian comandos de velocidad nulos. Ahora utilizo máscaras para, en la misma iteración, mezclar robots con comandos de velocidad nulos y robots con comandos de velocidad de 0.3m/s.

En la tercera fase, el 20% de los robots reciben un comando de velocidad lineal y angular nulo. Otro 20% recibe un comando de velocidad lineal acotado entre -1m/s y 1m/s y velocidad angular nula. El 60% restante recibe un comando de velocidad lineal acotado entre -1m/s y 1m/s y una velocidad angular de entre -1rad/s y 1rad/s.

### Cambios en `simplerobot_env_cfg.py`

{% 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 + 2 + 2 + 1 + 1 + 2 # gravity vector (3), angular velocity pitch y yaw (2), wheel velocities (2), linear velocity (1), angular yaw (1),  command velocity linear & yaw (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
    # 100 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

    phase = 0
    # reward weights (defaults / neutral)
    upright_reward_weight = 0.0
    alive_reward_weight = 0.0
    ang_vel_penalty_weight = 0.0
    vel_penalty_weight = 0.0
    no_still_penalty_weight = 0.0
    diff_penalty_weight = 0.0
    yaw_penalty_weight = 0.0
    max_lin_vel = 1.0
    max_yaw_vel = 1.0


@configclass
class SimplerobotEnvCfgPhase1(SimplerobotEnvCfg):
    """Phase 1: stabilization / balance"""
    phase = 1
    upright_reward_weight = 2.0
    alive_reward_weight = 0.2
    ang_vel_penalty_weight = -0.5
    vel_penalty_weight = 0.0
    no_still_penalty_weight = 0.0
    diff_penalty_weight = 0.0
    yaw_penalty_weight = 0.0

@configclass
class SimplerobotEnvCfgPhase2(SimplerobotEnvCfg):
    """Phase 2: velocity tracking"""
    phase = 2
    upright_reward_weight = 0.2
    alive_reward_weight = 0.2
    ang_vel_penalty_weight = -0.05
    vel_penalty_weight = -2.0
    no_still_penalty_weight = -5.0
    diff_penalty_weight = -0.2
    yaw_penalty_weight = 0.0

@configclass
class SimplerobotEnvCfgPhase3(SimplerobotEnvCfg):
    """Phase 3: yaw"""
    phase = 3
    upright_reward_weight = 0.2
    alive_reward_weight = 0.2
    ang_vel_penalty_weight = -0.05
    vel_penalty_weight = -2.0
    no_still_penalty_weight = -5.0
    diff_penalty_weight = 0.0
    yaw_penalty_weight = -1.0
```

{% endcode %}

Además de los cambios de las configuraciones que he mencionado anteriormente, he agregado la configuración para la fase 3. También he incluido los parámetros `max_lin_vel` y `max_yaw_vel` para normalizar las penalizaciones. Y, por último, en todas las configuraciones he ajustado los pesos de las recompensas y penalizaciones, así como he incluido el peso para la penalización de `yaw_penalty`

### Cambios en `__init__.py`

{% code title="**init**.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

import gymnasium as gym

from . import agents

##
# Register Gym environments.
##


gym.register(
    id="Template-Simplerobot-Direct-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfg",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)

gym.register(
    id="Template-Simplerobot-Direct-Phase1-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfgPhase1",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)

gym.register(
    id="Template-Simplerobot-Direct-Phase2-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfgPhase2",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)

gym.register(
    id="Template-Simplerobot-Direct-Phase3-v0",
    entry_point=f"{__name__}.simplerobot_env:SimplerobotEnv",
    disable_env_checker=True,
    kwargs={
        "env_cfg_entry_point": f"{__name__}.simplerobot_env_cfg:SimplerobotEnvCfgPhase3",
        "rsl_rl_cfg_entry_point": f"{agents.__name__}.rsl_rl_ppo_cfg:PPORunnerCfg",
    },
)
```

{% endcode %}

He registrado una nueva task llamada `Template-Simplerobot-Direct-Phase3-v0` para lanzar el entrenamiento de la Fase 3 con su configuración.

### Entrenamiento Fase 1: Estabilización

Ya estamos listos para comenzar con el entrenamiento. Al igual que hice anteriormente, voy a entrenar la Fase 1 desde cero. Además, voy a prescindir de la interfaz gráfica para que el entrenamiento sea más rápido utilizando la flag `--headless`:

`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/train.py --task=Template-Simplerobot-Direct-Phase1-v0 --headless`

Utilizando la herramienta Tensorboard, analizo cómo ha sido la evolución de la política durante el entrenamiento (ver Interpretación de resultados del entrenamiento):

**Run**: 2026-01-24\_21-05-26

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2FlrTupBjkhHjmVRvNbfKg%2Fimage.png?alt=media&amp;token=80fd3bb8-6696-43f8-ba6a-47ef4e4c3a37" alt=""><figcaption></figcaption></figure>

En las gráficas se puede ver claramente que tras aproximadamente 200 iteraciones la política parece haber encontrado una solución óptima alcanzando un máximo en la media de recompensas y en la duración del episodio. Por la pinta que tiene la gráfica, todo apunta a que ha encontrado el máximo y no parece que vayamos a conseguir una mejor solución por mucho que aumentemos el número de iteraciones del entrenamiento.

Voy a echar un ojo a la política del checkpoint 200 para ver si está bien:\
`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/play.py --task=Template-Simplerobot-Direct-Phase1-v0 --checkpoint /home/angellm/logs/rsl_rl/simplerobot_direct/2026-01-24_21-05-26/model_200.pt`

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2F5FK3DRVtV5HZp0Ho2V1P%2FFase1_3.gif?alt=media&amp;token=6a56977f-43e6-4cfa-87a5-00984a6b75db" alt=""><figcaption></figcaption></figure>

Visualmente puedo comprobar que el robot ha aprendido a mantener el equilibrio y no se cae. Es cierto que o se mantiene quieto en el sitio, pero dado que no hay ninguna recompensa por hacerlo ni existe tampoco ninguna penalización por moverse, es un comportamiento esperable. El robot ha aprendido a mantener el equilibrio mientras se desplaza despacio en una dirección.

Como el objetivo de esta fase es que el robot aprendiese a estabilizarse, lo damos por bueno y pasamos a la siguiente fase.

### Entrenamiento Fase 2: Movimiento básico

Para este entrenamiento utilizaré los pesos de la política del paso anterior:

`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/train_mod.py --task=Template-Simplerobot-Direct-Phase2-v0 --load_policy /home/angellm/logs/rsl_rl/simplerobot_direct/2026-01-24_21-05-26/model_200.pt --max_iterations 5000 --headless`

En este caso utilicé 5000 iteraciones porque con 1000 no llegué a una solución válida.

**Run**: 2026-01-24\_21-09-10

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2F3XMrxP5NTNJalZbhTCkw%2Fimage.png?alt=media&amp;token=2fe20e57-4fec-47cf-9794-14fc39eb8178" alt=""><figcaption></figcaption></figure>

En la gráfica de **Train/mean\_episode\_length** se puede ver que hacia la iteración 1000 se maximiza la duración de los episodios, pero la gráfica de **Train/mean\_reward** muestra que no es hasta unas iteraciones después (hacia la 1500 aproximadamente) que no se llega a un punto alto de recompensa media.

Como parece que el máximo de recompensa se alcanza sobre la iteración 2000, voy a echarle un ojo al comportamiento de la política en ese checkpoint:

`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/play.py --task=Template-Simplerobot-Direct-Phase2-v0 --checkpoint ~/logs/rsl_rl/simplerobot_direct/2026-01-24_21-09-10/model_2000.pt`

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2FmQIeLvM9SSEIxovZcg5u%2FFase2_3.gif?alt=media&amp;token=03046cd8-8631-49bb-b118-da83a920f092" alt=""><figcaption></figcaption></figure>

Además de comprobar visualmente que el robot se mueve cuando debe y se queda quieto cuando se le pide, he introducido un comando para visualizar el valor del comando enviado al robot 0, así como su valor de velocidad lineal (en las X+ de la base) y su velocidad angular (yaw):

```python
print(f"Cmd: ({self.commands[0,0].item():.2f}, {self.commands[0,1].item():.2f}) | Robot: ({self.robot.data.root_lin_vel_b[0,0].item():.2f}, {self.robot.data.root_ang_vel_b[0,2].item():.2f})")

```

Como el comportamiento es el adecuado, paso a la siguiente fase del entrenamiento:

### Entrenamiento Fase 3: Movimiento avanzado

Al igual que antes, ejecutaré la task correspondiente a esta fase de entrenamiento y cargaré los pesos de la política anterior:

`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/train_mod.py --task=Template-Simplerobot-Direct-Phase3-v0 --load_policy /home/angellm/logs/rsl_rl/simplerobot_direct/2026-01-24_21-09-10/model_2000.pt --max_iterations 10000 --headless`

En este caso, utilicé 10000 iteraciones porque ni con 1000 ni con 5000 logré llegar a una solución adecuada.

**Run**: 2026-01-24\_21-59-22

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2F6tazv80B53OzWth68RNG%2Fimage.png?alt=media&amp;token=3fc54386-137b-4727-a4b3-865716bab838" alt=""><figcaption></figcaption></figure>

En las gráficas podemos ver que cerca de la iteración 5000 se llega al máximo en duración de los episodios, y que aproximadamente a las 6000 iteraciones la política parece converger en una solución y se estabiliza la gráfica de recompensa.

Me llama la atención la pinta que tienen las gráficas, parecen un poco ruidosas. No sé si con un mejor diseño de las recompensas y penalizaciones hubiésemos obtenido un entrenamiento más directo. En cualquier caso, voy a revisar el comportamiento del último checkpoint, que en la gráfica parece ser el que más recompensa media tiene. Como lo que quiero es cargar el último checkpoint disponible del entrenamiento de la fase 3, basta con ejecutar:

`~/IsaacLab/isaaclab.sh -p ~/SimpleRobot/scripts/rsl_rl/play.py --task=Template-Simplerobot-Direct-Phase3-v0`

En el output de la consola saldrá una linea que nos confirmará qué checkpoint se ha cargado:

```
[INFO]: Loading model checkpoint from: /home/angellm/logs/rsl_rl/simplerobot_direct/2026-01-24_21-59-22/model_9999.pt
```

<figure><img src="https://4149706768-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F269LOCCa5jMjvHrpmwVL%2Fuploads%2F0LCZZ2g7pO0JseM5wD2g%2FFase3_3.gif?alt=media&amp;token=f9677bba-d62a-4032-8898-4901a4080656" alt=""><figcaption></figcaption></figure>

En la simulación observo que existen:

* Robots en equilibrio estático
* Robots que se mueven en linea recta sin girar
* Robots que se mueven realizando curvas

Además, el output de la consola me permite comprobar que los robots están cumpliendo con las velocidades lineales y angulares comandadas.

## Evaluación

La política entrenada se comporta como se espera. Se cumplen todos los objetivos marcados para esta fase.

¡Fase 3 completada con éxito!
