soma.cell

Stage separation — the ascent sequence as a proven order

Launch vehicle staging has exactly one correct order: first-stage cutoff, THEN separation, THEN second-stage ignition. Every permutation is a lost vehicle. Igniting stage 2 while still bolted to stage 1 fires an engine into a full propellant tank. Separating under thrust drives the stages into each other (recontact has ended real flights). Lighting stage 2 before separation completes burns the interstage. In flight software these are timer-sequenced events that a dropped packet or a stuck sensor can reorder. Here the sequence is a LINEAR state machine: each event is reachable only from its predecessor, and orbit_insertion is terminal — soma verify proves there is no path that skips, repeats, or reverses a staging event.

A Soma program from the aerospace part of the verified corpus: 90 lines, cells StageSequencer, SequenceTests. It passes soma check with soma 3.2.4.

How it is built

The cell StageSequencer declares 5 signals in its face: main_engine_cutoff(id: String) -> Map, separate_stage(id: String) -> Map, ignite_stage2(id: String) -> Map, confirm_orbit(id: String) -> Map and phase(id: String) -> String; keeps 1 memory slot: met_s (Map<String, Int>, persistent and consistent) under the invariant met_s >= 0 && met_s <= 3600. Its state machine ascent starts in stage1_burn and allows 4 transitions: stage1_burn -> stage1_cutoff, stage1_cutoff -> separated, separated -> stage2_burn and stage2_burn -> orbit_insertion. orbit_insertion is terminal. Handlers: step_event (line 47), main_engine_cutoff (line 54), separate_stage (line 55), ignite_stage2 (line 56), confirm_orbit (line 57) and phase (line 59).

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

Generated from soma describe on this file.

The source

stage_separation.cell

// ============================================================================
// STAGE SEPARATION — the ascent sequence as a proven order
// ============================================================================
//
// Launch vehicle staging has exactly one correct order: first-stage
// cutoff, THEN separation, THEN second-stage ignition. Every permutation
// is a lost vehicle. Igniting stage 2 while still bolted to stage 1
// fires an engine into a full propellant tank. Separating under thrust
// drives the stages into each other (recontact has ended real flights).
// Lighting stage 2 before separation completes burns the interstage.
//
// In flight software these are timer-sequenced events that a dropped
// packet or a stuck sensor can reorder. Here the sequence is a LINEAR
// state machine: each event is reachable only from its predecessor, and
// orbit_insertion is terminal — soma verify proves there is no path that
// skips, repeats, or reverses a staging event.
// ============================================================================

cell StageSequencer {
    face {
        signal main_engine_cutoff(id: String) -> Map
        signal separate_stage(id: String) -> Map
        signal ignite_stage2(id: String) -> Map
        signal confirm_orbit(id: String) -> Map
        signal phase(id: String) -> String
    }

    memory {
        // mission elapsed time of each event, seconds — strictly ordered
        // bookkeeping that can never record a negative or absurd time
        met_s: Map<String, Int> [persistent, consistent]
        invariant met_s >= 0 && met_s <= 3600
    }

    // A straight line. ABSENT: every shortcut.
    //   stage1_burn -/-> stage2_burn        (ignition while mated)
    //   stage1_burn -/-> separated          (separation under thrust)
    //   separated   -/-> orbit_insertion    (orbit without a burn)
    state ascent {
        initial: stage1_burn
        stage1_burn -> stage1_cutoff     // MECO: thrust at zero
        stage1_cutoff -> separated       // pyros fire only after cutoff
        separated -> stage2_burn         // ignition only with clear gap
        stage2_burn -> orbit_insertion   // terminal: the mission is up
    }

    on step_event(id: String, target: String, t: Int) {
        let r = try { transition(id, target) }
        if r.error != () { return map("ok", false, "reason", "{r.error}") }
        met_s.set("{id}:{target}", t)
        return map("ok", true, "phase", get_status(id), "met", t)
    }

    on main_engine_cutoff(id: String) { return step_event(id, "stage1_cutoff", 162) }
    on separate_stage(id: String)     { return step_event(id, "separated", 165) }
    on ignite_stage2(id: String)      { return step_event(id, "stage2_burn", 171) }
    on confirm_orbit(id: String)      { return step_event(id, "orbit_insertion", 540) }

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

cell test SequenceTests {
    rules {
        // every shortcut is unrepresentable from the pad
        assert_fails transition("v1", "stage2_burn")      // ignite while mated
        assert_fails transition("v1", "separated")        // separate under thrust
        assert_fails transition("v1", "orbit_insertion")

        // the one legal order works, step by step
        assert main_engine_cutoff("v1").ok == true
        assert_fails transition("v1", "stage2_burn")      // still mated
        assert separate_stage("v1").ok == true
        assert ignite_stage2("v1").ok == true
        assert confirm_orbit("v1").phase == "orbit_insertion"

        // orbit_insertion is TERMINAL: no event can rewind the mission
        assert_fails transition("v1", "stage1_burn")
        assert_fails transition("v1", "separated")

        // a second vehicle cannot replay an event out of order either
        assert main_engine_cutoff("v2").ok == true
        assert ignite_stage2("v2").ok == false            // not yet separated
        assert phase("v2") == "stage1_cutoff"

        // event times can never be recorded outside the mission window
        assert_fails met_s.set("v1:bogus", -3)
        assert_fails met_s.set("v1:bogus", 90000)
    }
}

What the toolchain says

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

$ soma check stage_separation.cell
✓ All checks passed.
$ soma test stage_separation.cell
test SequenceTests ...
  ✓ assert_fails transition("v1", "stage2_burn") — raised invalid transition: stage1_burn → stage2_burn. Current state: 'stage1_burn'. Valid targets: [stage1_cutoff]
  ✓ assert_fails transition("v1", "separated") — raised invalid transition: stage1_burn → separated. Current state: 'stage1_burn'. Valid targets: [stage1_cutoff]
  ✓ assert_fails transition("v1", "orbit_insertion") — raised invalid transition: stage1_burn → orbit_insertion. Current state: 'stage1_burn'. Valid targets: [stage1_cutoff]
  ✓ assert main_engine_cutoff("v1").ok == true
  ✓ assert_fails transition("v1", "stage2_burn") — raised invalid transition: stage1_cutoff → stage2_burn. Current state: 'stage1_cutoff'. Valid targets: [separated]
  ✓ assert separate_stage("v1").ok == true
  ✓ assert ignite_stage2("v1").ok == true
  ✓ assert confirm_orbit("v1").phase == "orbit_insertion"
  ✓ assert_fails transition("v1", "stage1_burn") — raised invalid transition: orbit_insertion → stage1_burn. Current state: 'orbit_insertion'. Valid targets: []
  ✓ assert_fails transition("v1", "separated") — raised invalid transition: orbit_insertion → separated. Current state: 'orbit_insertion'. Valid targets: []
  ✓ assert main_engine_cutoff("v2").ok == true
  ✓ assert ignite_stage2("v2").ok == false
  ✓ assert phase("v2") == "stage1_cutoff"
  ✓ assert_fails met_s.set("v1:bogus", -3) — raised memory invariant violated on 'met_s': met_s >= 0 && met_s <= 3600 — rejected write of -3 (key "v1:bogus"); the slot is unchanged
  ✓ assert_fails met_s.set("v1:bogus", 90000) — raised memory invariant violated on 'met_s': met_s >= 0 && met_s <= 3600 — rejected write of 90000 (key "v1:bogus"); the slot is unchanged

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

State machine 'ascent': 5 states, initial 'stage1_burn'
  States: [orbit_insertion, separated, stage1_burn, stage1_cutoff, stage2_burn]

  ✓ 5 states, 4 transitions
  ✓ all states reachable from 'stage1_burn'
  ✓ terminal states: [orbit_insertion]
  ✓ no deadlocks
  ✓ liveness: every state can eventually reach a terminal state
  ⚠ dynamic transition targets: handler `step_event` uses a dynamic (computed) transition target — the state these handlers move to is computed at run time (from input or an LLM answer), so the machine's safety is checked at run time for them, not proven
  ✓ termination: all 6 handlers structurally terminate
  ⚠ refinement (partial — dynamic targets fall back to runtime check): handler `step_event` calls transition() with a computed target — which edge it takes is checked at run time (invalid_transition), not proven; write the target as a literal state to have it verified
  ✓ refinement (partial — dynamic targets fall back to runtime check): handler `step_event` ⟶ <dynamic>

StageSequencer/invariants:

  ⚠ invariant met_s >= 0 && met_s <= 3600 — runtime-checked (computed values): step_event → met_s because `t` is a parameter (narrow it: `require t >= 0 && t <= 3600 else …`)

7 passed, 3 warnings, 0 failures

  Temporal properties for 'ascent':
  ✓ 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/stage_separation.cell · examples/corpus/aerospace/stage_separation.cell