soma.cell

Cryo tank heater — Apollo 13: the thermostat that welded shut

Apollo 13's oxygen tank 2 exploded 200,000 miles from Earth because of a heater interlock that failed YEARS earlier, on the ground. The tank heaters had thermostatic switches rated for 28 volts; a pre-flight detanking test ran them at 65 volts. The contacts welded closed. With the cutoff fused shut, a heater cycle that should have stopped at 80°F ran the tank interior to an estimated 1000°F, baking the Teflon off the fan wiring. In flight, a routine cryo stir sparked the bare wires and the tank blew — "Houston, we've had a problem." The defect was a LIMIT that lived in a relay that could fail welded. Here the limit lives in memory itself: the tank-temperature ledger refuses any write above the thermostat ceiling, so the 1000°F state is unrepresentable no matter what the heater controller commands. An overtemp ATTEMPT trips the heater machine to thermal_lockout — a terminal state that only maintenance (a new instance) escapes.

A Soma program from the aerospace part of the verified corpus: 110 lines, cells CryoHeater, HeaterTests. It passes soma check with soma 3.2.4.

How it is built

The cell CryoHeater declares 5 signals in its face: heater_on(id: String) -> Map, heater_off(id: String) -> Map, drive_temp(id: String, target_f: Int) -> Map, heater_state(id: String) -> String and tank_temp(id: String) -> Int; keeps 1 memory slot: temp_f (Map<String, Int>, persistent and consistent) under the invariant temp_f >= -340 && temp_f <= 80. Its state machine heater starts in heater_off_state and allows 3 transitions: heater_off_state -> heating, heating -> heater_off_state and heating -> thermal_lockout. thermal_lockout is terminal. Handlers: heater_on (line 48), heater_off (line 55), drive_temp (line 61), heater_state (line 76) and tank_temp (line 77).

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

Generated from soma describe on this file.

The source

oxygen_tank_heater.cell

// ============================================================================
// CRYO TANK HEATER — Apollo 13: the thermostat that welded shut
// ============================================================================
//
// Apollo 13's oxygen tank 2 exploded 200,000 miles from Earth because of
// a heater interlock that failed YEARS earlier, on the ground. The tank
// heaters had thermostatic switches rated for 28 volts; a pre-flight
// detanking test ran them at 65 volts. The contacts welded closed. With
// the cutoff fused shut, a heater cycle that should have stopped at 80°F
// ran the tank interior to an estimated 1000°F, baking the Teflon off
// the fan wiring. In flight, a routine cryo stir sparked the bare wires
// and the tank blew — "Houston, we've had a problem."
//
// The defect was a LIMIT that lived in a relay that could fail welded.
// Here the limit lives in memory itself: the tank-temperature ledger
// refuses any write above the thermostat ceiling, so the 1000°F state
// is unrepresentable no matter what the heater controller commands. An
// overtemp ATTEMPT trips the heater machine to thermal_lockout — a
// terminal state that only maintenance (a new instance) escapes.
// ============================================================================

cell CryoHeater {
    face {
        signal heater_on(id: String) -> Map
        signal heater_off(id: String) -> Map
        signal drive_temp(id: String, target_f: Int) -> Map
        signal heater_state(id: String) -> String
        signal tank_temp(id: String) -> Int
    }

    memory {
        // tank internal temperature, °F — the thermostat ceiling is 80;
        // cryo O2 sits near -340. The welded-contact runaway cannot be
        // RECORDED, let alone reached.
        temp_f: Map<String, Int> [persistent, consistent]
        invariant temp_f >= -340 && temp_f <= 80
    }

    // ABSENT: any edge out of thermal_lockout — a tripped heater stays
    // dead until maintenance replaces it (a new machine instance)
    state heater {
        initial: heater_off_state
        heater_off_state -> heating
        heating -> heater_off_state
        heating -> thermal_lockout       // terminal
    }

    on heater_on(id: String) {
        if temp_f.get(id) == () { temp_f.set(id, -340) }
        let r = try { transition(id, "heating") }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        return map("ok", true, "heater", get_status(id))
    }

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

    on drive_temp(id: String, target_f: Int) {
        if get_status(id) != "heating" {
            return map("ok", false, "reason", "heater is not on (state: {get_status(id)})")
        }
        // THE WALL: the ledger enforces the thermostat ceiling. Even a
        // controller with welded relay logic cannot push the tank past
        // 80°F — the write refuses, and the heater trips to lockout.
        let w = try { temp_f.set(id, target_f) }
        if w.error != () {
            transition(id, "thermal_lockout")
            return map("ok", false, "reason", "overtemp commanded ({target_f} F) — heater locked out", "heater", get_status(id))
        }
        return map("ok", true, "temp_f", target_f, "heater", get_status(id))
    }

    on heater_state(id: String) { return get_status(id) }
    on tank_temp(id: String) { return temp_f.get(id) ?? -340 }
}

cell test HeaterTests {
    rules {
        // normal cryo warm-up cycle: -340 toward the working band
        assert heater_on("o2_tank1").ok == true
        assert drive_temp("o2_tank1", -100).ok == true
        assert drive_temp("o2_tank1", 60).ok == true
        assert heater_off("o2_tank1").ok == true

        // a heater that is off cannot drive temperature at all
        assert drive_temp("o2_tank1", 70).ok == false

        // THE Apollo 13 ground test: heaters commanded past the
        // thermostat ceiling. The ledger refuses the write...
        assert heater_on("o2_tank2").ok == true
        assert drive_temp("o2_tank2", 1000).ok == false
        assert tank_temp("o2_tank2") <= 80

        // ...and the heater is now LOCKED OUT, terminally
        assert heater_state("o2_tank2") == "thermal_lockout"
        assert_fails transition("o2_tank2", "heating")
        assert_fails transition("o2_tank2", "heater_off_state")
        assert heater_on("o2_tank2").ok == false

        // the 1000°F tank is unrepresentable even by direct write,
        // as is a temperature below liquid-oxygen physics
        assert_fails temp_f.set("o2_tank2", 1000)
        assert_fails temp_f.set("o2_tank2", 81)
        assert_fails temp_f.set("o2_tank2", -500)
    }
}

What the toolchain says

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

$ soma check oxygen_tank_heater.cell
✓ All checks passed.
$ soma test oxygen_tank_heater.cell
test HeaterTests ...
  ✓ assert heater_on("o2_tank1").ok == true
  ✓ assert drive_temp("o2_tank1", -100).ok == true
  ✓ assert drive_temp("o2_tank1", 60).ok == true
  ✓ assert heater_off("o2_tank1").ok == true
  ✓ assert drive_temp("o2_tank1", 70).ok == false
  ✓ assert heater_on("o2_tank2").ok == true
  ✓ assert drive_temp("o2_tank2", 1000).ok == false
  ✓ assert tank_temp("o2_tank2") <= 80
  ✓ assert heater_state("o2_tank2") == "thermal_lockout"
  ✓ assert_fails transition("o2_tank2", "heating") — raised invalid transition: thermal_lockout → heating. Current state: 'thermal_lockout'. Valid targets: []
  ✓ assert_fails transition("o2_tank2", "heater_off_state") — raised invalid transition: thermal_lockout → heater_off_state. Current state: 'thermal_lockout'. Valid targets: []
  ✓ assert heater_on("o2_tank2").ok == false
  ✓ assert_fails temp_f.set("o2_tank2", 1000) — raised memory invariant violated on 'temp_f': temp_f >= -340 && temp_f <= 80 — rejected write of 1000 (key "o2_tank2"); the slot is unchanged
  ✓ assert_fails temp_f.set("o2_tank2", 81) — raised memory invariant violated on 'temp_f': temp_f >= -340 && temp_f <= 80 — rejected write of 81 (key "o2_tank2"); the slot is unchanged
  ✓ assert_fails temp_f.set("o2_tank2", -500) — raised memory invariant violated on 'temp_f': temp_f >= -340 && temp_f <= 80 — rejected write of -500 (key "o2_tank2"); the slot is unchanged

15 tests: 15 passed, 0 failed
$ soma verify oxygen_tank_heater.cell
Verifying oxygen_tank_heater.cell...

State machine 'heater': 3 states, initial 'heater_off_state'
  States: [heater_off_state, heating, thermal_lockout]

  ✓ 3 states, 3 transitions
  ✓ all states reachable from 'heater_off_state'
  ✓ terminal states: [thermal_lockout]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ✓ think-isolated: CTL safety properties hold regardless of LLM output (3 handlers, 3 literal transitions, 0 dynamic)
  ✓ termination: all 5 handlers structurally terminate
  ✓ refinement: handler `heater_on` ⟶ {heating}
  ✓ refinement: handler `heater_off` ⟶ {heater_off_state}
  ✓ refinement: handler `drive_temp` ⟶ {heating → thermal_lockout [if w.error != ()]}
  · transition sources: `heater_on`, `heater_off` 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)

CryoHeater/invariants:

  ✓ invariant temp_f >= -340 && temp_f <= 80 — writer 'heater_on' proven (writes -340)
  ⚠ invariant temp_f >= -340 && temp_f <= 80 — runtime-checked (computed values): drive_temp → temp_f because `target_f` is a parameter (narrow it: `require target_f >= -340 && target_f <= 80 else …`)

11 passed, 1 warnings, 0 failures

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

Builtins used

get_status, map, transition

Source file: /corpus/aerospace/oxygen_tank_heater.cell · examples/corpus/aerospace/oxygen_tank_heater.cell