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

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.

Generated from soma describe on this file.

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

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

$ 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

Builtins used

get_status, map, transition

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