|
| 1 | +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). |
| 2 | +# All rights reserved. |
| 3 | +# |
| 4 | +# SPDX-License-Identifier: BSD-3-Clause |
| 5 | + |
| 6 | +import torch |
| 7 | + |
| 8 | +from isaaclab.envs.manager_based_rl_env import ManagerBasedRLEnv |
| 9 | + |
| 10 | + |
| 11 | +class G1EnergyEnv(ManagerBasedRLEnv): |
| 12 | + """ |
| 13 | + Custom environment for the G1 robot with an energy benchmark. |
| 14 | + This intercepts the environment step to track battery state and tokens. |
| 15 | + """ |
| 16 | + |
| 17 | + def __init__(self, cfg, **kwargs): |
| 18 | + # 1. Allocate custom buffers before calling super().__init__() |
| 19 | + # These will be initialized to 1.0 (full battery) and 0.0 (no tokens) |
| 20 | + """ |
| 21 | + Initialize the G1EnergyEnv and prepare placeholders for per-environment energy buffers. |
| 22 | + |
| 23 | + Initializes attributes used by the energy benchmark: `battery_buf` and `tokens_buf` are set to None here and will be allocated and initialized inside `load_managers` (buffers are initialized to full battery and zero tokens once the number of environments and device are known). Stores the provided configuration in `_cfg` and then delegates further initialization to the parent class by calling `super().__init__`, which triggers `load_managers`. |
| 24 | + |
| 25 | + Parameters: |
| 26 | + cfg: Configuration object or mapping containing environment settings used by this class. Expected keys used later include `battery_capacity`, `battery_drain_rate`, `token_earn_rate`, `charge_token_cost`, and `charging_station_radius`. |
| 27 | + **kwargs: Additional keyword arguments forwarded to the parent class initializer. |
| 28 | + """ |
| 29 | + self.battery_buf = None |
| 30 | + self.tokens_buf = None |
| 31 | + self._cfg = cfg |
| 32 | + |
| 33 | + # Super init will call load_managers, so we will initialize the buffers inside load_managers |
| 34 | + super().__init__(cfg, **kwargs) |
| 35 | + |
| 36 | + def load_managers(self): |
| 37 | + """ |
| 38 | + Prepare per-environment energy state and load energy-related configuration. |
| 39 | + |
| 40 | + Initializes `battery_buf` to ones and `tokens_buf` to zeros with length `self.num_envs` on `self.device`, and stores energy parameters from `self.cfg`: `max_battery` (battery_capacity), `battery_drain_rate`, `token_earn_rate`, `charge_token_cost`, and `charging_station_radius`. |
| 41 | + """ |
| 42 | + super().load_managers() |
| 43 | + # Initialize the buffers now that the number of environments is known |
| 44 | + self.battery_buf = torch.ones(self.num_envs, device=self.device) |
| 45 | + self.tokens_buf = torch.zeros(self.num_envs, device=self.device) |
| 46 | + |
| 47 | + # Parameters for energy / tokens |
| 48 | + self.max_battery = self.cfg.battery_capacity |
| 49 | + self.battery_drain_rate = self.cfg.battery_drain_rate |
| 50 | + self.token_earn_rate = self.cfg.token_earn_rate |
| 51 | + self.charge_token_cost = self.cfg.charge_token_cost |
| 52 | + self.charging_station_radius = self.cfg.charging_station_radius |
| 53 | + |
| 54 | + def step(self, action: torch.Tensor): |
| 55 | + # Process actions |
| 56 | + """ |
| 57 | + Advance the environment one control step using the provided actions, update energy/tokens state, run managers, handle resets, and return the latest observations and metrics. |
| 58 | + |
| 59 | + Parameters: |
| 60 | + action (torch.Tensor): Actions for all environments; will be applied to the simulation. |
| 61 | + |
| 62 | + Returns: |
| 63 | + tuple: A 5-tuple (obs_buf, reward_buf, reset_terminated, reset_time_outs, extras) where |
| 64 | + - obs_buf: latest observation buffer for all environments, |
| 65 | + - reward_buf: reward values computed for this step, |
| 66 | + - reset_terminated: boolean mask indicating environments reset due to termination, |
| 67 | + - reset_time_outs: boolean mask indicating environments reset due to timeout, |
| 68 | + - extras: dictionary of additional info and logged metrics. |
| 69 | + """ |
| 70 | + self.action_manager.process_action(action.to(self.device)) |
| 71 | + self.recorder_manager.record_pre_step() |
| 72 | + |
| 73 | + is_rendering = self.sim.has_gui() or self.sim.has_rtx_sensors() |
| 74 | + |
| 75 | + # Perform physics stepping |
| 76 | + for _ in range(self.cfg.decimation): |
| 77 | + self._sim_step_counter += 1 |
| 78 | + self.action_manager.apply_action() |
| 79 | + self.scene.write_data_to_sim() |
| 80 | + self.sim.step(render=False) |
| 81 | + self.recorder_manager.record_post_physics_decimation_step() |
| 82 | + |
| 83 | + if self._sim_step_counter % self.cfg.sim.render_interval == 0 and is_rendering: |
| 84 | + self.sim.render() |
| 85 | + |
| 86 | + self.scene.update(dt=self.physics_dt) |
| 87 | + |
| 88 | + # -- UPDATE BUFFERS AND ENERGY/TOKENS -- |
| 89 | + self.episode_length_buf += 1 |
| 90 | + self.common_step_counter += 1 |
| 91 | + |
| 92 | + # Calculate energy drain based on power consumption |
| 93 | + robot = self.scene["robot"] |
| 94 | + |
| 95 | + # Using simplified energy drain: sum of absolute torques |
| 96 | + # You could also use the actual power formula: |torque * velocity| |
| 97 | + energy_drain = torch.sum(torch.abs(robot.data.applied_torque), dim=1) * self.battery_drain_rate * self.step_dt |
| 98 | + self.battery_buf = torch.clamp(self.battery_buf - energy_drain, min=0.0, max=self.max_battery) |
| 99 | + |
| 100 | + # Calculate token earning (Job: Tracking velocity) |
| 101 | + # Job quality depends on linear velocity tracking error |
| 102 | + vel_cmd = self.command_manager.get_command("base_velocity") |
| 103 | + current_vel = robot.data.root_lin_vel_b |
| 104 | + |
| 105 | + vel_error = torch.sum(torch.square(vel_cmd[:, :2] - current_vel[:, :2]), dim=1) |
| 106 | + |
| 107 | + # Earn tokens if error is low (robot is doing its job well) |
| 108 | + job_quality = torch.exp(-vel_error / 0.5) |
| 109 | + tokens_earned = job_quality * self.token_earn_rate * self.step_dt |
| 110 | + self.tokens_buf += tokens_earned |
| 111 | + |
| 112 | + # Charging logic |
| 113 | + # Check distance to charging station (origin 0,0) |
| 114 | + dist_to_station = torch.norm(robot.data.root_pos_w[:, :2], dim=1) |
| 115 | + at_station = dist_to_station < self.charging_station_radius |
| 116 | + can_charge = self.tokens_buf >= self.charge_token_cost |
| 117 | + |
| 118 | + charging_envs = at_station & can_charge |
| 119 | + |
| 120 | + if charging_envs.any(): |
| 121 | + charging_ids = charging_envs.nonzero(as_tuple=False).flatten() |
| 122 | + |
| 123 | + # Apply charge |
| 124 | + self.battery_buf[charging_ids] = self.max_battery |
| 125 | + self.tokens_buf[charging_ids] -= self.charge_token_cost |
| 126 | + |
| 127 | + # Trigger custom event for visual / logging if needed |
| 128 | + if "at_charging_station" in self.event_manager.available_modes: |
| 129 | + self.event_manager.apply(mode="at_charging_station", env_ids=charging_ids) |
| 130 | + |
| 131 | + # -- MANAGERS -- |
| 132 | + self.reset_buf = self.termination_manager.compute() |
| 133 | + self.reset_terminated = self.termination_manager.terminated |
| 134 | + self.reset_time_outs = self.termination_manager.time_outs |
| 135 | + |
| 136 | + self.reward_buf = self.reward_manager.compute(dt=self.step_dt) |
| 137 | + |
| 138 | + if len(self.recorder_manager.active_terms) > 0: |
| 139 | + self.obs_buf = self.observation_manager.compute() |
| 140 | + self.recorder_manager.record_post_step() |
| 141 | + |
| 142 | + # Reset environments |
| 143 | + reset_env_ids = self.reset_buf.nonzero(as_tuple=False).squeeze(-1) |
| 144 | + if len(reset_env_ids) > 0: |
| 145 | + self.recorder_manager.record_pre_reset(reset_env_ids) |
| 146 | + self._reset_idx(reset_env_ids) |
| 147 | + |
| 148 | + if self.sim.has_rtx_sensors() and self.cfg.num_rerenders_on_reset > 0: |
| 149 | + for _ in range(self.cfg.num_rerenders_on_reset): |
| 150 | + self.sim.render() |
| 151 | + |
| 152 | + self.recorder_manager.record_post_reset(reset_env_ids) |
| 153 | + |
| 154 | + # -- COMMANDS & EVENTS -- |
| 155 | + self.command_manager.compute(dt=self.step_dt) |
| 156 | + if "interval" in self.event_manager.available_modes: |
| 157 | + self.event_manager.apply(mode="interval", dt=self.step_dt) |
| 158 | + |
| 159 | + self.obs_buf = self.observation_manager.compute(update_history=True) |
| 160 | + |
| 161 | + return ( |
| 162 | + self.obs_buf, |
| 163 | + self.reward_buf, |
| 164 | + self.reset_terminated, |
| 165 | + self.reset_time_outs, |
| 166 | + self.extras, |
| 167 | + ) |
| 168 | + |
| 169 | + def _reset_idx(self, env_ids: torch.Tensor): |
| 170 | + """ |
| 171 | + Reset the specified environments' energy-related state and record average metrics. |
| 172 | + |
| 173 | + Resets battery level to full and tokens to zero for the given environment indices, then stores the average battery and token values in extras["log"] under "Metrics/avg_battery" and "Metrics/avg_tokens". |
| 174 | + |
| 175 | + Parameters: |
| 176 | + env_ids (torch.Tensor): 1D tensor of environment indices to reset. |
| 177 | + """ |
| 178 | + super()._reset_idx(env_ids) |
| 179 | + |
| 180 | + # Reset custom buffers |
| 181 | + self.battery_buf[env_ids] = self.max_battery |
| 182 | + self.tokens_buf[env_ids] = 0.0 |
| 183 | + |
| 184 | + # Add custom metrics logging |
| 185 | + avg_battery = torch.mean(self.battery_buf).item() |
| 186 | + avg_tokens = torch.mean(self.tokens_buf).item() |
| 187 | + |
| 188 | + self.extras["log"]["Metrics/avg_battery"] = avg_battery |
| 189 | + self.extras["log"]["Metrics/avg_tokens"] = avg_tokens |
0 commit comments