Black start — you cannot boot a power plant from a dead grid out of order
August 14, 2003: the Northeast blackout dropped 61,800 MW and 50 million people. Restoration took days, because a big thermal plant cannot start itself — its boiler feed pumps, fans, and lube-oil systems need tens of megawatts of "station service" power that normally comes FROM the grid. In February 2021, winter storm Uri brought ERCOT within minutes of a total collapse that engineers estimated would have taken weeks to black-start. The restoration sequence is rigid for physical reasons: a small self-starting unit (hydro, diesel) cranks first; it feeds the big plant's auxiliaries; only then can the big unit roll; the unit then carries a small stable island; and the island is synchronized to its neighbors LAST. Skip a step and you trip everything back to black. The sequence below is the state machine. Every skip is a transition that does not exist; collapse paths return to `blackout`; and `reconnected` is terminal for the event — the restoration log of a finished black start is history. soma verify proves all of it.
A Soma program from the energy part of the verified corpus: 118 lines, cells BlackStart, BlackStartTests. It passes soma check with soma 3.2.4.
How it is built
The cell BlackStart declares 4 signals in its face: advance_restore(ev: String, step: String) -> Map, island_collapse(ev: String) -> Map, restore_state(ev: String) -> String and record_island_mw(ev: String, mw: Int) -> Map; keeps 1 memory slot: island_mw (Map<String, Int>, persistent and consistent) under the invariant island_mw >= 0 && island_mw <= 200. Its state machine restoration starts in blackout and allows 9 transitions: blackout -> cranking, cranking -> aux_live, aux_live -> unit_online, unit_online -> island_stable, island_stable -> reconnected, cranking -> blackout, aux_live -> blackout, unit_online -> blackout and island_stable -> blackout. reconnected is terminal. Handlers: advance_restore (line 51), island_collapse (line 60), record_island_mw (line 67) and restore_state (line 78).
The file also holds 1 test cell (BlackStartTests) with 21 assertions, run by soma test.
The source
black_start.cell
// ============================================================================
// BLACK START — you cannot boot a power plant from a dead grid out of order
// ============================================================================
//
// August 14, 2003: the Northeast blackout dropped 61,800 MW and 50 million
// people. Restoration took days, because a big thermal plant cannot start
// itself — its boiler feed pumps, fans, and lube-oil systems need tens of
// megawatts of "station service" power that normally comes FROM the grid.
// In February 2021, winter storm Uri brought ERCOT within minutes of a
// total collapse that engineers estimated would have taken weeks to
// black-start. The restoration sequence is rigid for physical reasons:
// a small self-starting unit (hydro, diesel) cranks first; it feeds the
// big plant's auxiliaries; only then can the big unit roll; the unit then
// carries a small stable island; and the island is synchronized to its
// neighbors LAST. Skip a step and you trip everything back to black.
//
// The sequence below is the state machine. Every skip is a transition
// that does not exist; collapse paths return to `blackout`; and
// `reconnected` is terminal for the event — the restoration log of a
// finished black start is history. soma verify proves all of it.
// ============================================================================
cell BlackStart {
face {
signal advance_restore(ev: String, step: String) -> Map
signal island_collapse(ev: String) -> Map
signal restore_state(ev: String) -> String
signal record_island_mw(ev: String, mw: Int) -> Map
}
memory {
// load carried by the restoration island — a fledgling island
// above 200 MW is being grown too fast and will collapse
island_mw: Map<String, Int> [persistent, consistent]
invariant island_mw >= 0 && island_mw <= 200
}
state restoration {
initial: blackout
blackout -> cranking // hydro/diesel black-start unit self-starts
cranking -> aux_live // station service reaches the big plant
aux_live -> unit_online // main unit rolls, auxiliaries fed
unit_online -> island_stable // unit carries a small local island
island_stable -> reconnected // synced to neighbors — TERMINAL: done
cranking -> blackout // collapse paths: start over
aux_live -> blackout
unit_online -> blackout
island_stable -> blackout
}
on advance_restore(ev: String, step: String) {
let r = try { transition(ev, step) }
if r.error != () {
return map("ok", false, "state", get_status(ev),
"reason", "sequence violation: {r.error}")
}
return map("ok", true, "state", get_status(ev))
}
on island_collapse(ev: String) {
let r = try { transition(ev, "blackout") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
island_mw.set(ev, 0)
return map("ok", true, "state", get_status(ev))
}
on record_island_mw(ev: String, mw: Int) {
if get_status(ev) != "island_stable" {
return map("ok", false, "reason", "no island yet (state: {get_status(ev)})")
}
let w = try { island_mw.set(ev, mw) }
if w.error != () {
return map("ok", false, "reason", "island growth limit: {w.error}")
}
return map("ok", true, "island_mw", mw)
}
on restore_state(ev: String) { return get_status(ev) }
}
cell test BlackStartTests {
rules {
// THE IMPATIENT DISPATCHER: roll the big unit straight from black.
// Its feed pumps have no power; the transition does not exist.
assert_fails transition("r1", "unit_online")
assert_fails transition("r1", "aux_live")
assert advance_restore("r1", "unit_online").ok == false
// the real sequence, step by step
assert advance_restore("r1", "cranking").ok == true
assert advance_restore("r1", "aux_live").ok == true
// still cannot skip to the island
assert_fails transition("r1", "island_stable")
assert advance_restore("r1", "unit_online").ok == true
// and cannot declare victory before the island is proven stable
assert_fails transition("r1", "reconnected")
assert advance_restore("r1", "island_stable").ok == true
// grow the island within limits; overfeeding is refused
assert record_island_mw("r1", 150).ok == true
assert record_island_mw("r1", 250).ok == false
assert_fails island_mw.set("r1", 999)
// a frequency excursion collapses the island — back to black
assert island_collapse("r1").ok == true
assert restore_state("r1") == "blackout"
// second attempt succeeds and finishes — reconnected is TERMINAL
assert advance_restore("r1", "cranking").ok == true
assert advance_restore("r1", "aux_live").ok == true
assert advance_restore("r1", "unit_online").ok == true
assert advance_restore("r1", "island_stable").ok == true
assert advance_restore("r1", "reconnected").ok == true
assert_fails transition("r1", "blackout")
assert_fails transition("r1", "cranking")
}
}What the toolchain says
$ soma check black_start.cell
✓ All checks passed.$ soma test black_start.cell
test BlackStartTests ...
✓ assert_fails transition("r1", "unit_online") — raised invalid transition: blackout → unit_online. Current state: 'blackout'. Valid targets: [cranking]
✓ assert_fails transition("r1", "aux_live") — raised invalid transition: blackout → aux_live. Current state: 'blackout'. Valid targets: [cranking]
✓ assert advance_restore("r1", "unit_online").ok == false
✓ assert advance_restore("r1", "cranking").ok == true
✓ assert advance_restore("r1", "aux_live").ok == true
✓ assert_fails transition("r1", "island_stable") — raised invalid transition: aux_live → island_stable. Current state: 'aux_live'. Valid targets: [unit_online, blackout]
✓ assert advance_restore("r1", "unit_online").ok == true
✓ assert_fails transition("r1", "reconnected") — raised invalid transition: unit_online → reconnected. Current state: 'unit_online'. Valid targets: [island_stable, blackout]
✓ assert advance_restore("r1", "island_stable").ok == true
✓ assert record_island_mw("r1", 150).ok == true
✓ assert record_island_mw("r1", 250).ok == false
✓ assert_fails island_mw.set("r1", 999) — raised memory invariant violated on 'island_mw': island_mw >= 0 && island_mw <= 200 — rejected write of 999 (key "r1"); the slot is unchanged
✓ assert island_collapse("r1").ok == true
✓ assert restore_state("r1") == "blackout"
✓ assert advance_restore("r1", "cranking").ok == true
✓ assert advance_restore("r1", "aux_live").ok == true
✓ assert advance_restore("r1", "unit_online").ok == true
✓ assert advance_restore("r1", "island_stable").ok == true
✓ assert advance_restore("r1", "reconnected").ok == true
✓ assert_fails transition("r1", "blackout") — raised invalid transition: reconnected → blackout. Current state: 'reconnected'. Valid targets: []
✓ assert_fails transition("r1", "cranking") — raised invalid transition: reconnected → cranking. Current state: 'reconnected'. Valid targets: []
21 tests: 21 passed, 0 failed$ soma verify black_start.cell
Verifying black_start.cell...
State machine 'restoration': 6 states, initial 'blackout'
States: [aux_live, blackout, cranking, island_stable, reconnected, unit_online]
✓ 6 states, 9 transitions
✓ all states reachable from 'blackout'
✓ terminal states: [reconnected]
✓ no deadlocks
✓ liveness: every state can eventually reach a terminal state
⚠ dynamic transition targets: handler `advance_restore` uses a dynamic (computed) transition target — the state these handlers move to is computed at run time (from input or an LLM answer), so the machine's safety is checked at run time for them, not proven
✓ termination: all 4 handlers structurally terminate
⚠ refinement (partial — dynamic targets fall back to runtime check): handler `advance_restore` calls transition() with a computed target — which edge it takes is checked at run time (invalid_transition), not proven; write the target as a literal state to have it verified
✓ refinement (partial — dynamic targets fall back to runtime check): handler `advance_restore` ⟶ <dynamic>
✓ refinement (partial — dynamic targets fall back to runtime check): handler `island_collapse` ⟶ {blackout}
· transition sources: `island_collapse` calls transition(id, target) without declaring the source — the runtime refuses a move the graph forbids, but the proof does not see where the handler starts; `transition(id, from, to)` names the edge (checked by `soma check`, refused at run time from any other state)
BlackStart/invariants:
✓ invariant island_mw >= 0 && island_mw <= 200 — writer 'island_collapse' proven (writes 0)
⚠ invariant island_mw >= 0 && island_mw <= 200 — runtime-checked (computed values): record_island_mw → island_mw because `mw` is a parameter (narrow it: `require mw >= 0 && mw <= 200 else …`)
9 passed, 3 warnings, 0 failures
Temporal properties for 'restoration':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK