Arc 2 · Measure it  /  Tutorial 04 of 16

Telemetry & the loss budget

Arc 1 ended on a wall: you had opinions about your ascent and no numbers. This tutorial is the hinge of the whole series. You'll write flight data to CSV, integrate the loss terms in flight, and end every run with an itemised bill for where your Δv went. After this, every claim becomes checkable.

ideal Δv from the tanks speed you kept gravity loss drag loss steering loss
The bill you're about to itemise
Prelaunch

Writing to disk

kOS can write files. That single capability is what turns a script you watch into an experiment you analyse.

Volumes & paths
"0:/name.csv"The archive — volume 0. On disk it is your KSP Ships/Script/ folder, the same place this tutorial lives. Files written here survive the flight, the vessel, and the save.
"1:/name.csv"The local volume — the processor's own disk. Small, capacity-limited, and it dies with the craft. Not what you want for logs.
PATH("0:/x.csv")Turns a string into a path object. Rarely needed for logging, useful when you start building filenames.
File operations
LOG expr TO "0:/x.csv".Append one line, then a newline. Creates the file if absent. This is the whole logging API.
EXISTS("0:/x.csv")Boolean. Use it before deleting so you don't error on a fresh install.
DELETEPATH("0:/x.csv").Remove a file. There is no "overwrite" mode — LOG only ever appends, so deleting first is how you start a clean run.
Numbers & time
TIME:SECONDSUniversal time in seconds, as a decimal. Your clock for everything on this page.
SHIP:QDynamic pressure — in atmospheres, not pascals. Multiply by 101.325 for kPa.
SHIP:BODY:MU / :RADIUSGravitational parameter μ in m³/s², and body radius in m. Local gravity is μ/r².
VANG(v1, v2)Angle between two vectors, in degrees. You'll meet vectors properly in Tutorial 08; here you need exactly this one function.
SIN(x), COS(x), ARCSIN(x)kOS trig is in degrees, both ways in and out. Every radian habit you have will bite you here.
ROUND(x, n)Round to n decimals. Use it on every logged column — see the sizing task below.
the whole idea, in five lines
SET logFile TO "0:/run_a.csv".
IF EXISTS(logFile) { DELETEPATH(logFile). }
LOG "met,alt,vsrf" TO logFile.
LOG ROUND(met,2) + "," + ROUND(SHIP:ALTITUDE,1) + "," + ROUND(SHIP:VELOCITY:SURFACE:MAG,2) TO logFile.
Two ways this bites you

LOG appends. Fly twice without deleting and your second run is stapled onto the first, header and all, and every plot you make will be nonsense in a way that looks almost plausible.

Writing to the archive needs a connection to the archive. In stock kOS that's always true, but if you run RemoteTech or kOS's signal-delay option, volume 0: goes away the moment you lose the link — usually right as you pitch over. Check HOMECONNECTION:ISCONNECTED before you trust a log if you fly with either of those.

Reserved words, again. From Tutorial 02's list, two are about to be extremely tempting: log and q. You want a variable for dynamic pressure and you want one for the logfile. Name them dynp and logFile.
Ignition

How often should you sample?

The obvious answer — "every tick, in the loop I already have" — is wrong in two separate ways, and one of them is silent.

Your ascent loop runs WAIT 0, which yields until the next physics tick. KSP's physics tick is 0.02 s, so the loop runs about 50 times a second. A powered ascent to 75 km takes somewhere around three to four minutes.

Size it
  1. You're logging 23 columns. Assume each rounded number averages 8 characters plus a comma. How many bytes is one row? How many rows in a 220-second ascent at one row per physics tick, and how large is the file?
  2. You want to resolve the peak of the dynamic-pressure curve. Max-Q lasts roughly 20 seconds. If you want at least 40 samples across that feature, what sample interval do you need?
  3. Given your answer to (2), what is the file size for the same 220-second ascent? What did the tick-rate version buy you?
  4. The gravity-loss integral ∫g sin γ dt is a different job from plotting. At the sample interval from (2), roughly what error would you expect in that integral compared to accumulating at tick rate? (Think about how fast sin γ changes, not how fast you sample.)
Saved
Show the numbers

1 — Tick rate. 23 columns × 9 bytes ≈ 207 bytes/row. At 50 Hz for 220 s that's 11 000 rows, about 2.3 MB — per flight. Fly the six configurations this tutorial asks for and you have 14 MB of CSV to drag into a browser tab.

2 — Resolving max-Q. 40 samples across a 20-second feature is one every 0.5 s. Round down for margin: 0.25 s, or 4 Hz. That is comfortably enough to see the shape of every curve in this series, including the sharp ones.

3 — What you gave up. 220 s at 4 Hz is 880 rows, about 180 kB. You threw away 92% of the data and lost nothing you were going to look at. The tick-rate version bought you resolution finer than the thing you're measuring — and it cost you a file the analyser has to chew through, plus a disk write every 20 ms in the middle of a flight.

4 — And here is the split that matters. Nearly nothing. The integrand g sin γ is smooth and slow — γ swings 90° over two minutes — so a 0.25 s rectangle rule tracks it to well under a m/s across the whole ascent.

But that is a statement about this integrand. Thrust is not smooth: staging drops it to zero and back in a single tick. Sample the ideal-Δv integral at 4 Hz and a stage separation that happens between two samples gets integrated as though the engine never cut out.

So the answer isn't one rate. It's two:

integrate every tick · log every 0.25 s

The accumulators are cheap — a few multiplies — and they need every tick to stay honest. The file write is expensive and needs 4 Hz. Splitting them is the single most important structural decision in launch4.ks.

Ascent

Columns, and the run tag

You are going to compare runs against each other. That requirement, and not tidiness, is what fixes the file format.

The temptation is to write a header block: a few # comment lines at the top of each file recording which k you flew, then the CSV. Don't. Every parser you or anyone else writes then needs a special case, and the moment you concatenate two files the metadata is stranded in the middle of the data.

Instead: repeat the run's identity on every row. Four extra columns, constant down the whole file, and in exchange any tool that can read a CSV can group your runs, overlay them, and filter them — including one you write in twenty minutes.

ColumnUnitWhy it's here
runtextThe tag you group and colour by. Make it descriptive: k05_h1000.
k, h0, h1—, m, mThe pitch-program settings, so a file is self-describing six months later.
utsUniversal time. The only unambiguous clock; lets you line a log up against a save.
metsSeconds since liftoff. The x-axis you'll actually plot against.
altmAltitude ASL. The other x-axis you'll plot against.
vsrfm/sSurface-relative speed. What the airframe and the drag equation care about.
vorbm/sOrbital speed. What getting to orbit actually requires.
vspdm/sVertical speed. Signed — its zero crossing is apoapsis.
gammadegFlight path angle, from the horizon. The actual one, not the commanded pitch.
pitchdegCommanded pitch out of pitchAt(). Plot it against gamma — the gap is the story.
alphadegAngle between where you point and where you're going. Steering loss lives here.
masstVessel mass. Its staircase shows you staging events for free.
thrustkNActual thrust now, not available thrust. They differ, and the difference matters.
dynpkPaDynamic pressure. The max of this column is the number Tutorial 07 exists to control.
ap, pemApoapsis and periapsis. Where the flight is going, as opposed to where it is.
dvIdealm/s∫(F/m) dt — everything the tanks have given you so far.
lossGravm/s∫g sin γ dt, accumulated live.
lossSteerm/s∫(F/m)(1 − cos α) dt, accumulated live.
lossDragm/sThe residual. Read the section on it before you believe it.
stagenum—STAGE:NUMBER. Lets the analyser mark separations on the time axis.
Round on write, not on read. ROUND(x,2) on a speed costs you nothing physical and roughly halves the file. But don't round ut — it's a number in the millions and two decimals of it are the only thing making your rows distinguishable.
The equation

Where the delta-v went

This is the derivation the entire tutorial is built around. It is four lines long and it is the reason Arc 3 exists.

Take the rocket as a point mass. Let v be its speed, γ the flight path angle above the horizon, F the thrust, m the mass, D the drag force, and α the angle between the thrust direction and the velocity direction.

Resolve Newton's second law along the velocity vector. Only the component of each force parallel to v changes the speed; perpendicular components change the direction instead.

Derive it
  1. Write m dv/dt as the sum of three along-track terms: thrust, gravity, drag. Get the sign and the trig factor right on each. (Gravity points down; the along-track component of "down" when you're climbing at γ is what?)
  2. Divide by m and integrate both sides from liftoff to now. Call ∫(F/m) dt the ideal Δv — it's what Tsiolkovsky in Tutorial 03 predicted the tanks would give you.
  3. Now split the thrust term. You wrote F cos α/m; add and subtract F/m to pull out a term that is zero when α = 0 and positive otherwise. That term is the steering loss — name it and check its sign.
  4. Rearrange into: ideal Δv = (speed you actually have) + (three losses). This is your bill.
  5. Sanity check: a rocket hovering at constant altitude, engine at exactly TWR = 1, for 30 seconds. What does each of the four terms equal? Does it balance?
Saved
Show the algebra

1 — Along-track equation of motion. Thrust contributes F cos α (only its component along v). Drag acts directly backwards along v, so it's −D. Gravity points at the centre of the body; the angle between "straight down" and "backwards along v" is 90° − γ, so its along-track component is −mg sin γ.

m dvdt = F cosα − mg sinγ − D

Note that when γ = 0 — flying flat — gravity does nothing to your speed at all. It's still bending you, but it isn't slowing you. That single fact is the entire argument for the gravity turn.

2 — Divide and integrate.

dvdt = Fm cosα − g sinγ − Dm
Δvgained = ∫Fmcosα dt − ∫gsinγ dt − ∫Dmdt

3 — Pull the steering loss out. Write cos α as 1 − (1 − cos α):

∫Fmcosα dt = ∫Fmdt − ∫Fm(1 − cosα)dt

The first integral is Δvideal. The second is the steering loss, and because cos α ≤ 1 always, it is never negative — you cannot gain speed by pointing away from where you're going. It's also quadratic in α for small angles: cos α ≈ 1 − α²/2, so 5° of misalignment costs you 0.4% of your thrust and 20° costs 6%. Small angles are genuinely cheap, which is why a slightly-off gravity turn is fine and a badly-off one is not.

4 — The bill.

Δvideal = Δvgained + Lgrav + Ldrag + Lsteer

Lgrav = ∫g sin γ dt  ·  Ldrag = ∫(D/m) dt  ·  Lsteer = ∫(F/m)(1 − cos α) dt

Everything the tanks gave you is either speed you still have, or one of three named ways you threw it away. There is no fourth bucket and nothing is unaccounted for. That's what makes this worth measuring rather than estimating.

5 — The hover check. TWR = 1 means F/m = g. Pointing straight up and not moving, γ is 90° (you're going nowhere, but the instant you move it's straight up), α = 0, D = 0, and speed never changes.

  • Δvideal = g × 30 = 294 m/s
  • Lgrav = g sin 90° × 30 = 294 m/s
  • Ldrag = Lsteer = 0, and Δvgained = 0

294 = 0 + 294 + 0 + 0. It balances, and it tells you something brutal: thirty seconds of hovering costs you 294 m/s and buys you nothing. Every second your rocket spends pointed straight up is being billed at g per second. That is the number Arc 3 spends four tutorials attacking.

Anomaly

Speed relative to what?

The derivation says "v". Your ship has two of them, they differ by 175 m/s on the pad, and picking the wrong one quietly poisons two of your four buckets.

kOS offers SHIP:VELOCITY:SURFACE — relative to the rotating ground — and SHIP:VELOCITY:ORBIT — relative to the planet's centre, non-rotating. At KSC, sitting still on the pad, the first is 0 m/s and the second is about 175 m/s eastward, because the pad itself is moving.

Think it through before you pick
  1. At the instant of liftoff, using orbital velocity: what is γ? What is α, given the rocket points straight up? What does the steering-loss integrand (F/m)(1 − cos α) evaluate to?
  2. Same instant, using surface velocity: what are γ and α now? What's the problem with computing γ = arcsin(vvertical/v) at that instant?
  3. The derivation on the last page assumed an inertial frame. Which of the two is inertial? So which one is formally correct?
  4. Given (1) and (3) disagree with intuition, which do you log — and what do you have to write down about the choice so that future-you doesn't misread the numbers?
Saved
Show the answer — and why it's a compromise

1 — Orbital frame at liftoff. You're moving at 175 m/s horizontally (east, with the ground) and 0 m/s vertically. So γ = 0°. The rocket points straight up, so α = 90°, and the steering integrand is (F/m)(1 − cos 90°) = F/m — the entire thrust is booked as steering loss, and the gravity loss is g sin 0° = 0.

That is not a bug. It's the honest inertial answer: you are already orbiting slowly, and pointing straight up is 90° away from where that motion is going. But it means your budget reports a vertical launch as a steering problem and not a gravity problem, which is going to be a very hard sell in Arc 3.

2 — Surface frame at liftoff. γ = 90° once you're moving, α = 0, gravity loss runs at the full g. Exactly the intuition, and exactly the hover check from the last section.

The problem: at t = 0 the surface speed is 0, so γ = arcsin(0/0) and your script divides by zero on its first tick. Guard it — below about 1 m/s of surface speed, just declare γ = 90° and α = 0. You're on the pad; it's true.

3 — Formally. The orbital frame is the inertial one, so the derivation is exactly valid there and only approximately valid in the rotating surface frame. In the surface frame there are centrifugal and Coriolis terms that the derivation dropped.

4 — What to actually do. Log the budget in the surface frame, and write down why.

Two reasons. First, it matches every other ascent analysis you will ever read, so your numbers are comparable to other people's. Second, and more importantly for this series: the surface frame is the one where the losses are attributed to the thing you can change. Your pitch program is written in surface-relative terms; you want a budget that tells you what your pitch program cost you.

The price is that the frame terms you dropped don't vanish — they land in the residual, which is the drag bucket. At Kerbin's rotation rate they're small compared to drag through the lower atmosphere, but "small" is a claim, and in Tutorial 06 you'll build a drag model good enough to test it.

So the honest column header is not "drag loss". It's "drag loss and everything else I didn't model" — and the next section is about taking that seriously.

Log both speeds anyway. vsrf and vorb are two columns and one of them is free. The 175 m/s gap between them is a gift from Kerbin's rotation that you'll spend all of Arc 5 learning to aim properly.
Ascent

Accumulating in the loop

An integral in a control loop is a running total and a time step. Both halves have a trap in them.

The pattern is a rectangle rule: every pass, work out how long it's been since the last pass, evaluate the integrand now, multiply, add to a global.

the accumulator pattern
SET lossGrav TO 0.
SET lastTick TO TIME:SECONDS.

// ...inside the loop, every tick...
LOCAL now IS TIME:SECONDS.
LOCAL dt IS now - lastTick.
SET lastTick TO now.
IF dt > 0 {
  SET lossGrav TO lossGrav + gNow() * SIN(gammaNow()) * dt.
}
Three traps — find them before you read on
  1. The first tick. What is dt on the very first pass, and what happens if you initialise lastTick at the top of the file rather than immediately before the loop? (Your script does a five-second countdown in between.)
  2. Zero-length steps. WAIT 0 yields one tick, but kOS can run your loop more than once per physics tick if the instruction budget allows. What is dt then, and what does that do to a rectangle rule? Now: what does it do to a trapezoid rule?
  3. Time warp. Physics warp at 4× makes each tick cover 0.08 s of game time. Does TIME:SECONDS account for that automatically? Does the accuracy of your rectangle rule survive it?
Saved
Show the answers

1 — Five seconds of phantom gravity loss. If lastTick is set at the top of the file and the countdown runs for five seconds, the first dt inside the loop is 5.0, not 0.02. The rocket was sitting on the pad the whole time, but your accumulator books g sin 90° × 5 ≈ 49 m/s of gravity loss before liftoff.

49 m/s is small enough to look plausible and large enough to change your conclusions. Set lastTick on the line immediately before the loop — after STAGE., not before the countdown.

2 — Zero dt is harmless; the guard is for something else. A pass with dt = 0 adds exactly zero to a rectangle rule, so it costs nothing but a wasted evaluation. The IF dt > 0 guard exists for a different reason: if you're computing anything by dividing by dt — a numerical derivative, say — that pass is a division by zero.

The trapezoid rule is a different story. It averages the current integrand with the previous one, which means it has to store the previous one. A zero-length step overwrites your stored value with an identical one — harmless — but it does mean the trapezoid's error advantage over the rectangle rule evaporates when your step sizes are erratic. At 50 Hz over a smooth integrand, neither is worth arguing about. Use the rectangle rule and spend the attention elsewhere.

3 — Yes, and mostly. TIME:SECONDS is game time, so it advances at 0.08 s per tick under 4× physics warp and your dt is automatically right. The integral stays unbiased.

What degrades is resolution: you get a quarter as many samples across the same manoeuvre. Through staging events and max-Q that matters. The practical rule for this series is don't physics-warp a logged ascent — and since you'll want to compare runs, warping some and not others is worse than warping none.

On-rails time warp is a different beast entirely: physics is off, your loop barely runs, and thrust is impossible. That's not a logging problem, it's a "you can't do that" problem, and kOS will refuse to warp on rails while the throttle is up.

Units — this one is free and people still get it wrong kOS gives mass in tonnes and thrust in kilonewtons. Thrust divided by mass is therefore kN/t = 1000 N / 1000 kg = m/s², exactly. No conversion factor. But SHIP:Q is in atmospheres and SHIP:BODY:MU is in m³/s² — mixed unit systems in the same API. Check every new suffix the first time you use it.
Instrument

Where the delta-v goes

A point-mass model of the ascent you've been flying, with the four buckets computed exactly the way launch4.ks will compute them. Drag the profile and watch the bill change.

Ascent loss explorer—
speed kept gravity drag steering
Δv spent to MECO—
Circularise at AP—
Total to orbit—
Peak dynamic pressure—
Use it to answer these
  1. Start at k = 1 and walk down to k = 0.3. Gravity loss falls and drag loss rises. Where is the minimum total?
  2. Now push CdA up to 8 and repeat. Which way did the optimal k move, and does that make physical sense?
  3. Raise liftoff TWR. Gravity loss drops — you spend less time fighting it. So why isn't the answer "build the highest-TWR rocket you can"? (Look at what happens to peak q, and remember Tutorial 03's mass ratio.)
  4. Set k = 2 and note "Δv spent to MECO". It's the lowest of any setting. Explain why that profile is nonetheless the worst one on the list.
This is a model, not an answer Point mass, exponential atmosphere, one stage, a drag coefficient that doesn't vary with Mach, and no steering lag. Every one of those is wrong in KSP. Its job is to show you the shape of the tradeoff — that there is an optimum, and which way it moves — so that when your own logs disagree with it, you know what you're looking at. Believe your CSVs over this widget.
Anomaly

The bucket that isn't drag

Three of your four numbers are measured. The fourth is whatever's left over, and it will happily absorb every mistake you make.

You can measure Δvideal: thrust and mass are both live telemetry. You can measure the gravity loss: g from μ/r², γ from the velocity. You can measure the steering loss: α is an angle between two vectors you have. And Δvgained is just your current surface speed.

You cannot measure drag. There is no SHIP:DRAG. So you get it the only way available — by rearranging the identity:

Ldrag = Δvideal − Δvgained − Lgrav − Lsteer

Which works. And which means that if any of the other three is wrong, the error appears — with the wrong sign, attached to the wrong physics — in the column labelled "drag".

List what's actually in there

Before reading on, write down everything you can think of that would end up in that residual besides aerodynamic drag. You should be able to get at least four.

Saved
Show the list
  • Rotating-frame terms. The centrifugal and Coriolis contributions you dropped when you chose the surface frame. Small at Kerbin's rotation rate, but not zero, and they grow as you build horizontal speed.
  • Thrust misreporting. If you accumulate SHIP:AVAILABLETHRUST instead of actual thrust, then every second at part throttle inflates Δvideal, and the whole inflation lands in drag. This is the big one, and it's why the schema logs actual thrust.
  • The α convention. VANG(SHIP:FACING:VECTOR, …) is where the nose points, which is not where the engines push if you have gimballed engines mid-correction, or a control-surface trim. Usually within a degree; occasionally not.
  • Integration error at staging. Thrust steps discontinuously. Any tick where the accumulator's thrust reading and the actual thrust disagree gets billed to drag.
  • Engine spool time. Real engines take a moment to reach full thrust; if you read thrust at the top of the tick and it changes during the tick, the difference accumulates.
  • Vertical speed vs. radial speed. SHIP:VERTICALSPEED is the rate of change of altitude, which is radial. Over a curved surface at high horizontal speed, "up" is rotating underneath you. Tiny, but it's in there.

So how do you know if your drag number is any good? Two checks, both cheap:

  1. The vacuum test. Fly the same script on the Mun, or above 70 km on Kerbin. True aerodynamic drag is exactly zero there. Whatever your residual reports is pure error, and now you know its size.
  2. The shape test. Plot lossDrag against altitude. Real drag loss accumulates almost entirely between about 5 and 20 km and then flattens completely. If your curve keeps climbing above 40 km, you are not looking at drag.

Do the vacuum test before you trust a single drag number in Arc 3. It takes one flight and it calibrates every measurement you make afterwards.

This is the actual skill. Not the CSV writing — that's twenty lines. Knowing which of your numbers are measured, which are inferred, and what the inferred ones absorb when you're wrong is the difference between instrumenting a rocket and decorating it.
Your mission

launch4.ks and the analyser

Requirements
  1. Start from launch3.ks. Keep the pitch program and the autostaging exactly as they are — this tutorial adds instrumentation and changes nothing about how the rocket flies.
  2. Add a runTag variable at the top and build the log filename from it. Delete the file if it exists, then write the header row.
  3. Write accumulate() and call it every tick. It maintains dvIdeal, lossGrav, lossSteer, and its own lastTick.
  4. Write logRow() and call it at 4 Hz, using the additive-deadline pattern with a catch-up guard.
  5. Use actual thrust in the accumulator — sum :THRUST over active engines, not AVAILABLETHRUST. Log both if you want to see the gap.
  6. Guard γ and α for the pad case where surface speed is ~0.
  7. At MECO, print the four-way budget to the terminal and log one final row.
  8. Fly six runs on the same rocket: k ∈ {0.4, 0.5, 1.0} at turn start 1000 m, then k = 0.5 at turn start 250 m, 2000 m, and 4000 m. Tag each one. Do not physics-warp any of them.
  9. Drop all six CSVs into the analyser and answer: which had the lowest total loss, and which single loss term explains the difference?
analyzer.html
Sitting next to this file. Open it in a browser and drag your CSVs onto it — it groups by the run column, overlays every run on one set of axes, and draws the loss budget as a stacked bar per run. Nothing is uploaded anywhere; it parses in the tab. Your logs land in Ships/Script/, which is the same folder this tutorial is in, so the files are already where you need them.
Try the analyser before you fly. sample-logs/ holds six CSVs in the exact schema above, with run tags prefixed SIM_. They come from the point-mass model in the explorer section, not from KSP — they exist so you can learn the tool and check your parser against a known-good file. Load them once, see what the tool does, then delete them from your working set so you never confuse a simulated curve with a flown one.
Fly them properly or don't bother Same vessel, same launch, no warp, no manual input. Six runs where one of them had a slightly different payload is six runs that tell you nothing, and you will not be able to tell from the CSV that it happened. If you rebuild the rocket, re-fly the whole set.
Saved
Solution

Open after you've flown all six

Show launch4.ks
launch4.ks
// launch4.ks — launch3 plus instrumentation.
// Flies identically. Ends with an itemised delta-v bill.

CLEARSCREEN.

// ---- constants ----
SET g0 TO 9.80665.

// ---- run identity ----
SET runTag TO "k05_h1000".
SET logFile TO "0:/run_" + runTag + ".csv".

// ---- ascent target ----
SET targetAp TO 75000.
SET azimuth  TO 90.

// ---- ascent profile ----
SET turnStart TO 1000.
SET turnEnd   TO 45000.
SET pitch0    TO 90.
SET pitch1    TO 0.
SET shapeK    TO 0.5.

// ---- staging ----
SET cooldown     TO 1.
SET lastCooldown TO TIME:SECONDS.

// ---- instrumentation state ----
SET sampleDt   TO 0.25.
SET nextSample TO 0.
SET lastTick   TO 0.
SET t0         TO 0.
SET dvIdeal    TO 0.
SET lossGrav   TO 0.
SET lossSteer  TO 0.
SET telemetryRow TO 0.

// ---- open the log ----
IF EXISTS(logFile) { DELETEPATH(logFile). }
LOG "run,k,h0,h1,ut,met,alt,vsrf,vorb,vspd,gamma,pitch,alpha," +
    "mass,thrust,dynp,ap,pe,dvIdeal,lossGrav,lossSteer,lossDrag,stagenum" TO logFile.

// ---- countdown ----
FROM {LOCAL c IS 5.} UNTIL c = 0 STEP {SET c TO c-1.} DO {
  PRINT "T-MINUS          " + c AT (0,0).
  WAIT 1.
}

SAS OFF. RCS OFF.
LOCK THROTTLE TO 1.
LOCK STEERING TO HEADING(azimuth, pitchAt(SHIP:ALTITUDE)).
STAGE.
PRINT "LIFTOFF             " AT (0,0).

// Start the clocks HERE — not at the top of the file, or the
// countdown's five seconds get integrated as gravity loss.
SET t0         TO TIME:SECONDS.
SET lastTick   TO TIME:SECONDS.
SET nextSample TO TIME:SECONDS.

// ---- ascent loop ----
UNTIL SHIP:APOAPSIS > targetAp {
  accumulate().            // every tick — the integrals need it
  IF TIME:SECONDS >= nextSample {
    logRow().
    SET nextSample TO nextSample + sampleDt.
    // If we fell behind (lag, a long tick), don't spend the next
    // N passes catching up at tick rate — resync to now.
    IF nextSample < TIME:SECONDS { SET nextSample TO TIME:SECONDS + sampleDt. }
  }
  printTelemetry().
  autoStage().
  WAIT 0.
}

// ---- MECO ----
LOCK THROTTLE TO 0.
accumulate().
logRow().
PRINT "MECO                          " AT (0,0).
WAIT 1.
UNLOCK STEERING. UNLOCK THROTTLE.
printBudget().

// ================= instrumentation =================

FUNCTION gNow {
  LOCAL r IS SHIP:BODY:RADIUS + SHIP:ALTITUDE.
  RETURN SHIP:BODY:MU / (r * r).
}

// Flight path angle above the horizon, surface frame, in degrees.
// On the pad vsrf is 0 and the ratio is undefined — we're pointed
// up and going nowhere, so 90 is the honest answer.
FUNCTION gammaNow {
  LOCAL sp IS SHIP:VELOCITY:SURFACE:MAG.
  IF sp < 1 { RETURN 90. }
  RETURN ARCSIN(MIN(MAX(SHIP:VERTICALSPEED / sp, -1), 1)).
}

// Angle between where we point and where we're going, in degrees.
FUNCTION alphaNow {
  IF SHIP:VELOCITY:SURFACE:MAG < 1 { RETURN 0. }
  RETURN VANG(SHIP:FACING:VECTOR, SHIP:VELOCITY:SURFACE).
}

// ACTUAL thrust, not available thrust. The difference is
// throttle setting and atmospheric derating, and it goes
// straight into dvIdeal if you get it wrong.
FUNCTION actualThrust {
  LOCAL total IS 0.
  LIST ENGINES IN es.
  FOR e IN es {
    IF e:IGNITION AND NOT e:FLAMEOUT {
      SET total TO total + e:THRUST.
    }
  }
  RETURN total.
}

// Called EVERY tick. Rectangle rule on three integrals.
FUNCTION accumulate {
  LOCAL now IS TIME:SECONDS.
  LOCAL dt  IS now - lastTick.
  SET lastTick TO now.
  IF dt <= 0 { RETURN. }

  // kN / t is m/s^2 exactly — no conversion.
  LOCAL aT IS actualThrust() / SHIP:MASS.

  SET dvIdeal   TO dvIdeal   + aT * dt.
  SET lossGrav  TO lossGrav  + gNow() * SIN(gammaNow()) * dt.
  SET lossSteer TO lossSteer + aT * (1 - COS(alphaNow())) * dt.
}

// Drag is the only term we can't measure, so it's the residual.
// Read the tutorial section on what else ends up in here.
FUNCTION lossDrag {
  RETURN dvIdeal - SHIP:VELOCITY:SURFACE:MAG - lossGrav - lossSteer.
}

FUNCTION logRow {
  LOG runTag
    + "," + shapeK
    + "," + turnStart
    + "," + turnEnd
    + "," + ROUND(TIME:SECONDS, 2)
    + "," + ROUND(TIME:SECONDS - t0, 2)
    + "," + ROUND(SHIP:ALTITUDE, 1)
    + "," + ROUND(SHIP:VELOCITY:SURFACE:MAG, 2)
    + "," + ROUND(SHIP:VELOCITY:ORBIT:MAG, 2)
    + "," + ROUND(SHIP:VERTICALSPEED, 2)
    + "," + ROUND(gammaNow(), 2)
    + "," + ROUND(pitchAt(SHIP:ALTITUDE), 2)
    + "," + ROUND(alphaNow(), 2)
    + "," + ROUND(SHIP:MASS, 3)
    + "," + ROUND(actualThrust(), 2)
    + "," + ROUND(SHIP:Q * 101.325, 3)   // atm -> kPa
    + "," + ROUND(SHIP:APOAPSIS, 1)
    + "," + ROUND(SHIP:PERIAPSIS, 1)
    + "," + ROUND(dvIdeal, 2)
    + "," + ROUND(lossGrav, 2)
    + "," + ROUND(lossSteer, 2)
    + "," + ROUND(lossDrag(), 2)
    + "," + STAGE:NUMBER
    TO logFile.
}

FUNCTION printBudget {
  LOCAL kept IS SHIP:VELOCITY:SURFACE:MAG.
  PRINT "---- delta-v bill ----------" AT (0,2).
  PRINT "ideal      " + ROUND(dvIdeal,1)   + "     " AT (0,3).
  PRINT "kept       " + ROUND(kept,1)      + "     " AT (0,4).
  PRINT "gravity    " + ROUND(lossGrav,1)  + "     " AT (0,5).
  PRINT "steering   " + ROUND(lossSteer,1) + "     " AT (0,6).
  PRINT "drag+resid " + ROUND(lossDrag(),1) + "     " AT (0,7).
  PRINT "apoapsis   " + ROUND(SHIP:APOAPSIS)  + "     " AT (0,8).
  PRINT "logged to  " + logFile AT (0,9).
}

// ================= from launch3 =================

FUNCTION turnFraction {
  DECLARE PARAMETER h.
  LOCAL f IS (h - turnStart) / (turnEnd - turnStart).
  RETURN MIN(MAX(f, 0), 1).
}

FUNCTION pitchAt {
  DECLARE PARAMETER h.
  RETURN pitch0 + (pitch1 - pitch0) * (turnFraction(h) ^ shapeK).
}

// ... allEnginesBurned(), shouldStage(), autoStage(),
// ... printTelemetry() — unchanged from launch3.ks

Why logRow is one enormous expression

Because LOG writes a line per call, and a line is one string. Building it with twenty-two + operators is ugly and it is also the only shape that doesn't cost you an extra file write per column. If it bothers you, build a LIST of values and join it — kOS has JOIN on lists — but be aware you're now allocating a list every quarter second for the rest of the flight.

On e:IGNITION AND NOT e:FLAMEOUT

Tutorial 03's currentThrust() filtered engines by e:stage = stage:number. That's the right question for "what will this stage do", and the wrong question for "what is pushing me right now" — an engine from a previous stage that's still lit doesn't match, and a not-yet-ignited engine in the current stage does. For the accumulator you want the engines actually burning. Two different questions, two different filters; keep both functions.

What you should have seen across the six runs

Lower k should give lower gravity loss and higher drag loss, with the total bottoming out somewhere around k ≈ 0.4–0.5 for a typical stock rocket. If your k = 0.4 run reached the same apoapsis with less total loss than k = 1, you have just measured the claim Tutorial 02 asked you to take on trust.

The turn-start sweep is the more interesting one. Turning at 250 m usually looks great in the gravity-loss column and terrible in the drag column, and on many rockets it also loses control authority low and slow — which shows up as a spike in the alpha column that the steering-loss integral then bills you for. Three columns, one story. That's the analyser earning its keep.

If your total losses come out under about 1000 m/s or over about 2500 m/s, be suspicious before you're pleased. Sanity-check dvIdeal at MECO against a hand-computed Tsiolkovsky from Tutorial 03 — they should agree within a few percent, and if they don't, your thrust filter is wrong.

Arc complete

The hinge

Arc 2 is one tutorial long because it does one thing, and everything after it depends on that thing being done.

Go back to the list Arc 1 ended on. Was k = 0.5 better than k = 1, and by how many m/s? How much Δv did you lose to gravity? Where was peak dynamic pressure? You now have a file that answers each of those, for six different configurations, in a form you can plot.

And you have something more useful than the answers: a measurement apparatus you know the limits of. You know which three numbers are measured and which one is a residual. You know what the residual absorbs. You know the frame you chose and what it cost you. That's what makes the next four tutorials possible — Arc 3 is going to make claims about gravity loss, drag, and steering loss, and every one of them is now a claim you can check against a CSV instead of a claim you have to believe.

Keep every log. Name them and don't delete them. Tutorial 08 replaces the pitch program with a prograde follower, and the only way to know whether it actually helped is to overlay it on the runs you flew today. Those files are your control group.