← Back to Menu
tiopez@university_

Part 8 — Closed-Loop Airbrake Control


Part 8: Closed-Loop Airbrake Control — Bang-Bang, PID, and Predictive

Post-game write-up, now covering all three planned control algorithms for the ARC 2026 airbrake problem: bang-bang, PID (live-gain), and predictive (model-inversion via bisection). Section 8.6 documents the diagnostics chart built partway through the predictive work, which turned out to be valuable enough — for predictive AND for re-diagnosing PID's D-term — that Section 8.9 flags it as its own lesson for how this whole chapter's development process should change going forward.


8.1 The Problem: Why Airbrakes At All

A model rocket motor is certified to burn exactly one way, every time — you don't get to throttle it, restart it, or shut it down early. That means the only lever you have over how high the rocket goes is how much drag it experiences during coast. More drag during coast → lower apogee. Less drag → higher apogee.

The ARC (American Rocketry Challenge) scoring problem is to hit a precise target altitude — not "as high as possible," not "close enough," but a specific number, 228m (750 ft) in our case — plus a target flight duration. Motors, by design, have some performance margin built in (you don't want to certify a motor that might underperform and never leave the rod), which means most flights naturally overshoot the target if left alone.

Aerobrakes solve this by deploying drag-increasing surfaces during coast, after burnout, to bleed off exactly enough excess energy to land on target. They don't add thrust and they don't change the ballistic trajectory before burnout — they only ever subtract energy, and only during the coast phase.

This means every algorithm in this chapter is really answering one question, asked over and over, many times a second, during the few seconds between burnout and apogee:

"Given where I am right now, how much more drag do I need, right now, to land on target?"

The three algorithms in this project — bang-bang, PID, and predictive/model-based — are three different strategies for answering that question. They form a natural teaching progression: each one fixes a real, demonstrated weakness in the one before it, and each one is more sophisticated because the simpler one genuinely wasn't good enough — not because sophistication is inherently better.


8.2 Shared Foundation: predictApogeeNoBrake()

All three algorithms lean on the same piece of physics: a closed-form estimate of how much higher the rocket will coast, given its current altitude, velocity, mass, and drag coefficient, if the brakes were fully retracted starting right now:

yc = (M / 2k) · ln( (M·g + k·v²) / (M·g) )

where k = ½·ρ·Cd_base·A is the drag term with brakes removed. This is the Fehskens-Malewicki coast-phase equation, and it's the single most-used function in the whole plugin — every algorithm calls it, every frame, to answer "how much more altitude is already baked in from here, if I do nothing more?"

The key trick that makes this reusable everywhere: Cd_base = Cd - cd_adj, where cd_adj is whatever drag the airbrakes are currently contributing. Subtracting it out means this function always answers "what if brakes retract now," regardless of what they're doing at this exact instant — which is exactly the counterfactual every algorithm needs to reason about.

Validation: this closed-form estimate was checked against the full RK4 integration (the "ground truth" physics engine) at the end of a real flight and matched to within 5mm on a ~272m trajectory. This is a genuinely trustworthy model, not just a convenient approximation.


8.3 Algorithm 1: Bang-Bang

How it works

The simplest possible controller. Every frame:

if predicted_apogee > target:
    angle = max_deploy_angle   (full brakes)
else:
    angle = min_deploy_angle   (brakes retracted)

No gains to tune, no history, no memory of previous frames. It just asks "am I still going to overshoot if I do nothing more?" and slams the brakes fully open or fully closed based on the answer.

Strengths

Weaknesses

Verdict

Bang-bang is the right first algorithm to build and a genuinely useful teaching tool — but it's not going to win precision-landing points on its own. It's the baseline everything else is measured against.


8.4 Algorithm 2: PID (Live-Gain, Physically-Derived)

This is where almost all of the real engineering work in this project happened, and it's worth walking through why each piece exists, in the order it was actually needed — because every piece was added to fix a specific, demonstrated failure of the piece before it, not added speculatively.

8.4.1 Why not a textbook fixed-gain PID?

The very first attempt used a single, fixed Kp, derived once from the burnout state via finite-difference (perturbing Cd slightly, seeing how much predictApogeeNoBrake moved, then chaining that through the airbrake geometry to get degrees-per-meter-of-error).

This failed in a specific, instructive way: the fixed gain gave a reasonable initial correction, but as the flight progressed, the sensitivity of apogee to brake angle collapsed — both because the brake geometry itself is least effective near 0° and 90° (a sin(θ) term), and because drag effectiveness in general falls off as velocity drops. A gain that was well-calibrated at burnout became badly undersized by mid-coast. The controller converged to a steady-state offset well short of target — a textbook proportional-control symptom, and a good one to have seen firsthand rather than just read about.

8.4.2 Fix: make the gain live — computeSensitivity()

Instead of deriving Kp once, recompute the sensitivity every frame, from the rocket's current state:

Kp_live = -1 / d(apogee)/d(angle)

where d(apogee)/d(angle) is itself a chained finite-difference: perturb Cd slightly to get d(apogee)/d(Cd), perturb angle slightly (via the known brake-geometry formula) to get d(Cd)/d(angle), multiply them together.

This fixed the steady-state offset — but introduced the next problem.

8.4.3 The 90° singularity

The brake-geometry formula for projected area includes a sin(θ) term, whose true derivative (cos θ) goes to exactly zero at θ = 90°. Once the controller commanded full deployment (which happens often, especially early in coast), computeSensitivity() was evaluating a derivative at exactly the point where it collapses to zero — and Kp_live = -1/0 blew up toward infinity, producing commanded angles in the millions of degrees (harmlessly clamped, but numerically nonsensical).

Fix: never linearize exactly at the geometric edge. computeSensitivity() clamps its reference angle to stay a small margin (5°) away from 0° and 90° before taking the finite difference. Cheap, physically motivated (a real servo has finite resolution near its end-stops anyway), and it fully resolved the blow-up at 90°.

8.4.4 A second singularity, later in flight

Fixing the 90° edge case wasn't the whole story. As velocity approaches zero near apogee, d(apogee)/d(Cd) also collapses toward zero — a real physical effect: with almost no kinetic energy left, no amount of added drag can move the predicted apogee anymore. Kp_live blows up a second time, later in the flight, for a genuinely different physical reason than the 90° case.

Fix: Kp_live is explicitly capped. The cap isn't an arbitrary round number — it's derived from the same physical threshold used to decide "no useful authority remains" (see 8.4.6 below): the largest gain that still does useful work before that boundary is reached.

Kp_live_max = max_deploy_angle / authority_diff_threshold   (= 90° / 3m = 30)

8.4.5 Deriving Ki, live, at Coast Begin — not guessed

Rather than hand-tune an integral gain, it's derived automatically the moment coast begins, from that flight's own burnout telemetry:

  1. Predict the no-brake apogee and the resulting error at burnout.
  2. Use the same sensitivity chain to estimate how much additional angle would be needed to close that error in one shot.
  3. Estimate remaining coast time (ballistic, no-drag estimate — conservative, since real coast with drag runs a little longer).
  4. Ki = additional_angle_needed / (residual_error × coast_time_estimate)

This is the answer to "how is this generic across different rockets/flights?" — nothing about Kp or Ki is typed in ahead of time. Both are computed from whatever flight is actually happening, using only rocket-specific geometry constants (tube diameter, brake panel size) as fixed inputs.

8.4.6 Integral windup near authority collapse — isTargetAchievable()

Once live gains were working, a new failure appeared near apogee: with velocity nearly zero, the brakes genuinely can't move the predicted apogee anymore (same physics as 8.4.4) — but the integral term didn't know that, and kept accumulating against an error it could no longer correct, driving the commanded angle to climb continuously for no benefit.

Fix: isTargetAchievable() compares the no-brake prediction against the best-case, full-brake prediction. When that gap shrinks below a threshold (3m, later tightened to 1.5m — see 8.4.9), real control authority is gone, and both the integral accumulation and the Kp_live update freeze. This achievability check does double duty — it also identifies genuinely unreachable targets (a rocket that simply doesn't have enough excess energy left to correct in the remaining time), which is valuable information in its own right, independent of windup.

8.4.7 Actuator dynamics matter — rate limiting

An early "successful" run showed the commanded angle swinging from 90° to ~3° in 0.1 seconds — a transition no real servo can execute. The controller's own math was internally consistent, but it was implicitly assuming an actuator that could teleport.

Fix: grounded the actuator in a real datasheet spec (Savox SH-0257MG, metal-gear digital micro servo — worst-case 0.13s/60° at 4.8V, giving ~461°/s max slew rate) and clamped the change in commanded angle per frame to what that servo can physically produce:

max_angle_delta = max_slew_rate_deg_per_sec × dt
angle = clamp(angle, prev_angle − max_angle_delta, prev_angle + max_angle_delta)

This is real, and it matters beyond the sim — any physical implementation needs this constraint regardless of what the pure control math wants.

8.4.8 Adding Kd — and why it had to wait

Classical PID theory says add a derivative term to damp oscillation. It was deliberately not added until real oscillation was actually observed in data — adding a damping term for a symptom that hasn't appeared is tuning against noise, not against the system.

Once Kd was justified (see 8.4.9), it was built with two things this project had already learned the hard way: (1) differentiate a low-pass-filtered version of the error, not the raw error, since raw sensor noise differentiated directly produces spikes; (2) guard the very first control-loop frame, where dt can legitimately be zero, from a 0/0 division.

Kd's magnitude is tied to Kp_live × Kd_Td, where Kd_Td is the actuator's own full-sweep time (max_deploy_angle / max_slew_rate_deg_per_sec) — another number derived from real hardware, not guessed.

8.4.9 Realistic sensor noise, and the real reason it mattered

Real flight hardware (BMP180 barometer, LSM6DSOX IMU) doesn't report perfect altitude and velocity. Noise was modeled from the actual datasheet specs (0.25m RMS altitude noise; accelerometer noise density converted to a velocity random-walk term) and applied only to the copy of the frame the controller sees — the true simulated physics stays clean, exactly mirroring how a real sensor's noise doesn't change the real world, only the flight computer's perception of it.

With noise active, the controller reliably landed within about a meter of target and — after tightening authority_diff_threshold from 3m to 1.5m — repeatably within 0.07m across six independent noisy runs (228.87m–228.94m against a 228m target).

8.4.10 Kd, Revisited: Chart-Driven Isolation, and the Decision to Disable It

Section 8.4.8 justified adding Kd once oscillation was actually observed — but at the time, "observed" meant reading console-logged angles and inferring a pattern by eye. Once the diagnostics chart existed (Section 8.6), the same question could be asked properly: does Kd actually help, or does it just look justified?

The chart made the answer obvious in a way the logs alone hadn't. At full Kd, the angle trace showed a large, sustained oscillation — swinging the full 28°–90° range, ringing for the entire second half of coast — and, critically, it kicked in almost exactly where Authority Margin collapsed toward zero. That's the signature of a derivative term amplifying sensor noise once the true signal (the error) gets small, not of it damping a real overshoot.

Three isolation runs, same rocket, only Kd changed, each one charted:

Kd Angle trace 5-run apogee spread
Full (Kp_live × Kd_Td) Sustained full-range oscillation from ~4s onward Not measured — visibly unstable
10% of full Smaller oscillation, same character, still present Not separately measured
0 (disabled) Clean, smooth decay; no sustained oscillation 227.85–227.97m

The 10%-Kd run was the deciding data point: the oscillation shrank proportionally with the gain rather than persisting at a fixed size. That rules out a structural resonance (which wouldn't scale away with a smaller gain) and confirms straightforward noise amplification — the fix is a gain question, not a redesign.

But the more important finding is the comparison against Kd = 0, which wasn't just "less bad" — it was the best of all three, both visually (a clean chart, no jitter after initial settle) and numerically (a 0.12m 5-run spread, tighter than any Kd > 0 configuration, and tighter even than the predictive algorithm's own 0.67m spread — Section 8.5.6).

Decision: Kd is implemented, was tested rigorously, and is disabled by default. The original formula is kept in the code, commented, with the finding documented inline — not because D-terms are wrong in general, but because this specific system (a rate-limited actuator, a noisy velocity estimate, and an authority margin that collapses fast near apogee) has no overshoot problem for D to solve, and differentiating near-zero-signal noise only hurts. This is a more honest and more useful result for the book than "we tuned D and it worked" would have been — it's a real, tested, system-specific finding, not a textbook recitation.

This also reframes the six-run, 228.87–228.94m PID result quoted just above: that run predates this isolation testing, and its exact Kd configuration wasn't recorded precisely enough to say whether it matches "full," "10%," or "0." Treat the 227.85–227.97m, Kd=0 result above as the current best-characterized PID configuration going forward, and the six-run figure as an earlier, less-isolated data point worth revisiting if its exact historical Kd value can be reconstructed.

Strengths

Weaknesses

Verdict

A genuinely strong, precision controller once fully derived and grounded — but it earned every piece of its complexity by fixing a real, demonstrated failure mode, in order. That order is the lesson: none of this complexity would be justified without the data that motivated each step.


8.5 Algorithm 3: Predictive / Model-Based

8.5.1 How it works: direct model inversion, not gain scheduling

Bang-bang answers "should I add drag." PID answers "how far off am I, and how hard should I correct." Predictive asks a fundamentally different question: "what angle, right now, makes the predicted apogee exactly equal target?" — and then solves for it directly, every frame, rather than approaching it through a gain.

The physics is the same closed-form coast equation every algorithm in this chapter shares (Section 8.2). The obstacle is that it can't be algebraically inverted for angle — the drag term k depends on Cd, which depends on angle, and it sits both inside and outside a logarithm. So "predictive" really means "numerically solve for the angle that zeroes the error," via bisection between min_deploy_angle and max_deploy_angle:

apogeeAtAngle(θ) = alt + predictApogeeNoBrake(alt, v, mass, cd + adj_cd_at_angle(θ))

This function is monotonic — more angle → more drag → lower predicted apogee — so a standard bisection search converges reliably, and if the target falls outside the reachable range at either end (apogee_hi > target or apogee_lo < target), that's detected immediately rather than found by accident.

No integral, no accumulated error, no linearized gain — in principle, no steady-state offset to chase, because it isn't doing linear correction at all. That was the theoretical appeal going in; getting there in practice took three more rounds of chart-driven (and, before the chart existed, log-driven) debugging.

8.5.2 First failure: chatter that looked like noise sensitivity, but wasn't

The first working version, wired in and run for real, produced apogee results close to target (five runs: 227.97, 228.12, 228.03, 233.24, 228.10) — but the commanded-angle log was alarming: frame-to-frame jumps of ~23°, repeatedly, throughout mid-coast. The natural first hypothesis: velocity noise, amplified through a numerically sensitive inversion (drag scales with v², so small v errors can matter a lot), re-solved fresh from scratch every frame with nothing to damp it.

That hypothesis led to a real fix — input-side low-pass filtering of the noisy alt/v before they reached the solver, reusing the same alpha = dt/(Kd_Td + dt) filter already built for PID's derivative term (Section 8.4.8). It's a physically sound idea — filter noise before it hits a sensitive calculation, the same thing a real sensor-fusion layer would need to do regardless of which algorithm is running.

It didn't work. Re-run with filtering active, the chatter was essentially unchanged in magnitude — still ~23° jumps, still every few frames. This is the valuable part of the story: a plausible, well-reasoned fix that measurably fails is worth more than a lucky guess that happens to work, because it forces the next question to be asked properly instead of declaring victory on a hunch.

8.5.3 Root cause: quantized bisection, not noise

The actual clue was hiding in a second logged value: the solver's iteration count. Frames with large angle jumps consistently showed low iteration counts (2–3), while smoothly-converging frames showed high counts (16–21).

The mechanism: solveAngleForTarget() always restarted bisection from the same full [0°, 90°] bracket and stopped the instant |predicted − target| < tolerance was satisfied. Because bisection always visits the same fixed set of candidate angles in the same order (45° first, then 22.5° or 67.5°, and so on), where it happens to stop is a function of whether the target lands close to one of those specific coarse points early in the search — not a continuous function of the input. Two frames with nearly-identical (even filtered) alt/v could have their target sit on opposite sides of an early coarse split, producing wildly different reported angles even though the true answer barely moved. Filtering the input couldn't fix this, because the discontinuity was in the solver's stopping rule, not in the input noise.

8.5.4 Fix: tighten the tolerance

Tightening tolerance_m from 0.5m to 0.03m forced bisection deeper on every frame, before it could exit onto the coarse, sparse grid points. Re-run: the mid-coast angle trace became a smooth ~1° ripple — 51.3° → 51.9° → 52.9° → 53.4° → 54.1° → … — instead of a sawtooth. The chatter was gone.

8.5.5 Second failure: flailing near apogee — a different problem entirely

Tightening tolerance fixed mid-coast, but the tail of the same log — the last handful of frames before apogee — got worse if anything: full-range swings between 0° and 90°, iteration counts back down to 2–3, no obvious pattern. This isn't the same bug reappearing; it's a genuinely different physical effect.

Drag scales with v². As velocity collapses toward zero near apogee, apogeeAtAngle(0°) and apogeeAtAngle(90°) converge toward each other — there's barely any velocity left for extra drag to act on. When that gap gets small, almost every angle satisfies the tolerance, and which one bisection happens to land on is effectively arbitrary, flipping with each frame's noise draw. This is a genuine loss-of-authority condition, not a solver bug — the same physical effect isTargetAchievable() (Section 8.4.6) was built to catch for PID, just showing up at the opposite end: not "can't reach the target at all" but "brake angle barely matters anymore, don't trust the solve."

8.5.6 Fix: authority-collapse guard

Inside solveAngleForTarget(), before attempting to bisect, check the width of the achievable range:

if (apogee_lo - apogee_hi) < authority_diff_threshold:
    return { angle: current_deploy_angle, authority_collapsed: true }

If the brake's total commandable spread is smaller than the same physical threshold PID already uses to detect authority loss, the right move is to hold current position, not chase an ill-conditioned "solution." Re-run: the tail of the log went from wild 0°–90° flailing to a tight ~10° band that settled cleanly and held flat through the end of coast — exactly the behavior the guard was designed to produce.

With both fixes in place, five repeat runs landed at 227.55, 228.18, 228.10, 227.96, 228.22 — a 0.67m spread, centered almost exactly on the 228m target, with the earlier 233.24m outlier gone. No steady-state bias (unlike early fixed-gain PID, Section 8.4.1) — direct model inversion really does sidestep that failure mode structurally, once the solver itself is solid.

Strengths

Weaknesses

Verdict

A genuinely strong controller, and the cleanest of the three once fully debugged — but "direct model inversion" turned out not to mean "no failure modes," just different ones than PID's. The lesson (Section 8.7) isn't "predictive is better," it's "every control strategy trades one class of failure mode for another, and you don't know which ones until you chart the data."


8.6 Building the Diagnostics Chart: From Table to Visualization

This section exists because of a direct piece of feedback worth recording verbatim: "Looking back, this would have been a great teaching tool. Would have helped me visualize what you were trying to tell me verbally." The chart wasn't built until partway through debugging Predictive — Sections 8.5.2–8.5.6 above were diagnosed largely from console-logged numbers read by eye, correlating jump sizes against iteration counts across dozens of log lines. It worked, but it was slow, and every "here's why the angle is doing that" explanation was verbal and abstract until this existed.

8.6.1 From table to arrays

The project already had a live per-frame diagnostics table (UI.logAerobrakeFrame(), built earlier in this project specifically to support debugging PID). Charting needed almost nothing new — the same per-frame data already being computed and logged to the table just also gets pushed into parallel arrays (ab_t, ab_angle, ab_apogee, ab_v, ab_margin) inside that same logging call, keeping the "encapsulate the UI interaction in one place" pattern already established for the table.

8.6.2 What to chart, and why each series earns its place

Four series, chosen specifically to explain — not just display — the failure modes already being chased by eye:

  1. Commanded Angle — the thing every debugging conversation kept describing verbally ("it jumped," "it's chattering," "it settled"). Seeing it as a line made every one of those descriptions immediately checkable.
  2. Predicted Apogee, with a target line — the actual objective. Without it, an angle change can't be judged as "correcting toward target" versus "reacting to nothing."
  3. Velocity — the physical root cause behind two separate effects discovered this session: why noise gets amplified early in coast (v² scaling) and why authority collapses near apogee (v → 0). Plotting it turns those from claims into something visibly self-evident.
  4. Authority Margin (apogee_at_min − apogee_at_max) — the exact quantity the collapse guard (Section 8.5.6) checks against threshold. Charting it turns "the solver held position because authority collapsed" from an assertion into something you can watch cross the threshold in real time.

Built on the project's existing Plotly convention (multi-trace, multiple y-axes, already used for the flight-profile chart), so no new charting library or pattern was introduced.

8.6.3 The payoff arrived somewhere unexpected

The chart was built to help finish Predictive — and it did (Sections 8.5.5–8.5.6 were diagnosed directly from it: the authority margin trace crossing threshold lined up exactly with the angle trace going flat). But its more dramatic payoff was retroactive: re-running PID with the chart active, purely as practice, immediately showed something the earlier log-reading approach had never surfaced — a sustained, full-range angle oscillation late in coast, lining up visually with Authority Margin approaching zero. That observation led directly to the Kd isolation testing and disablement documented in Section 8.4.10.

The general lesson (restated in Section 8.7 as a cross-cutting item): build the visualization before the second algorithm, not after the third. The verbal, log-reading approach to debugging Bang-Bang and PID worked, but it worked harder than it needed to — several of the same conclusions that took paragraphs of log-correlation to establish for Predictive were immediately, visually obvious once charted, and the same tool then found a real bug (Kd's oscillation) in an algorithm that had already been considered "done."


8.7 Cross-Cutting Lessons Learned

These apply across all three algorithms and are worth internalizing independent of which controller you're building.

1. A working simulation doesn't mean a working design — it means a working model of a design. The bang-bang and early-PID results looked clean and convincing in the sim. Several of the "failures" chased later in this project turned out to be genuine physics (windup, singularities, actuator limits) that a clean noise-free sim simply never exercises. Add noise, add actuator limits, add real hardware constraints before trusting a sim result.

2. Fixed-gain control breaks down whenever the system's sensitivity to control input changes significantly during the maneuver. This is a general control-theory point, not specific to rockets — anywhere a system's response to your actuator changes a lot over the timescale of the control action (shrinking coast velocity, in this case), a fixed gain will either under- or over-correct somewhere along the way.

3. Live/adaptive gains fix that — but introduce their own numerical fragility, especially near mathematical singularities. This is a real trade-off, not a free upgrade. Anywhere a control law includes a 1/x term, ask what happens as x → 0, and build the guard rails (margins, caps, authority checks) before they're needed, not after a mysterious failure.

4. Anti-windup isn't optional once you have an integral term and any possibility of saturated or exhausted control authority. The failure mode (commanding ever-larger corrections for zero benefit) is subtle in a log and obvious in a plot — watch for angle climbing while predicted outcome stays flat.

5. Real actuators are not instantaneous, and pretending they are hides real constraints. A control law that's internally consistent can still be physically unrealizable. Ground actuator behavior in a real datasheet spec as early as practical, not as an afterthought.

6. Derive gains from physics and hardware specs wherever possible, not from guessing-and-checking. Every gain in the final PID controller traces back to either a finite-difference sensitivity calculation or a real component datasheet. This is what makes the controller generic — portable to a different rocket without re-tuning by hand — and it's also what made every piece of this debuggable, because a wrong number could be checked against its derivation rather than just re-guessed.

7. Validate the whole signal path, not just the math. The single most time-consuming issue of this entire session was not a physics bug or a tuning problem — it was a missing return statement in a wrapper function, which silently discarded every commanded correction before it ever reached the simulated rocket. The control math had been correct for a long time before this was found; the actuator link was the broken part. The lesson: when a well-reasoned fix doesn't produce the expected result, check that the signal is actually reaching its destination before doubting the reasoning itself. A few hours were spent this session chasing floating-point precision and sensor-noise theories for a problem that was, in the end, a disconnected wire. That's a genuinely common and easy-to-miss class of bug in any layered system, and it's worth teaching explicitly: verify the plumbing before re-deriving the physics.

8. A chart earns its keep the moment it turns a verbal argument into something checkable. Every "the angle is chattering because of noise" or "it's holding because authority collapsed" explanation in Sections 8.4–8.5 was, at the time, a claim backed by log-reading, not something the person being told could independently verify. Once the diagnostics chart existed, those same claims became visible facts, and — more importantly — it caught a real bug (Kd's oscillation, Section 8.4.10) that had gone unnoticed through an algorithm considered already finished. Build the visualization before debugging the second algorithm, not after the third — see Section 8.6.3 and Section 8.9 for what a future revision of this chapter's development process should do differently.

9. A textbook control term isn't owed a place in the design just because theory says it should help. Kd was implemented correctly, justified by a real, observed oscillation (Section 8.4.8), and then removed once chart-driven isolation testing (Section 8.4.10) showed the underlying P+I system had no overshoot problem for it to solve — only noise for it to amplify. Rigorously testing a component and disabling it with data is a stronger engineering result than tuning it until the numbers look acceptable.


8.8 Algorithm Comparison Summary

Bang-Bang PID (live-gain, Kd disabled) Predictive (bisection)
Tuning required None Derived from physics + hardware specs None (direct solve); solver tolerance chosen empirically (8.5.4)
Smooth approach to target No — two states only Yes Yes, once quantization (8.5.3) and authority-collapse (8.5.5) were fixed
Precision (5-run apogee spread, 228m target) Hits target but with abrupt transitions 227.85–227.97m (Kd=0, Section 8.4.10) 227.55–228.22m (Section 8.5.6)
Handles collapsing control authority near apogee Naturally (all-or-nothing) Explicit achievability gating (8.4.6) Explicit authority-collapse guard (8.5.6) — same underlying bracket calc
Numerically fragile near singularities No Yes — live gain requires margins/caps (8.4.3–8.4.4) Yes, differently — early-exit bisection is quantized, not singular (8.5.3)
Actuator-realistic Requires rate limiting to be added Rate limiting built in Rate limiting built in
Complexity / moving parts Minimal High — gain scheduling, windup guard, rate limit, (disabled) derivative filter Moderate — bisection solver, tolerance, authority guard; no gain scheduling

All three algorithms now also share a single ACHIEVABILITY classification (REACHABLE / UNREACHABLE_HIGH / UNREACHABLE_LOW), computed generically from the same min/max-angle apogee bracket regardless of which controller is active — see Section 8.9.


8.9 Next Steps