import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import json
import os
import importlib.util
from env.environment import EcoGridEnv
from models.schemas import GridAction
from baseline import heuristic_agent, local_llm_agent, load_trained_model, LORA_DIR, is_lora_valid
# Use wide mode with a custom icon
st.set_page_config(page_title="EcoGrid Dashboard", layout="wide", page_icon="🌍")
@st.cache_data
def init_llm_model():
"""Fast availability check (no heavyweight model load)."""
required_files = (
"adapter_config.json",
"adapter_model.safetensors",
"tokenizer.json",
"tokenizer_config.json",
)
files_ok = is_lora_valid()
deps_ok = (
importlib.util.find_spec("transformers") is not None
and importlib.util.find_spec("peft") is not None
and importlib.util.find_spec("torch") is not None
)
return files_ok and deps_ok
TRAINED_AVAILABLE = init_llm_model()
def load_reward_curve():
try:
if os.path.exists("./logs/reward_curve.json"):
with open("./logs/reward_curve.json", "r") as f:
return json.load(f)
except:
pass
return []
def init_session():
if "env" not in st.session_state:
st.session_state.env = EcoGridEnv()
if "current_task" not in st.session_state:
st.session_state.current_task = "medium"
if "state" not in st.session_state:
st.session_state.state = st.session_state.env.reset(task=st.session_state.current_task, seed=42)
if "history" not in st.session_state:
st.session_state.history = []
if "cumulative_reward" not in st.session_state:
st.session_state.cumulative_reward = 0.0
if "trained_runtime_checked" not in st.session_state:
st.session_state.trained_runtime_checked = False
if "trained_runtime_ready" not in st.session_state:
st.session_state.trained_runtime_ready = False
if "trained_fallback_used" not in st.session_state:
st.session_state.trained_fallback_used = False
def step_env(agent_type):
env = st.session_state.env
state = st.session_state.state
task = st.session_state.current_task
if agent_type == "Random Agent":
action = env.action_space.sample()
action = GridAction(renewable_ratio=action[0], fossil_ratio=action[1], battery_action=action[2])
elif agent_type == "Heuristic Rule-Based":
action = heuristic_agent(state, task)
elif agent_type == "AI Agent (Trained LoRA)":
if not st.session_state.trained_runtime_checked:
# First time load attempt
model, error = load_trained_model()
st.session_state.trained_runtime_ready = (model is not None)
st.session_state.trained_runtime_error = error if not st.session_state.trained_runtime_ready else None
st.session_state.trained_runtime_checked = True
if st.session_state.trained_runtime_ready:
action = local_llm_agent(state, task)
else:
st.session_state.trained_fallback_used = True
action = heuristic_agent(state, task)
# Execute step
try:
result = env.step(action)
st.session_state.state = result.observation
st.session_state.cumulative_reward += result.reward
# Save history for plotting
log_entry = {
"step": env.current_step,
"demand": state.demand,
"reward": result.reward,
"cost_score": result.info["reward_breakdown"]["cost_score"],
"carbon_score": result.info["reward_breakdown"]["carbon_score"],
"stability_score": result.info["reward_breakdown"]["stability_score"],
"emissions": result.info["carbon_emitted_step"]
}
st.session_state.history.append(log_entry)
except Exception as e:
st.error(f"Environment Error: {e}")
init_session()
# ─── THEME TOKENS ───
COLOR_TEXT = "#f8fafc"
COLOR_MUTED = "#94a3b8"
COLOR_GRID = "rgba(255, 255, 255, 0.05)"
COLOR_PRIMARY = "#00f2fe" # Vibrant teal
COLOR_SECONDARY = "#4facfe" # Soft blue
COLOR_WARN = "#facc15" # Yellow
COLOR_DANGER = "#ff4b4b" # Red/Pink
COLOR_SUCCESS = "#00f260" # Green
COLOR_PURPLE = "#c084fc" # Accent purple
# ─── SIDEBAR CONTROL PANEL ───
with st.sidebar:
st.markdown("""
⚡ CONTROL ROOM
EcoGrid Intelligence Unit
""", unsafe_allow_html=True)
task_labels = {"easy": "Easy (No Battery, Flat Demand)", "medium": "Medium (Small Battery, Spikes)", "hard": "Hard (Carbon Cap, High Volatility)"}
task = st.selectbox(
"SIMULATION DIFFICULTY",
["easy", "medium", "hard"],
index=1,
format_func=lambda x: task_labels[x],
help="Changes the weather volatility, demand curves, and carbon constraints."
)
if task != st.session_state.current_task:
st.session_state.current_task = task
st.session_state.env = EcoGridEnv()
st.session_state.state = st.session_state.env.reset(task=task, seed=42)
st.session_state.history = []
st.session_state.cumulative_reward = 0.0
st.session_state.trained_runtime_checked = False
st.session_state.trained_runtime_ready = False
st.session_state.trained_fallback_used = False
st.markdown("", unsafe_allow_html=True)
agent_options = ["Random Agent", "Heuristic Rule-Based"]
if TRAINED_AVAILABLE:
agent_options.append("AI Agent (Trained LoRA)")
agent = st.radio(
"ACTIVE INTELLIGENCE",
agent_options,
index=1,
help="Select which intelligence is controlling the grid."
)
if not TRAINED_AVAILABLE:
st.markdown("""
⚠️ AI weights missing. LFS pull required for LoRA inference.
""", unsafe_allow_html=True)
elif st.session_state.trained_fallback_used and not st.session_state.trained_runtime_ready:
err_detail = st.session_state.get('trained_runtime_error', 'Unknown Error')
st.markdown(f"""
🚨 Fallback Active.
{err_detail}
""", unsafe_allow_html=True)
st.markdown("", unsafe_allow_html=True)
col_btn1, col_btn2 = st.columns(2)
with col_btn1:
if st.button("▶ Step Once", use_container_width=True):
step_env(agent)
with col_btn2:
if st.button("⏩ Run Full", use_container_width=True):
while not st.session_state.env.is_done:
step_env(agent)
if st.button("🔄 Reset Simulation", use_container_width=True):
st.session_state.env = EcoGridEnv()
st.session_state.state = st.session_state.env.reset(task=task, seed=42)
st.session_state.history = []
st.session_state.cumulative_reward = 0.0
st.session_state.trained_runtime_checked = False
st.session_state.trained_runtime_ready = False
st.session_state.trained_fallback_used = False
# ─── MAIN UI HEADER ───
st.markdown("""
🌍 EcoGrid Intelligence
AI-Powered Sustainable Energy Grid Management
""", unsafe_allow_html=True)
if TRAINED_AVAILABLE and st.session_state.trained_runtime_ready:
st.markdown("""
""", unsafe_allow_html=True)
col_live, col_reward, col_emissions = st.columns(3)
# ─── PANEL 1: LIVE GRID STATE ───
with col_live:
with st.container(border=True):
st.markdown('📡 Live Grid State
', unsafe_allow_html=True)
st.markdown('Real-time supply and demand metrics.
', unsafe_allow_html=True)
state = st.session_state.state
# Timestep Metric
ep_len = st.session_state.env.get_task_config(st.session_state.current_task)['episode_length']
progress_pct = (state.time_step / ep_len) * 100
st.markdown(f"""
Timestep Progress
{state.time_step}
/ {ep_len}
""", unsafe_allow_html=True)
# Battery Gauge
fig = go.Figure(go.Indicator(
mode = "gauge+number",
value = state.battery_level * 100,
number = {'suffix': "%", 'font': {'color': COLOR_TEXT, 'size': 28, 'family': 'Outfit'}},
title = {'text': "Battery Charge State", 'font': {'size': 14, 'color': COLOR_MUTED}},
gauge = {
'axis': {'range': [0, 100], 'tickwidth': 1, 'tickcolor': COLOR_GRID},
'bar': {'color': COLOR_PRIMARY, 'thickness': 0.25},
'bgcolor': "rgba(0,0,0,0)",
'borderwidth': 0,
'steps': [
{'range': [0, 20], 'color': "rgba(255, 75, 75, 0.15)"},
{'range': [80, 100], 'color': "rgba(0, 242, 96, 0.15)"}
]
}
))
fig.update_layout(height=180, margin=dict(l=25, r=25, t=40, b=10), paper_bgcolor="rgba(0,0,0,0)", font={'family': 'Inter'})
st.plotly_chart(fig, use_container_width=True, config={'displayModeBar': False})
# Capacity Bars
fig2 = go.Figure()
fig2.add_trace(go.Bar(name='Demand', x=['DEMAND'], y=[state.demand], marker_color=COLOR_DANGER, opacity=0.9, marker_line_width=0, hoverinfo="y+name"))
fig2.add_trace(go.Bar(name='Solar', x=['SOLAR'], y=[state.solar_capacity * 100], marker_color=COLOR_WARN, opacity=0.9, marker_line_width=0, hoverinfo="y+name"))
fig2.add_trace(go.Bar(name='Wind', x=['WIND'], y=[state.wind_capacity * 100], marker_color=COLOR_SECONDARY, opacity=0.9, marker_line_width=0, hoverinfo="y+name"))
fig2.update_layout(
height=200, margin=dict(l=10, r=10, t=10, b=20), barmode='group', showlegend=False,
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
yaxis=dict(gridcolor=COLOR_GRID, showticklabels=False, zeroline=False),
xaxis=dict(tickfont=dict(color=COLOR_MUTED, size=11, family='Outfit'), zeroline=False),
font=dict(family='Inter')
)
st.plotly_chart(fig2, use_container_width=True, config={'displayModeBar': False})
# ─── PANEL 2: AGENT PERFORMANCE ───
with col_reward:
with st.container(border=True):
st.markdown('📈 Performance Analytics
', unsafe_allow_html=True)
st.markdown('Multi-objective optimization scoring.
', unsafe_allow_html=True)
if st.session_state.history:
df = pd.DataFrame(st.session_state.history)
# Area Chart for Overall Reward
fig3 = go.Figure()
fig3.add_trace(go.Scatter(
x=df['step'], y=df['reward'], mode='lines', fill='tozeroy',
name='Step Reward',
line=dict(color=COLOR_PRIMARY, width=3),
fillcolor='rgba(0, 242, 254, 0.15)',
hovertemplate="Step %{x}
Reward: %{y:.2f}"
))
fig3.update_layout(
title=dict(text="CUMULATIVE STEP REWARD", font=dict(color=COLOR_MUTED, size=11, family='Outfit')),
height=190, margin=dict(l=10, r=10, t=35, b=10),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
xaxis=dict(gridcolor=COLOR_GRID, color=COLOR_MUTED, zeroline=False),
yaxis=dict(gridcolor=COLOR_GRID, color=COLOR_MUTED, range=[0, 1.05], zeroline=False),
font=dict(family='Inter')
)
st.plotly_chart(fig3, use_container_width=True, config={'displayModeBar': False})
# Breakdown Lines
fig4 = go.Figure()
fig4.add_trace(go.Scatter(x=df['step'], y=df['cost_score'], name='Cost', line=dict(color=COLOR_WARN, width=2, dash='dot'), hovertemplate="%{y:.2f}"))
fig4.add_trace(go.Scatter(x=df['step'], y=df['carbon_score'], name='Eco', line=dict(color=COLOR_SUCCESS, width=2), hovertemplate="%{y:.2f}"))
fig4.add_trace(go.Scatter(x=df['step'], y=df['stability_score'], name='Grid', line=dict(color=COLOR_PURPLE, width=2), hovertemplate="%{y:.2f}"))
fig4.update_layout(
title=dict(text="OBJECTIVE BREAKDOWN", font=dict(color=COLOR_MUTED, size=11, family='Outfit')),
height=210, margin=dict(l=10, r=10, t=35, b=10),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, font=dict(color=COLOR_MUTED, size=10)),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
xaxis=dict(gridcolor=COLOR_GRID, color=COLOR_MUTED, zeroline=False),
yaxis=dict(gridcolor=COLOR_GRID, color=COLOR_MUTED, range=[0, 1.05], zeroline=False),
font=dict(family='Inter'),
hovermode="x unified"
)
st.plotly_chart(fig4, use_container_width=True, config={'displayModeBar': False})
else:
st.info("Initiate simulation to view live performance data.")
st.markdown("", unsafe_allow_html=True)
# ─── PANEL 3: CARBON & TRAINING ───
with col_emissions:
with st.container(border=True):
st.markdown('🌱 Eco Constraints
', unsafe_allow_html=True)
st.markdown('Carbon limits and model convergence.
', unsafe_allow_html=True)
# Carbon Budget Gauge
max_budget = st.session_state.env.get_task_config(st.session_state.current_task)['carbon_budget']
current_budget = state.carbon_budget_remaining
is_strict = st.session_state.env.get_task_config(st.session_state.current_task)['carbon_strict']
budget_color = COLOR_SUCCESS if current_budget > max_budget * 0.2 else COLOR_DANGER
if current_budget < 0: budget_color = "#8b0000"
fig5 = go.Figure(go.Indicator(
mode = "gauge+number",
value = max(0, current_budget),
number = {'valueformat': ".0f", 'font': {'color': COLOR_TEXT, 'size': 28, 'family': 'Outfit'}},
title = {'text': f"Carbon Budget (kgCO2) {'STRICT' if is_strict else ''}", 'font': {'size': 14, 'color': COLOR_MUTED}},
gauge = {
'axis': {'range': [0, max_budget], 'tickwidth': 1, 'tickcolor': COLOR_GRID},
'bar': {'color': budget_color, 'thickness': 0.25},
'bgcolor': "rgba(0,0,0,0)",
'borderwidth': 0,
'steps': [
{'range': [0, max_budget * 0.2], 'color': "rgba(255, 75, 75, 0.15)"}
]
}
))
fig5.update_layout(height=180, margin=dict(l=25, r=25, t=40, b=10), paper_bgcolor="rgba(0,0,0,0)", font=dict(family='Inter'))
st.plotly_chart(fig5, use_container_width=True, config={'displayModeBar': False})
# Training Convergence
st.markdown("🧠 GRPO Training Convergence
", unsafe_allow_html=True)
curve_data = load_reward_curve()
if not curve_data and os.path.exists("training_metrics.json"):
try:
with open("training_metrics.json", "r") as f:
metrics = json.load(f)
# Extract history and filter for valid reward entries
raw_history = metrics.get("log_history", [])
curve_data = [
{"step": e["step"], "reward": e["reward"]}
for e in raw_history
if "step" in e and "reward" in e
]
except Exception:
curve_data = []
if curve_data:
df_curve = pd.DataFrame(curve_data)
fig6 = go.Figure()
fig6.add_trace(go.Scatter(
x=df_curve['step'], y=df_curve['reward'], mode='lines',
line=dict(color=COLOR_PRIMARY, width=2),
fill='tozeroy', fillcolor='rgba(0, 242, 254, 0.08)'
))
fig6.update_layout(
height=190, margin=dict(l=10, r=10, t=10, b=20),
paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
xaxis=dict(gridcolor=COLOR_GRID, color=COLOR_MUTED, title=dict(text="TRAINING STEPS", font=dict(size=10)), zeroline=False),
yaxis=dict(gridcolor=COLOR_GRID, color=COLOR_MUTED, title=dict(text="REWARD", font=dict(size=10)), zeroline=False),
font=dict(family='Inter')
)
st.plotly_chart(fig6, use_container_width=True, config={'displayModeBar': False})
else:
if os.path.exists("docs/reward_curve.png"):
st.image("docs/reward_curve.png", caption="Historical Training Performance")
else:
st.info("Convergence telemetry unavailable.")
st.markdown("""
""", unsafe_allow_html=True)
# ─── GLOBAL STYLING (Rich Aesthetics & Glassmorphism) ───
st.markdown("""
""", unsafe_allow_html=True)