| import numpy as np |
| import sys |
| import os |
|
|
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) |
|
|
| from twin.market import MarketTwin |
| from twin.load import LoadTwin |
| from twin.solar import SolarTwin |
| from twin.grid import GridTwin |
| from twin.battery import BatteryTwin |
|
|
|
|
|
|
| class EnergyEnv: |
| """ |
| Reinforcement Learning Environment for: |
| Market Arbitrage + Grid Stability + Battery Optimization |
| |
| Core Design: |
| - RL handles arbitrage decisions |
| - Grid intelligence handles ancillary + safety |
| - Battery executes constrained actions |
| """ |
|
|
| def __init__( |
| self, |
| price_data_path, |
| episode_length=96 |
| ): |
|
|
| |
| self.market = MarketTwin(price_data_path) |
| self.load = LoadTwin() |
| self.solar = SolarTwin() |
| self.grid = GridTwin() |
| self.battery = BatteryTwin() |
|
|
| self.episode_length = episode_length |
| self.timestep = 0 |
|
|
| self.total_profit = 0.0 |
|
|
| |
|
|
| def reset(self): |
| """Reset environment for new RL episode""" |
|
|
| self.market.reset() |
| self.load.reset() |
| self.solar.reset() |
| self.grid.reset() |
| self.battery.reset() |
|
|
| self.timestep = 0 |
| self.total_profit = 0.0 |
|
|
| return self._get_state() |
|
|
| |
|
|
| def step(self, action): |
| """ |
| Execute one environment step. |
| |
| Parameters |
| ---------- |
| action : float |
| RL agent output ∈ [-1,1] |
| -1 → discharge |
| 0 → idle |
| +1 → charge |
| |
| Returns |
| ------- |
| tuple |
| next_state, reward, done, info |
| """ |
|
|
| hour = (self.timestep // 4) % 24 |
|
|
| |
| price = self.market.step() |
| market_features = self.market.get_price_state() |
|
|
| |
| load = self.load.step(hour) |
| solar = self.solar.step(hour) |
|
|
| |
| grid_info = self.grid.grid_state( |
| price=price, |
| volatility=market_features["volatility"], |
| load=load, |
| solar=solar |
| ) |
|
|
| reserve_ratio = grid_info["reserve_ratio"] |
|
|
| |
| soc, power, deg_cost = self.battery.step( |
| action=action, |
| reserve_ratio=reserve_ratio |
| ) |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| arbitrage_profit = -power * price * 0.25 |
|
|
| ancillary_reward = reserve_ratio * self.battery.max_power * 200 * 0.25 |
|
|
| self.total_profit += arbitrage_profit + ancillary_reward |
|
|
| |
| grid_risk_penalty = 50 * grid_info["failure_probability"] |
|
|
| |
| reward = ( |
| arbitrage_profit + |
| ancillary_reward - |
| grid_risk_penalty - |
| deg_cost |
| ) |
|
|
| |
| self.timestep += 1 |
| done = self.timestep >= self.episode_length |
|
|
| next_state = self._get_state() |
|
|
| info = { |
| "price": price, |
| "load": load, |
| "solar": solar, |
| "soc": soc, |
| "grid_stress": grid_info["grid_stress"], |
| "predicted_stress": grid_info["predicted_stress"], |
| "reserve_ratio": reserve_ratio, |
| "failure_probability": grid_info["failure_probability"], |
| "arbitrage_profit": arbitrage_profit, |
| "ancillary_reward": ancillary_reward, |
| "total_profit": self.total_profit, |
| "power": power |
| } |
|
|
| return next_state, reward, done, info |
|
|
| |
|
|
| def _get_state(self): |
| """ |
| Construct RL state vector. |
| |
| Returns |
| ------- |
| numpy.ndarray |
| State vector for RL agent |
| """ |
|
|
| market_state = self.market.get_price_state() |
| battery_state = self.battery.state() |
|
|
| state = np.array([ |
| market_state["price"] / 6000, |
| market_state["volatility"], |
| market_state["trend"], |
| battery_state["soc_fraction"], |
| self.timestep / self.episode_length |
| ], dtype=np.float32) |
|
|
| return state |
|
|