soma.cell

Gas turbine light-off — purge before every ignition attempt, no exceptions

February 7, 2010, Middletown, Connecticut: the Kleen Energy power plant, weeks from commissioning, exploded during a natural-gas pipe-cleaning operation. Six workers died. The blast led NFPA 56 to ban flammable-gas blows outright — but the same physics lives inside every gas turbine and HRSG: admit fuel without flame, and the exhaust path fills with a combustible cloud waiting for one spark. That is why NFPA 85 requires a forced-air purge of several full volume changes before EVERY ignition attempt — including the retry after a failed light-off, which is exactly when an impatient operator (or a buggy auto-restart) wants to skip it. Here the purge is unskippable: `firing` is reachable only from `purge_complete`, and the ONLY exit from `failed_light` is back into `purging`. soma verify proves no path admits fuel into an unpurged machine. The purge-volume ledger refuses credit for less than the mandated 5 volume changes.

A Soma program from the energy part of the verified corpus: 134 lines, cells Combustor, PurgeTests. It passes soma check with soma 3.2.4.

How it is built

The cell Combustor declares 7 signals in its face: start_purge(id: String) -> Map, purge_credit(id: String, volumes: Int) -> Map, ignite(id: String) -> Map, flame_on(id: String) -> Map, flame_out(id: String) -> Map, stop_unit(id: String) -> Map and unit_state(id: String) -> String; keeps 1 memory slot: purged (Map<String, Int>, persistent and consistent) under the invariant purged >= 0 && purged <= 50. Its state machine lightoff starts in shutdown and allows 7 transitions: shutdown -> purging, purging -> purge_complete, purge_complete -> firing, firing -> running, firing -> failed_light, failed_light -> purging and running -> shutdown. Handlers: start_purge (line 51), purge_credit (line 58), ignite (line 71), flame_on (line 78), flame_out (line 84), stop_unit (line 90) and unit_state (line 96).

The file also holds 1 test cell (PurgeTests) with 19 assertions, run by soma test.

Generated from soma describe on this file.

The source

gas_purge.cell

// ============================================================================
// GAS TURBINE LIGHT-OFF — purge before every ignition attempt, no exceptions
// ============================================================================
//
// February 7, 2010, Middletown, Connecticut: the Kleen Energy power plant,
// weeks from commissioning, exploded during a natural-gas pipe-cleaning
// operation. Six workers died. The blast led NFPA 56 to ban flammable-gas
// blows outright — but the same physics lives inside every gas turbine and
// HRSG: admit fuel without flame, and the exhaust path fills with a
// combustible cloud waiting for one spark. That is why NFPA 85 requires a
// forced-air purge of several full volume changes before EVERY ignition
// attempt — including the retry after a failed light-off, which is exactly
// when an impatient operator (or a buggy auto-restart) wants to skip it.
//
// Here the purge is unskippable: `firing` is reachable only from
// `purge_complete`, and the ONLY exit from `failed_light` is back into
// `purging`. soma verify proves no path admits fuel into an unpurged
// machine. The purge-volume ledger refuses credit for less than the
// mandated 5 volume changes.
// ============================================================================

cell Combustor {
    face {
        signal start_purge(id: String) -> Map
        signal purge_credit(id: String, volumes: Int) -> Map
        signal ignite(id: String) -> Map
        signal flame_on(id: String) -> Map
        signal flame_out(id: String) -> Map
        signal stop_unit(id: String) -> Map
        signal unit_state(id: String) -> String
    }

    memory {
        // volume changes pushed through since the last fuel admission;
        // 5 are required, more than 50 in one credit is a sensor fault
        purged: Map<String, Int> [persistent, consistent]
        invariant purged >= 0 && purged <= 50
    }

    state lightoff {
        initial: shutdown
        shutdown -> purging          // forced-draft air through the whole path
        purging -> purge_complete    // >= 5 volume changes verified
        purge_complete -> firing     // igniters live, fuel admitted
        firing -> running            // flame proven
        firing -> failed_light       // no flame — unburned fuel in the exhaust
        failed_light -> purging      // the ONLY exit: purge it out first
        running -> shutdown
    }

    on start_purge(id: String) {
        let r = try { transition(id, "purging") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        purged.set(id, 0)
        return map("ok", true, "state", get_status(id))
    }

    on purge_credit(id: String, volumes: Int) {
        if get_status(id) != "purging" {
            return map("ok", false, "reason", "no purge running (state: {get_status(id)})")
        }
        let w = try { purged.set(id, (purged.get(id) ?? 0) + volumes) }
        if w.error != () { return map("ok", false, "reason", "{w.error}") }
        if (purged.get(id) ?? 0) < 5 {
            return map("ok", true, "state", "purging", "volumes", purged.get(id))
        }
        transition(id, "purge_complete")
        return map("ok", true, "state", get_status(id), "volumes", purged.get(id))
    }

    on ignite(id: String) {
        let r = try { transition(id, "firing") }
        if r.error != () { return map("ok", false, "reason", "purge interlock: {r.error}") }
        purged.set(id, 0)        // fuel is in — the purge credit is SPENT
        return map("ok", true, "state", get_status(id))
    }

    on flame_on(id: String) {
        let r = try { transition(id, "running") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", get_status(id))
    }

    on flame_out(id: String) {
        let r = try { transition(id, "failed_light") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", get_status(id))
    }

    on stop_unit(id: String) {
        let r = try { transition(id, "shutdown") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "state", get_status(id))
    }

    on unit_state(id: String) { return get_status(id) }
}

cell test PurgeTests {
    rules {
        // fuel into a cold, unpurged machine: the transition is absent
        assert_fails transition("u1", "firing")
        assert ignite("u1").ok == false

        // purge, but 3 volume changes is not 5 — still purging
        assert start_purge("u1").ok == true
        assert purge_credit("u1", 3).ok == true
        assert unit_state("u1") == "purging"
        assert_fails transition("u1", "firing")

        // full credit, then light-off — and the flame fails
        assert purge_credit("u1", 2).ok == true
        assert unit_state("u1") == "purge_complete"
        assert ignite("u1").ok == true
        assert flame_out("u1").ok == true

        // THE KLEEN ENERGY MOMENT: retry ignition with raw gas still in
        // the exhaust path. The only exit from failed_light is a purge.
        assert_fails transition("u1", "firing")
        assert ignite("u1").ok == false

        // re-purge, then the retry is legal
        assert start_purge("u1").ok == true
        assert purge_credit("u1", 5).ok == true
        assert ignite("u1").ok == true
        assert flame_on("u1").ok == true
        assert unit_state("u1") == "running"

        // the purge ledger rejects nonsense credits
        assert_fails purged.set("u1", -1)
        assert_fails purged.set("u1", 99)
    }
}

What the toolchain says

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

$ soma check gas_purge.cell
✓ All checks passed.
$ soma test gas_purge.cell
test PurgeTests ...
  ✓ assert_fails transition("u1", "firing") — raised invalid transition: shutdown → firing. Current state: 'shutdown'. Valid targets: [purging]
  ✓ assert ignite("u1").ok == false
  ✓ assert start_purge("u1").ok == true
  ✓ assert purge_credit("u1", 3).ok == true
  ✓ assert unit_state("u1") == "purging"
  ✓ assert_fails transition("u1", "firing") — raised invalid transition: purging → firing. Current state: 'purging'. Valid targets: [purge_complete]
  ✓ assert purge_credit("u1", 2).ok == true
  ✓ assert unit_state("u1") == "purge_complete"
  ✓ assert ignite("u1").ok == true
  ✓ assert flame_out("u1").ok == true
  ✓ assert_fails transition("u1", "firing") — raised invalid transition: failed_light → firing. Current state: 'failed_light'. Valid targets: [purging]
  ✓ assert ignite("u1").ok == false
  ✓ assert start_purge("u1").ok == true
  ✓ assert purge_credit("u1", 5).ok == true
  ✓ assert ignite("u1").ok == true
  ✓ assert flame_on("u1").ok == true
  ✓ assert unit_state("u1") == "running"
  ✓ assert_fails purged.set("u1", -1) — raised memory invariant violated on 'purged': purged >= 0 && purged <= 50 — rejected write of -1 (key "u1"); the slot is unchanged
  ✓ assert_fails purged.set("u1", 99) — raised memory invariant violated on 'purged': purged >= 0 && purged <= 50 — rejected write of 99 (key "u1"); the slot is unchanged

19 tests: 19 passed, 0 failed
$ soma verify gas_purge.cell
Verifying gas_purge.cell...

State machine 'lightoff': 6 states, initial 'shutdown'
  States: [failed_light, firing, purge_complete, purging, running, shutdown]

  ✓ 6 states, 7 transitions
  ✓ all states reachable from 'shutdown'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'shutdown'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'shutdown' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (6 handlers, 6 literal transitions, 0 dynamic)
  ✓ termination: all 7 handlers structurally terminate
  ✓ refinement: handler `start_purge` ⟶ {purging}
  ✓ refinement: handler `purge_credit` ⟶ {purging → purge_complete}
  ✓ refinement: handler `ignite` ⟶ {firing}
  ✓ refinement: handler `flame_on` ⟶ {running}
  ✓ refinement: handler `flame_out` ⟶ {failed_light}
  ✓ refinement: handler `stop_unit` ⟶ {shutdown}
  · transition sources: `start_purge`, `ignite`, `flame_on`, `flame_out`, `stop_unit` 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)

Combustor/invariants:

  ✓ invariant purged >= 0 && purged <= 50 — writer 'ignite' proven (writes 0)
  ✓ invariant purged >= 0 && purged <= 50 — writer 'start_purge' proven (writes 0)
  ⚠ invariant purged >= 0 && purged <= 50 — runtime-checked (computed values): purge_credit → purged because `purged` has no known range; `volumes` is a parameter (narrow it: `require ((purged.get(id) ?? 0) + volumes) >= 0 && ((purged.get(id) ?? 0) + volumes) <= 50 else …`)

15 passed, 1 warnings, 0 failures

  Temporal properties for 'lightoff':
  ✓ 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/gas_purge.cell · examples/corpus/energy/gas_purge.cell