Source code for smartcer.gym.controllers.replay
"""
Replay controller: plays the optimal actions of the MILP oracle.
"""
from typing import Any
import gymnasium as gym
import numpy as np
from smartcer.gym.controllers.base import Controller
# largest violation of a dynamic bound, as energy on the node side (kWh), that the replay clips
# into the bound without penalty: the round-off of a MILP solver
REPLAY_TOLERANCE_KWH = 1e-6
[docs]
class ReplayController(Controller):
"""
Plays the optimal actions a* of the environment's oracle (`env.oracle`).
An oracle action beyond a dynamic bound of the environment by at most 1e-6 kWh
(`REPLAY_TOLERANCE_KWH`, the energy (a* - A^upper) P^cha ΔT or (-a* - A^lower) P^dis ΔT on the
node side) is moved onto the bound, so that the environment does not penalise it, and counted.
Larger violations are played unchanged: the environment clips and penalises them, which is
what the replay check (`check_replay`) detects. The actions are float64.
Attributes:
clipped_rows: Actions (step and battery) of the episode moved onto a bound.
clipped_max_kwh: Largest violation moved onto a bound in the episode (kWh).
"""
name = 'replay'
def __init__(self, env: gym.Env) -> None:
"""
Bind the controller to an environment with an oracle.
Args:
env: A `SmartCerEnv` created with `oracle`, possibly wrapped.
Raises:
TypeError: If the unwrapped environment is not a `SmartCerEnv`.
ValueError: If the environment has no oracle.
"""
super().__init__(env)
if self.env.oracle is None:
raise ValueError('ReplayController needs an environment created with an oracle')
community = self.env.community
self._nodes = [community.node(i) for i in self.env.storage_node_ids]
self.clipped_rows = 0
self.clipped_max_kwh = 0.0
[docs]
def reset(self) -> None:
"""
Start a new episode: the counts of clipped actions restart from 0.
"""
self.clipped_rows = 0
self.clipped_max_kwh = 0.0
[docs]
def act(self, observation: np.ndarray, info: dict[str, Any]) -> np.ndarray:
"""
Oracle actions of the step `env.current_step`, with violations of the dynamic bounds up to
`REPLAY_TOLERANCE_KWH` moved onto the bounds.
Args:
observation: Latest observation (not used).
info: Latest info (not used).
Returns:
np.ndarray: a* of the step, float64, in `storage_node_ids` order.
"""
t = self.env.current_step
actions = self.env.oracle.action(t)
for u, node in enumerate(self._nodes):
a_upper, a_lower = node.get_bounds(t)
battery, action = node.battery, actions[u]
if action > a_upper:
excess, bound = (action - a_upper) * battery.power_charge_max, a_upper
elif action < -a_lower:
excess, bound = (-a_lower - action) * battery.power_discharge_max, -a_lower
else:
continue
excess *= node.delta_t
if excess <= REPLAY_TOLERANCE_KWH:
actions[u] = bound
self.clipped_rows += 1
self.clipped_max_kwh = max(self.clipped_max_kwh, excess)
return actions