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