# GRIDLOCK — Model Reference

How the simulation decides what happens each tick: the decision logic, the math,
the variables, and the physical reasoning behind them. This documents the model
as implemented — where it simplifies reality, it says so.

---

## 0. The tick — order of operations

Every physics step advances `dt` *sim-seconds* (the loop runs fixed `0.5 s`
substeps for numerical stability, however fast you set game speed). One tick, in
order ([`game.js` → `tick()`](js/game.js)):

1. **Advance the clock** (and roll the day at 24:00).
2. **Step weather** — wind speed, cloud cover, hydrology drift.
3. **Apply environment** — write time-of-day demand onto every load and
   resource availability onto variable generators.
4. **Solve the grid** ([`grid.js` → `solve()`](js/grid.js)): island detection →
   per-island dispatch → power flow → swing equation → protection → diagnostics.
5. **Economy & score** — revenue, fuel cost, CO₂, reliability.
6. **Log diagnostics** as events and **sample telemetry**.

This ordering matters: demand and resource are fixed *before* dispatch sees
them, and protection runs *after* the frequency update so trips reflect the new
state.

---

## 1. Topology and islanding

The grid is a graph: **components** (generators, storage, loads, substations)
are nodes; **transmission lines** are edges. Before solving, the grid is
partitioned into **electrical islands** via union-find over the *energised*
(non-tripped) lines ([`grid.js` → `computeIslands()`](js/grid.js)).

**Why islands matter:** each island is solved independently and **carries its
own frequency**. A lone generator with no load spins up; a lone load with no
generator blacks out; cut the one line joining two halves of your grid and you
now have two systems whose frequencies drift apart. Frequency continuity is
preserved across ticks by matching islands on node-overlap.

---

## 2. Frequency — the swing equation

Frequency is the master vital sign. It is governed by the **swing equation**,
the rotational form of Newton's second law applied to the aggregate spinning
mass of an island's synchronous machines:

```
d(Δf)/dt  =  f₀ / (2 · ΣH·S) · ( P_gen − P_load − P_loss − D·Δf )
```

where `Δf = f − f₀`, `f₀ = 60 Hz`. ([`physics.js` → `stepFrequency()`](js/physics.js))

| Term | Meaning | Physical role |
|---|---|---|
| `ΣH·S` | Σ over online synchronous machines of inertia constant `H` (s) × rating `S` (MVA) | Stored rotational kinetic energy. Large ΣH·S → frequency changes *slowly*. |
| `P_gen − P_load − P_loss` | Real-power imbalance (MW) | Surplus accelerates the machines (f↑), deficit decelerates them (f↓). |
| `D·Δf` | Load self-regulation | Real loads draw a little less when frequency sags, damping the swing. |

**Inertia is the key intuition.** `H` is seconds of full-output energy stored in
the rotor. Nuclear/coal/hydro have `H ≈ 4–6 s`; **wind and solar are
inverter-coupled with `H ≈ 0`** — they contribute energy but no rotational
buffer. If an island's `ΣH·S` falls below ~1 MW·s while it carries load, the
model treats frequency as unsupportable and drives it to collapse: a physically
faithful warning that **an inverter-only grid cannot hold its own frequency**
without grid-forming support. Batteries can emulate a synthetic `H` while
charged.

---

## 3. Generation control — droop, AGC, dispatch

Three layers decide each generator's output ([`grid.js` → `solveIsland()`](js/grid.js)):

### 3a. Dispatch (who runs, and how much)
- **Manual units** are pinned first: the operator's setpoint
  `P = manualFrac × capacity` (clamped to what's available). They *hold* this and
  do **not** regulate frequency.
- **Auto units** then fill the remaining need — `load + losses + AGC_bias −
  manual` — in **merit order** (cheapest marginal fuel first). This is why
  nuclear/hydro run flat-out as baseload and gas fills the peaks.

Each unit's commanded setpoint is chased by its actual output under a **ramp
limit** `rampFrac × capacity` per minute. Nuclear ramps ~1%/min (a glacier);
a peaker ~40%/min; batteries effectively instantly. Ramp limits are why losing a
big unit can't be instantly back-filled — the core of operator difficulty.

### 3b. Primary control — governor droop
Every *auto* unit instantly trades frequency error for output:

```
ΔP = −(1/R) · (Δf / f₀) · S
```

`R` is the per-unit **droop** (e.g. 0.05 → a 5% frequency change drives 100% of
output). Response is capped at the unit's **spinning reserve** (~20% headroom),
because a real governor can only open the valve so far. Droop arrests a
disturbance in seconds but settles at a small steady-state frequency *offset*.

### 3c. Secondary control — AGC
A slow integral term (`AGC_bias`) with a ±0.02 Hz deadband nudges the dispatch
target until the residual offset is erased and `f → 60.000`. It is deliberately
gentle so it never fights the fast droop loop or wind up into oscillation.

---

## 4. The network — DC power flow & losses

Given each node's net injection (effective generation − served load), line MW
come from a **DC power flow** ([`grid.js` → `dcPowerFlow()`](js/grid.js)):

```
P_inj = B · θ        (solve for bus voltage angles θ)
flow_ij = (θ_i − θ_j) / x_ij
```

`B` is the nodal susceptance matrix built from line reactances `x = X/km · L`.
The system is solved by Gaussian elimination after dropping a **slack** node
(chosen as the largest generator), which absorbs the residual imbalance — it
stands in for the aggregate inertial response already modelled by the swing
equation. Flows honour Kirchhoff's laws, so power splits across parallel paths by
inverse reactance, just like the real thing.

### Losses and thermal limits
For each line carrying `P` MW at line-to-line voltage `V`:

```
I = P / (√3 · V)            ← three-phase current
P_loss = 3 · I² · R         ← ohmic (I²R) heating
R = ρ · L / A               ← from the conductor material
```
([`physics.js` → `lineLoss()`, `lineResistance()`](js/physics.js))

Two design levers fall straight out of this:

- **Voltage.** For fixed MW, current scales as `1/V`, so **losses scale as
  `1/V²`** and reach extends. That's why bulk power moves at 345–765 kV. The
  cost is towers, insulation, and right-of-way (priced per km, rising with kV).
- **Conductor.** Material sets `ρ` (resistivity) and `A` (cross-section), hence
  resistance *and* the thermal **ampacity** ceiling
  `P_max = √3 · V · I_max`. Copper has the lowest `ρ` but is heavy and dear;
  ACSR is the cheap standard; composite-core (ACCC) runs hotter for the most
  capacity. A line held above 100% ampacity for several seconds **trips**.

---

## 5. Protection & failure modes

Realistic relays, in escalating order ([`grid.js` → `solveIsland()`](js/grid.js)):

| Mechanism | Trigger | Effect |
|---|---|---|
| **Thermal line trip** | loading > 100% ampacity for `thermalTripSeconds` | line opens (auto-recloses after a cooldown) |
| **Under-frequency load shedding (UFLS)** | f < ~59.2 Hz | sheds load in proportion to the dip — negative feedback that arrests the decline |
| **Generator frequency trip** | f outside 57.5–62.0 Hz, *sustained* (stress timer) | trips the largest contributor — removing inertia, risking a **cascade** |
| **Dead island** | no generation present | all load unserved |

**Cascade to blackout** is the signature failure: lose a large unit → frequency
falls → if ramp/reserve/inertia can't catch it, UFLS sheds and machines trip →
each trip removes more inertia → collapse. The defence is the operator's job:
keep spinning reserve, keep inertia, keep lines below their limits.

---

## 6. Demand, weather, and resource

- **Loads** carry a 24-hour normalized shape (residential double-peak, commercial
  daytime plateau, industrial near-flat) scaled by peak MW; interpolated each
  tick. Power factor is tracked (and reactive demand displayed) but — see
  simplifications — not enforced on the DC network.
- **Solar** ∝ a daylight irradiance bell × `(1 − 0.85·cloud)`; zero at night.
- **Wind** ∝ `v³` between cut-in (3.5 m/s) and rated (13 m/s), flat to cut-out
  (25 m/s), zero outside — the real turbine power curve.
- **Hydro** is capped by a slowly-draining reservoir fraction.

Weather wanders via a smoothed pseudo-random walk so output is variable but not
jumpy. ([`game.js` → `solarFactor()`, `windFactor()`, `stepWeather()`](js/game.js))

---

## 7. Economy & score

- **CapEx**: charged up front when you place a component or add a unit/line
  (lines priced by length × kV cost × conductor multiplier).
- **Operating**: fuel + variable O&M charged per MWh generated, every tick.
- **Revenue**: served energy × electricity price ($/MWh).
- **CO₂**: generation × carbon intensity (t/MWh), accumulated.
- **Reliability score** is a *smoothed gauge of current health* (not an
  integral): it falls with frequency error and shed load, and recovers when you
  fix the problem — so a brief wobble dings it but doesn't doom the run.

---

## 8. Variable glossary

| Symbol | Units | Description |
|---|---|---|
| `f`, `Δf` | Hz | Frequency and deviation from 60 Hz |
| `H` | s | Inertia constant — stored energy / rating |
| `S` | MVA | Machine rating |
| `R` | pu | Governor droop |
| `D` | — | Load damping coefficient |
| `ρ` | Ω·m | Conductor resistivity |
| `A` | mm² | Conductor cross-sectional area |
| `X`, `x` | Ω, Ω/km | Line series reactance |
| `I_max` | A | Conductor ampacity (thermal limit) |
| `rampFrac` | /min | Max output change as fraction of capacity |
| `SOC` | MWh / % | Storage state of charge |
| capacity factor | — | Variable-resource output as fraction of nameplate |

---

## 9. Where the model simplifies

It is built to teach the *intuitions* of grid operation, not to replace a
production load-flow tool. Known edges:

- **DC power flow only.** No reactive-power / voltage-magnitude solve. Power
  factor is shown for realism but not enforced on the network; there is no
  var dispatch or voltage collapse.
- **Aggregate per-island frequency** — one frequency per island (a true center
  of inertia), not per-machine rotor angle dynamics or inter-area oscillations.
- **Lumped governor/AGC** with simplified gains and reserve caps.
- **No transient stability / fault current** — protection is steady-state
  threshold + timer, not a fault-and-clear sequence.
- **Gameplay-tuned costs and magnitudes** — directionally real, not sourced
  to a specific tariff or interconnection.

---

## 10. Code map

| File | Responsibility |
|---|---|
| [`js/data.js`](js/data.js) | Parts catalog — components, conductors, voltage classes, physical constants |
| [`js/physics.js`](js/physics.js) | Pure physics: swing equation, droop, DC-flow linear solver, I²R losses |
| [`js/grid.js`](js/grid.js) | Topology, islanding, the per-tick electrical solve, diagnostics |
| [`js/game.js`](js/game.js) | Clock, weather, demand, economy, scoring, event log |
| [`js/history.js`](js/history.js) | Rolling time-series recorder for telemetry |
| [`js/charts.js`](js/charts.js) | Canvas line-chart renderer |
| [`js/render.js`](js/render.js) | Grid/map rendering |
| [`js/ui.js`](js/ui.js) | Palette, interaction, inspector, HUD, drawer (telemetry/events/dispatch) |
| [`js/main.js`](js/main.js) | Bootstrap, loop, event detection, starter scenario |
