ssb / app.py
Boby Villari(UST,IN)
Repush to Hugging Face
594361e
Raw History Blame Contribute Delete
21.4 kB
import gradio as gr
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import os
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
# ============================================================================
# DATA LOADER: SolarDataLake
# ============================================================================
class SolarDataLake:
"""Data ingestion engine that loads physics calculations and time series data."""
def __init__(self):
print("\n" + "="*40)
print("🚀 STARTING SOLAR AGENTIC SIMULATION")
print("="*40)
self.constants = self._load_assumptions()
self.df = self._load_time_series()
if self.df.empty:
print("\n❌ CRITICAL: No data loaded. Please check file paths.")
else:
self.max_steps = len(self.df)
self.current_step = 0
print(f"\n✅ SUCCESS: Simulation Ready with {self.max_steps} synchronized steps.")
def _load_assumptions(self):
"""Load physics assumptions from Excel."""
filename = "Power Calculation_v1.xlsx"
sheet_name = "Assumptions - Loss calculations"
defaults = {'PLANNED_CAPACITY_KW': 6012.44, 'TEMP_COEFF': -0.00279, 'LOSS_FACTOR': 0.8197, 'STC_TEMP': 25.0}
if not os.path.exists(filename):
return defaults
try:
df = pd.read_excel(filename, sheet_name=sheet_name, header=1)
def get_val(key):
mask = df.iloc[:, 0].astype(str).str.contains(key, case=False, na=False)
return float(df.loc[mask].iloc[0, 1]) if mask.any() else None
cap = get_val('Planned Power')
coeff = get_val('Temperature coefficient')
loss = get_val('efficiency')
stc = get_val('STC')
return {
'PLANNED_CAPACITY_KW': cap if cap else defaults['PLANNED_CAPACITY_KW'],
'TEMP_COEFF': coeff if coeff else defaults['TEMP_COEFF'],
'LOSS_FACTOR': loss if loss else defaults['LOSS_FACTOR'],
'STC_TEMP': stc if stc else defaults['STC_TEMP']
}
except Exception as e:
print(f"Warning: Could not load assumptions: {e}")
return defaults
def _estimate_module_temp(self, gii):
"""Estimate module temperature from irradiance using linear model."""
# Model trained on Excel data: Temp = 20.46 + 0.0247 * GII
return 20.46 + 0.0247 * gii
def _load_time_series(self):
"""Load and align all time series data sources."""
# 1. LOAD PHYSICS DATA
excel_file = "Power Calculation_v1.xlsx"
calc_df = pd.DataFrame()
if os.path.exists(excel_file):
try:
calc_df = pd.read_excel(excel_file, sheet_name="Energy_Power Calculation - Tabl", header=1)
calc_df = calc_df.rename(columns={'Time (Hrs)': 'Time_Raw', 'GII (W/m2)': 'GII_Excel', 'Module Temp (oC)': 'Mod_Temp'})
calc_df = calc_df.dropna(subset=['Time_Raw'])
calc_df['Temp_DT'] = pd.to_datetime(calc_df['Time_Raw'])
calc_df['Timestamp'] = pd.to_datetime("2026-02-08 " + calc_df['Temp_DT'].dt.strftime('%H:%M:%S'))
calc_df = calc_df.set_index('Timestamp')[['GII_Excel', 'Mod_Temp']]
except Exception as e:
print(f"Warning: Could not load physics data: {e}")
pass
# 2. LOAD SENSORS - Use ONLY 2-month data file
def read_sensor(fname):
if not os.path.exists(fname):
return pd.DataFrame()
try:
df = pd.read_csv(fname)
t_col = next((c for c in df.columns if "Time" in str(c)), None)
if t_col:
df['Timestamp'] = pd.to_datetime(df[t_col])
return df.set_index('Timestamp').drop(columns=[t_col])
except Exception as e:
print(f"Warning: Could not load {fname}: {e}")
return pd.DataFrame()
# Use ONLY 2-month data for all information
grid_df = read_sensor("ICOG Panel data_2 months.csv")
if grid_df.empty:
return pd.DataFrame()
# 3. PROCESS 2-MONTH DATA
master = grid_df.copy()
# Rename columns for consistency
master = master.rename(columns={
'ICOG PANEL MFM - Irradiance (W/m2)': 'GII',
'ICOG PANEL MFM - Active Power (kW)': 'Act_Power_kW',
'ICOG PANEL MFM - OutputFrequency (Hz)': 'Grid_Freq'
})
# Estimate module temperature from irradiance
master['Mod_Temp'] = self._estimate_module_temp(master['GII'])
# Add inverter temperature data (from Excel assumptions, using default values)
# Since 2-month file doesn't have per-inverter temp, use estimated module temp
master['Inv-2.1 - InverterTemperature (°C)'] = master['Mod_Temp'] - 0.5 # Slightly lower for inverter
master['Inv-2.2 - InverterTemperature (°C)'] = master['Mod_Temp'] + 0.5 # Slightly higher for inverter
return master.sort_index().fillna(0)
def get_next_tick(self):
"""Get next time step data."""
if self.df.empty:
return None, None
if self.current_step >= self.max_steps:
self.current_step = 0
row = self.df.iloc[self.current_step]
timestamp = self.df.index[self.current_step]
self.current_step += 1
return timestamp, row
# ============================================================================
# POLICY ENGINE (simplified from claudecode_policy_engine.py)
# ============================================================================
class PolicyEngine:
"""Simplified policy engine for command validation."""
def __init__(self):
self.policies = {
'safety_envelope': {
'temperature': {'warning': 75, 'critical': 85},
'frequency': {'min_safe': 49.5, 'max_safe': 50.2},
'irradiance': {'min_generation': 50}
},
'lockouts': {
'hard': ['E205', 'E208'], # Ground fault, Isolation fault
'soft': ['E201', 'E202'] # Voltage, Frequency faults
}
}
def validate_command(self, inverter_id, action, telemetry, active_faults):
"""Validate if a command can be executed."""
violations = []
# Check for active faults that create hard lockouts
for fault in active_faults:
if fault['code'] in self.policies['lockouts']['hard']:
violations.append(f"Hard lockout: {fault['description']}")
# Check grid conditions
freq = telemetry.get('Grid_Freq', 50.0)
if freq < self.policies['safety_envelope']['frequency']['min_safe']:
violations.append(f"Grid frequency too low: {freq} Hz")
if freq > self.policies['safety_envelope']['frequency']['max_safe']:
violations.append(f"Grid frequency too high: {freq} Hz")
return {
'allowed': len(violations) == 0,
'violations': violations,
'reason': 'All checks passed' if len(violations) == 0 else '; '.join(violations)
}
# ============================================================================
# ANOMALY DETECTOR (simplified from claudecode_anomaly_detector.py)
# ============================================================================
class AnomalyDetector:
"""Detects anomalies in solar farm operations."""
def __init__(self, baseline_threshold=0.20):
self.baseline_threshold = baseline_threshold
self.anomalies = []
def detect_all_anomalies(self, row, theo_power):
"""Run all anomaly detection algorithms."""
anomalies = []
# Power underperformance detection
act_power = row.get('Act_Power_kW', 0)
if theo_power > 100 and act_power < (theo_power * (1 - self.baseline_threshold)):
anomalies.append({
'type': 'power_underperformance',
'severity': 'warning',
'confidence': 0.85,
'details': f'Actual {act_power:.1f}kW vs Theoretical {theo_power:.1f}kW'
})
# Temperature anomaly detection
temp_cols = [col for col in row.index if 'InverterTemperature' in str(col)]
max_temp = row[temp_cols].max() if temp_cols else 0
if max_temp > 75:
anomalies.append({
'type': 'temperature_anomaly',
'severity': 'critical' if max_temp > 85 else 'warning',
'confidence': 0.95,
'details': f'Max temperature {max_temp:.1f}°C'
})
# Grid anomaly detection
freq = row.get('Grid_Freq', 50.0)
if freq < 49.5 or freq > 50.2:
anomalies.append({
'type': 'grid_anomaly',
'severity': 'critical',
'confidence': 0.90,
'details': f'Grid frequency {freq:.2f} Hz'
})
self.anomalies = anomalies
return anomalies
# ============================================================================
# INCIDENT RESPONSE AGENT (new - from claudecode documentation)
# ============================================================================
class IncidentResponseAgent:
"""Handles fault recovery and incident response procedures."""
def __init__(self):
self.procedures = {
'E201': {'name': 'Grid Voltage Fault', 'response': 'Check grid connections, verify voltage levels'},
'E202': {'name': 'Grid Frequency Fault', 'response': 'Verify grid frequency stability'},
'E205': {'name': 'Ground Fault', 'response': 'IMMEDIATE: Stop operation, inspect grounding'},
'E208': {'name': 'Isolation Fault', 'response': 'IMMEDIATE: Stop operation, check insulation'},
'E210': {'name': 'Overtemperature Fault', 'response': 'Check cooling system, reduce load'}
}
self.active_incidents = []
def handle_incident(self, fault_code, inverter_id, telemetry):
"""Handle an incident based on fault code."""
if fault_code not in self.procedures:
return {'handled': False, 'procedure': 'Unknown fault code'}
procedure = self.procedures[fault_code]
incident = {
'fault_code': fault_code,
'inverter_id': inverter_id,
'procedure_name': procedure['name'],
'recommended_action': procedure['response'],
'timestamp': datetime.now().strftime('%H:%M:%S'),
'status': 'active'
}
self.active_incidents.append(incident)
return incident
# ============================================================================
# AGENT LOGIC
# ============================================================================
class AgentOrchestrator:
def __init__(self, lake):
self.lake = lake
self.inventory = {"Inverter Module": 2, "DC Fuses": 50}
self.history_actual, self.history_theoretical, self.history_timestamps = [], [], []
# Store last agent decisions
self.last_decision = {
'perf': "🟢 Nominal",
'maint': "✅ System Healthy",
'grid': "Stable",
'safety': "Normal (Max 0.0°C)",
'asset': "Inventory: OK",
'incident': "No active incidents",
'log': ""
}
# Initialize sub-agents
self.policy_engine = PolicyEngine()
self.anomaly_detector = AnomalyDetector()
self.incident_response = IncidentResponseAgent()
def process_frame(self):
"""Process one time step and return all agent decisions."""
ts, row = self.lake.get_next_tick()
if ts is None:
return "NODATA"
c = self.lake.constants
gii = row.get('GII', 0)
t_mod = row.get('Mod_Temp', 25)
# Physics calculation: P = Planned * (1 + temp_coeff * temp_diff) * (GII/1000) * efficiency
theo_kw = (c['PLANNED_CAPACITY_KW'] * (1 + c['TEMP_COEFF'] * (t_mod - c['STC_TEMP'])) * (gii / 1000) * c['LOSS_FACTOR']) if gii > 10 else 0
# Get actual power from 2-month data (already in kW)
act_kw = row.get('Act_Power_kW', 0)
# Performance Agent
perf_status = "🟢 Nominal"
if theo_kw > 50 and (act_kw / theo_kw) < 0.85:
perf_status = f"🔴 Low Efficiency ({(act_kw/theo_kw)*100:.1f}%)"
self.history_timestamps.append(ts)
self.history_actual.append(act_kw)
self.history_theoretical.append(theo_kw)
# Maintenance Agent
i22 = row.get('Inv-2.2 - InverterTemperature (°C)', 0)
i21 = row.get('Inv-2.1 - InverterTemperature (°C)', 0)
maint_alert = "✅ System Healthy"
if i21 > 500 and (i22 / i21) < 0.85:
maint_alert = f"⚠️ FAULT: Inv-2.2 Mismatch (Ratio: {i22/i21:.2f})"
# Grid Operations Agent
freq = row.get('Grid_Freq', 50.0)
grid_status = "Stable"
if freq > 50.2:
grid_status = "⚠️ High Freq (Curtailing)"
elif freq < 49.8:
grid_status = "⚠️ Low Freq (Boosting)"
# Safety Agent
temp_cols = [col for col in row.index if "InverterTemperature" in str(col)]
max_t = row[temp_cols].max() if temp_cols else 0
safety_status = f"Normal (Max {max_t:.1f}°C)"
if max_t > 45:
safety_status = f"🔥 WARNING: High Temp ({max_t:.1f}°C)"
# Asset Management Agent
asset_msg = "Inventory: OK"
if "FAULT" in maint_alert:
asset_msg = "📦 ACTION: Reserved 1x Inverter Module"
# Anomaly Detection Agent
anomalies = self.anomaly_detector.detect_all_anomalies(row, theo_kw)
anomaly_msg = "✅ No anomalies detected"
if anomalies:
anomaly_types = [a['type'] for a in anomalies]
anomaly_msg = f"⚠️ {len(anomalies)} anomaly(s): {', '.join(anomaly_types)}"
# Incident Response Agent
incident_msg = "No active incidents"
if anomalies:
# Simulate incident handling for temperature anomalies
for anomaly in anomalies:
if anomaly['type'] == 'temperature_anomaly':
incident = self.incident_response.handle_incident(
'E210', 'Inv-2.2', {'Grid_Freq': freq}
)
incident_msg = f"🔥 Incident: {incident['procedure_name']} - {incident['recommended_action']}"
# Policy Engine validation
active_faults = [{'code': 'E210', 'description': 'Overtemperature'}] if 'High Temp' in safety_status else []
policy_check = self.policy_engine.validate_command('Inv-2.2', 'on', {'Grid_Freq': freq}, active_faults)
policy_msg = "✅ Commands allowed" if policy_check['allowed'] else f"❌ {policy_check['reason']}"
# Generate log message
log = f"[{ts.strftime('%H:%M')}] Power:{act_kw:.0f}kW"
if "FAULT" in maint_alert:
log += " | CRITICAL: Inv-2.2"
if "WARNING" in safety_status:
log += " | SAFETY: Temp High"
# Store last decisions for pause functionality
self.last_decision = {
'perf': perf_status,
'maint': maint_alert,
'grid': grid_status,
'safety': safety_status,
'asset': asset_msg,
'anomaly': anomaly_msg,
'incident': incident_msg,
'policy': policy_msg,
'log': log
}
return ts, act_kw, theo_kw, perf_status, maint_alert, grid_status, safety_status, asset_msg, anomaly_msg, incident_msg, policy_msg, log
# ============================================================================
# MAIN APPLICATION
# ============================================================================
# Initialize
lake = SolarDataLake()
bot = AgentOrchestrator(lake)
# Global state for play/pause control
play_state = {"playing": True}
def update_ui():
"""Update UI with next simulation frame."""
if not hasattr(bot, 'lake') or bot.lake.df.empty:
err = "Data Load Failed"
return (err, None, "0", "0", err, err, err, err, err, err)
# Only process frame if playing
if play_state["playing"]:
result = bot.process_frame()
if result == "NODATA":
return ("End of Stream", None, "0", "0", "End", "End", "End", "End", "End", "End", "End", "End", "End")
ts, act, theo, perf, maint, grid, safety, asset, anomaly, incident, policy, log = result
else:
# When paused, return last values from stored decisions
if len(bot.history_timestamps) == 0:
return ("Paused", None, "0", "0", "Paused", "Paused", "Paused", "Paused", "Paused", "Paused", "Paused", "Paused", "Paused")
ts = bot.history_timestamps[-1]
act = bot.history_actual[-1] if bot.history_actual else 0
theo = bot.history_theoretical[-1] if bot.history_theoretical else 0
perf = bot.last_decision['perf']
maint = bot.last_decision['maint']
grid = bot.last_decision['grid']
safety = bot.last_decision['safety']
asset = bot.last_decision['asset']
anomaly = bot.last_decision['anomaly']
incident = bot.last_decision['incident']
policy = bot.last_decision['policy']
log = bot.last_decision['log']
# Create performance chart
fig = go.Figure()
if len(bot.history_timestamps) > 0:
fig.add_trace(go.Scatter(x=bot.history_timestamps[-60:], y=bot.history_actual[-60:], name="Actual", line=dict(color='#00ff00')))
fig.add_trace(go.Scatter(x=bot.history_timestamps[-60:], y=bot.history_theoretical[-60:], name="Theoretical", line=dict(color='#0000ff', dash='dot')))
fig.update_layout(template="plotly_dark", margin=dict(l=10, r=10, t=30, b=10), height=250, title="Real-time Physics vs Reality")
return (ts.strftime("%Y-%m-%d %H:%M:%S"), fig, f"{act:.2f} kW", f"{theo:.2f} kW", perf, maint, grid, safety, asset, anomaly, incident, policy, log)
def toggle_play_pause():
"""Toggle play/pause state."""
play_state["playing"] = not play_state["playing"]
return "▶️ Play" if not play_state["playing"] else "⏸️ Pause"
with gr.Blocks(theme=gr.themes.Glass(), title="Solar Agentic Control Center") as demo:
gr.Markdown("# 🌞 Solar Farm Agentic Control Center")
with gr.Row():
# === PLAY/PAUSE BUTTON ===
play_pause_btn = gr.Button("⏸️ Pause", variant="primary", size="sm")
# === SPEED CONTROL SLIDER ===
speed_slider = gr.Slider(minimum=0.1, maximum=2, value=0.25, step=0.01, label="Update Speed (Seconds per Step) - Lower is Faster")
time_box = gr.Label(label="Simulation Clock")
with gr.Row():
kpi_1 = gr.Label(label="Actual Power")
kpi_2 = gr.Label(label="Theoretical Power")
chart_box = gr.Plot(label="Performance Agent Vision")
gr.Markdown("### 🤖 Live Agent Decisions")
with gr.Row():
with gr.Column():
gr.Markdown("**Performance**")
out_perf = gr.Textbox(show_label=False)
with gr.Column():
gr.Markdown("**Maintenance**")
out_maint = gr.Textbox(show_label=False)
with gr.Column():
gr.Markdown("**Grid Ops**")
out_grid = gr.Textbox(show_label=False)
with gr.Row():
with gr.Column():
gr.Markdown("**Safety**")
out_safe = gr.Textbox(show_label=False)
with gr.Column():
gr.Markdown("**Asset Mgmt**")
out_asset = gr.Textbox(show_label=False)
with gr.Row():
with gr.Column():
gr.Markdown("**Anomaly Detection**")
out_anomaly = gr.Textbox(show_label=False)
with gr.Column():
gr.Markdown("**Incident Response**")
out_incident = gr.Textbox(show_label=False)
with gr.Column():
gr.Markdown("**Policy Engine**")
out_policy = gr.Textbox(show_label=False)
gr.Markdown("**Incident Commander Log**")
out_log = gr.Textbox(show_label=False)
# === DYNAMIC TIMER ===
timer = gr.Timer(value=1)
# Update timer interval when slider changes
speed_slider.change(lambda x: gr.Timer(value=x), speed_slider, timer)
# Play/Pause button functionality
play_pause_btn.click(
fn=toggle_play_pause,
inputs=None,
outputs=play_pause_btn
)
timer.tick(
fn=update_ui,
inputs=None,
outputs=[time_box, chart_box, kpi_1, kpi_2, out_perf, out_maint, out_grid, out_safe, out_asset, out_anomaly, out_incident, out_policy, out_log]
)
if __name__ == "__main__":
port = int(os.environ.get("PORT", 7860))
demo.launch(
server_name="0.0.0.0",
server_port=port,
share=False # share=True won't work on Render
)