soma.cell

Pressure relief valve — a live vessel can never lose its relief path

Williams Olefins, Geismar 2013: a reboiler was isolated from its relief valve, heat crept in, and the blocked-in vessel ruptured — 2 dead, 167 injured. The century-old version is the gagged boiler safety valve. Here the joint state (vessel pressure x relief path) is one machine, and the state {in service, relief blocked} does not exist: you can only block the relief valve for maintenance from the cold, offline state, and a blocked-relief vessel has no edge into service. The gauge ledger independently refuses pressure above MAWP.

A Soma program from the safety_interlocks part of the verified corpus: 88 lines, cells ReliefInterlock, ReliefTests. It passes soma check with soma 3.2.4.

How it is built

The cell ReliefInterlock declares 5 signals in its face: commission(id: String, psi: Int) -> Map, take_offline(id: String) -> String, block_relief_for_test(id: String) -> String, restore_relief(id: String) -> String and gauge(id: String) -> Int; keeps 1 memory slot: psi_now (Map<String, Int>, persistent and consistent) under the invariant psi_now >= 0 && psi_now <= 150. Its state machine vessel starts in offline_relief_open and allows 4 transitions: offline_relief_open -> in_service, in_service -> offline_relief_open, offline_relief_open -> relief_blocked_cold and relief_blocked_cold -> offline_relief_open. Handlers: commission (line 36), take_offline (line 49), block_relief_for_test (line 55), restore_relief (line 56) and gauge (line 57).

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

Generated from soma describe on this file.

The source

pressure_relief.cell

// PRESSURE RELIEF VALVE — a live vessel can never lose its relief path.
// Williams Olefins, Geismar 2013: a reboiler was isolated from its
// relief valve, heat crept in, and the blocked-in vessel ruptured —
// 2 dead, 167 injured. The century-old version is the gagged boiler
// safety valve. Here the joint state (vessel pressure x relief path) is
// one machine, and the state {in service, relief blocked} does not
// exist: you can only block the relief valve for maintenance from the
// cold, offline state, and a blocked-relief vessel has no edge into
// service. The gauge ledger independently refuses pressure above MAWP.

cell ReliefInterlock {
    face {
        signal commission(id: String, psi: Int) -> Map
        signal take_offline(id: String) -> String
        signal block_relief_for_test(id: String) -> String
        signal restore_relief(id: String) -> String
        signal gauge(id: String) -> Int
    }

    memory {
        // vessel pressure, psi; MAWP is 150 — the wall the gauge enforces
        psi_now: Map<String, Int> [persistent, consistent]
        invariant psi_now >= 0 && psi_now <= 150
    }

    // What is ABSENT is the proof: no state pairs pressure with a
    // blocked relief path.
    state vessel {
        initial: offline_relief_open
        offline_relief_open -> in_service          // pressurize: relief armed
        in_service -> offline_relief_open          // depressurize first, always
        offline_relief_open -> relief_blocked_cold // valve test: vessel cold only
        relief_blocked_cold -> offline_relief_open
    }

    on commission(id: String, psi: Int) {
        let r = try { transition(id, "in_service") }
        if r.error != () {
            return map("ok", false, "reason", "relief path not proven open")
        }
        let w = try { psi_now.set(id, psi) }
        if w.error != () {
            transition(id, "offline_relief_open")
            return map("ok", false, "reason", "gauge refused {psi} psi (MAWP 150)")
        }
        return map("ok", true, "state", get_status(id), "psi", psi)
    }

    on take_offline(id: String) {
        psi_now.set(id, 0)                      // vent BEFORE the state moves
        transition(id, "offline_relief_open")
        return get_status(id)
    }

    on block_relief_for_test(id: String) { transition(id, "relief_blocked_cold")  return get_status(id) }
    on restore_relief(id: String)        { transition(id, "offline_relief_open")  return get_status(id) }
    on gauge(id: String)                 { return psi_now.get(id) ?? 0 }
}

cell test ReliefTests {
    rules {
        // a commissioned vessel runs at pressure with relief armed
        assert commission("v1", 120).ok == true

        // THE Geismar move: blocking the relief on a LIVE vessel.
        // The edge does not exist — it raises.
        assert_fails transition("v1", "relief_blocked_cold")

        // the gauge itself refuses pressure past MAWP, and a vacuum
        assert_fails psi_now.set("v1", 200)
        assert_fails psi_now.set("v1", -10)

        // legal maintenance: vent, go offline, THEN block the relief
        assert take_offline("v1") == "offline_relief_open"
        assert gauge("v1") == 0
        assert block_relief_for_test("v1") == "relief_blocked_cold"

        // a blocked-relief vessel has no road into service
        assert_fails transition("v1", "in_service")
        assert commission("v1", 100).ok == false
        assert restore_relief("v1") == "offline_relief_open"

        // overpressure commissioning is refused and the vessel stays safe
        assert commission("v2", 151).ok == false
        assert gauge("v2") == 0
    }
}

What the toolchain says

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

$ soma check pressure_relief.cell
✓ All checks passed.
$ soma test pressure_relief.cell
test ReliefTests ...
  ✓ assert commission("v1", 120).ok == true
  ✓ assert_fails transition("v1", "relief_blocked_cold") — raised invalid transition: in_service → relief_blocked_cold. Current state: 'in_service'. Valid targets: [offline_relief_open]
  ✓ assert_fails psi_now.set("v1", 200) — raised memory invariant violated on 'psi_now': psi_now >= 0 && psi_now <= 150 — rejected write of 200 (key "v1"); the slot is unchanged
  ✓ assert_fails psi_now.set("v1", -10) — raised memory invariant violated on 'psi_now': psi_now >= 0 && psi_now <= 150 — rejected write of -10 (key "v1"); the slot is unchanged
  ✓ assert take_offline("v1") == "offline_relief_open"
  ✓ assert gauge("v1") == 0
  ✓ assert block_relief_for_test("v1") == "relief_blocked_cold"
  ✓ assert_fails transition("v1", "in_service") — raised invalid transition: relief_blocked_cold → in_service. Current state: 'relief_blocked_cold'. Valid targets: [offline_relief_open]
  ✓ assert commission("v1", 100).ok == false
  ✓ assert restore_relief("v1") == "offline_relief_open"
  ✓ assert commission("v2", 151).ok == false
  ✓ assert gauge("v2") == 0

12 tests: 12 passed, 0 failed
$ soma verify pressure_relief.cell
Verifying pressure_relief.cell...

State machine 'vessel': 3 states, initial 'offline_relief_open'
  States: [in_service, offline_relief_open, relief_blocked_cold]

  ✓ 3 states, 4 transitions
  ✓ all states reachable from 'offline_relief_open'
  ✓ reactive machine: no terminal state, and every reachable state can return to 'offline_relief_open'
  ✓ no deadlocks
  ✓ liveness: every reachable state can return to 'offline_relief_open' (reactive machine)
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (4 handlers, 5 literal transitions, 0 dynamic)
  ✓ termination: all 5 handlers structurally terminate
  ✓ refinement: handler `commission` ⟶ {in_service, offline_relief_open [if w.error != ()]}
  ✓ refinement: handler `take_offline` ⟶ {offline_relief_open}
  ✓ refinement: handler `block_relief_for_test` ⟶ {relief_blocked_cold}
  ✓ refinement: handler `restore_relief` ⟶ {offline_relief_open}
  · transition sources: `commission`, `take_offline`, `block_relief_for_test`, `restore_relief` 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)

ReliefInterlock/invariants:

  ✓ invariant psi_now >= 0 && psi_now <= 150 — writer 'take_offline' proven (writes 0)
  ⚠ invariant psi_now >= 0 && psi_now <= 150 — runtime-checked (computed values): commission → psi_now because `psi` is a parameter (narrow it: `require psi >= 0 && psi <= 150 else …`)

12 passed, 1 warnings, 0 failures

  Temporal properties for 'vessel':
  ✓ deadlock_free — no deadlocks in any reachable state
Temporal: 1 passed, 0 failed
VERIFY OK

Builtins used

get_status, map, transition

Source file: /corpus/safety_interlocks/pressure_relief.cell · examples/corpus/safety_interlocks/pressure_relief.cell