Arc 1 · Make it fly  /  Tutorial 02 of 16

Functions & the pitch program

That hardcoded 80° in launch1.ks is a cliff. Replace it with a continuous function of altitude, and your rocket stops fighting itself. You'll derive the interpolation, clamp it, and then find the flaw hiding inside it.

launch1 — a step launch2 — a ramp pitch altitude
Step vs. ramp
Prelaunch

New vocabulary

Small list this time. The work on this page is mathematical, not syntactic.

Functions
FUNCTION name { … }Declare. kOS reads the whole file first, so order doesn't matter.
DECLARE PARAMETER x.First line(s) of the body. Multiple params: one statement each, or comma-separated.
DECLARE PARAMETER x IS 5.Default value — the argument becomes optional.
RETURN expr.Hand a value back and exit the function immediately.
LOCAL x IS 3.Scope a variable to this block. Use it inside functions, always.
Arithmetic & logic
MIN(a,b) / MAX(a,b)Exactly two arguments each.
ABS(x), SQRT(x), ROUND(x,n)The usual. ROUND takes an optional digit count.
x ^ nExponent. Works with fractional powers.
IF cond { } ELSE { }No ELSE IF keyword — nest an IF inside the ELSE block.
AND, OR, NOTBoolean operators, spelled out.
= vs >=, <=, <>A single = is equality here, not assignment. Assignment is SET. Inequality is <>.
Reserved — do not name a variable any of these
rvqvdpathstepnodelistlograngeheadinglatlngcharbodystagecore
These fail in ways that don't look like naming errors — you'll get a parse error pointing at the wrong line, or worse, silently wrong behaviour. q and v in particular are the ones you'll instinctively reach for when writing physics.
A quirk worth knowing now. Functions declared inside a program file can't be called from the terminal prompt — only from other code in a running program. If you want to poke at a function interactively, wrap the call in a tiny throwaway script and RUN that.
Ignition

Your first function

A function is just a named expression with holes in it. Here's the whole syntax:

shape.ks
FUNCTION twrNow {
  RETURN SHIP:AVAILABLETHRUST / (SHIP:MASS * 9.81).
}

FUNCTION lerp {
  DECLARE PARAMETER a.
  DECLARE PARAMETER b.
  DECLARE PARAMETER f.
  RETURN a + (b - a) * f.
}

PRINT twrNow().
PRINT lerp(90, 0, 0.25).   // 67.5

Now the connection back to Tutorial 01. Because LOCK stores an expression and re-evaluates it, a function call inside a lock becomes a live control law — the function runs again on every steering update:

the pattern
LOCK STEERING TO HEADING(90, pitchAt(SHIP:ALTITUDE)).

One line, locked once, before liftoff. It steers the entire ascent. Everything left to do on this page is deciding what pitchAt should return.

Ascent

Build the interpolation

Don't look this up. You have two points and you want the straight line between them — that's all a linear interpolation is.

You want pitch to be 90° (straight up) at some altitude h0, and 0° (flat, at the horizon) at some higher altitude h1. Between them it should slide smoothly.

Derive it
  1. Define a progress fraction f that is 0 at h0 and 1 at h1. Write it in terms of h, h0, h1.
  2. Using f, write pitch p(h) so that it equals p0 when f = 0 and p1 when f = 1.
  3. Substitute and simplify into a single expression in h. Check both endpoints.
  4. With h0 = 1000, h1 = 45000, p0 = 90, p1 = 0 — what is the pitch at 12 km? What is dp/dh in degrees per kilometre?
Saved
Show the algebra

1 — The fraction. Distance travelled divided by total distance:

f = h − h0h1 − h0

At h = h0 the numerator vanishes so f = 0. At h = h1 numerator and denominator are identical so f = 1.

2 — The blend. Start at p0 and add the fraction f of the total change:

p = p0 + f (p1 − p0)

You may have written the equivalent p = (1−f)p0 + f p1. Same thing, expanded. The first form is better in code: one multiply, and the "start plus a correction" shape generalises to every interpolation you'll write later.

3 — Combined.

p(h) = p0 + (p1 − p0) h − h0h1 − h0

4 — Numbers. At h = 12 000:

f = 12000 − 100045000 − 1000 = 1100044000 = 0.25

So p = 90 + 0.25 × (0 − 90) = 67.5°.

And the slope is constant everywhere: dp/dh = (0 − 90) / 44 000 = −0.002045 °/m, or about −2.05 degrees per kilometre. Hold on to that number — it's the villain of the last section on this page.

Ascent

Clamping, without an if

Your formula is happily defined outside the range you care about, and it will produce nonsense there.

Below h0, f goes negative and pitch exceeds 90° — your rocket leans backwards. Above h1, f exceeds 1 and pitch goes negative, aiming you at the ground. You need f confined to [0, 1].

Work it out

kOS gives you MIN(a,b) and MAX(a,b), two arguments each. Write clamp(f, 0, 1) using only those two functions, no IF. Then say which one enforces the floor and which the ceiling — people get this backwards constantly.

Saved
Show the answer
clamp = MIN( MAX(f, 0), 1 )

MAX(f, 0) enforces the floor: it returns whichever is larger, so anything below 0 comes back as 0. MIN(…, 1) enforces the ceiling: anything above 1 comes back as 1.

The mnemonic that actually sticks: MAX raises, MIN lowers. MAX can only push a number up to the floor; MIN can only push it down to the ceiling.

Order doesn't matter here — MAX(MIN(f,1),0) is identical — as long as your bounds are the right way round. If lo > hi, the two orderings disagree, which is a genuinely nasty bug in generic clamp helpers.

Clamp the fraction, not the pitch. Clamping f works no matter which direction the pitch runs. Clamping p to [0, 90] would silently break the moment you want a profile ending below the horizon — which you will, in Tutorial 09.

pitchAt
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.
}
Instrument

Shape the curve

Drag the values. The straight line is your linear program; the curve adds one exponent.

A real gravity turn isn't linear. Raising f to a power k before blending lets you pitch aggressively early — while you're slow and drag is cheap — then ease off:

p = p0 + (p1 − p0) f k

k < 1 pitches over early  ·  k = 1 is linear  ·  k > 1 holds vertical longer

Pitch program—
pitch — your program |dp/dh| — how fast it turns
Try k = 0.5. That's close to what most hand-tuned KSP ascents converge on, and it's not a coincidence — it's the shape that roughly keeps the rocket following its own velocity vector. In Tutorial 08 you'll stop approximating that shape and just read the velocity vector directly.
Anomaly

The flaw you just built in

Your pitch program is smooth in altitude. The rocket doesn't experience altitude — it experiences time. Those are not the same thing, and the gap between them is a real problem.

You computed dp/dh ≈ −2.05 °/km, constant across the whole turn. But what the airframe feels is the pitch rate, dp/dt.

Two questions
  1. Relate dp/dt to dp/dh. What connects them? (It's the chain rule, and the connecting quantity is something you can read straight off the telemetry.)
  2. A rocket passes 5 km climbing at 180 m/s, and passes 40 km climbing at 900 m/s. Using dp/dh = −2.05 °/km, what is the pitch rate in °/s at each? What's the ratio?
Saved
Show the algebra

1 — Chain rule.

dpdt = dpdh · dhdt

And dh/dt is simply vertical speed — the rate at which you're gaining altitude. So your pitch program's rate is its slope multiplied by how fast you're climbing.

2 — Numbers. Take those climb rates as vertical speed:

  • At 5 km: −2.05 °/km × 0.180 km/s = −0.37 °/s
  • At 40 km: −2.05 °/km × 0.900 km/s = −1.84 °/s

A factor of 5×, and in exactly the wrong direction.

Why that's wrong. Down low the air is thick, the rocket is heavy and sluggish, and you want to commit to the turn — but your program is turning at its slowest. Up high the air is nearly gone, the rocket is light and the turn is almost free — and your program is whipping the nose around fastest, right where you'd rather be settling onto a shallow trajectory.

You've built a pitch program that is timid when it should be bold and frantic when it should be calm. It works — it will get you to orbit — but it is fighting physics the entire way, and the wasted energy shows up as steering loss.

Three tutorials from now you'll measure that loss in m/s. Five from now you'll delete this function entirely and replace it with a rocket that simply follows its own velocity vector, where the pitch rate falls out of the physics for free.

Keep the function anyway. It's your baseline. Every improvement in Arc 3 gets judged against a logged run of this exact profile, so don't throw it away when it stops being the best thing you have.
Your mission

launch2.ks

Requirements
  1. Start from your launch1.ks. Put the tunable numbers — turn start, turn end, start and end pitch, exponent, target apoapsis, azimuth — in named variables at the very top.
  2. Write pitchAt(h) with the clamped fraction and the exponent k.
  3. Lock steering once, before liftoff, to HEADING(azimuth, pitchAt(SHIP:ALTITUDE)). Nothing after that line may touch the steering lock.
  4. Add commanded pitch and vertical speed to your telemetry printout.
  5. Also print the current pitch rate in °/s — compute it from the chain rule, using SHIP:VERTICALSPEED.
  6. Fly it twice on the same rocket: once with k = 1, once with k = 0.5. Note the apoapsis you reach and the fuel remaining at MECO.
Requirement 5 has a subtlety Your pitchAt is clamped, so once you're past turnEnd the true pitch rate is zero — but the chain-rule formula doesn't know that. Decide how to handle it. Printing a rate of −1.8 °/s while the nose is locked flat is a lie, and lying telemetry is worse than none.
Saved
Solution

Open after you've flown both

Show launch2.ks
launch2.ks
// launch2.ks — continuous pitch program.

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.

// ---- launch ----
FROM {LOCAL t IS 5.} UNTIL t = 0 STEP {SET t TO t - 1.} DO {
  PRINT "T-MINUS " + t 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).

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

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

// ---- pitch program ----
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) ^ kShape).
}

// Pitch rate via chain rule: dp/dt = (dp/dh)(dh/dt).
// Differentiate numerically — one clean line instead of a derivative
// that would have to change every time we change the curve shape.
FUNCTION pitchRate {
  LOCAL h IS SHIP:ALTITUDE.
  LOCAL dh IS 10.
  LOCAL slope IS (pitchAt(h + dh) - pitchAt(h - dh)) / (2 * dh).
  RETURN slope * SHIP:VERTICALSPEED.
}

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 "VSPD   " + ROUND(SHIP:VERTICALSPEED,1) + "      "     AT (0,5).
  PRINT "PITCH  " + ROUND(pitchAt(SHIP:ALTITUDE),1) + "      "  AT (0,6).
  PRINT "dP/dt  " + ROUND(pitchRate(),2) + " deg/s   "      AT (0,7).
}

On the numerical derivative

You could differentiate p(h) = p0 + (p1−p0)fk by hand and get

dpdh = k (p1 − p0) fk−1h1 − h0

That's correct, and it's worth doing on paper. But it goes stale the instant you change the curve shape, and it blows up at f = 0 when k < 1. The central difference — sample the function 10 m either side, divide by 20 — costs two extra function calls and stays right no matter what pitchAt becomes. It also solves the requirement-5 problem for free: inside the clamped region both samples return the same value, the slope comes out as exactly zero, and the readout tells the truth.

That trick generalises. When a quantity is awkward to differentiate but cheap to evaluate, sample it instead.

What you should have seen

k = 0.5 almost certainly reached the target apoapsis with more fuel left. It commits to the turn early, which means more of the thrust goes into horizontal velocity — the velocity you actually need for orbit — instead of being spent climbing against gravity and then having to be bent sideways later.

But "almost certainly" and "more fuel left" are not measurements. How much? Where did the saving come from — less gravity loss, or less steering loss? Would k = 0.4 be better still? You cannot answer any of that from a fuel gauge, and that's the wall this arc ends on.