# Electrical grid ## Microsim <iframe src="https://editor.p5js.org/sciencenibber/full/o-KZ5R9hV" width="100%" height="620" frameborder="0" sandbox="allow-scripts allow-same-origin"></iframe> <img src="../SPINTRONICS Images/Electrical_grid.png" alt="Electrical_grid microsim"> *Live sketch: [open in the p5.js editor](https://editor.p5js.org/sciencenibber/sketches/o-KZ5R9hV). The poster image above is a placeholder pending an attended or server-side canvas capture.* ### p5.js source ```js // Electrical_grid.js -- Wikitube MicroSim // Hub: SPINTRONICS · Branch: P - Power transmission // Pattern: systems/balance schematic + analog gauge + time-series trace // (A-V pattern G composed with H). The whole synchronous grid is // lumped into ONE rotating machine whose speed is the system // frequency. CONTINUOUS-LOOP (loop()) because the model evolves in // time -- but per-frame work is deliberately tiny (one ODE step, a // handful of shapes, one bounded polyline) to stay clear of the // editor loop-protect guard. // // CONCEPT // An AC grid generates and consumes power in the same instant, so total // generation must match total load continuously. The mismatch does not // change "how much power is left" -- it changes the SYSTEM FREQUENCY. Treat // every spinning generator + motor as one aggregate rotor with inertia // constant H (seconds = stored kinetic energy per S_base). Newton's 2nd law // for that rotor, in frequency form, is the SWING EQUATION: // // df/dt = f0 * P_acc / (2 * H * S_base) // P_acc = P_gen + P_gov - P_load - P_damp // // Governor droop R gives PRIMARY frequency response, // P_gov = -(1/R) * S_base * (f - f0)/f0 (clamped to a finite reserve) // and load damping D makes load fall as frequency falls, // P_damp = D * S_base * (f - f0)/f0 . // Setting df/dt = 0 gives the droop steady state (a deliberate residual // error that real grids remove with slower secondary control / AGC): // (f - f0) = (P_gen - P_load) / beta , beta = (1/R + D) * S_base / f0. // The classic event is a generator TRIP (sudden -150 MW): the frequency // dives at a rate set by inertia, governors arrest it at a nadir, and it // settles at the droop steady state. (Single-area lumped model; tie-line // exchange, voltage/reactive power and network topology are abstracted away.) // // GOLDEN RULES honoured: 720x520 + pixelDensity(2); layout from width/height; // ASCII-only strings (Unicode only in comments); SI-ish engineering units // (MW, Hz, s) convert at the input; dt = min(deltaTime/1000, 0.05); a damped // forced FIRST-order system, so a dt-capped semi-implicit step is stable and // correct (velocity-Verlet is for energy-conserving 2nd-order systems and // does not apply); static scenery baked once into an offscreen buffer; one // concept per control; reset restores ALL state; HUD watermark drawn last; // p5.disableFriendlyErrors = true. No identifier collides with a p5 global or // method name -- in particular the WEBGL-reserved name GRID is never used as // an identifier (only in comments / on-canvas strings). const ARTICLE = "Electrical_grid"; // single source of truth (HUD + save + URL) // ---- fixed system constants ---- const F0 = 50; // f0, Hz nominal frequency (50 Hz region; 60 Hz elsewhere) const S_BASE = 1000; // S_base, MW system base power (a ~1 GW control area) const D_DAMP = 1.0; // D, per unit load frequency-damping (fixed) const TRIP_MW = 150; // MW generation lost when a unit trips (N-1 event) const GOV_HEADROOM= 0.30; // governor reserve as a fraction of S_base (finite) const TG = 4.0; // s governor/turbine response time constant (lag) // ---- frequency display window + protection thresholds ---- const FLO = 47, FHI = 53; // gauge + trace span, Hz const F_SHED_LO = 48.5; // under-frequency load-shedding threshold const F_SHED_HI = 51.5; // over-frequency threshold const F_OK_LO = 49.8, F_OK_HI = 50.2; // green nominal band // ---- rotor visual spin (a visualization, not the real 50 rev/s) ---- const BASE_SPIN = 1.30; // rad/s at nominal const DEV_GAIN = 0.90; // rad/s per Hz of deviation (exaggerates dev) // ---- trace ring buffer ---- const TRACE_DT = 0.10; // s between stored samples const TRACE_N = 300; // samples -> 30 s window // ---- control ranges (real symbols, meaningful ranges) ---- const LOAD_MIN = 200, LOAD_MAX = 1000, LOAD_DEF = 600; // P_load, MW const GEN_MIN = 200, GEN_MAX = 1000, GEN_DEF = 600; // P_gen, MW const H_MIN = 1, H_MAX = 10, H_DEF = 5; // H, s const DROOP_MIN= 2, DROOP_MAX= 10, DROOP_DEF= 5; // R, percent const MAXBAR = 1300; // MW at full balance-bar height // ---- dynamic state ---- let fHz; // current system frequency, Hz let pgovState; // MW delivered governor power (lags its droop target) let rotorAngle; // rotor visual phase, rad let tripOffset; // MW accumulated tripped generation (<= 0); reset clears let rocof; // df/dt, Hz/s (stored for HUD) let traceBuf, traceIdx, traceCount, traceTimer; // ---- controls ---- let loadSlider, genSlider, inertiaSlider, droopSlider, tripButton, resetButton; // ---- layout (all derived; bake-once friendly) ---- let rotorCX, rotorCY, rotorR; let gaugeCX, gaugeCY, gaugeR; let barX0, barX1, barBaseY, barMaxH, barW; let chartX0, chartX1, chartTop, chartBot; let ctrlDivY; // ---- baked static scenery ---- let scenery; // ---- palette (ASCII identifiers; none collide with p5 globals) ---- let BG, INK, MUTE, FRAME, COOL, WARM, GENC, LOADC, GREENB, AMBERB, REDB, ACCENT, GOOD, BAD; function setup() { createCanvas(720, 520); pixelDensity(2); p5.disableFriendlyErrors = true; // clean + cheap; FES off at runtime textFont("monospace"); // --- palette --- BG = color(13, 17, 28); INK = color(232, 238, 248); MUTE = color(122, 136, 160); FRAME = color(58, 70, 94); COOL = color(86, 170, 255); // slow / under-frequency WARM = color(255, 96, 78); // fast / over-frequency GENC = color(108, 222, 150); // generation (green) LOADC = color(255, 150, 96); // load (amber) GREENB = color(96, 214, 140); // gauge nominal band AMBERB = color(240, 188, 92); // gauge caution band REDB = color(232, 96, 96); // gauge danger band ACCENT = color(255, 200, 96); // needle / markers GOOD = color(120, 230, 150); BAD = color(255, 110, 120); // --- derive geometry --- rotorCX = 118; rotorCY = 132; rotorR = 60; gaugeCX = 372; gaugeCY = 156; gaugeR = 82; barX0 = 556; barX1 = 700; barBaseY = 188; barMaxH = 120; barW = 36; chartX0 = 60; chartX1 = 694; chartTop = 214; chartBot = 344; ctrlDivY = 354; // --- dynamic state --- traceBuf = new Array(TRACE_N).fill(F0); resetState(); buildControls(); buildScenery(); // bake static layers once // CONTINUOUS LOOP: the swing equation evolves in time; draw() stays cheap. } function resetState() { fHz = F0; pgovState = 0; rotorAngle = 0; tripOffset = 0; rocof = 0; traceIdx = 0; traceCount = 0; traceTimer = 0; for (let k = 0; k < TRACE_N; k++) traceBuf[k] = F0; // integer-bounded } function buildControls() { // one slider per concept, carrying the field's real symbol + a real range loadSlider = createSlider(LOAD_MIN, LOAD_MAX, LOAD_DEF, 10); // P_load, MW genSlider = createSlider(GEN_MIN, GEN_MAX, GEN_DEF, 10); // P_gen, MW inertiaSlider = createSlider(H_MIN, H_MAX, H_DEF, 1); // H, s (integer) droopSlider = createSlider(DROOP_MIN,DROOP_MAX,DROOP_DEF, 1); // R, percent loadSlider.position(24, 392); loadSlider.style("width", "170px"); genSlider.position(24, 424); genSlider.style("width", "170px"); inertiaSlider.position(24, 456); inertiaSlider.style("width", "170px"); droopSlider.position(24, 488); droopSlider.style("width", "170px"); tripButton = createButton("trip generator (-150 MW)"); tripButton.position(250, 470); tripButton.mousePressed(tripGen); resetButton = createButton("reset"); resetButton.position(250, 500); resetButton.mousePressed(resetAll); } function resetAll() { // reset restores ALL state, not just some loadSlider.value(LOAD_DEF); genSlider.value(GEN_DEF); inertiaSlider.value(H_DEF); droopSlider.value(DROOP_DEF); resetState(); } function tripGen() { // sudden loss of a generation block (N-1 contingency); reset clears it tripOffset -= TRIP_MW; } // ---- model helpers (MW in / MW out) ---- function governorMW(f, Rpu) { // primary frequency response: more generation when f sags, clamped to reserve let p = -(1 / Rpu) * S_BASE * (f - F0) / F0; return constrain(p, -GOV_HEADROOM * S_BASE, GOV_HEADROOM * S_BASE); } function dampingMW(f) { // load draws less power as frequency falls (negative when f < f0) return D_DAMP * S_BASE * (f - F0) / F0; } // frequency -> needle/band angle on the top semicircle (PI=left .. TWO_PI=right) function angleFor(f) { return map(constrain(f, FLO, FHI), FLO, FHI, PI, TWO_PI); } // integrate the swing equation one capped step; advance visuals + trace function advanceModel(dt, demand, genSet, Hsec, Rpu) { const genEff = max(0, genSet + tripOffset); // effective scheduled gen // governor/turbine LAG: delivered governor power chases its droop target with // time constant TG. This lag is what makes the frequency undershoot to a // NADIR before recovering to the droop steady state -- a purely algebraic // (instantaneous) governor would give a monotonic decay with no nadir. const target = governorMW(fHz, Rpu); pgovState += (target - pgovState) / TG * dt; const pdamp = dampingMW(fHz); const pacc = genEff + pgovState - demand - pdamp; // accelerating power, MW rocof = F0 * pacc / (2 * Hsec * S_BASE); // df/dt, Hz/s fHz = constrain(fHz + rocof * dt, FLO, FHI); // semi-implicit, clamped const omega = BASE_SPIN + DEV_GAIN * (fHz - F0); // visual spin rate, rad/s rotorAngle += omega * dt; // sample the scrolling trace at a fixed cadence (bounded ring buffer) traceTimer += dt; if (traceTimer >= TRACE_DT) { traceTimer -= TRACE_DT; traceBuf[traceIdx] = fHz; traceIdx = (traceIdx + 1) % TRACE_N; if (traceCount < TRACE_N) traceCount++; } } function draw() { // --- read every control ONCE into named locals (Golden Rule 3) --- const demand = loadSlider.value(); // P_load, MW const genSet = genSlider.value(); // P_gen setpoint, MW const Hsec = inertiaSlider.value(); // H, s const Rpu = droopSlider.value() / 100; // R, per unit const dt = min(deltaTime / 1000, 0.05); // frame-rate independent, capped advanceModel(dt, demand, genSet, Hsec, Rpu); // --- derived quantities (recomputed from the updated fHz) --- const genEff = max(0, genSet + tripOffset); const pgov = pgovState; // delivered governor power (lagged) const pdamp = dampingMW(fHz); const genTot = genEff + pgov; // effective generation, MW const loadTot = demand + pdamp; // effective load, MW const pacc = genTot - loadTot; // accelerating power, MW const beta = (1 / Rpu + D_DAMP) * S_BASE / F0; // frequency response, MW/Hz const ssDev = (genEff - demand) / beta; // steady-state f - f0, Hz background(BG); image(scenery, 0, 0); // blit baked static scenery drawRotor(pacc); drawGauge(); drawBalance(genTot, loadTot, pacc); drawTrace(); drawReadouts(demand, genSet, genEff, Hsec, Rpu, pgov, pacc, beta, ssDev); drawHUD(); // HUD watermark, drawn LAST } // ---- the grid as one spinning rotor (speed encodes frequency) ---- function drawRotor(pacc) { push(); translate(rotorCX, rotorCY); // disk colour: blue (slow/under) -> grey (nominal) -> red (fast/over) const t = constrain(map(fHz, F0 - 1, F0 + 1, 0, 1), 0, 1); const disk = lerpColor(COOL, WARM, t); noStroke(); fill(red(disk), green(disk), blue(disk), 60); circle(0, 0, rotorR * 2 + 14); // soft halo fill(disk); circle(0, 0, rotorR * 2); // rotor body // 6 spokes + a bright index mark (integer-bounded loop) stroke(13, 17, 28, 200); strokeWeight(4); for (let k = 0; k < 6; k++) { const a = rotorAngle + k * TWO_PI / 6; line(0, 0, cos(a) * rotorR * 0.9, sin(a) * rotorR * 0.9); } const ai = rotorAngle; // index spoke stroke(ACCENT); strokeWeight(5); line(0, 0, cos(ai) * rotorR * 0.9, sin(ai) * rotorR * 0.9); noStroke(); fill(ACCENT); circle(cos(ai) * rotorR * 0.9, sin(ai) * rotorR * 0.9, 9); fill(INK); circle(0, 0, 12); // hub // accelerating / decelerating indicator (sign of P_acc) const accCol = pacc >= 0 ? GENC : BAD; noFill(); stroke(accCol); strokeWeight(3); const a0 = -HALF_PI, a1 = -HALF_PI + (pacc >= 0 ? 1 : -1) * 1.1; arc(0, 0, rotorR * 2 + 26, rotorR * 2 + 26, min(a0, a1), max(a0, a1)); pop(); noStroke(); textAlign(CENTER, TOP); textSize(10); fill(MUTE); text("rotor speed ~ frequency", rotorCX, rotorCY + rotorR + 20); textSize(11); fill(pacc >= 0 ? GENC : BAD); text(pacc >= 0 ? "accelerating (gen > load)" : "decelerating (load > gen)", rotorCX, rotorCY + rotorR + 34); } // ---- analog frequency gauge (needle is the only dynamic part) ---- function drawGauge() { const a = angleFor(fHz); const rN = gaugeR - 8; // needle stroke(ACCENT); strokeWeight(3); line(gaugeCX, gaugeCY, gaugeCX + cos(a) * rN, gaugeCY + sin(a) * rN); noStroke(); fill(ACCENT); circle(gaugeCX, gaugeCY, 12); // big numeric readout under the hub textAlign(CENTER, TOP); noStroke(); const off = abs(fHz - F0); const col = off <= 0.2 ? GOOD : (fHz < F_SHED_LO || fHz > F_SHED_HI ? BAD : ACCENT); fill(col); textSize(26); text(nf(fHz, 0, 2) + " Hz", gaugeCX, gaugeCY + 8); fill(MUTE); textSize(11); text("ROCOF df/dt = " + fmtRocof(rocof), gaugeCX, gaugeCY + 36); // protection flag textSize(12); if (fHz <= F_SHED_LO) { fill(BAD); text("UNDER-FREQ: load-shedding zone", gaugeCX, gaugeCY + 54); } else if (fHz >= F_SHED_HI) { fill(BAD); text("OVER-FREQ: trip zone", gaugeCX, gaugeCY + 54); } } // ---- generation-vs-load balance bars ---- function drawBalance(genTot, loadTot, pacc) { const gh = map(constrain(genTot, 0, MAXBAR), 0, MAXBAR, 0, barMaxH); const lh = map(constrain(loadTot, 0, MAXBAR), 0, MAXBAR, 0, barMaxH); const gx = barX0 + 14, lx = barX0 + 14 + barW + 26; noStroke(); fill(GENC); rect(gx, barBaseY - gh, barW, gh, 3); fill(LOADC); rect(lx, barBaseY - lh, barW, lh, 3); // bracket showing the gap (= accelerating power) const yTopG = barBaseY - gh, yTopL = barBaseY - lh; stroke(pacc >= 0 ? GENC : BAD); strokeWeight(1.5); line(gx + barW / 2, yTopG, lx + barW / 2, yTopL); noStroke(); fill(pacc >= 0 ? GENC : BAD); textAlign(CENTER, BOTTOM); textSize(11); text((pacc >= 0 ? "+" : "") + pacc.toFixed(0) + " MW", (gx + lx + barW) / 2, min(yTopG, yTopL) - 4); // value labels fill(INK); textSize(11); textAlign(CENTER, TOP); text(genTot.toFixed(0), gx + barW / 2, barBaseY + 4); text(loadTot.toFixed(0), lx + barW / 2, barBaseY + 4); } // ---- scrolling frequency-vs-time trace (one bounded polyline) ---- function drawTrace() { if (traceCount < 2) return; noFill(); stroke(ACCENT); strokeWeight(2); beginShape(); for (let i = 0; i < traceCount; i++) { // integer-bounded const idx = (traceIdx - traceCount + i + TRACE_N) % TRACE_N; const x = map(i, 0, TRACE_N - 1, chartX0, chartX1); const y = map(traceBuf[idx], FLO, FHI, chartBot, chartTop); vertex(x, y); } endShape(); // moving dot at the newest sample const xNow = map(traceCount - 1, 0, TRACE_N - 1, chartX0, chartX1); const yNow = map(fHz, FLO, FHI, chartBot, chartTop); noStroke(); fill(ACCENT); circle(xNow, yNow, 7); } // ---- control-region numeric readouts (labels + values) ---- function drawReadouts(demand, genSet, genEff, Hsec, Rpu, pgov, pacc, beta, ssDev) { noStroke(); textAlign(LEFT, CENTER); textSize(12); fill(INK); text("P_load = " + demand + " MW", 210, 401); text("P_gen = " + genSet + " MW" + (tripOffset < 0 ? " (trip " + tripOffset + ")" : ""), 210, 433); text("H = " + Hsec + " s", 210, 465); text("R = " + (Rpu * 100).toFixed(0) + " %", 210, 497); // results block (right column) const bx = 470, by = 392; textAlign(LEFT, TOP); textSize(12); fill(INK); text("P_gov = " + (pgov >= 0 ? "+" : "") + pgov.toFixed(0) + " MW", bx, by); text("P_acc = " + (pacc >= 0 ? "+" : "") + pacc.toFixed(0) + " MW", bx, by + 17); text("beta = " + beta.toFixed(0) + " MW/Hz", bx, by + 34); const settle = F0 + ssDev; fill(abs(ssDev) <= 0.2 ? GOOD : ACCENT); text("settles -> " + settle.toFixed(2) + " Hz", bx, by + 51); fill(MUTE); textSize(10); text("(droop residual " + (ssDev >= 0 ? "+" : "") + ssDev.toFixed(2) + " Hz; AGC would null it)", bx, by + 68); } // ---- HUD watermark: title, URL, control hints, live equation footer ---- function drawHUD() { noStroke(); textAlign(LEFT, TOP); fill(INK); textSize(15); text("Electrical Grid -- balancing generation against load in real time", 16, 12); fill(MUTE); textSize(11); text("en.wikitube.io/wiki/Electrical_grid", 16, 33); // control hints near the controls text("drag P_load / P_gen / H / R | trip a unit | reset", 210, 372); // live equation footer (drawn last, bottom) fill(MUTE); textSize(11.5); textAlign(LEFT, BOTTOM); text("df/dt = f0*P_acc/(2 H S_base) P_gov = -(1/R) S_base (f-f0)/f0 f0=50 Hz S_base=1000 MW", 16, height - 8); } // ---- baked static scenery (never changes -> offscreen buffer) ---- function buildScenery() { scenery = createGraphics(720, 520); const g = scenery; g.pixelDensity(2); g.background(BG); g.textFont("monospace"); // --- rotor stator ring + label --- g.noFill(); g.stroke(FRAME); g.strokeWeight(2); g.circle(rotorCX, rotorCY, rotorR * 2 + 30); g.noStroke(); g.fill(INK); g.textSize(12); g.textAlign(CENTER, BOTTOM); g.text("GRID ROTOR", rotorCX, rotorCY - rotorR - 22); g.fill(MUTE); g.textSize(9); g.text("all generators in sync", rotorCX, rotorCY - rotorR - 10); // --- gauge face: coloured bands + ticks --- buildGaugeFace(g); // --- balance panel frame + labels --- g.stroke(FRAME); g.strokeWeight(1.5); g.line(barX0, barBaseY, barX1, barBaseY); // baseline g.noStroke(); g.fill(INK); g.textSize(12); g.textAlign(CENTER, BOTTOM); g.text("power balance (MW)", (barX0 + barX1) / 2, barBaseY - barMaxH - 8); g.fill(GENC); g.textSize(11); g.textAlign(CENTER, TOP); g.text("GEN", barX0 + 14 + barW / 2, barBaseY + 18); g.fill(LOADC); g.text("LOAD", barX0 + 14 + barW + 26 + barW / 2, barBaseY + 18); // --- trace chart frame --- buildTraceFrame(g); // --- divider between drawing region and control region --- g.stroke(FRAME); g.strokeWeight(1); g.line(16, ctrlDivY, 704, ctrlDivY); } function buildGaugeFace(g) { // coloured arc bands across the top semicircle (fixed thresholds -> static) g.noFill(); g.strokeWeight(11); drawBand(g, FLO, F_SHED_LO, REDB); drawBand(g, F_SHED_LO, F_OK_LO, AMBERB); drawBand(g, F_OK_LO, F_OK_HI, GREENB); drawBand(g, F_OK_HI, F_SHED_HI, AMBERB); drawBand(g, F_SHED_HI, FHI, REDB); // ticks + labels at each integer Hz (integer-bounded loop) g.textAlign(CENTER, CENTER); g.textSize(9); for (let k = 0; k <= 6; k++) { const fv = FLO + k; // 47..53 const a = angleFor(fv); const xo = gaugeCX + cos(a) * (gaugeR + 4); const yo = gaugeCY + sin(a) * (gaugeR + 4); const xi = gaugeCX + cos(a) * (gaugeR - 14); const yi = gaugeCY + sin(a) * (gaugeR - 14); g.stroke(MUTE); g.strokeWeight(1); g.line(xi, yi, xo, yo); g.noStroke(); g.fill(fv === F0 ? INK : MUTE); const xt = gaugeCX + cos(a) * (gaugeR + 16); const yt = gaugeCY + sin(a) * (gaugeR + 16); g.text(fv, xt, yt); } g.noStroke(); g.fill(MUTE); g.textAlign(CENTER, TOP); g.textSize(11); g.text("system frequency (Hz)", gaugeCX, gaugeCY - gaugeR - 16); } // stroke one gauge band arc from frequency fa to fb function drawBand(g, fa, fb, c) { g.stroke(c); g.arc(gaugeCX, gaugeCY, gaugeR * 2, gaugeR * 2, angleFor(fa), angleFor(fb)); } function buildTraceFrame(g) { // title g.noStroke(); g.fill(INK); g.textSize(12); g.textAlign(LEFT, BOTTOM); g.text("frequency vs time (last 30 s)", chartX0, chartTop - 8); // danger bands (shaded) + nominal line g.noStroke(); g.fill(232, 96, 96, 26); // under-freq band let yb = map(F_SHED_LO, FLO, FHI, chartBot, chartTop); g.rect(chartX0, yb, chartX1 - chartX0, chartBot - yb); let yt = map(F_SHED_HI, FLO, FHI, chartBot, chartTop); // over-freq band g.rect(chartX0, chartTop, chartX1 - chartX0, yt - chartTop); // axes g.stroke(FRAME); g.strokeWeight(1.5); g.line(chartX0, chartTop, chartX0, chartBot); // y axis g.line(chartX0, chartBot, chartX1, chartBot); // x axis // y gridlines + labels at 48,49,50,51,52 (integer-bounded) g.textSize(9); g.textAlign(RIGHT, CENTER); for (let k = 0; k <= 4; k++) { const fv = 48 + k; // 48..52 const y = map(fv, FLO, FHI, chartBot, chartTop); g.stroke(fv === F0 ? GREENB : color(32, 42, 60)); g.line(chartX0, y, chartX1, y); g.noStroke(); g.fill(fv === F0 ? GREENB : MUTE); g.text(fv, chartX0 - 6, y); g.stroke(FRAME); } g.noStroke(); g.fill(MUTE); g.textAlign(LEFT, TOP); g.textSize(10); g.text("f (Hz)", chartX0 + 2, chartTop + 2); g.textAlign(RIGHT, TOP); g.text("now ->", chartX1, chartBot + 4); } // ---- ASCII engineering-unit formatters ---- function fmtRocof(r) { const s = r >= 0 ? "+" : ""; return s + r.toFixed(3) + " Hz/s"; } function fmtMW(p) { return p.toFixed(0) + " MW"; } ``` <!-- REAL-GENERATIVE-MEDIA:START --> ## MicroSim The grid is drawn as one large **rotor** (all the synchronous machines lumped together) whose spokes spin at a rate proportional to the system frequency — speed up when generation wins, slow down when load wins. A green **generation** torque arrow pushes the rotor; a red **load** torque arrow drags it; the rotor's size stands in for its inertia. A semicircular **frequency gauge** reads `f` against a green nominal band with amber/red danger zones, and a big numeric panel shows `f` and the live ROCOF. Two **balance bars** compare total generation (setpoint + governor) against total load (demand + damping) so the accelerating power is visible at a glance. Below, a scrolling **frequency-vs-time trace** records the last ~30 seconds, with the 50 Hz nominal line and the load-shedding danger band marked — this is where the generator-trip **dip and recovery** is most striking. Four sliders set load, generation, inertia `H`, and droop `R`; a **trip generator** button fires the -150 MW contingency and **reset** restores every control and re-centres the frequency at nominal. The sketch evolves continuously in time (`loop()` with `dt = min(deltaTime/1000, 0.05)`), integrating the swing equation together with a first-order governor/turbine lag, using a dt-capped semi-implicit step — appropriate here because the governor and load damping are dissipative (a damped, forced system, not an energy-conserving one, so [[Velocity|velocity]]-Verlet does not apply). Per-frame work is deliberately tiny (one ODE step, a handful of shapes, and a single bounded polyline) to stay clear of the editor's loop-protect guard. **Possible extensions (publish/refine):** add a secondary-control (AGC) toggle that restores `f` exactly to nominal and watch the steady-state error vanish; expose the load-damping `D`; add automatic under-frequency load shedding that sheds a block when `f` crosses the threshold; add a 60 Hz region switch; split the single rotor into two areas joined by a tie-line to show inter-area power exchange and [[Oscillation|oscillation]]. ## Links (Wikipedia order) <!-- injected from _registry/childlinks/Electrical_grid.json (2026-07-30T02:09:12Z) --> `AC-to-AC_converter` · `AC_power` · `Aachen` · [[Alternating_current]] · `Ancillary_services` · `Arc-fault_circuit_interrupter` · `Automatic_generation_control` · `Availability_factor` · `Backfeeding` · `Balancing_authority` · `Base_load` · `Battery_energy_storage_system` · `Battery_room` · `Biofuel` · `Biogas` · `Biomass` · `Black_start` · `Boiler_feedwater_pump` · `Bootstrapping` · `Broadband` · `Brownout_(electricity)` · `Bus_duct` · `Busbar` · `Capacitor` · `Capacity_factor` · `Carbon_capture_and_storage` · `Carbon_monoxide` · `Carbon_offsets_and_credits` · `Cascading_failure` · `Central_Electricity_Board` · `Central_station_(electricity)` · `Circuit_breaker` · `Climate_change_mitigation` · `Clock` · `Coal` · `Coal_gas` · `Cogeneration` · `Communes_of_France` · `Compressed-air_energy_storage` · `Compressed_carbon_dioxide_energy_storage` · `Conductor_gallop` · `Contingency_(electrical_grid)` · `Cooling_tower` · `Cost_of_electricity_by_source` · `DC-to-DC_converter` · `Demand_factor` · `Demand_response` · `Diesel_generator` · `Dispatchable_generation` · `Distributed_generation` · `Distribution_board` · `Distribution_transformer` · `Droop_speed_control` · `Dynamic_demand_(electric_power)` · `Earth-leakage_circuit_breaker` · `Eastern_Interconnection` · `Economies_of_scale` · `Efficient_energy_use` · `Electranet` · `Electric_clock` · `Electric_energy_consumption` · `Electric_generator` · `Electric_power` · `Electric_power_conversion` · `Electric_power_distribution` · `Electric_power_quality` · `Electric_power_system` · [[Electric_power_transmission]] · `Electric_utility` · `Electric_vehicle` · `Electrical_busbar_system` · `Electrical_energy` · `Electrical_fault` · `Electricity` · `Electricity_(Supply)_Act_1919` · `Electricity_(Supply)_Act_1926` · `Electricity_delivery` · `Electricity_generation` · `Electricity_market` · `Electricity_retailing` · `Electrification` · `Emergency_power_system` · `Energy_Policy_Act_of_1992` · `Energy_Policy_Act_of_2005` · `Energy_crisis` · `Energy_demand_management` · `Energy_return_on_investment` · `Energy_security` · `Energy_storage` · `Energy_subsidy` · `Energy_transition` · [[Engineering]] · `Environmental_tax` · `European_Energy_Exchange` · `European_Technology_Platform_for_the_Electricity_Networks_of_the_Future` · `Excitation_(magnetic)` · `Failure_rate` · `Federal_Energy_Management_Program` · `Feed-in_tariff` · `Flexible_AC_transmission_system` · `Flywheel_energy_storage` · `Fossil_fuel` · `Fossil_fuel_phase-out` · `Fossil_fuel_power_station` · `Fuse_(electrical)` · `Gasification` · `Generator_interlock_kit` · `Geothermal_power` · `Governor_(device)` · `Grand_Coulee_Dam` · `Greenhouse_gas_emissions` · `Grid-tie_inverter` · `Grid_balancing` · `Grid_code` · `Grid_energy_storage` · `HVDC_converter_station` · `Haoji_Railway` · `Heat_engine` · `Henrik_Lund_(academic)` · `Hertz` · `High-voltage_direct_current` · `High-voltage_shore_connection` · `Home_energy_storage` · `Hospital` · `Hydroelectricity` · [[Hydrogen]] · `IPS/UPS` · `Induction_generator` · `Inertial_response` · `Interconnector` · `Inverter-based_resource` · `Islanding` · `Kinetic_energy` · `Kosovo` · `Lead–acid_battery` · `Leipzig` · `Lightning_arrester` · `List_of_electricity_sectors` · `List_of_major_power_outages` · `Load-following_power_plant` · `Load_factor_(electrical)` · `Load_management` · `Mains_electricity_by_country` · `Marginal_cost` · `Marine_current_power` · `Marine_energy` · `Merit_order` · `Merz_&_McLellan` · `Micro_combined_heat_and_power` · `Micro_hydro` · `Microgeneration_(energy)` · `Microgrid` · `Mining` · `Nameplate_capacity` · `National_Development_and_Reform_Commission` · `National_Interest_Electric_Transmission_Corridor` · [[Natural_gas]] · `Negawatt_market` · `Neptune_Bank_Power_Station` · `Net_metering` · `Newcastle_upon_Tyne` · `Non-renewable_resource` · `Nonintrusive_load_monitoring` · `North_American_Electric_Reliability_Corporation` · `North_American_power_transmission_grid` · `Northeast_blackout_of_2003` · `Nuclear_power` · `Numerical_relay` · `Ocean_thermal_energy_conversion` · `Oil_shale` · `OpenStreetMap` · `Osmotic_power` · `Overhead_power_line` · `PACE_financing` · `Peak_demand` · `Peak_oil` · `Peaking_power_plant` · `Petroleum` · `Phasor_measurement_unit` · `Photovoltaics` · `Pickens_Plan` · `Pigouvian_tax` · `Planning` · `Power-flow_study` · `Power-line_communication` · `Power-to-X` · `Power-to-gas` · `Power_Grid` · [[Power_engineering]] · `Power_factor` · `Power_inverter` · `Power_outage` · `Power_pool` · `Power_station` · `Power_supply` · `Power_system_protection` · `Power_system_reliability` · `Primary_energy` · `Protective_relay` · `Pumped-storage_hydroelectricity` · `RWTH_Aachen_University` · `Rankine_cycle` · `Recloser` · `Rectifier` · [[Redundancy_(engineering)]] · `Renewable_Energy_Certificate_(United_States)` · `Renewable_energy` · `Renewable_energy_commercialization` · `Renovation` · `Repowering` · `Residual-current_device` · `Reuters` · `Ring_main_unit` · `Rolling_blackout` · `Rural_electrification` · `Seasonal_thermal_energy_storage` · `Sector_coupling` · `Serbia` · `Sewage_treatment` · `Short_circuit` · `Single-wire_earth_return` · `Single_point_of_failure` · `Smart_grid` · `Smart_meter` · `Soft_energy_path` · `Solar_energy` · `Solar_power` · `Spark_spread` · `Substation` · `Sulfur_hexafluoride_circuit_breaker` · `SuperSmart_Grid` · `Super_grid` · `Superconducting_magnetic_energy_storage` · `Sustainable_biofuel` · `Sustainable_energy` · `Switch` · `Synchronous_condenser` · `Tap_changer` · `Texas_Interconnection` · `The_Indian_Express` · `Thermal_energy_storage` · `Three-phase_electric_power` · `Tidal_power` · `Title_42_of_the_United_States_Code` · `Tokyo` · `Tokyo_Tower` · `Transformer` · `Transmission_system_operator` · `Transmission_tower` · `Turbo_generator` · `Unified_Smart_Grid` · `United_States_Department_of_Energy` · `United_States_energy_independence` · `Utility_frequency` · `Utility_pole` · `Variable-frequency_transformer` · `Variable_renewable_energy` · `Vehicle-to-grid` · `Virtual_power_plant` · `Volt` · [[Voltage]] · `Voltage_control_and_reactive_power_management` · `Voltage_converter` · `Voltage_multiplier` · `Voltage_reduction` · `Voltage_sag` · `War_of_the_currents` · `Wave_power` · [[Wayback_Machine]] · `Western_Interconnection` · `Wide_area_synchronous_grid` · `William_Weir,_1st_Viscount_Weir` · `Wind_power` · `Zhengzhou–Wanzhou_high-speed_railway` · `Électricité_de_France` ## From the Real GENERATIVE library ![Electrical grid](https://upload.wikimedia.org/wikipedia/commons/thumb/4/41/Electricity_grid_simple-_North_America.svg/370px-Electricity_grid_simple-_North_America.svg.png) *Electrical grid — placed from the Real G.E.N.E.R.A.T.I.V.E. course library (STEM and Music room). Source: Wikimedia Commons (via Wikipedia article media). [Details & license](https://commons.wikimedia.org/wiki/File:Electricity_grid_simple-_North_America.svg).* > An electrical grid (or electricity network) is an interconnected network for electricity delivery from producers to consumers. Electrical grids consist of power stations, electrical substations to step voltage up or down, electric power transmission to carry power over long distances, and finally electric power distribution to customers. ([Wikipedia](https://en.wikipedia.org/wiki/Electrical_grid)) <!-- REAL-GENERATIVE-MEDIA:END --> *SPINTRONICS · branch **P — Power transmission** · MicroSim pattern **systems/balance schematic + analog gauge + time-series trace** (pattern G composed with H) — the whole grid lumped into one spinning machine whose speed is the [[System|system]] frequency. The user tips generation against load and watches the swing equation move the frequency in real time; a generator-trip button fires the classic nadir-and-recover event. CONTINUOUS-LOOP (`loop()`), per-frame work kept tiny. Draft staged by the headless draft queue; the publish stage adds frontmatter, the live editor iframe, and routes this into the Power-transmission branch folder.* ## Overview An **electrical grid** (or power grid) is the interconnected network that carries electricity from generators to consumers: the generating stations, the high-[[Voltage|voltage]] **transmission** lines that move bulk power across distance, and the lower-voltage **distribution** network that delivers it to the street. Most of the world's grids are **synchronous AC** systems — every generator across an entire interconnection spins in step at the same **system frequency** (nominally **50 Hz** in much of the world, **60 Hz** in the Americas). The defining operational fact of such a grid is that **electricity is generated and consumed in the same instant** — there is almost no storage in the wires themselves. Total generation must therefore match total load **continuously**, second by second. The [[Signal|signal]] that reports whether that balance holds is the system frequency: when generation exceeds load the spinning machines speed up and the frequency **rises**; when load exceeds generation they are dragged down and the frequency **falls**. Keeping the lights on is, in large part, keeping the frequency at its nominal value. ## The physics / derivation **The grid as one flywheel.** Treat every synchronous generator and motor on the system as a single aggregated rotating mass. Its stored rotational kinetic [[Energy|energy]] is summarised by the **inertia constant** `H` (in seconds): the kinetic energy stored at nominal frequency, divided by the system base power `S_base`. So the stored energy is `E_kin = H * S_base` (in MW*s) — a grid with `H = 5 s` carries five seconds' worth of its rated power as kinetic energy in spinning iron. **The swing equation.** Newton's second law for that aggregate rotor, written for frequency, is ``` df/dt = f0 * P_acc / (2 * H * S_base) ``` where `f0` is the nominal frequency and `P_acc` is the **accelerating power** — the net power pushing the rotor: ``` P_acc = P_gen + P_gov - P_load - P_damp ``` `df/dt` is the **rate of change of frequency (ROCOF)**. A large imbalance on a low-inertia system (small `H`) makes the frequency move fast; a high-inertia system (large `H`) coasts through the same imbalance slowly. This is why grid operators worry about falling inertia as spinning thermal plants are replaced by inverter-based wind and solar. **Primary frequency response (governor droop).** Generators carry **governors** that automatically adjust output when the frequency moves, with a **droop** setting `R` (per unit, e.g. `R = 0.05` = 5%): ``` P_gov = -(1/R) * S_base * (f - f0) / f0 ``` A 5% droop means a 5% change in frequency commands 100% of the unit's adjustable range. Droop is [[Negative_feedback|negative feedback]]: a frequency dip calls up more generation, which arrests the fall. Stiffer governors (smaller `R`) hold the frequency closer to nominal. Real governors and turbines do not respond instantly — the sim ramps the delivered governor power toward its droop target with a response time `TG ~ 4 s`. That lag is exactly what makes the frequency **undershoot to a nadir** before recovering to the droop steady state, reproducing the familiar dip-and-recover frequency trace. **Load [[Damping|damping]].** Loads also help: motors and many devices draw less power as frequency falls, an effect captured by a damping constant `D` (per unit, here fixed at `D = 1.0`): ``` P_damp = D * S_base * (f - f0) / f0 ``` **Steady state and the residual error.** Setting `df/dt = 0` gives the frequency the system settles to after an imbalance: ``` (f - f0)/f0 = (P_gen - P_load) / [ (1/R + D) * S_base ] ``` Primary response alone leaves a **non-zero steady-state error** — droop deliberately settles below (or above) nominal. The term `beta = (1/R + D) * S_base / f0`, in MW per Hz, is the system's **frequency response** or stiffness. Real grids restore the frequency exactly to nominal with a slower **secondary control** layer (automatic generation control, AGC) that nudges the generation setpoints; that layer is left as an extension here so the droop behaviour stays visible. **The contingency (generator trip).** The textbook grid event is the sudden loss of a large generator (an "N-1" contingency). Generation steps down instantly, `P_acc` goes negative, and the frequency falls along `df/dt`. Inertia sets how fast it dives, governors arrest it at a **nadir** (the lowest point), and the system settles at the droop steady-state above the nadir. If the dip crosses a protection threshold (typically about `f0 - 1.5 Hz`), automatic **under-frequency load shedding** disconnects blocks of load to save the grid from collapse — the sim flags this danger band rather than letting the frequency run away. ## Parameter table (controls -> real symbols) | Control | Symbol | Meaning | Range (sim) | |---------|:------:|---------|-------------| | load (demand) | `P_load` | total power consumed; the thing generation must match | 200 – 1000 MW | | generation setpoint | `P_gen` | scheduled output of online generators | 200 – 1000 MW | | system inertia | `H` | stored kinetic energy per `S_base`; sets ROCOF | 1 – 10 s | | governor droop | `R` | primary frequency response; smaller = stiffer | 2 – 10 % | | trip generator | — | instantaneous -150 MW generation loss (N-1 event) | button | *Fixed:* nominal frequency `f0 = 50 Hz`, system base `S_base = 1000 MW`, load damping `D = 1.0`. *Derived and displayed:* accelerating power `P_acc`, frequency `f`, ROCOF `df/dt`, governor response `P_gov`, steady-state deviation, and the frequency-response stiffness `beta` (MW/Hz). ## Learning objective Understand that an AC grid must **balance generation against load every instant**, and that any imbalance does not change voltage or "run out" — it changes the **system frequency**, at a rate set by the grid's rotational **inertia** (`df/dt = f0 * P_acc / (2 H S_base)`). See how **governor droop** arrests a frequency excursion but settles at a deliberate steady-state error, and watch the canonical **generator-trip nadir-and-recovery** play out when you fire the contingency. <!-- CRAFT-LINK:START g12 --> *Built to the [[WT!P5_js_Microsim_Master_Class|p5.js Master Class]].* <!-- CRAFT-LINK:END --> <!-- MATTERSIM:BEGIN g33 — Matter & Energy Cluster microsim (framework build, specs/sims/Electrical_grid.json); do not hand-edit inside --> **Microsim — three.js (Wikitube framework):** *Electrical grid* <div class="wt-sim" data-src="https://wikitube-3d-microsims.netlify.app/matter/Electrical_grid.html" data-title="Electrical grid"></div> *Built from `MICROSIM_GUIDE/specs/sims/Electrical_grid.json`; part of the [[PORTAL_Matter|Matter portal]] spine (section sims and See-also variants).* <!-- MATTERSIM:END --> ## Wikipedia : Wikitube **Strict pair:** [Wikipedia](https://en.wikipedia.org/wiki/Electrical_grid) : [Wikitube](https://en.wikitube.io/wiki/Electrical_grid) ## Previous hub tags Tree parent: [[Complex_system]]. Legacy hubs: none. --- *Sources: 1 legacy note. Minted wave 1, 2026-07-30 (v1.6 order).*