|
|
import streamlit as st |
|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
import pandas as pd |
|
|
import math |
|
|
if 'env' not in st.session_state: |
|
|
st.session_state.env = None |
|
|
if 'config' not in st.session_state: |
|
|
st.session_state.config = None |
|
|
if 'history' not in st.session_state: |
|
|
st.session_state.history = [] |
|
|
if 'total_reward' not in st.session_state: |
|
|
st.session_state.total_reward = 0.0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DynamicEnergyGridEnvV7: |
|
|
"""Dynamic Energy Grid Environment v7""" |
|
|
|
|
|
def __init__(self, config): |
|
|
self.cfg = config |
|
|
self.horizon = config["horizon"] |
|
|
self.world = config["world"] |
|
|
self.demand_series = config["demand"] |
|
|
self.budget_series = config["budget"] |
|
|
self.capacity = config["capacity"] |
|
|
self.initial_rated_cfg = config["initial_rated"] |
|
|
self.initial_stability = config["initial_stability"] |
|
|
self.prices = config["prices"] |
|
|
self.penalty = config["penalty"] |
|
|
self.reset() |
|
|
|
|
|
def reset(self): |
|
|
self.t = 0 |
|
|
self.thermal_rated = self.initial_rated_cfg["thermal"] |
|
|
self.wind_rated = self.initial_rated_cfg["wind"] |
|
|
self.solar_rated = self.initial_rated_cfg["solar"] |
|
|
self.battery_rated = self.initial_rated_cfg["battery"] |
|
|
self.prev_rated = dict(self.initial_rated_cfg) |
|
|
self.stability = self.initial_stability |
|
|
self.thermal_actual = 0 |
|
|
self.wind_actual = 0 |
|
|
self.solar_actual = 0 |
|
|
self.battery_actual = 0 |
|
|
self.cum_unmet = 0 |
|
|
self.cum_carbon = 0 |
|
|
self.cum_budget_violation = 0 |
|
|
self.cum_ramp = 0 |
|
|
self.done = False |
|
|
return self._get_obs() |
|
|
|
|
|
def step(self, action): |
|
|
if self.done: |
|
|
raise RuntimeError("Episode finished. Call reset() first.") |
|
|
|
|
|
t = self.t |
|
|
|
|
|
|
|
|
self.thermal_rated = self._clamp(action.get("thermal", 0.0), 0, self.capacity["thermal"]) |
|
|
self.wind_rated = self._clamp(action.get("wind", 0.0), 0, self.capacity["wind"]) |
|
|
self.solar_rated = self._clamp(action.get("solar", 0.0), 0, self.capacity["solar"]) |
|
|
self.battery_rated = self._clamp(action.get("battery", 0.0), 0, self.capacity["battery"]) |
|
|
|
|
|
|
|
|
eff_th = self.world["eff_thermal"][t] |
|
|
eff_w = self.world["eff_wind"][t] |
|
|
eff_s = self.world["eff_solar"][t] |
|
|
|
|
|
|
|
|
self.thermal_actual = self.thermal_rated * eff_th |
|
|
self.wind_actual = self.wind_rated * eff_w |
|
|
self.solar_actual = self.solar_rated * eff_s |
|
|
self.battery_actual = self.battery_rated * 1.0 |
|
|
|
|
|
supply = (self.thermal_actual + self.wind_actual + |
|
|
self.solar_actual + self.battery_actual) |
|
|
|
|
|
demand = self.demand_series[t] |
|
|
|
|
|
if demand > 1e-6: |
|
|
unmet = max(0, 1 - supply / demand) |
|
|
else: |
|
|
unmet = 0 |
|
|
|
|
|
self.cum_unmet += unmet |
|
|
|
|
|
|
|
|
cost_today = (self.thermal_rated * self.prices["thermal"] + |
|
|
self.wind_rated * self.prices["wind"] + |
|
|
self.solar_rated * self.prices["solar"] + |
|
|
self.battery_rated * self.prices["battery"]) |
|
|
|
|
|
budget_today = self.budget_series[t] |
|
|
budget_violation = max(0, (cost_today - budget_today) / budget_today) |
|
|
self.cum_budget_violation += budget_violation |
|
|
|
|
|
|
|
|
ramp = (abs(self.thermal_rated - self.prev_rated["thermal"]) + |
|
|
abs(self.wind_rated - self.prev_rated["wind"]) + |
|
|
abs(self.solar_rated - self.prev_rated["solar"]) + |
|
|
abs(self.battery_rated - self.prev_rated["battery"])) |
|
|
self.cum_ramp += ramp |
|
|
self.prev_rated = { |
|
|
"thermal": self.thermal_rated, |
|
|
"wind": self.wind_rated, |
|
|
"solar": self.solar_rated, |
|
|
"battery": self.battery_rated, |
|
|
} |
|
|
|
|
|
|
|
|
if supply > 1e-6: |
|
|
share_thermal = self.thermal_actual / supply |
|
|
else: |
|
|
share_thermal = 0 |
|
|
self.cum_carbon += share_thermal |
|
|
|
|
|
|
|
|
max_ramp = sum(self.capacity.values()) |
|
|
normalized_ramp = min(1.0, ramp / max_ramp) |
|
|
|
|
|
a = 0.7 |
|
|
b = 0.3 |
|
|
|
|
|
stability = 1 - a*unmet - b*normalized_ramp |
|
|
self.stability = self._clamp(stability, 0, 1) |
|
|
|
|
|
|
|
|
self.t += 1 |
|
|
done = (self.t >= self.horizon) |
|
|
self.done = done |
|
|
|
|
|
|
|
|
if done: |
|
|
avg_unmet = self.cum_unmet / self.horizon |
|
|
avg_carbon = self.cum_carbon / self.horizon |
|
|
avg_budget_vio = self.cum_budget_violation / self.horizon |
|
|
|
|
|
reward = (-self.penalty["unmet"] * avg_unmet - |
|
|
self.penalty["carbon"] * avg_carbon - |
|
|
self.penalty["budget"] * avg_budget_vio - |
|
|
self.penalty["ramp"] * self.cum_ramp + |
|
|
self.penalty["stability"] * self.stability) |
|
|
else: |
|
|
reward = 0 |
|
|
|
|
|
|
|
|
obs = self._get_obs() |
|
|
|
|
|
info = { |
|
|
"cost_today": cost_today, |
|
|
"budget_today": budget_today, |
|
|
"cost_vs_budget": cost_today / budget_today if budget_today > 1e-6 else 0, |
|
|
"budget_violation": budget_violation, |
|
|
"unmet": unmet, |
|
|
"share_thermal": share_thermal, |
|
|
"nl_forecast": obs["nl_forecast"], |
|
|
"is_terminal_reward": done, |
|
|
} |
|
|
|
|
|
return obs, reward, done, info |
|
|
|
|
|
def _clamp(self, x, lo, hi): |
|
|
return max(lo, min(hi, x)) |
|
|
|
|
|
def _trend_sentence(self, today, tomorrow, typ): |
|
|
delta = tomorrow - today |
|
|
x = abs(delta) |
|
|
if x < 0.03: |
|
|
phrase = "remain roughly stable" |
|
|
elif x < 0.08: |
|
|
phrase = "slightly increase" if delta > 0 else "slightly decrease" |
|
|
elif x < 0.15: |
|
|
phrase = "moderately increase" if delta > 0 else "moderately decrease" |
|
|
else: |
|
|
phrase = "sharply increase" if delta > 0 else "sharply decrease" |
|
|
|
|
|
if typ == "wind": |
|
|
return f"Tomorrow wind potential is expected to {phrase}." |
|
|
else: |
|
|
return f"Tomorrow solar potential is expected to {phrase}." |
|
|
|
|
|
def _get_obs(self): |
|
|
t = self.t |
|
|
h = self.horizon |
|
|
|
|
|
t_today = max(0, t-1) |
|
|
t_next = min(h-1, t) |
|
|
|
|
|
demand_today = self.demand_series[t_today] |
|
|
demand_next = self.demand_series[t_next] |
|
|
|
|
|
budget_today = self.budget_series[t_today] |
|
|
budget_next = self.budget_series[t_next] |
|
|
|
|
|
w_today = self.world["weather_wind_raw"][t_today] |
|
|
s_today = self.world["weather_solar_raw"][t_today] |
|
|
w_next = self.world["weather_wind_raw"][t_next] |
|
|
s_next = self.world["weather_solar_raw"][t_next] |
|
|
|
|
|
nl_forecast = (self._trend_sentence(w_today, w_next, "wind") + " " + |
|
|
self._trend_sentence(s_today, s_next, "solar")) |
|
|
|
|
|
supply = (self.thermal_actual + self.wind_actual + |
|
|
self.solar_actual + self.battery_actual) |
|
|
|
|
|
obs = { |
|
|
"day": t, |
|
|
"demand_today": demand_today, |
|
|
"demand_next": demand_next, |
|
|
"budget_today": budget_today, |
|
|
"budget_next": budget_next, |
|
|
"rated": { |
|
|
"thermal": self.thermal_rated, |
|
|
"wind": self.wind_rated, |
|
|
"solar": self.solar_rated, |
|
|
"battery": self.battery_rated, |
|
|
}, |
|
|
"actual": { |
|
|
"thermal": self.thermal_actual, |
|
|
"wind": self.wind_actual, |
|
|
"solar": self.solar_actual, |
|
|
"battery": self.battery_actual, |
|
|
"supply": supply, |
|
|
"demand_met": self._clamp(supply / max(1e-6, demand_today), 0, 1) |
|
|
}, |
|
|
"efficiency": { |
|
|
"thermal": self.world["eff_thermal"][t_today], |
|
|
"wind": self.world["eff_wind"][t_today], |
|
|
"solar": self.world["eff_solar"][t_today], |
|
|
}, |
|
|
"stability": self.stability, |
|
|
"nl_forecast": nl_forecast, |
|
|
} |
|
|
|
|
|
return obs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_world_profile_v11(days=120, seed=0): |
|
|
rng = np.random.default_rng(seed) |
|
|
|
|
|
|
|
|
phase_wind = rng.uniform(0, 2 * math.pi) |
|
|
delta_phase_solar = rng.uniform(-0.3 * math.pi, 0.3 * math.pi) |
|
|
phase_solar = phase_wind + math.pi + delta_phase_solar |
|
|
|
|
|
amp_wind = rng.uniform(0.15, 0.35) |
|
|
amp_solar = rng.uniform(0.15, 0.35) |
|
|
|
|
|
center_wind = 0.75 |
|
|
center_solar = 0.75 |
|
|
|
|
|
|
|
|
amp_thermal = 0.03 |
|
|
center_thermal = 0.95 |
|
|
|
|
|
|
|
|
season_wind = np.zeros(days) |
|
|
season_solar = np.zeros(days) |
|
|
season_thermal = np.zeros(days) |
|
|
|
|
|
weather_wind_raw = np.zeros(days) |
|
|
weather_solar_raw = np.zeros(days) |
|
|
weather_thermal_raw = np.zeros(days) |
|
|
|
|
|
eff_wind = np.zeros(days) |
|
|
eff_solar = np.zeros(days) |
|
|
eff_thermal = np.zeros(days) |
|
|
|
|
|
|
|
|
n_storm_events = 3 |
|
|
n_cloudy_events = 3 |
|
|
|
|
|
all_days = np.arange(days) |
|
|
storm_starts = rng.choice(all_days, size=n_storm_events, replace=False) |
|
|
cloudy_starts = rng.choice(all_days, size=n_cloudy_events, replace=False) |
|
|
|
|
|
storm_days, cloudy_days = set(), set() |
|
|
for d in storm_starts: |
|
|
for k in range(rng.integers(2, 4)): |
|
|
if 0 <= d + k < days: |
|
|
storm_days.add(d + k) |
|
|
for d in cloudy_starts: |
|
|
for k in range(rng.integers(2, 4)): |
|
|
if 0 <= d + k < days: |
|
|
cloudy_days.add(d + k) |
|
|
|
|
|
|
|
|
trend = 0.0 |
|
|
trend_decay = 0.85 |
|
|
noise_scale = 0.12 |
|
|
thermal_noise_scale = 0.02 |
|
|
|
|
|
for t in range(days): |
|
|
|
|
|
season_wind[t] = center_wind + amp_wind * math.sin(2 * math.pi * (t % 30) / 30 + phase_wind) |
|
|
season_solar[t] = center_solar + amp_solar * math.sin(2 * math.pi * (t % 30) / 30 + phase_solar) |
|
|
season_thermal[t] = center_thermal + amp_thermal * math.sin(2 * math.pi * (t % 30) / 30) |
|
|
|
|
|
|
|
|
noise = rng.normal(0, noise_scale) |
|
|
trend = trend_decay * trend + (1 - trend_decay) * noise |
|
|
weather_factor = 1.0 + trend |
|
|
|
|
|
weather_wind_raw[t] = weather_factor |
|
|
weather_solar_raw[t] = weather_factor |
|
|
weather_thermal_raw[t] = 1.0 + rng.normal(0, thermal_noise_scale) |
|
|
|
|
|
|
|
|
ew = season_wind[t] * weather_factor * (1 + rng.normal(0, 0.03)) |
|
|
es = season_solar[t] * weather_factor * (1 + rng.normal(0, 0.03)) |
|
|
et = season_thermal[t] * weather_thermal_raw[t] |
|
|
|
|
|
|
|
|
if t in storm_days: |
|
|
ew *= 1.15 |
|
|
es *= 0.60 |
|
|
if t in cloudy_days: |
|
|
ew *= 1.05 |
|
|
es *= 0.50 |
|
|
|
|
|
eff_wind[t] = np.clip(ew, 0.1, 1.2) |
|
|
eff_solar[t] = np.clip(es, 0.1, 1.2) |
|
|
eff_thermal[t] = np.clip(et, 0.85, 1.05) |
|
|
|
|
|
return { |
|
|
"days": days, |
|
|
"eff_wind": eff_wind.tolist(), |
|
|
"eff_solar": eff_solar.tolist(), |
|
|
"eff_thermal": eff_thermal.tolist(), |
|
|
"season_wind": season_wind.tolist(), |
|
|
"season_solar": season_solar.tolist(), |
|
|
"season_thermal": season_thermal.tolist(), |
|
|
"weather_wind_raw": weather_wind_raw.tolist(), |
|
|
"weather_solar_raw": weather_solar_raw.tolist(), |
|
|
"weather_thermal_raw": weather_thermal_raw.tolist(), |
|
|
"storm_days": sorted(list(storm_days)), |
|
|
"cloudy_days": sorted(list(cloudy_days)), |
|
|
"phase_wind": phase_wind, |
|
|
"phase_solar": phase_solar, |
|
|
"amp_wind": amp_wind, |
|
|
"amp_solar": amp_solar, |
|
|
"seed": seed, |
|
|
} |
|
|
|
|
|
|
|
|
def generate_demand_v11(days=120, seed=0): |
|
|
rng = np.random.default_rng(seed) |
|
|
|
|
|
base = rng.uniform(320, 480) |
|
|
amp = rng.uniform(0.25, 0.35) |
|
|
noise = 0.04 |
|
|
|
|
|
phase_demand = rng.uniform(0, 2 * math.pi) |
|
|
demand = np.zeros(days) |
|
|
|
|
|
for t in range(days): |
|
|
season = math.sin(2 * math.pi * (t % 30) / 30 + phase_demand) |
|
|
demand[t] = base * (1 + amp * season) * (1 + rng.normal(0, noise)) |
|
|
|
|
|
return demand.tolist() |
|
|
|
|
|
|
|
|
def generate_budget_v11(demand, multiplier=4.2): |
|
|
return [multiplier * d for d in demand] |
|
|
|
|
|
|
|
|
def generate_initial_rated_v11(capacity, demand_day1, rng): |
|
|
""" |
|
|
1) 随机比例 → 保留 diversity |
|
|
2) normalize → 与 day1 demand 匹配(±5%) |
|
|
3) clip → 不超过 capacity |
|
|
""" |
|
|
|
|
|
p_th = rng.uniform(0.55, 0.75) |
|
|
p_w = rng.uniform(0.20, 0.40) |
|
|
p_s = rng.uniform(0.15, 0.35) |
|
|
p_b = rng.uniform(0.10, 0.30) |
|
|
|
|
|
raw = np.array([p_th, p_w, p_s, p_b]) |
|
|
raw = raw / raw.sum() |
|
|
|
|
|
|
|
|
target_total = demand_day1 * rng.uniform(0.95, 1.05) |
|
|
|
|
|
|
|
|
thermal_r0 = min(raw[0] * target_total, capacity["thermal"]) |
|
|
wind_r0 = min(raw[1] * target_total, capacity["wind"]) |
|
|
solar_r0 = min(raw[2] * target_total, capacity["solar"]) |
|
|
battery_r0 = min(raw[3] * target_total, capacity["battery"]) |
|
|
|
|
|
return { |
|
|
"thermal": thermal_r0, |
|
|
"wind": wind_r0, |
|
|
"solar": solar_r0, |
|
|
"battery": battery_r0, |
|
|
} |
|
|
|
|
|
|
|
|
def generate_energy_grid_config_v11(days=120, seed=0): |
|
|
rng = np.random.default_rng(seed) |
|
|
|
|
|
world = generate_world_profile_v11(days, seed) |
|
|
demand = generate_demand_v11(days, seed) |
|
|
budget = generate_budget_v11(demand, multiplier=4.2) |
|
|
|
|
|
capacity = { |
|
|
"thermal": 600.0, |
|
|
"wind": 350.0, |
|
|
"solar": 250.0, |
|
|
"battery": 120.0, |
|
|
} |
|
|
|
|
|
initial_rated = generate_initial_rated_v11(capacity, demand_day1=demand[0], rng=rng) |
|
|
|
|
|
prices = { |
|
|
"thermal": 3.0, |
|
|
"wind": 5.0, |
|
|
"solar": 6.0, |
|
|
"battery": 8.0, |
|
|
} |
|
|
|
|
|
penalty = { |
|
|
"unmet": 3.0, |
|
|
"carbon": 1.0, |
|
|
"budget": 2.0, |
|
|
"ramp": 0.0005, |
|
|
"stability": 1.0, |
|
|
} |
|
|
|
|
|
config = { |
|
|
"horizon": days, |
|
|
"world": world, |
|
|
"demand": demand, |
|
|
"budget": budget, |
|
|
"capacity": capacity, |
|
|
"initial_rated": initial_rated, |
|
|
"initial_stability": 1.0, |
|
|
"prices": prices, |
|
|
"penalty": penalty, |
|
|
"seed": seed, |
|
|
} |
|
|
|
|
|
return config |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if 'env' not in st.session_state: |
|
|
st.session_state.env = None |
|
|
if 'config' not in st.session_state: |
|
|
st.session_state.config = None |
|
|
if 'history' not in st.session_state: |
|
|
st.session_state.history = [] |
|
|
if 'total_reward' not in st.session_state: |
|
|
st.session_state.total_reward = 0.0 |
|
|
|
|
|
|
|
|
def initialize_env(days, seed): |
|
|
"""初始化环境""" |
|
|
config = generate_energy_grid_config_v11(days=days, seed=seed) |
|
|
env = DynamicEnergyGridEnvV7(config) |
|
|
|
|
|
st.session_state.env = env |
|
|
st.session_state.config = config |
|
|
st.session_state.history = [] |
|
|
st.session_state.total_reward = 0.0 |
|
|
|
|
|
|
|
|
def step_simulation(thermal, wind, solar, battery): |
|
|
"""执行一步仿真""" |
|
|
env = st.session_state.env |
|
|
|
|
|
if env is None: |
|
|
st.error("❌ Please initialize environment first!") |
|
|
return |
|
|
|
|
|
if env.done: |
|
|
st.warning("⚠️ Simulation finished! Please reset to start again.") |
|
|
return |
|
|
|
|
|
action = { |
|
|
"thermal": thermal, |
|
|
"wind": wind, |
|
|
"solar": solar, |
|
|
"battery": battery, |
|
|
} |
|
|
|
|
|
obs, reward, done, info = env.step(action) |
|
|
|
|
|
|
|
|
st.session_state.history.append({ |
|
|
"day": obs["day"] - 1, |
|
|
"demand": obs["demand_today"], |
|
|
"supply": obs["actual"]["supply"], |
|
|
"stability": obs["stability"], |
|
|
"carbon": info["share_thermal"], |
|
|
"cost": info["cost_today"], |
|
|
"budget": info["budget_today"], |
|
|
"thermal": obs["actual"]["thermal"], |
|
|
"wind": obs["actual"]["wind"], |
|
|
"solar": obs["actual"]["solar"], |
|
|
"battery": obs["actual"]["battery"], |
|
|
"demand_met": obs["actual"]["demand_met"], |
|
|
"budget_violation": info["budget_violation"], |
|
|
"cost_vs_budget": info["cost_vs_budget"], |
|
|
}) |
|
|
|
|
|
if done: |
|
|
st.session_state.total_reward = reward |
|
|
st.success(f"🎉 Simulation Complete! Final Reward: {reward:.2f}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.title("⚡ Dynamic Energy Grid Simulator V11") |
|
|
st.markdown(""" |
|
|
Manage a multi-source power grid by adjusting rated power for thermal, wind, solar, and battery generation. |
|
|
**V11 Features:** 30-day seasonal cycles with diversity, normalized initial ratings, and improved weather patterns. |
|
|
""") |
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
st.header("🎛️ Environment Setup") |
|
|
|
|
|
days_input = st.slider("Simulation Days", 10, 120, 30) |
|
|
seed_input = st.number_input("Random Seed", value=42, step=1) |
|
|
|
|
|
if st.button("🔄 Initialize Environment", type="primary", use_container_width=True): |
|
|
initialize_env(days_input, seed_input) |
|
|
st.success("✅ Environment initialized!") |
|
|
st.rerun() |
|
|
|
|
|
st.divider() |
|
|
|
|
|
st.header("⚙️ Power Control (Rated MW)") |
|
|
|
|
|
if st.session_state.env is not None: |
|
|
obs = st.session_state.env._get_obs() |
|
|
|
|
|
thermal_slider = st.slider( |
|
|
"🔥 Thermal Power", |
|
|
0.0, 600.0, |
|
|
float(obs["rated"]["thermal"]), |
|
|
10.0 |
|
|
) |
|
|
wind_slider = st.slider( |
|
|
"💨 Wind Power", |
|
|
0.0, 350.0, |
|
|
float(obs["rated"]["wind"]), |
|
|
10.0 |
|
|
) |
|
|
solar_slider = st.slider( |
|
|
"☀️ Solar Power", |
|
|
0.0, 250.0, |
|
|
float(obs["rated"]["solar"]), |
|
|
10.0 |
|
|
) |
|
|
battery_slider = st.slider( |
|
|
"🔋 Battery Power", |
|
|
0.0, 120.0, |
|
|
float(obs["rated"]["battery"]), |
|
|
5.0 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.divider() |
|
|
st.subheader("🔮 Real-time Projections") |
|
|
|
|
|
|
|
|
prices = st.session_state.config["prices"] |
|
|
eff = obs["efficiency"] |
|
|
demand_today = obs["demand_today"] |
|
|
budget_today = obs["budget_today"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
est_supply = ( |
|
|
thermal_slider * eff["thermal"] + |
|
|
wind_slider * eff["wind"] + |
|
|
solar_slider * eff["solar"] + |
|
|
battery_slider * 1.0 |
|
|
) |
|
|
|
|
|
|
|
|
est_cost = ( |
|
|
thermal_slider * prices["thermal"] + |
|
|
wind_slider * prices["wind"] + |
|
|
solar_slider * prices["solar"] + |
|
|
battery_slider * prices["battery"] |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
p_col1, p_col2 = st.columns(2) |
|
|
|
|
|
p_col1.metric( |
|
|
"Est. Supply", |
|
|
f"{est_supply:.1f} MW", |
|
|
delta=f"{est_supply - demand_today:.1f} MW", |
|
|
help="Sum of (Rated × Efficiency). This is what actually hits the grid." |
|
|
) |
|
|
p_col2.metric( |
|
|
"Target Demand", |
|
|
f"{demand_today:.1f} MW", |
|
|
delta=None |
|
|
) |
|
|
|
|
|
|
|
|
if est_supply < demand_today: |
|
|
st.error(f"⚠️ Shortage: {demand_today - est_supply:.1f} MW") |
|
|
else: |
|
|
|
|
|
surplus = est_supply - demand_today |
|
|
if surplus > demand_today * 0.2: |
|
|
st.info(f"⚠️ High Surplus: +{surplus:.1f} MW (Waste?)") |
|
|
else: |
|
|
st.success("✅ Demand Met") |
|
|
|
|
|
|
|
|
st.write("") |
|
|
c_col1, c_col2 = st.columns(2) |
|
|
|
|
|
c_col1.metric("Est. Cost", f"${est_cost:.1f}k") |
|
|
c_col2.metric("Budget", f"${budget_today:.1f}k") |
|
|
|
|
|
|
|
|
if est_cost > budget_today: |
|
|
st.warning(f"💸 Over Budget: ${est_cost - budget_today:.1f}k") |
|
|
else: |
|
|
st.caption(f"💰 Remaining: ${budget_today - est_cost:.1f}k") |
|
|
|
|
|
|
|
|
if st.button("▶️ Execute Step", type="primary", use_container_width=True): |
|
|
step_simulation(thermal_slider, wind_slider, solar_slider, battery_slider) |
|
|
st.rerun() |
|
|
else: |
|
|
st.info("Please initialize the environment first") |
|
|
|
|
|
|
|
|
if st.session_state.env is not None: |
|
|
obs = st.session_state.env._get_obs() |
|
|
|
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
|
|
with col1: |
|
|
demand_met_pct = obs['actual']['demand_met'] * 100 |
|
|
color = "🟢" if demand_met_pct >= 95 else "🟡" if demand_met_pct >= 85 else "🔴" |
|
|
st.metric( |
|
|
"Demand Met", |
|
|
f"{demand_met_pct:.1f}%", |
|
|
delta=None, |
|
|
help="Percentage of demand satisfied" |
|
|
) |
|
|
st.markdown(f"{color}") |
|
|
|
|
|
with col2: |
|
|
stability_pct = obs['stability'] * 100 |
|
|
color = "🟢" if stability_pct >= 70 else "🟡" if stability_pct >= 40 else "🔴" |
|
|
st.metric( |
|
|
"Grid Stability", |
|
|
f"{stability_pct:.1f}%", |
|
|
delta=None, |
|
|
help="Overall grid stability score" |
|
|
) |
|
|
st.markdown(f"{color}") |
|
|
|
|
|
with col3: |
|
|
if len(st.session_state.history) > 0: |
|
|
carbon_pct = st.session_state.history[-1]["carbon"] * 100 |
|
|
else: |
|
|
carbon_pct = 0 |
|
|
color = "🟢" if carbon_pct < 40 else "🟡" if carbon_pct < 70 else "🔴" |
|
|
st.metric( |
|
|
"Carbon Intensity", |
|
|
f"{carbon_pct:.1f}%", |
|
|
delta=None, |
|
|
help="Percentage of thermal power in total supply" |
|
|
) |
|
|
st.markdown(f"{color}") |
|
|
|
|
|
with col4: |
|
|
st.metric( |
|
|
"Day", |
|
|
f"{obs['day']} / {st.session_state.config['horizon']}", |
|
|
delta=None |
|
|
) |
|
|
if st.session_state.total_reward != 0: |
|
|
st.metric("Total Reward", f"{st.session_state.total_reward:.2f}") |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.subheader("📊 Current Status") |
|
|
|
|
|
st.markdown(f""" |
|
|
**Power Generation (MW):** |
|
|
- 🔥 Thermal: {obs['actual']['thermal']:.1f} MW |
|
|
- 💨 Wind: {obs['actual']['wind']:.1f} MW |
|
|
- ☀️ Solar: {obs['actual']['solar']:.1f} MW |
|
|
- 🔋 Battery: {obs['actual']['battery']:.1f} MW |
|
|
- **Total Supply: {obs['actual']['supply']:.1f} MW** |
|
|
""") |
|
|
|
|
|
st.markdown(f""" |
|
|
**Demand & Budget:** |
|
|
- Current Demand: {obs['demand_today']:.1f} MW |
|
|
- Next Demand: {obs['demand_next']:.1f} MW |
|
|
- Budget Today: ${obs['budget_today']:.1f}K |
|
|
- Budget Next: ${obs['budget_next']:.1f}K |
|
|
""") |
|
|
|
|
|
|
|
|
st.info(f"🔮 **Weather Forecast:**\n{obs['nl_forecast']}") |
|
|
|
|
|
with col2: |
|
|
st.subheader("⚡ Efficiency (Today)") |
|
|
|
|
|
eff_data = pd.DataFrame({ |
|
|
'Source': ['Thermal', 'Wind', 'Solar'], |
|
|
'Efficiency': [ |
|
|
obs['efficiency']['thermal'], |
|
|
obs['efficiency']['wind'], |
|
|
obs['efficiency']['solar'] |
|
|
] |
|
|
}) |
|
|
|
|
|
st.dataframe(eff_data, use_container_width=True, hide_index=True) |
|
|
|
|
|
|
|
|
max_eff = 1.2 |
|
|
st.progress( |
|
|
min(1.0, obs['efficiency']['thermal'] / max_eff), |
|
|
text=f"Thermal: {obs['efficiency']['thermal']:.2f}" |
|
|
) |
|
|
st.progress( |
|
|
min(1.0, obs['efficiency']['wind'] / max_eff), |
|
|
text=f"Wind: {obs['efficiency']['wind']:.2f}" |
|
|
) |
|
|
st.progress( |
|
|
min(1.0, obs['efficiency']['solar'] / max_eff), |
|
|
text=f"Solar: {obs['efficiency']['solar']:.2f}" |
|
|
) |
|
|
|
|
|
|
|
|
if len(st.session_state.history) > 0: |
|
|
st.divider() |
|
|
st.subheader("📈 Historical Data") |
|
|
|
|
|
tab1, tab2, tab3, tab4 = st.tabs(["Power Generation", "Performance Metrics", "Budget Analysis", "Data Table"]) |
|
|
|
|
|
with tab1: |
|
|
history = st.session_state.history |
|
|
|
|
|
fig, ax = plt.subplots(figsize=(12, 6)) |
|
|
|
|
|
days = [h["day"] for h in history] |
|
|
ax.plot(days, [h["thermal"] for h in history], 'r-', label='Thermal', linewidth=2) |
|
|
ax.plot(days, [h["wind"] for h in history], 'b-', label='Wind', linewidth=2) |
|
|
ax.plot(days, [h["solar"] for h in history], 'y-', label='Solar', linewidth=2) |
|
|
ax.plot(days, [h["battery"] for h in history], 'g-', label='Battery', linewidth=2) |
|
|
ax.plot(days, [h["demand"] for h in history], 'k--', label='Demand', linewidth=2, alpha=0.7) |
|
|
|
|
|
ax.set_xlabel('Day', fontsize=12) |
|
|
ax.set_ylabel('Power (MW)', fontsize=12) |
|
|
ax.set_title('Power Generation Over Time', fontsize=14, fontweight='bold') |
|
|
ax.legend(loc='best') |
|
|
ax.grid(True, alpha=0.3) |
|
|
|
|
|
plt.tight_layout() |
|
|
st.pyplot(fig) |
|
|
|
|
|
with tab2: |
|
|
fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10)) |
|
|
|
|
|
days = [h["day"] for h in history] |
|
|
|
|
|
|
|
|
stability = [h["stability"] * 100 for h in history] |
|
|
ax1.plot(days, stability, 'g-', linewidth=2) |
|
|
ax1.axhline(y=70, color='orange', linestyle='--', alpha=0.5, label='Warning (70%)') |
|
|
ax1.axhline(y=40, color='r', linestyle='--', alpha=0.5, label='Critical (40%)') |
|
|
ax1.set_ylabel('Stability (%)', fontsize=12) |
|
|
ax1.set_title('Grid Stability', fontsize=13, fontweight='bold') |
|
|
ax1.legend() |
|
|
ax1.grid(True, alpha=0.3) |
|
|
|
|
|
|
|
|
carbon = [h["carbon"] * 100 for h in history] |
|
|
ax2.plot(days, carbon, 'brown', linewidth=2) |
|
|
ax2.axhline(y=70, color='orange', linestyle='--', alpha=0.5, label='High (70%)') |
|
|
ax2.axhline(y=40, color='g', linestyle='--', alpha=0.5, label='Low (40%)') |
|
|
ax2.set_ylabel('Carbon Intensity (%)', fontsize=12) |
|
|
ax2.set_title('Carbon Intensity', fontsize=13, fontweight='bold') |
|
|
ax2.legend() |
|
|
ax2.grid(True, alpha=0.3) |
|
|
|
|
|
|
|
|
demand_met = [h["demand_met"] * 100 for h in history] |
|
|
ax3.plot(days, demand_met, 'purple', linewidth=2) |
|
|
ax3.axhline(y=95, color='g', linestyle='--', alpha=0.5, label='Target (95%)') |
|
|
ax3.axhline(y=85, color='orange', linestyle='--', alpha=0.5, label='Warning (85%)') |
|
|
ax3.set_xlabel('Day', fontsize=12) |
|
|
ax3.set_ylabel('Demand Met (%)', fontsize=12) |
|
|
ax3.set_title('Demand Satisfaction', fontsize=13, fontweight='bold') |
|
|
ax3.legend() |
|
|
ax3.grid(True, alpha=0.3) |
|
|
|
|
|
plt.tight_layout() |
|
|
st.pyplot(fig) |
|
|
|
|
|
with tab3: |
|
|
df = pd.DataFrame(st.session_state.history) |
|
|
st.dataframe(df, use_container_width=True, hide_index=True) |
|
|
|
|
|
|
|
|
csv = df.to_csv(index=False) |
|
|
st.download_button( |
|
|
label="📥 Download Data as CSV", |
|
|
data=csv, |
|
|
file_name="energy_grid_simulation.csv", |
|
|
mime="text/csv" |
|
|
) |
|
|
|
|
|
else: |
|
|
st.info("👈 Please initialize the environment from the sidebar to begin") |
|
|
|
|
|
st.markdown(""" |
|
|
### 📖 Instructions |
|
|
|
|
|
1. **Initialize**: Set simulation days and seed in the sidebar, then click "Initialize Environment" |
|
|
2. **Adjust Power**: Use sliders to set rated power for each generation type |
|
|
3. **Execute Step**: Click to advance one day and see results |
|
|
4. **Monitor**: Watch the charts to track performance over time |
|
|
|
|
|
**Goals**: |
|
|
- Maximize demand satisfaction (>95%) |
|
|
- Maintain grid stability (>70%) |
|
|
- Minimize carbon emissions (<40%) |
|
|
- Stay within budget |
|
|
""") |