Arc 1 · Make it fly  /  Tutorial 03 of 16

Staging & the delta-v budget

Two things here. The equation that governs every rocket ever built — which you're going to derive the useful forms of yourself — and an autostaging routine that doesn't jettison a live stage, fire during a coast, or dump your whole rocket in one tick.

sep sep mass time
Mass history of a three-stage ascent
Prelaunch

Reading the vessel

Autostaging means asking the rocket about itself. These are the questions you can ask.

Vessel mass & thrust
SHIP:MASSCurrent total mass, tonnes.
SHIP:DRYMASSMass with all resources drained from the whole vessel — not just this stage.
SHIP:WETMASSMass with all tanks full.
SHIP:AVAILABLETHRUSTkN from active, unflamed engines at full throttle, at current ambient pressure.
SHIP:MAXTHRUSTSimilar, but includes engines that have flamed out. The difference between these two is a useful signal.
Parts & lists
LIST ENGINES IN es.Fill es with every engine on the vessel. Note the syntax — this is a statement, not a function.
FOR e IN es { … }Iterate. e is each engine part in turn.
e:IGNITIONTrue if this engine is currently lit (or was, and has since flamed out).
e:FLAMEOUTTrue if it's lit but out of propellant.
e:ISPSpecific impulse at current conditions, seconds.
e:AVAILABLETHRUSTThis engine's contribution, kN.
es:LENGTHNumber of items in a list.
Staging
STAGE.Fire the next stage.
STAGE:READYFalse for a moment after staging. Calling STAGE. when not ready throws an error.
STAGE:NUMBERCurrent stage index, counting down to zero.
THROTTLEThe current throttle value, 0–1. Readable even while locked.
TIME:SECONDSUniversal time. This is how you build a cooldown.
On WHEN … THEN kOS has a trigger form — WHEN cond THEN { … } — that runs in the background. It looks perfect for autostaging and it is a trap for now: triggers fire between your loop's instructions, must complete within one tick, and interact badly with WAIT. Poll from your main loop instead. You'll know when you actually want a trigger, and it won't be here.
The equation

Tsiolkovsky, rearranged

You know this one. What you probably haven't done is bend it into the three shapes you actually need in flight software.

Δv = Isp g0 ln m0mf

m0 = mass at ignition  ·  mf = mass at burnout  ·  g0 = 9.80665 m/s² always, everywhere, even at Duna

g₀ is not local gravity In Tutorial 01, g was the local gravitational acceleration and it changed with altitude. Here g0 is a unit conversion constant, fixed at 9.80665. It exists only because specific impulse is quoted in seconds instead of exhaust velocity in m/s. Using local g here is a classic and very confusing bug.
Rearrange it three ways
  1. You have a target Δv and a starting mass. Solve for the burnout mass mf.
  2. From that, write the propellant mass you must expend, mprop = m0 − mf.
  3. A stage has Isp = 320 s, ignition mass 40 t, burnout mass 14 t. How much Δv? Then: how much would it have if you shaved 2 t off the dry mass, keeping propellant the same?
Saved
Show the algebra

1 — Burnout mass. Divide by Ispg0 and exponentiate both sides:

ΔvIsp g0 = ln m0mf   ⟹   eΔv/Ispg0 = m0mf
mf = m0 e−Δv/Ispg0

Note the negative exponent — that's the form you want in code, because it's numerically better behaved and reads as "shrink the mass by this factor".

2 — Propellant required.

mprop = m0 ( 1 − e−Δv/Ispg0 )

This is the single most useful rearrangement in the whole game. It answers "can I make this burn with what's in the tank?" without simulating anything.

3 — Numbers. Ispg0 = 320 × 9.80665 = 3138.1 m/s.

Δv = 3138.1 × ln(40/14) = 3138.1 × 1.0498 ≈ 3294 m/s

Now shave 2 t of dry mass. Propellant was 40 − 14 = 26 t, so the new masses are m0 = 38 t, mf = 12 t:

Δv = 3138.1 × ln(38/12) = 3138.1 × 1.1526 ≈ 3617 m/s

+323 m/s for 2 tonnes. That's the whole argument for staging in one line: mass you're no longer accelerating is Δv you get to keep. An empty tank is dead weight that the logarithm punishes you for, and dropping it mid-flight is the only way to move the ratio.

The equation

How long will it burn?

Tsiolkovsky tells you nothing about time. It doesn't contain thrust at all — a 5 kN ion engine and a 5 MN booster with the same mass ratio and Isp give identical Δv. Time comes from mass flow.

Two facts to start from. Thrust is momentum thrown per second:

F = ṁ ve

and specific impulse is just exhaust velocity in disguise:

ve = Isp g0
Derive the burn time
  1. Combine those two into an expression for mass flow rate ṁ in terms of F, Isp, g0.
  2. Assuming constant thrust and constant Isp, write burn time in terms of m0, mf and ṁ.
  3. Now substitute your result from the previous section to get burn time as a function of Δv alone — no masses on the right-hand side except m0. This is the form flight software actually uses.
  4. Sanity check the units. Mass is in tonnes and thrust in kN. Does your ṁ come out in t/s, and does the time come out in seconds?
Saved
Show the algebra

1 — Mass flow. Substitute ve and solve for ṁ:

ṁ = FIsp g0

2 — Time to expend the propellant. Constant flow, so it's just quantity over rate:

t = m0 − mfṁ = (m0 − mf) Isp g0F

3 — In terms of Δv. Substitute mf = m0e−Δv/Ispg0, factor out m0:

t = m0 Isp g0F ( 1 − e−Δv/Ispg0 )

Give this one a name — it's how you'll split a maneuver node in half in Tutorial 10, so that you start burning early enough to finish late enough. Every burn you plan from here on runs through it.

4 — Units. kN ÷ (s × m/s²) = kN ÷ (m/s) = (kg·m/s²·10³) ÷ (m/s) = 10³ kg/s = t/s. So ṁ is in tonnes per second, and tonnes ÷ (t/s) gives seconds. The kilo-prefix on newtons and the kilo-prefix on grams cancel exactly. This is why KSP's unit choices are quietly excellent and why you should never introduce a conversion factor in kOS — if you find yourself writing 1000 somewhere, you've made an error.

Where this breaks. Isp changes with ambient pressure, and thrust with it, so during a first stage this is an approximation. In vacuum it's essentially exact. When you need in-atmosphere accuracy, read SHIP:AVAILABLETHRUST live rather than trusting a number computed at ignition.
Instrument

Stage calculator

Everything you just derived, wired together. Put a real stage from your VAB into it.

Single stage performance
Delta-v—
Mass ratio—
Mass flow—
Burn time—
TWR ignition → burnout—
TWR here uses sea-level Kerbin gravity, so it's the pad figure. Watch how far it climbs by burnout — a booster that lifts off at 1.5 often ends up past 4, which is uncomfortable for the airframe and a hint that you should be throttling back. That's Tutorial 07.
Anomaly

Why the obvious autostage fails

Nearly everyone writes this first. It works on one rocket and then destroys the next one.

the naive version — do not use
WHEN SHIP:AVAILABLETHRUST < 0.1 THEN {
  STAGE.
  PRESERVE.
}

Four separate ways this bites:

1
It fires during coasts Throttle at zero means available thrust is still nonzero — but the moment you add a real coast phase, or the check is on actual thrust rather than available, the condition trips and you shed stages while drifting. Never stage at zero throttle.
2
It fires repeatedly in one tick Right after separation there's a moment where the new engine hasn't ignited and thrust is still zero. The condition is still true, so it stages again. And again. Rockets have been reduced to a command pod in under a second this way. You need a cooldown.
3
It fires while engines are still burning With asymmetric boosters or a mix of engine types, one flameout doesn't mean the stage is spent. Requiring only that some engine is out throws away live propellant. Require that all currently-burning engines have flamed out.
4
It calls STAGE when staging isn't ready There's a brief interval after a separation when the staging system is busy. Calling STAGE. then raises a hard error and kills your script mid-ascent, leaving the rocket unsteered. Check STAGE:READY.
Design it before you code it

Write the staging condition as plain boolean logic — four clauses joined by AND. Then decide: what should your cooldown duration be, and what should the function return in the edge case where no engines are ignited at all? Both answers depend on when your loop starts running relative to liftoff.

Saved
Your mission

launch3.ks

Requirements
  1. Start from launch2.ks. Keep the pitch program exactly as it is.
  2. Write shouldStage() returning a boolean, implementing all four guards.
  3. Write autoStage() that calls it, stages when true, and records the time so the cooldown works.
  4. Call autoStage() from your ascent loop, every pass.
  5. Add a stageDeltaV() function: current mass, current available thrust, a mass-weighted Isp across active engines, and the propellant remaining in the current stage. Print the result live.
  6. Print stage number and remaining stage Δv in your telemetry block.
  7. Fly a three-stage rocket to 75 km without touching the space bar.
Requirement 5 is genuinely hard "Propellant remaining in the current stage" is not SHIP:MASS − SHIP:DRYMASS — that's the whole vessel's propellant, including upper stages you haven't lit. Getting a correct per-stage figure means looking at which resources are actually reachable by the burning engines. Attempt it, get it wrong, and read the solution's discussion — this is a case where the honest answer is that the clean version needs tools you don't have yet.
Saved
Solution

Open after you've flown it

Show launch3.ks
launch3.ks
// launch3.ks — pitch program + autostaging + live stage dV.

CLEARSCREEN.

// ---- tuning ----
SET azimuth   TO 90.
SET targetAp  TO 75000.
SET turnStart TO 1000.
SET turnEnd   TO 45000.
SET pitch0    TO 90.
SET pitch1    TO 0.
SET kShape    TO 0.5.

// ---- staging state ----
SET g0            TO 9.80665.
SET stageCooldown TO 1.0.
SET lastStageAt   TO 0.

// ---- launch ----
SAS OFF. RCS OFF.
LOCK THROTTLE TO 1.
LOCK STEERING TO HEADING(azimuth, pitchAt(SHIP:ALTITUDE)).
SET lastStageAt TO TIME:SECONDS.
STAGE.
PRINT "LIFTOFF          " AT (0,0).

UNTIL SHIP:APOAPSIS > targetAp {
  autoStage().
  telemetry().
  WAIT 0.
}

LOCK THROTTLE TO 0.
PRINT "MECO             " AT (0,0).
WAIT 1.
UNLOCK STEERING. UNLOCK THROTTLE. SAS ON.

// =========== pitch program (from tutorial 02) ===========
FUNCTION pitchAt {
  DECLARE PARAMETER h.
  LOCAL f IS (h - turnStart) / (turnEnd - turnStart).
  SET f TO MIN(MAX(f, 0), 1).
  RETURN pitch0 + (pitch1 - pitch0) * (f ^ kShape).
}

// =========== staging ===========
FUNCTION shouldStage {
  // Guard 1: never while coasting.
  IF THROTTLE <= 0.01 { RETURN FALSE. }

  // Guard 4: never when the staging system is busy.
  IF NOT STAGE:READY { RETURN FALSE. }

  // Guard 2: debounce.
  IF TIME:SECONDS < lastStageAt + stageCooldown { RETURN FALSE. }

  // Guard 3: every lit engine must be spent.
  LOCAL es IS LIST().
  LIST ENGINES IN es.
  LOCAL lit IS 0.
  LOCAL spent IS 0.
  FOR e IN es {
    IF e:IGNITION {
      SET lit TO lit + 1.
      IF e:FLAMEOUT { SET spent TO spent + 1. }
    }
  }
  IF lit = 0 { RETURN TRUE. }   // nothing lit and we want thrust
  RETURN spent = lit.
}

FUNCTION autoStage {
  IF shouldStage() {
    SET lastStageAt TO TIME:SECONDS.
    STAGE.
    PRINT "STAGE " + STAGE:NUMBER + "          " AT (0,0).
  }
}

// =========== performance ===========
FUNCTION activeIsp {
  LOCAL es IS LIST().
  LIST ENGINES IN es.
  LOCAL fTot IS 0.
  LOCAL fOverIsp IS 0.
  FOR e IN es {
    IF e:IGNITION AND NOT e:FLAMEOUT AND e:ISP > 0 {
      SET fTot TO fTot + e:AVAILABLETHRUST.
      SET fOverIsp TO fOverIsp + e:AVAILABLETHRUST / e:ISP.
    }
  }
  IF fOverIsp <= 0 { RETURN 0. }
  RETURN fTot / fOverIsp.
}

// Approximate: treats all remaining onboard propellant as usable.
// Honest about being an upper bound. See the notes below.
FUNCTION vesselDeltaV {
  LOCAL isp IS activeIsp().
  IF isp <= 0 { RETURN 0. }
  LOCAL m0 IS SHIP:MASS.
  LOCAL mf IS SHIP:DRYMASS.
  IF mf <= 0 { RETURN 0. }
  RETURN isp * g0 * LN(m0 / mf).
}

FUNCTION burnTimeFor {
  DECLARE PARAMETER dv.
  LOCAL isp IS activeIsp().
  LOCAL f IS SHIP:AVAILABLETHRUST.
  IF isp <= 0 OR f <= 0 { RETURN 0. }
  LOCAL ve IS isp * g0.
  RETURN (SHIP:MASS * ve / f) * (1 - CONSTANT:E ^ (-dv / ve)).
}

FUNCTION telemetry {
  PRINT "ALT    " + ROUND(SHIP:ALTITUDE) + "     "       AT (0,2).
  PRINT "AP     " + ROUND(SHIP:APOAPSIS) + "     "       AT (0,3).
  PRINT "SPD    " + ROUND(SHIP:VELOCITY:SURFACE:MAG,1)+"   " AT (0,4).
  PRINT "PITCH  " + ROUND(pitchAt(SHIP:ALTITUDE),1)+"     " AT (0,5).
  PRINT "STAGE  " + STAGE:NUMBER + "          "         AT (0,6).
  PRINT "ISP    " + ROUND(activeIsp(),1) + "       "    AT (0,7).
  PRINT "dV     " + ROUND(vesselDeltaV()) + "        "   AT (0,8).
  PRINT "TWR    " + ROUND(SHIP:AVAILABLETHRUST/(SHIP:MASS*9.81),2)+"  " AT (0,9).
}

On the mass-weighted Isp

Combining engines of different Isp is not an average of their Isp values. The correct combination weights by mass flow, and since flow is F/(Ispg0), the total flow is ΣFi/Isp,i divided by g0. Setting total thrust over total flow equal to an effective Isp gives:

Isp,eff = Σ FiΣ Fi / Isp,i

That's a thrust-weighted harmonic mean, which is exactly what activeIsp() computes. Take a plain arithmetic average of a 320 s vacuum engine and a 170 s solid booster and you'll overestimate your Δv badly — the low-Isp engine is gulping propellant far faster than its thrust share suggests.

On requirement 5 — and why it's marked approximate

vesselDeltaV() uses SHIP:DRYMASS, which is the whole vessel with every tank empty. So during your first stage it counts the propellant sitting in stages two and three as though the first stage's engines could burn it. The number is an upper bound, and it's wrong by more the earlier in the flight you are.

Doing this properly means walking the fuel-crossfeed graph: for each burning engine, find which tanks it can actually draw from, sum only those resources, and account for tanks that will separate. kOS exposes what you'd need — e:CONSUMEDRESOURCES, part-level resource lists, decoupler stage assignments — but assembling it correctly is a genuine project, not a paragraph.

So the right call for now is the one taken above: compute the approximation, and know it's an approximation. Flight software full of quantities whose error you haven't characterised is how missions get lost. A number you understand the bias of is more useful than a number you merely hope is right. When you build the per-stage version in Arc 3, the logged data will tell you exactly how far off this one was.

The edge case in guard 3

IF lit = 0 { RETURN TRUE. } reads as reckless — no engines lit, so stage. It's safe here only because guards 1, 2 and 4 have already run: throttle is up, the cooldown has elapsed, and staging is ready. Together those mean "we are asking for thrust, we've waited, and nothing is burning" — which genuinely is the signal to fire the next stage, and it's what catches a separation where the next engine needs an explicit ignition.

Reorder those four checks and this becomes a rocket-shredder. Guard ordering is load-bearing, not stylistic.

Arc complete

What you have, and what you can't answer

Three tutorials in, you have a script that flies a multi-stage rocket to orbit unattended. That's the point where most people stop.

You now know: that LOCK binds an expression and makes control laws live; how to build a function and call it from inside a lock; how to derive an interpolation, clamp it, and differentiate it numerically; the three useful rearrangements of Tsiolkovsky; where burn time comes from; and why an autostage routine needs four independent guards rather than one clever condition.

You cannot yet answer any of these:

Every one of those is a question about a number you didn't record. Arc 2 is one tutorial long and it's the hinge of the whole series: you'll write telemetry to CSV, accumulate the loss integrals in-flight, and build the analyser that overlays runs against each other. After that, every claim any tutorial makes — including the ones above about k = 0.5 — becomes something you can check rather than accept.

Before you move on. Fly launch3.ks at least three times with different turn parameters, on the same rocket, and write down apoapsis and remaining fuel each time. In the next tutorial you'll re-fly those same configurations with logging on — and having the crude numbers first makes it much more obvious how much you were missing.