Anti-islanding — a solar inverter must never energize a "dead" line
When a feeder breaker opens, every lineworker on that circuit treats the wire as dead. But a neighborhood of rooftop solar can keep the islanded segment energized — backfeeding lethal voltage into a line the crew has every reason to believe is isolated. This is why IEEE 1547 mandates that a grid-tied inverter CEASE energizing within 2 seconds of grid loss and stay off until the grid has been stable for a full hold-off window (classically 5 minutes) — not "until the voltage flickers back". The lethal shortcut is `ceased -> exporting`: the grid blinks, the inverter jumps straight back to full output while a recloser is mid- sequence or a crew is mid-repair. Below, that transition DOES NOT EXIST. The only exit from `ceased` is `monitoring`, where the hold-off clock restarts. soma verify proves the reachable graph; the export-watts slot is a second wall — output is zeroed on cessation and capped at nameplate.
A Soma program from the energy part of the verified corpus: 107 lines, cells Inverter, IslandTests. It passes soma check with soma 3.2.4.
How it is built
The cell Inverter declares 6 signals in its face: grid_detected(id: String) -> Map, hold_off_elapsed(id: String) -> Map, grid_lost(id: String) -> Map, set_export(id: String, watts: Int) -> Map, inverter_state(id: String) -> String and export_of(id: String) -> Int; keeps 1 memory slot: export_w (Map<String, Int>, persistent and consistent) under the invariant export_w >= 0 && export_w <= 10000. Its state machine island_guard starts in offline and allows 5 transitions: offline -> monitoring, monitoring -> exporting, exporting -> ceased, ceased -> monitoring and monitoring -> offline. Handlers: grid_detected (line 46), hold_off_elapsed (line 52), grid_lost (line 58), set_export (line 65), inverter_state (line 74) and export_of (line 75).
The file also holds 1 test cell (IslandTests) with 13 assertions, run by soma test.
The source
anti_islanding.cell
// ============================================================================
// ANTI-ISLANDING — a solar inverter must never energize a "dead" line
// ============================================================================
//
// When a feeder breaker opens, every lineworker on that circuit treats the
// wire as dead. But a neighborhood of rooftop solar can keep the islanded
// segment energized — backfeeding lethal voltage into a line the crew has
// every reason to believe is isolated. This is why IEEE 1547 mandates that
// a grid-tied inverter CEASE energizing within 2 seconds of grid loss and
// stay off until the grid has been stable for a full hold-off window
// (classically 5 minutes) — not "until the voltage flickers back".
//
// The lethal shortcut is `ceased -> exporting`: the grid blinks, the
// inverter jumps straight back to full output while a recloser is mid-
// sequence or a crew is mid-repair. Below, that transition DOES NOT EXIST.
// The only exit from `ceased` is `monitoring`, where the hold-off clock
// restarts. soma verify proves the reachable graph; the export-watts slot
// is a second wall — output is zeroed on cessation and capped at nameplate.
// ============================================================================
cell Inverter {
face {
signal grid_detected(id: String) -> Map
signal hold_off_elapsed(id: String) -> Map
signal grid_lost(id: String) -> Map
signal set_export(id: String, watts: Int) -> Map
signal inverter_state(id: String) -> String
signal export_of(id: String) -> Int
}
memory {
// watts being pushed onto the feeder; nameplate is 10 kW
export_w: Map<String, Int> [persistent, consistent]
invariant export_w >= 0 && export_w <= 10000
}
state island_guard {
initial: offline
offline -> monitoring // grid voltage/frequency detected
monitoring -> exporting // stable for the FULL hold-off window
exporting -> ceased // grid loss: cease within 2 s, mandatory
ceased -> monitoring // grid back — restart the clock at zero
monitoring -> offline // operator shutdown
}
on grid_detected(id: String) {
let r = try { transition(id, "monitoring") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(id))
}
on hold_off_elapsed(id: String) {
let r = try { transition(id, "exporting") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
return map("ok", true, "state", get_status(id))
}
on grid_lost(id: String) {
let r = try { transition(id, "ceased") }
if r.error != () { return map("ok", false, "reason", "{r.error}") }
export_w.set(id, 0) // output to zero, within 2 s
return map("ok", true, "state", get_status(id), "watts", 0)
}
on set_export(id: String, watts: Int) {
if get_status(id) != "exporting" {
return map("ok", false, "reason", "not exporting (state: {get_status(id)})")
}
let w = try { export_w.set(id, watts) }
if w.error != () { return map("ok", false, "reason", "nameplate wall: {w.error}") }
return map("ok", true, "watts", watts)
}
on inverter_state(id: String) { return get_status(id) }
on export_of(id: String) { return export_w.get(id) ?? 0 }
}
cell test IslandTests {
rules {
// normal commissioning: detect grid, wait out the hold-off, export
assert grid_detected("i1").ok == true
assert hold_off_elapsed("i1").ok == true
assert set_export("i1", 8000).ok == true
// the feeder breaker opens — the inverter MUST cease
assert grid_lost("i1").ok == true
assert export_of("i1") == 0
// THE KILLER: grid blinks back, firmware jumps straight to export.
// The transition does not exist — the lineworker stays alive.
assert_fails transition("i1", "exporting")
// pushing watts while ceased is refused by the state gate
assert set_export("i1", 5000).ok == false
assert export_of("i1") == 0
// the only way back is a fresh hold-off window
assert grid_detected("i1").ok == true
assert hold_off_elapsed("i1").ok == true
assert set_export("i1", 8000).ok == true
// the nameplate wall holds regardless of state
assert_fails export_w.set("i1", 20000)
assert_fails export_w.set("i1", -100)
}
}What the toolchain says
$ soma check anti_islanding.cell
✓ All checks passed.$ soma test anti_islanding.cell
test IslandTests ...
✓ assert grid_detected("i1").ok == true
✓ assert hold_off_elapsed("i1").ok == true
✓ assert set_export("i1", 8000).ok == true
✓ assert grid_lost("i1").ok == true
✓ assert export_of("i1") == 0
✓ assert_fails transition("i1", "exporting") — raised invalid transition: ceased → exporting. Current state: 'ceased'. Valid targets: [monitoring]
✓ assert set_export("i1", 5000).ok == false
✓ assert export_of("i1") == 0
✓ assert grid_detected("i1").ok == true
✓ assert hold_off_elapsed("i1").ok == true
✓ assert set_export("i1", 8000).ok == true
✓ assert_fails export_w.set("i1", 20000) — raised memory invariant violated on 'export_w': export_w >= 0 && export_w <= 10000 — rejected write of 20000 (key "i1"); the slot is unchanged
✓ assert_fails export_w.set("i1", -100) — raised memory invariant violated on 'export_w': export_w >= 0 && export_w <= 10000 — rejected write of -100 (key "i1"); the slot is unchanged
13 tests: 13 passed, 0 failed$ soma verify anti_islanding.cell
Verifying anti_islanding.cell...
State machine 'island_guard': 4 states, initial 'offline'
States: [ceased, exporting, monitoring, offline]
✓ 4 states, 5 transitions
✓ all states reachable from 'offline'
✓ reactive machine: no terminal state, and every reachable state can return to 'offline'
✓ no deadlocks
✓ liveness: every reachable state can return to 'offline' (reactive machine)
✓ think-isolated: CTL safety properties hold regardless of LLM output (3 handlers, 3 literal transitions, 0 dynamic)
✓ termination: all 6 handlers structurally terminate
⚠ refinement: declared transition `monitoring → offline` is never reached by any handler — spec may be aspirational or stale
✓ refinement: handler `grid_detected` ⟶ {monitoring}
✓ refinement: handler `hold_off_elapsed` ⟶ {exporting}
✓ refinement: handler `grid_lost` ⟶ {ceased}
· transition sources: `grid_detected`, `hold_off_elapsed`, `grid_lost` call 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)
Inverter/invariants:
✓ invariant export_w >= 0 && export_w <= 10000 — writer 'grid_lost' proven (writes 0)
⚠ invariant export_w >= 0 && export_w <= 10000 — runtime-checked (computed values): set_export → export_w because `watts` is a parameter (narrow it: `require watts >= 0 && watts <= 10000 else …`)
11 passed, 2 warnings, 0 failures
Temporal properties for 'island_guard':
✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK