soma.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.

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.

Generated from soma describe on this file.

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

Run on a fresh copy of the program at build time; the outputs are pasted unedited.

$ 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

Builtins used

get_status, map, transition

Source file: /corpus/energy/black_start.cell · examples/corpus/energy/black_start.cell