Energy / app.py
Fangzhi Xu
U
13b7d2e
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
# ===================================================
# 环境类 V7 (保持不变,只是使用新的 config)
# ===================================================
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
# 1. clamp 到容量
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"])
# 2. 效率
eff_th = self.world["eff_thermal"][t]
eff_w = self.world["eff_wind"][t]
eff_s = self.world["eff_solar"][t]
# 3. 实际发电
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
# 4. 预算开销
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
# 5. ramp
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,
}
# 6. 碳排
if supply > 1e-6:
share_thermal = self.thermal_actual / supply
else:
share_thermal = 0
self.cum_carbon += share_thermal
# 7. 新稳定性(即时)
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)
# 8. 时间推进
self.t += 1
done = (self.t >= self.horizon)
self.done = done
# 9. reward(only at final)
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
# 10. obs & info
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
# ===================================================
# 配置生成 V11
# ===================================================
def generate_world_profile_v11(days=120, seed=0):
rng = np.random.default_rng(seed)
# Season phase & amplitude
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
# thermal weak season
amp_thermal = 0.03
center_thermal = 0.95
# storage
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)
# Extreme Events
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)
# AR(1) Weather Noise
trend = 0.0
trend_decay = 0.85
noise_scale = 0.12
thermal_noise_scale = 0.02
for t in range(days):
# season wave (30-day cycle)
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)
# AR(1)
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)
# Efficiency
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]
# Extreme events
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
"""
# Step 1: random raw proportions
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()
# Step 2: target total generation
target_total = demand_day1 * rng.uniform(0.95, 1.05)
# Step 3: scale and clip
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
# ===================================================
# Session State 初始化
# ===================================================
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}")
# ===================================================
# UI 界面
# ===================================================
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
)
# ==========================
# 新增:实时预测面板 (Power & Cost)
# ==========================
st.divider()
st.subheader("🔮 Real-time Projections")
# --- 1. 数据准备 ---
prices = st.session_state.config["prices"]
eff = obs["efficiency"] # 获取今日效率
demand_today = obs["demand_today"]
budget_today = obs["budget_today"]
# --- 2. 核心计算 ---
# A. 预计实际发电量 (考虑天气效率!)
# 注意:电池效率在 Env 代码中固定为 1.0
est_supply = (
thermal_slider * eff["thermal"] +
wind_slider * eff["wind"] +
solar_slider * eff["solar"] +
battery_slider * 1.0
)
# B. 预计开销
est_cost = (
thermal_slider * prices["thermal"] +
wind_slider * prices["wind"] +
solar_slider * prices["solar"] +
battery_slider * prices["battery"]
)
# --- 3. 显示 Power 指标 (电量) ---
# 使用 Delta 展示供需差额:绿色表示满足需求,红色表示短缺
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")
# --- 4. 显示 Cost 指标 (资金) ---
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
""")
# NL Forecast
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)
# 进度条显示效率(归一化到0-1范围,效率最大值为1.2)
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
""")