Arc 2 · Measure it / Tutorial 04 of 16
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.
kOS can write files. That single capability is what turns a script you watch into an experiment you analyse.
Ships/Script/ folder, the same place this tutorial lives. Files written here survive the flight, the vessel, and the save.LOG only ever appends, so deleting first is how you start a clean run.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.
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.
log and q. You want a variable for dynamic pressure and you want one for the logfile. Name them dynp and logFile.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.
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:
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.
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.
| Column | Unit | Why it's here |
|---|---|---|
| run | text | The tag you group and colour by. Make it descriptive: k05_h1000. |
| k, h0, h1 | —, m, m | The pitch-program settings, so a file is self-describing six months later. |
| ut | s | Universal time. The only unambiguous clock; lets you line a log up against a save. |
| met | s | Seconds since liftoff. The x-axis you'll actually plot against. |
| alt | m | Altitude ASL. The other x-axis you'll plot against. |
| vsrf | m/s | Surface-relative speed. What the airframe and the drag equation care about. |
| vorb | m/s | Orbital speed. What getting to orbit actually requires. |
| vspd | m/s | Vertical speed. Signed — its zero crossing is apoapsis. |
| gamma | deg | Flight path angle, from the horizon. The actual one, not the commanded pitch. |
| pitch | deg | Commanded pitch out of pitchAt(). Plot it against gamma — the gap is the story. |
| alpha | deg | Angle between where you point and where you're going. Steering loss lives here. |
| mass | t | Vessel mass. Its staircase shows you staging events for free. |
| thrust | kN | Actual thrust now, not available thrust. They differ, and the difference matters. |
| dynp | kPa | Dynamic pressure. The max of this column is the number Tutorial 07 exists to control. |
| ap, pe | m | Apoapsis and periapsis. Where the flight is going, as opposed to where it is. |
| dvIdeal | m/s | ∫(F/m) dt — everything the tanks have given you so far. |
| lossGrav | m/s | ∫g sin γ dt, accumulated live. |
| lossSteer | m/s | ∫(F/m)(1 − cos α) dt, accumulated live. |
| lossDrag | m/s | The residual. Read the section on it before you believe it. |
| stagenum | — | STAGE:NUMBER. Lets the analyser mark separations on the time axis. |
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.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.
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 γ.
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.
3 — Pull the steering loss out. Write cos α as 1 − (1 − cos α):
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.
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.
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.
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.
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.
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.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.
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. }
lastTick at the top of the file rather than immediately before the loop? (Your script does a five-second countdown in between.)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?TIME:SECONDS account for that automatically? Does the accuracy of your rectangle rule survive it?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.
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.
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.
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:
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".
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.
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.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.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:
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.
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.runTag variable at the top and build the log filename from it. Delete the file if it exists, then write the header row.accumulate() and call it every tick. It maintains dvIdeal, lossGrav, lossSteer, and its own lastTick.logRow() and call it at 4 Hz, using the additive-deadline pattern with a catch-up guard.:THRUST over active engines, not AVAILABLETHRUST. Log both if you want to see the gap.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.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.// 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
logRow is one enormous expressionBecause 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.
e:IGNITION AND NOT e:FLAMEOUTTutorial 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.
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 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.