jsx import React, { useState, useEffect, useRef, useCallback } from 'react'; // --- CONFIGURATION & TOPOLOGY DATA --- // 1. Node Definitions (Geographic Layout roughly based on a region) const INITIAL_ASSETS = [ // Substations { id: 'S1', type: 'substation', name: 'North Hub', x: 100, y: 100, voltage: 400, capacity: 1000 }, { id: 'S2', type: 'substation', name: 'East Industrial', x: 450, y: 150, voltage: 400, capacity: 1200 }, { id: 'S3', type: 'substation', name: 'West Residential', x: 150, y: 350, voltage: 400, capacity: 800 }, { id: 'S4', type: 'substation', name: 'South Metro', x: 350, y: 450, voltage: 400, capacity: 1500 }, { id: 'S5', type: 'substation', name: 'Central Grid', x: 300, y: 280, voltage: 400, capacity: 2000 }, { id: 'S6', type: 'substation', name: 'Coastal', x: 550, y: 350, voltage: 400, capacity: 900 }, { id: 'S7', type: 'substation', name: 'Mountain', x: 50, y: 250, voltage: 400, capacity: 600 }, { id: 'S8', type: 'substation', name: 'Airport', x: 400, y: 100, voltage: 400, capacity: 700 }, // Generation { id: 'G_NUC', type: 'nuclear', name: 'Nuclear Plant A', x: 50, y: 50, output: 0, maxOutput: 1200 }, { id: 'G_GAS', type: 'gas', name: 'CCGT Plant B', x: 500, y: 50, output: 0, maxOutput: 800 }, { id: 'G_WIND', type: 'wind', name: 'Wind Farm C', x: 600, y: 450, output: 0, maxOutput: 500 }, // 40 Turbines simulated // Extra substations to reach 12 { id: 'S9', type: 'substation', name: 'Substation 9', x: 200, y: 150, voltage: 400, capacity: 700 }, { id: 'S10', type: 'substation', name: 'Substation 10', x: 450, y: 300, voltage: 400, capacity: 900 }, { id: 'S11', type: 'substation', name: 'Substation 11', x: 250, y: 450, voltage: 400, capacity: 800 }, { id: 'S12', type: 'substation', name: 'Substation 12', x: 100, y: 400, voltage: 400, capacity: 600 }, ]; // 2. Line Definitions const INITIAL_LINES = [ { id: 'L1', source: 'G_NUC', target: 'S1', capacity: 1000 }, { id: 'L2', source: 'S1', target: 'S9', capacity: 600 }, { id: 'L3', source: 'S1', target: 'S7', capacity: 400 }, { id: 'L4', source: 'S9', target: 'S5', capacity: 800 }, { id: 'L5', source: 'S5', target: 'S2', capacity: 900 }, { id: 'L6', source: 'S5', target: 'S3', capacity: 700 }, { id: 'L7', source: 'S2', target: 'S8', capacity: 500 }, { id: 'L8', source: 'S2', target: 'S10', capacity: 600 }, { id: 'L9', source: 'S10', target: 'S4', capacity: 800 }, { id: 'L10', source: 'S10', target: 'S6', capacity: 700 }, { id: 'L11', source: 'S6', target: 'G_WIND', capacity: 600 }, { id: 'L12', source: 'S4', target: 'S11', capacity: 900 }, { id: 'L13', source: 'S3', target: 'S12', capacity: 500 }, { id: 'L14', source: 'S12', target: 'S11', capacity: 400 }, { id: 'L15', source: 'G_GAS', target: 'S8', capacity: 900 }, ]; // --- HELPER: SIMPLE GRID PHYSICS --- const useGridSimulation = (assets, lines) => { const [state, setState] = useState({ assets: JSON.parse(JSON.stringify(assets)), lines: JSON.parse(JSON.stringify(lines)), frequency: 50.0, alarms: [], totalLoad: 2500, }); const [simRunning, setSimRunning] = useState(true); const [selectedId, setSelectedId] = useState(null); // Main Loop: 1Hz update useEffect(() => { if (!simRunning) return; const interval = setInterval(() => { setState((prev) => { let newAssets = JSON.parse(JSON.stringify(prev.assets)); let newLines = JSON.parse(JSON.stringify(prev.lines)); let newAlarms = []; // 1. Update Generation based on demand and regulation let totalGen = 0; let nuclearOut = 0; let gasOut = 0; let windOut = 0; // Calculate base load (simulate daily curve + noise) const baseLoad = 2500 + Math.sin(Date.now() / 100000) * 500; const currentLoad = baseLoad + (Math.random() * 100 - 50); // Renewable generation (Wind) windOut = Math.min(500, 400 + Math.random() * 50); // Wind varies totalGen += windOut; // Frequency Droop Logic: // F = 50 - k * (Pgen - Pload) / SystemInertia // For simulation, we assume gas responds fast, nuclear slow. // Fill gap with Gas if (totalGen < currentLoad) { gasOut = Math.min(800, (currentLoad - totalGen) + 100); // Gas usually covers base + some peak totalGen += gasOut; } // Fill gap with Nuclear if (totalGen < currentLoad) { nuclearOut = Math.min(1200, (currentLoad - totalGen)); totalGen += nuclearOut; } // Update Asset outputs const findAsset = (id) => newAssets.find(a => a.id === id); const nuc = findAsset('G_NUC'); if(nuc) nuc.output = nuclearOut; const gas = findAsset('G_GAS'); if(gas) gas.output = gasOut; const wind = findAsset('G_WIND'); if(wind) wind.output = windOut; // 2. Simplified DC Power Flow (Redistribute based on generation injection at nodes) // Reset line flows newLines.forEach(l => l.flow = 0); // Calculate Net Injection at nodes newAssets.forEach(a => { a.netInjection = 0; if (a.type === 'nuclear' || a.type === 'gas' || a.type === 'wind') { a.netInjection = a.output; } else { // Substations act as loads (approx 10-15% of capacity as load) const load = (a.capacity * 0.1) * (currentLoad / 2500); a.netInjection = -load; } }); // Simple flow assignment logic (Heuristic: Path of least resistance) // In a real system, use Matpower/DcOpf. Here we simulate path logic. // We assume power flows from Gen to nearest Load substations primarily. // Manual routing simulation for demo const route = (src, tgt, mw) => { // Find path let path = []; // Simple BFS or predefined routes for stability in this demo const graph = {}; newLines.forEach(l => { if(!graph[l.source]) graph[l.source] = []; if(!graph[l.target]) graph[l.target] = []; graph[l.source].push({id: l.id, target: l.target}); graph[l.target].push({id: l.id, target: l.source}); }); // BFS implementation const queue = [[src]]; const visited = new Set(); while(queue.length > 0) { const path = queue.shift(); const node = path[path.length-1]; if (node === tgt) { break; } if (visited.has(node)) continue; visited.add(node); const neighbors = graph[node] || []; for(const n of neighbors) { if (!visited.has(n.target)) { const newPath = [...path, n.target]; queue.push(newPath); } } } if(path.length > 0 && path[path.length-1] === tgt) { // Assign flow to lines in path for(let i=0; i (l.source === path[i] && l.target === path[i+1]) || (l.target === path[i] && l.source === path[i+1])); if(l) l.flow += mw / path.length; // Distribute slightly } } }; // Route Gen -> Loads route('G_NUC', 'S12', nuclearOut * 0.4); route('G_NUC', 'S4', nuclearOut * 0.6); route('G_GAS', 'S8', gasOut); route('G_WIND', 'S6', windOut); // 3. Frequency Deviation Calculation // ACE = Gen - Load const ACE = totalGen - currentLoad; // Positive = Over gen (Freq rises), Negative = Under gen (Freq drops) // Simplified Inertia Response // Frequency drops if load > generation const freqDelta = -0.01 * (ACE / 100); // Damping factor const newFreq = Math.max(49.5, Math.min(50.5, 50.0 + freqDelta + (Math.random() * 0.02 - 0.01))); // 4. Alarms newLines.forEach(l => { if (l.flow > l.capacity * 0.9) { newAlarms.push({ id: l.id, severity: l.flow > l.capacity ? 'CRITICAL' : 'WARNING', msg: `Overload: ${l.id}` }); } }); if (newFreq < 49.8 || newFreq > 50.2) { newAlarms.push({ id: 'FREQ', severity: 'WARNING', msg: `Freq Deviation: ${newFreq.toFixed(2)} Hz` }); } return { assets: newAssets, lines: newLines, frequency: newFreq, alarms: newAlarms, totalLoad: currentLoad }; }); }, 1000); return () => clearInterval(interval); }, [simRunning]); // Trigger Event (Frequency Regulation Simulation) const triggerLoadSpike = () => { setState(prev => ({ ...prev, totalLoad: prev.totalLoad + 800 })); }; return { state, simRunning, setSimRunning, selectedId, setSelectedId, triggerLoadSpike }; }; // --- COMPONENTS --- const Gauge = ({ value, max, label, unit, color = 'cyan' }) => { const pct = Math.min(100, (value / max) * 100); return (
{value.toFixed(1)}
{label} {unit}
); }; const AlarmPanel = ({ alarms }) => { return (

Active Alarms

{alarms.length === 0 &&
System Normal
} {alarms.map((alarm, i) => (
{alarm.severity}: {alarm.msg}
))}
); }; // --- MAIN DASHBOARD COMPONENT --- export default function SCADADashboard() { const { state, simRunning, setSimRunning, selectedId, setSelectedId, triggerLoadSpike } = useGridSimulation(INITIAL_ASSETS, INITIAL_LINES); // N-1 Contingency Logic const analyzeContingency = (id) => { setSelectedId(id); // Logic: Remove element, simulate overload. // In a real UI, we would clone the state and re-run power flow. // Here we visually flag assets in the same "zone" or connected paths. alert(`N-1 Analysis for ${id}: \nSimulating trip...\nCalculating re-dispatch...`); }; return (
{/* HEADER */}

GRID_CONTROL_V2.0 // REGIONAL SCADA

FREQ: {state.frequency.toFixed(3)} Hz
SYSTEM LOAD: {(state.totalLoad/1000).toFixed(1)} GW
{/* LEFT SIDEBAR: ASSETS & CONTROLS */}

Frequency Regulation

Simulate sudden loss of generation or load spike to test grid inertia response.

Asset List (Click to Analyze)

    {state.assets.map(a => (
  • analyzeContingency(a.id)} className={`cursor-pointer p-2 rounded text-xs hover:bg-gray-800 border border-transparent hover:border-cyan-800 ${selectedId === a.id ? 'bg-gray-800 border-cyan-500' : ''}`} >
    {a.name} {a.type.toUpperCase()}
    V: {a.voltage || '-'} {a.output !== undefined && MW: {a.output.toFixed(0)}}
  • ))}
{/* CENTER: TOPOLOGY VIEW */}
{/* Grid Background Pattern */}
{/* Render Lines */} {state.lines.map(line => { const src = state.assets.find(a => a.id === line.source); const tgt = state.assets.find(a => a.id === line.target); // Calculate line loading color and width const loadPct = line.flow / line.capacity; const strokeColor = loadPct > 0.9 ? 'stroke-red-500' : loadPct > 0.7 ? 'stroke-yellow-500' : 'stroke-cyan-400'; const strokeWidth = Math.max(1, loadPct * 8); // N-1 Highlight const isSelectedLine = selectedId === line.id; const isOverload = loadPct > 0.9; return ( analyzeContingency(line.id)} className="cursor-pointer group"> {/* Glow Effect for Overload */} {isOverload && } {/* Main Line */} {/* Flow Particles (Animation) */} {line.flow > 0 && ( )} {/* Labels */} {line.flow.toFixed(0)} MW ); })} {/* Render Nodes */} {state.assets.map(node => ( analyzeContingency(node.id)} className="cursor-pointer"> {node.type === 'substation' && ( <> )} {node.type === 'nuclear' && ( )} {node.type === 'gas' && ( )} {node.type === 'wind' && ( <> {/* Simple 40 turbine representation */} {[0, 60, 120, 180, 240, 300].map(angle => ( ))} )} {/* Node Label */} {node.name} {/* Live Data on Node */} {node.type !== 'substation' && ( {node.output.toFixed(0)} MW )} ))}
Power Flow: Animated Dashed Lines
Thickness: MW Capacity Loading
{/* RIGHT SIDEBAR: METRICS & ALARMS */}

Grid Status

Nuclear {(state.assets.find(a=>a.id==='G_NUC')?.output || 0).toFixed(0)} MW
Gas (CCGT) {(state.assets.find(a=>a.id==='G_GAS')?.output || 0).toFixed(0)} MW
Wind {(state.assets.find(a=>a.id==='G_WIND')?.output || 0).toFixed(0)} MW
Efficiency 98.2%
N-1 CONTINGENCY MODE
{selectedId ? `Selected: ${selectedId}. Click 'Analyze' in Asset List to simulate failure cascade.` : 'Select an Asset or Line to begin.'}
); }