Foundations to FrontierPart X: Frontier engineeringBezhig

F01: Feedback control: derivatives that fly drones and steer cars

12 exercises, each with a complete solution. 8 figures. 1 code listing.

On this page
  1. A. Prerequisites and observable goals
  2. B. The central idea: measure, compare, push, repeat
  3. C. The plant: a drone that wants to fall
  4. D. Proportional control and the error that never goes away
  5. E. Three fixes, one idea each
  6. F. Motors have limits: saturation and windup
  7. G. The car: a bicycle with a steering wheel
  8. H. Pure pursuit: steer toward a carrot on the path
  9. I. A real bug, and why it became a test
  10. J. Where this shows up in real stacks
  11. K. The lab
  12. L. Independent practice
  13. M. Complete solutions: F01
  14. N. Retention and readiness

A. Prerequisites and observable goals#

Before

Modules 08–12 (derivatives as rates, the chain rule, related rates); basic Python from C01–C02. Module 25 and A06 help with the optional second-order analysis.

After

Explain every term of a PID controller as a rate or an accumulation; predict steady-state error before simulating; handle motor limits without windup; derive pure-pursuit steering from one line of algebra; tune two controllers against measured targets and defend the numbers.

Level: Frontier track, reachable in Calculus I. Simulated vehicles only. Real aircraft and vehicles add hardware, safety, and regulatory work that this chapter does not replace.

B. The central idea: measure, compare, push, repeat#

Every autopilot is a loop. A drone holding altitude, a car staying in its lane, a rocket landing on a pad: each one measures where it is, compares that with where it should be, and pushes a little in the right direction. Then it does it again, hundreds of times a second.

The loop in every autopilot. Each box has a job, and each arrow is a signal with units.

Four words carry the whole subject. The plant is the thing being controlled (the drone and its physics). The setpoint or target rr is where you want it. The error is e=r−ze=r-z. The controller turns error into a command. Everything else in this chapter is about choosing that command well, and about the ways a real loop fights back: motor limits, noisy sensors, and actuators that cannot move instantly.

C. The plant: a drone that wants to fall#

Put a drone of mass mm on a vertical line. Its altitude is z(t)z(t), its vertical velocity is z′(t)z'(t), and its acceleration is z′′(t)z''(t), exactly the position, velocity, and acceleration chain from Module 12. Three forces act on it: motor thrust TT upward, weight mgmg downward, and air drag cz′cz' opposing motion. Newton’s second law gives the plant model

m z′′=T−mg−c z′.m\,z''=T-mg-c\,z'.

Read it like a mechanic. If thrust equals weight and the drone is still, then z′′=0z''=0 and nothing changes: that is a hover. More thrust accelerates it up; less lets gravity win. The controller’s only lever is TT, and the motors can supply only 0≤T≤Tmax⁡0\le T\le T_{\max}. They cannot pull the drone down.

The lab uses m=1.5m=1.5 kg, c=0.3c=0.3 N⋅\cdots/m, g=9.81g=9.81 m/s2^2, and Tmax⁡=2mg≈29.43T_{\max}=2mg\approx29.43 N. A computer cannot solve the model with calculus symbols, so it steps time forward in ticks of Δt=0.01\Delta t=0.01 s:

vk+1=vk+Tk−mg−cvkmΔt,zk+1=zk+vk+1Δt.v_{k+1}=v_k+\frac{T_k-mg-cv_k}{m}\Delta t,\qquad z_{k+1}=z_k+v_{k+1}\Delta t.

This is the derivative used backwards: a rate times a small time step gives a small change. It is the same update a game engine’s fixed-step physics performs every frame.

D. Proportional control and the error that never goes away#

The simplest controller pushes in proportion to the error: T=KPeT=K_Pe. Try it and the drone climbs, then stops short of the target and stays there (orange curve below). Before running anything, you can predict exactly how short.

Three controllers climbing to 10 m. Proportional control alone stalls short by mg/KPmg/K_P. Adding the expected hover thrust fixes that until the drone carries a payload the controller doesn’t know about. The integral term removes the remaining gap.

Raising KPK_P shrinks the gap, but a gain large enough for a 5 cm error (about 294 N/m) asks for 294 N of thrust per metre of error. The motors saturate, and sensor noise gets multiplied by the same huge number. Better tools exist.

E. Three fixes, one idea each#

Feedforward: add what you already know. You know hovering needs about mgmg of thrust, so add it before feedback does anything: T=mgest+KPeT=mg_{\text{est}}+K_Pe. With a perfect mass estimate, the steady-state error becomes zero (dashed blue, until a 0.3 kg payload appears). Now the leftover weight 0.3g0.3g must come from KPeK_Pe, giving e=0.3(9.81)/4≈0.74e=0.3(9.81)/4\approx0.74 m.

Integral: remember the past. Add KI∫0te  dτK_I\int_0^t e\,\dd\tau. While any error remains, the integral keeps growing, and so does the thrust it adds. The only resting state is e=0e=0, which is why the aqua curve reaches the target even though nobody told the controller about the payload. This is an accumulation, the idea behind every integral in Module 16.

Derivative: damp the motion. Add −KD z′-K_D\,z', a push against vertical velocity. It acts like a shock absorber. It resists fast motion without caring where the drone is, which cuts overshoot. The complete controller is

T=mgest⏟feedforward+KPe⏟present+KI ⁣∫e  dτ⏟past−KD z′⏟motion.T=\underbrace{mg_{\text{est}}}_{\text{feedforward}}+\underbrace{K_Pe}_{\text{present}}+\underbrace{K_I\!\int e\,\dd\tau}_{\text{past}}-\underbrace{K_D\,z'}_{\text{motion}}.

The catch: derivatives amplify noise. A computer estimates z′z' from samples with a backward difference, (zk−zk−1)/Δt(z_k-z_{k-1})/\Delta t. That is the secant slope from Module 08. Suppose the altimeter adds independent noise of standard deviation σ\sigma to each reading. The difference of two readings has noise 2 σ\sqrt{2}\,\sigma, and dividing by a tiny Δt\Delta t magnifies it:

σvelocity=2 σΔt=2(0.02)0.01≈2.83 m/sfor 2 cm of altimeter noise at 100 Hz.\sigma_{\text{velocity}}=\frac{\sqrt2\,\sigma}{\Delta t}=\frac{\sqrt2(0.02)}{0.01}\approx2.83\text{ m/s}\quad\text{for 2 cm of altimeter noise at 100 Hz.}
Two centimetres of noise is invisible in the altitude plot and enormous in the velocity estimate. Real flight stacks estimate velocity with a filter that fuses the accelerometer, barometer, and GPS (PX4’s EKF2, ArduPilot’s EKF3) instead of differentiating one noisy sensor.

F. Motors have limits: saturation and windup#

Command a 50 m climb and the controller asks for more thrust than the motors have. The output is clipped at Tmax⁡T_{\max}, the saturation limit, but the integral does not know that. It keeps accumulating error through the whole climb, and when the drone finally reaches 50 m, that stored integral keeps the thrust pinned at maximum. The drone shoots far past the target (orange). This failure is called integrator windup.

Same gains, same climb. Without anti-windup the stored integral drives a 95% overshoot and a long oscillation. Freezing the integral while the output is pinned in the error’s direction (blue) cuts the overshoot to 12%.

The fix in the lab is conditional integration. Accept the new integral value unless the output is at a limit and the error is pushing further into that limit. It takes three lines of code, and it is the difference between a drone that levels off and one that climbs about 47 m past its target.

G. The car: a bicycle with a steering wheel#

Collapse a car’s four wheels into two, one rear and one front, a wheelbase LL apart. Track the rear axle at (x,y)(x,y) with heading ψ\psi, speed vv, and front-wheel angle δ\delta. The kinematic bicycle model is

x′=vcos⁡ψ,y′=vsin⁡ψ,ψ′=vLtan⁡δ.x'=v\cos\psi,\qquad y'=v\sin\psi,\qquad \psi'=\frac{v}{L}\tan\delta.

The first two equations split velocity into components (the unit-circle cos⁡\cos and sin⁡\sin from Module 04). The third says the car turns faster when it drives faster or steers harder.

Holding the steering at δ\delta drives the rear axle around a circle of radius R=L/tan⁡δR=L/\tan\delta.

Hold δ\delta constant and the car traces a circle. The right triangle formed by the rear axle, the front wheel, and the turning centre gives tan⁡δ=L/R\tan\delta=L/R, so R=L/tan⁡δR=L/\tan\delta. For a 2.9 m wheelbase and δ=5∘\delta=5^\circ, R≈33.1R\approx33.1 m. The lab’s circle test confirms the simulation stays on that radius.

H. Pure pursuit: steer toward a carrot on the path#

Pure pursuit is a classic path-following method in robotics, and a standard baseline for any learned controller. Pick a point on the path a fixed look-ahead distance ℓ\ell ahead of the car (the carrot). Then steer along the one circle that passes through the rear axle, is tangent to the current heading, and reaches the carrot.

Put the car at the origin facing +x+x. The circle it can drive, tangent to the heading, has its centre at (0,R)(0,R).

The derivation is a single line of Algebra First. The circle is x2+(y−R)2=R2x^2+(y-R)^2=R^2. The carrot sits at (ℓcos⁡α, ℓsin⁡α)(\ell\cos\alpha,\ \ell\sin\alpha). Substitute and expand:

ℓ2cos⁡2α+ℓ2sin⁡2α−2Rℓsin⁡α=0⟹ℓ2=2Rℓsin⁡α⟹κ=1R=2sin⁡αℓ.\ell^2\cos^2\alpha+\ell^2\sin^2\alpha-2R\ell\sin\alpha=0 \quad\Longrightarrow\quad \ell^2=2R\ell\sin\alpha \quad\Longrightarrow\quad \kappa=\frac1R=\frac{2\sin\alpha}{\ell}.

The bicycle model turns curvature into a steering angle, tan⁡δ=Lκ\tan\delta=L\kappa, so

δ=arctan⁡ ⁣(2Lsin⁡αℓ).\boxed{\delta=\arctan\!\left(\frac{2L\sin\alpha}{\ell}\right).}

The look-ahead distance is the one tuning knob, and it trades two failures against each other. A short look-ahead reacts fast and hugs corners, but with a steering motor that turns the wheels at only 0.5 rad/s, it overcorrects and rings. At 6 m the ringing grows until the car loses the lane. A long look-ahead is smooth but cuts corners: on a lane change, the worst error grows from 2 cm at 4 m to 34 cm at 16 m. Production systems usually scale look-ahead with speed.

A 3.5 m lane change at 10 m/s. Longer look-ahead gives a smoother path but cuts the curve.
Same controller at 20 m/s with a steering motor limited to 0.5 rad/s. Too short a look-ahead turns actuator lag into oscillation.

I. A real bug, and why it became a test#

The first version of this lab searched the entire path for the closest point on every tick. It worked on straight roads and lane changes, then failed on a 1.5-lap circle. As the car passed the top of the loop, the closest segment was the one from the second lap, lying exactly on top of the first. The look-ahead search started from the end of the path, the carrot collapsed onto the car, and the car spun in a tight circle.

The fix was a bounded search: look only a short distance ahead of the previous closest segment. A regression test, test_tracks_a_looping_path, now drives the loop so the bug cannot quietly return. This is what debugging looks like on autonomy teams. Find the scenario the method assumed away, prove the failure, fix the smallest thing, and pin it with a test.

J. Where this shows up in real stacks#

  • Drones. PX4’s multicopter controller is a cascade of loops: position feeds velocity, velocity feeds attitude, and attitude feeds angular rate. Each loop is a P or PID controller like the one above, running inside the flight stack alongside the EKF2 estimator. ArduPilot follows the same pattern with EKF3.
  • Self-driving. Tesla’s FSD now maps camera pixels to driving controls with one end-to-end network rather than hand-written planning code. Engineers still judge any controller, learned or not, with this chapter’s vocabulary: stability, overshoot, saturation, delay, and steady-state error. Pure pursuit and PID remain standard baselines and fallbacks throughout robotics.
  • Game engines. A game’s fixed-step simulation, such as a physics loop ticking at 64 Hz, runs the same measure–compare–act loop every tick. Porting this lab into C++ inside a game engine, with live plots, is the portfolio step at the end of this chapter.

K. The lab#

The lab ships with its starters and tests in the companion code’s frontier_code folder (bezhig.com/book). It needs only Python’s standard library. starters/control_lab.py has three blanks: PID.update (F01.L1), bicycle_step (F01.L2), and pure_pursuit_steer (F01.L3). Each docstring gives the recipe. Fill them in, then run the same 20 tests used for the reference solution:

cd frontier_code
$env:MANUAL_IMPL = "starters"          # PowerShell
python -m unittest discover -s tests -p "test_*.py" -v

In bash or zsh, set the variable with export MANUAL_IMPL=starters instead. Three of the tests each pin one idea from this chapter:

  • test_no_derivative_kick_on_setpoint_jump (section E),
  • test_integral_frozen_while_saturated… (section F),
  • test_tracks_a_looping_path (section I).

When one fails, read its name and the section it points to before touching the code.

L. Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

F01.01FSolution

The altitude controller measures error in metres and outputs thrust in newtons. Give the units of KPK_P, KIK_I, and KDK_D (for the −KDz′-K_Dz' term).

F01.02CSolution

A 1.5 kg drone uses proportional control only, T=KPeT=K_Pe with KP=5K_P=5 N/m, and no feedforward. Find its steady-state altitude error. What KPK_P would shrink the error to 5 cm, and why is that a bad idea?

F01.03CSolution

The controller adds hover thrust for 1.5 kg, but the drone carries an unknown 0.3 kg payload. With KP=4K_P=4 N/m and no integral term, find the steady-state error. Which term removes it, and why can it only rest at e=0e=0?

F01.04TSolution

A gimbal camera sits on the ground 40 m from the take-off point and tracks a drone climbing straight up at 3 m/s. How fast must the camera tilt, in rad/s and in degrees per second, when the drone is 30 m up? (Related rates, Module 12.)

F01.05TSolution

An altimeter has independent noise with standard deviation 2 cm. Find the noise in a backward-difference velocity estimate at 100 Hz and at 10 Hz. What does the slower rate cost you?

F01.06CSolution

A car with a 2.9 m wheelbase holds δ=5∘\delta=5^\circ at 10 m/s. Find its turning radius and its yaw rate ψ′\psi'.

F01.07TSolution

A car at the origin faces +x+x with L=2.9L=2.9 m. The carrot is at (8,1)(8,1) m. Find ℓ\ell, show that κ=2ycarrot/ℓ2\kappa=2y_{\text{carrot}}/\ell^2 in this frame, and compute the steering angle in degrees.

F01.08TSolution

Explain in words why a longer look-ahead cuts corners on a lane change and why a shorter one rings when the steering motor is slow. Use the two figures.

F01.09SSolution

With exact feedforward and PD control (no integral), show that the error obeys me′′+(c+KD)e′+KPe=0m e''+(c+K_D)e'+K_Pe=0 after a step. Using m=1.5m=1.5, c=0.3c=0.3, KP=4K_P=4, find the KDK_D that gives critical damping. Predict the overshoot for KD=3K_D=3 from ζ=(c+KD)/(2mKP)\zeta=(c+K_D)/(2\sqrt{mK_P}) and 100e−πζ/1−ζ2%100e^{-\pi\zeta/\sqrt{1-\zeta^2}}\%, then explain why the lab measures a little less.

F01.10LSolution

Tune the payload case (hover thrust for 1.5 kg, actual 1.8 kg, KP=4K_P=4) for under 10% overshoot, settling within the 2% band in under 6 s, and ∣ess∣<2|e_{\text{ss}}|<2 cm within 20 s. Report your gains and the three measured numbers from step_metrics.

F01.11LSolution

Complete F01.L1–L3 in the starter file until all 20 tests pass with MANUAL_IMPL=starters. Record which test failed first and the first incorrect step in your error log.

F01.12SSolution

The windup figure shows 95% overshoot without anti-windup and 12% with it. Explain, tick by tick, where the extra overshoot comes from, and why freezing the integral only while the output is pinned in the error’s direction is safe.

Optional small hints

For F01.02, set z′′=z′=0z''=z'=0 in the plant model. For F01.04, write tan⁡θ=h/40\tan\theta=h/40 and differentiate both sides with respect to time before substituting. For F01.07, sin⁡α=y/ℓ\sin\alpha=y/\ell. For F01.09, substitute z=r−ez=r-e into the plant with T=mg+KPe−KDz′T=mg+K_Pe-K_Dz'.

M. Complete solutions: F01#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

F01.01Show solution

Units follow from “output unit per input unit”

KPeK_Pe must be newtons with ee in metres, so KPK_P is N/m. The integral ∫e  dt\int e\,\dd t has units m⋅\cdots, so KIK_I is N/(m⋅\cdots). z′z' is m/s, so KDK_D is N⋅\cdots/m. Gains are physical quantities, not bare numbers, which is why values tuned for one drone rarely transfer to a heavier one.

Back to F01.01
F01.02Show solution

The error pays for the thrust

At rest 0=KPe−mg0=K_Pe-mg, so ess=mg/KP=(1.5)(9.81)/5=2.943 me_{\text{ss}}=mg/K_P=(1.5)(9.81)/5=\boxed{2.943\text{ m}}. For 0.05 m, KP=mg/0.05=294.3 N/mK_P=mg/0.05=\boxed{294.3\text{ N/m}}. A 1 m error would then request 294 N, ten times the 29.4 N the motors can give, so the controller lives in saturation. Any 1 cm of altimeter noise also becomes a 2.9 N thrust twitch. Feedforward and integral action remove the error without these costs.

Back to F01.02
F01.03Show solution

Feedforward covers the known weight; the integral covers the rest

The extra weight 0.3g=2.9430.3g=2.943 N must come from KPeK_Pe, so e=2.943/4≈0.74 me=2.943/4\approx\boxed{0.74\text{ m}}. The integral term removes it. While e>0e>0, ∫e  dt\int e\,\dd t keeps increasing and so does thrust, so no steady state exists until e=0e=0. The simulation confirms it: the aqua curve in the hover figure is within a centimetre of 10 m by the 12 s mark.

Back to F01.03
F01.04Show solution

Differentiate the relationship, then substitute

tan⁡θ=h/40\tan\theta=h/40. Differentiate with respect to tt: sec⁡2θ θ′=h′/40\sec^2\theta\,\theta'=h'/40, so θ′=h′40cos⁡2θ\theta'=\dfrac{h'}{40}\cos^2\theta. At h=30h=30 the line of sight is 402+302=50\sqrt{40^2+30^2}=50 m long, so cos⁡θ=40/50=0.8\cos\theta=40/50=0.8. Then θ′=340(0.64)=0.048 rad/s≈2.75∘/s\theta'=\dfrac{3}{40}(0.64)=\boxed{0.048\text{ rad/s}}\approx\boxed{2.75^\circ/\text{s}}. The rate shrinks as the drone climbs, because cos⁡2θ\cos^2\theta falls toward zero as θ\theta approaches 90∘90^\circ.

Back to F01.04
F01.05Show solution

Dividing by a small step magnifies noise

σv=2 σ/Δt\sigma_v=\sqrt2\,\sigma/\Delta t. At 100 Hz: 2(0.02)/0.01≈2.83 m/s\sqrt2(0.02)/0.01\approx\boxed{2.83\text{ m/s}}. At 10 Hz: 2(0.02)/0.1≈0.28 m/s\sqrt2(0.02)/0.1\approx\boxed{0.28\text{ m/s}}. The slower rate is ten times quieter but reacts a tenth of a second later, and delay in a feedback loop causes the ringing seen in the look-ahead figure. Flight stacks escape the trade-off by fusing several sensors in a filter.

Back to F01.05
F01.06Show solution

Geometry, then rate

R=L/tan⁡δ=2.9/tan⁡5∘=2.9/0.08749≈33.15 mR=L/\tan\delta=2.9/\tan5^\circ=2.9/0.08749\approx\boxed{33.15\text{ m}}. The yaw rate is ψ′=(v/L)tan⁡δ=v/R=10/33.15≈0.302 rad/s\psi'=(v/L)\tan\delta=v/R=10/33.15\approx\boxed{0.302\text{ rad/s}}, about 17.3∘17.3^\circ per second.

Back to F01.06
F01.07Show solution

The carrot’s sideways offset sets the curvature

ℓ=82+12=65≈8.062\ell=\sqrt{8^2+1^2}=\sqrt{65}\approx8.062 m. Because sin⁡α=y/ℓ\sin\alpha=y/\ell, we get κ=2sin⁡α/ℓ=2y/ℓ2=2/65≈0.03077 m−1\kappa=2\sin\alpha/\ell=2y/\ell^2=2/65\approx\boxed{0.03077\text{ m}^{-1}} (R=32.5R=32.5 m). Then δ=arctan⁡(2.9×0.03077)=arctan⁡(0.08923)≈5.10∘\delta=\arctan(2.9\times0.03077)=\arctan(0.08923)\approx\boxed{5.10^\circ}. The lab’s pure_pursuit_steer returns the same angle to machine precision.

Back to F01.07
F01.08Show solution

Look-ahead is a lever on anticipation

A far carrot sits on the path well past the start of a curve, and the circle toward it passes inside the bend, so the car cuts the corner by up to 34 cm at 16 m. A near carrot makes large heading corrections for small offsets. When the steering motor cannot follow those corrections quickly, the wheels are still turning the old way when the car crosses the lane centre. It overshoots, corrects late again, and oscillates. Longer look-ahead gives the slow actuator time to keep up.

Back to F01.08
F01.09Show solution

A spring and a shock absorber

With T=mg+KPe−KDz′T=mg+K_Pe-K_Dz', the plant becomes mz′′=KPe−(KD+c)z′mz''=K_Pe-(K_D+c)z'. For a constant target, z=r−ez=r-e gives z′=−e′z'=-e' and z′′=−e′′z''=-e'', so me′′+(c+KD)e′+KPe=0me''+(c+K_D)e'+K_Pe=0. This is a mass on a spring (KPK_P) with a damper (c+KDc+K_D). Critical damping needs (c+KD)2=4mKP(c+K_D)^2=4mK_P, so KD=2mKP−c=26−0.3≈4.60 N⋅s/mK_D=2\sqrt{mK_P}-c=2\sqrt6-0.3\approx\boxed{4.60\text{ N}\cdot\text{s/m}}, and the simulation then shows 0% overshoot. For KD=3K_D=3: ζ=3.3/(26)≈0.674\zeta=3.3/(2\sqrt6)\approx0.674, and the predicted overshoot is 100e−π(0.674)/1−0.6742≈5.7%100e^{-\pi(0.674)/\sqrt{1-0.674^2}}\approx\boxed{5.7\%}. The lab measures 5.2% because for the first second the controller asks for more than the motor limit allows. The linear model ignores that saturation, which slows the climb slightly.

Back to F01.09
F01.10Show solution

One passing set of gains, measured

KP=4K_P=4, KI=0.5K_I=0.5, KD=4K_D=4 measures 2.6% overshoot, 5.18 s settling, and a −1.6-1.6 cm steady-state error at 20 s, so it passes all three targets. Larger KIK_I removes the error faster but overshoots more (KI=1K_I=1 gives about 11.6% with KD=3K_D=3). Other passing sets exist; report yours with the command that produced the numbers, then run verify_f01.py to confirm the reference values on your machine.

Back to F01.10
F01.11Show solution

The reference implementation

The reference is solutions/control_lab.py in the companion code’s frontier_code folder. Compare only after all tests pass or after a full, honest attempt. Common first failures: taking the derivative of the error instead of the measurement (test_no_derivative_kick… fails); updating the integral while saturated (test_integral_frozen…); and using the new heading to update xx and yy (the circle test drifts). Log the category and the first incorrect step, not just “fixed”.

Back to F01.11
F01.12Show solution

Windup is stored error with nowhere to go

During the climb the output is pinned at Tmax⁡T_{\max} for several seconds while ee is large and positive. Without anti-windup, ∫e  dt\int e\,\dd t keeps growing the whole time. When the drone reaches 50 m, ee turns negative, but the integral still holds a large positive value, so thrust stays at maximum until enough negative error has cancelled it. That takes tens of metres of overshoot. Freezing the integral only while the output is pinned and the error pushes further into the limit stops the useless accumulation. It never blocks the integral from moving back toward the unsaturated range, so the controller can still remove steady-state error once it recovers.

Back to F01.12

N. Retention and readiness#

Explain aloud, without notes, why P-only control stalls at mg/KPmg/K_P, and derive pure-pursuit curvature from x2+(y−R)2=R2x^2+(y-R)^2=R^2. Revisit M12.05 and F01.04 together: both differentiate a geometric relationship before substituting. Log any F01 error under its error family from LM (Part I). Advance to the next Frontier chapter when all 20 lab tests pass on your own starter code.