FRC Shooter Design · A Beginner's Tutorial

Shoot on the Move, Explained

Shooting while driving is two problems wearing one coat. One is about where to point the turret while the robot moves. The other is about how hard to shoot once it is pointed. This tutorial builds both from zero — the geometry, the formula, a bug worth knowing about, and the design trade-off underneath it all.

Terms with a dotted underline have definitions — hover them, tap them, or tab to them.

Groundwork · How the pieces fit together

Everything below rests on one distinction, so it's worth nailing down before any maths: a shooting robot aims with two separate mechanisms, and the two halves of the problem map onto them exactly one-to-one.

exit velocity — how fast it leaves backspin — the top rolls backward as it flies on HOOD A curved plate that pivots up and down. Sets the launch ANGLE. Nothing else. FLYWHEEL (the "shooter") A wheel spun to a few thousand RPM (revolutions per minute). Sets the launch SPEED, and the spin. TURRET A powered turntable carrying the whole shooter. Aims LEFT and RIGHT only. CHASSIS (drivebase) Everything below. It drives and spins — which is what ruins the turret's aim.
Fig 1Turret vs. hood — the distinction the whole document rests on. The turret answers which way to shoot: it swivels horizontally, like turning your head. The hood and flywheel together answer how to shoot: the hood picks the angle, the flywheel picks the speed. They're independent mechanisms with independent control problems.
The roadmap — and why the two get tangled

A robot shooting while driving has two independent jobs, owned by two different mechanisms, with two completely different characters.

Job one — point the turret. Pure geometry. No physics at all, just where things are and how fast they're moving. It has one exact answer — a short two-term formula, derived in full in Part One.

Job two — pick the hood angle and flywheel speed. Physics: air, spin, energy transfer, a ball that squishes. There is no exact answer here, only a tradeoff between trusting measurements and trusting a model. That tradeoff is what Part Two works through.

A useful way to hold the whole document: there are three things in play. The situation — where you are and how you are moving — is the question. The turret answers which way, with geometry. The shooter answers how hard, with physics. And the shooter's answer needs two separate pieces of knowledge, which is why Part Two runs longer than Part One: what the mechanism does, and what the ball does after it leaves.

The two are constantly discussed as one tangled subject because both jobs get called "aiming". They aren't the same problem, and separating them is most of the clarity.

How motion is described — speed, velocity, acceleration

Start here — speed, velocity and acceleration

Three words that get used interchangeably in ordinary talk and mean strictly different things here.

termwhat it tells youunitscan be negative?
speedhow fast — magnitude only, direction discardedm/sno
velocityhow fast and which waym/sits components can
accelerationhow fast the velocity is changingm/s²yes

Speed is simply the size of velocity with the direction thrown away. A speedometer reads 30 whether you are heading up the field or reversing down it; that is a speed. An arrow on a map is a velocity.

The word that misleads is "acceleration". It does not mean "speeding up" — it means any change in velocity, and velocity includes direction. So speeding up is acceleration, slowing down is acceleration, and turning at perfectly constant speed is also acceleration. A robot circling the goal at a steady 3 m/s has an unchanging speed and a continuously changing velocity. It is accelerating the whole way round. That acceleration is what you feel pushing you sideways in a car on a roundabout.

Each is the rate of change of the one before, and the rotational versions run exactly parallel — which is why they keep appearing in pairs below:

position  ──d/dt──▶  velocity        ──d/dt──▶  acceleration
   m                    m/s                       m/s²

angle θ   ──d/dt──▶  ang. velocity ω ──d/dt──▶  ang. acceleration α
  rad                  rad/s                     rad/s²

And how is a velocity actually written down? Two numbers, because it is two-dimensional — but there are two different pairs you can use, and speed is one number of one pair, never a component of the other.

components   v = (vx, vy)      both m/s — neither one is the speed
polar        v = (|v|, θ)      the speed, and a direction as an angle

Direction in two dimensions takes exactly one number, which is why both forms need exactly two. The animation in Part Two shows the same velocity in both: (2.41, 0.97) m/s is (2.60 m/s, 22°), since √(2.41² + 0.97²) = 2.60 and atan2(0.97, 2.41) = 21.9° — the heading the slider is set to. Converting runs both ways:

|v| = √(vx² + vy²)          vx = |v|·cos θ
 θ  = atan2(vy, vx)         vy = |v|·sin θ

There is a third form worth recognising, because the turret code uses it: a magnitude times a unit vector, an arrow of length one that carries direction alone. r = d · r̂ is exactly that, and it is why v∥ = v · r̂ needs no trigonometry — dotting with a unit vector pulls out how much of a vector lies along it.

That little dot has a name. v · r̂ is a dot product: multiply the matching components and add them up, v · r̂ = vx·r̂x + vy·r̂y, giving one number out of two vectors. When the second one is a unit vector it answers exactly one question: how much of this vector lies along that direction? The answer is signed — negative when the vector leans the opposite way.

Components are what the code sticks to. Angles have to be wrapped at ±180°, and adding two velocities in polar form is genuinely fiddly, whereas in components it is adding two pairs of numbers.

"Differentiate the turret angle", when it comes up later, means taking one step along that chain: you have θ written out as a formula, and you want θ̇.

And it matters for the feedforward — the turret-rate formula this tutorial builds in Part One. θ̇turret = −(ω + v/d) is built entirely from velocities. The moment the robot accelerates, the turret rate it demands is itself changing, and a velocity feedforward has nothing to say about that — it lags. That is a real limitation of the approach, separate from the 1/d problem, and it is why hard, jerky driving costs you more accuracy than driving that is fast but smooth.

The two splits that organise everything below

Two splits that organise everything below SPLIT ONE · YOUR VELOCITY, MEASURED AGAINST THE LINE TO THE GOAL Identical driving in both halves — only the goal moves. goal line of sight your velocity 97 51 goal ahead-right → mostly toward it — bearing barely moves goal your velocity 46 100 goal nearly overhead → mostly across it — bearing swings fast SPLIT TWO · YOUR CHASSIS, WHICH CAN ONLY DO THESE TWO THINGS slides SLIDING — the nose still points the same way turns SPINNING — the centre has not moved at all In both splits the two parts add back up to the original — the dashed lines close the parallelogram exactly. That is what makes a split a decomposition, and it is why the turret formula has two terms and no third.
Fig 2 The two decompositions. Top: the black arrow is identical on both sides — same heading, same speed. Only the goal has moved, and that alone changes the split from mostly-orange to mostly-teal. So "sideways" and "toward-or-away" are not properties of your velocity: they are measured against the line to the goal. The orange half (radial, 97 then 46) changes your range, so the shooter re-solves. The teal half (tangential, 51 then 100) swings your bearing — the direction you must point to face the goal — so the turret turns. Neither component is cancelled by anything — the robot keeps moving exactly as it was. The faint dashed lines close each parallelogram — the two halves really do rebuild the original arrow, and both rebuild one of the same length. Bottom: a chassis can slide without turning or turn without sliding, and every motion it can make is some mixture of exactly those two. That is why the turret formula ends up with two terms and no third.
"The turret's half" does not mean the turret cancels it

A turret only rotates on the chassis — it cannot move the robot, so it never undoes any velocity. The split is by which subsystem's problem each component creates, and it happens that the sideways component creates rotation problems.

Sideways motion changes where the goal is from your point of view. Slide past a goal and the direction you must point swings round, even though the goal never moved. A changing direction is answered by turning, and turning is what a turret does. Driving straight at a goal, by contrast, changes the direction not at all — so the turret has nothing to do, while the range changes underneath you, which is the shooter's to fix.

The same allocation turns up a second time, from a completely different cause. The ball inherits the robot's velocity when it leaves. Split that inherited velocity along the same two directions. The sideways part makes the ball drift left or right of where you pointed — the turret fixes that by aiming off-target. The toward-or-away part makes it land long or short — the shooter fixes that by re-solving hood and flywheel.

So each component reaches its subsystem twice over. The tangential half counts twice because your bearing is moving and the ball drifts sideways. The radial half counts twice because your range is changing and the ball carries that speed with it.

A note on the word: v is not a speed

A speed is a magnitude and can never be negative. v can. Strictly it is a signed component — the number you get by projecting your velocity onto the sideways direction. One number, but carrying a sign, and the sign is doing real work:

If it were an unsigned speed, θ̇turret = −(ω + v/d) would command the same rotation whichever way you drove past, and the turret would swing the wrong way half the time. The same holds for the radial component, and the convention follows from r = target − robotPose, which points from you to the goal: positive means closing on it, negative means receding.

"Tangential velocity" is the usual shorthand. It is fine, so long as you read it as the component of velocity along a stated direction — a single signed number, not a vector and not a speed.

All four component numbers are one operation

The dot product from the primer above now pays off. The field components and the goal components are the same operation four times, differing only in the direction projected onto:

vx = v · x̂        vy = v · ŷ        axes bolted to the field
v∥ = v · r̂        v⊥ = v · p̂        axes pinned to the goal

So v∥ is not a different kind of thing from vx, and radial is not "the x part" of your velocity — it is the same kind of part, taken along a different direction. Each pair is perpendicular, so each rebuilds the same speed:

vx² + vy²  =  v∥² + v⊥²  =  |v|²

The one real difference is that x̂ and ŷ never move, while r̂ and p̂ swivel as you drive — which the animation in Part Two makes visible.

How the two problems connect

Separate problems, but not independent ones. They're chained together, and the chain loops back on itself — which is the single most important thing to understand before reading further.

1 · ESTIMATE STATE How far away is the goal, and how fast am I moving right now? out: distance d, velocity v PART TWO 2 · SHOT SOLVER Physics. One question in — a matching PAIR out, plus how long the ball flies. out: hood angle + flywheel speed + time of flight (TOF) — how long it flies 3 · VIRTUAL GOAL The ball drifts sideways by v × TOF while airborne. Shift the aim point back by that. out: a fake target, at a NEW distance d′ d′ ≠ d, so solve again once it settles PART ONE 4 · TURRET Geometry. Point at the virtual goal, and add the two-term feedforward to track it. out: turret angle + turret velocity SHOOT Notice step 4 needs TOF — a number only step 2 knows. That is why the turret cannot be solved on its own.
Fig 3The whole system on one page. Run steps 2 and 3 around the dashed loop two or three times and the answer settles — shifting the aim point changes the distance, which changes the shot, which changes the flight time, which shifts the aim point again. Only then does the turret get pointed. Part One of this tutorial is box 4. Part Two is box 2. The two-term feedforward in box 4 is the speed the turret is told to turn at, and it has two terms because two separate things push the turret off target: (1) the chassis spinning underneath it, and (2) the line of sight to the goal swivelling as the robot slides sideways. Each term is the speed of one of those, with a minus sign — so the turret is told to turn exactly as fast as it is being pushed off, in the opposite direction, and the two sum to nothing. Part One derives them. This is the runtime loop only — everything built beforehand is mapped at the end of Part Two, in Fig 41.
The easiest thing to get wrong

Three mechanisms, but only two problems — and two answers, not three.

It's natural to read "turret, hood, flywheel" as three mechanisms, so three formulas. It isn't. Hood and flywheel are never worked out separately, because at any distance a high-and-slow shot and a low-and-fast shot both score — so the correct hood angle depends entirely on which speed you picked, and vice versa. There is no hood formula.

What exists is one solver that returns both numbers together as a matched pair. Change one and the other must change too, or you miss. That's why the storage question is about polynomial surfaces and lookup tables — both store pairs. It's also the entire reason "which pair should we pick?" is a real question, which is what Part Two is about.

Part One · Aiming the turret

Job one. Geometry only — and it works out to a formula with exactly two terms.

First — angular velocity, since it's about to be everywhere

There are two completely different kinds of "fast", and every formula below depends on not confusing them.

They're independent: a robot spinning on the spot has zero linear velocity and plenty of angular velocity.

And the word for splitting something this way is "decomposition"

It appears throughout this tutorial, and it means something simple: breaking one thing into separate parts that add back up to it.

Rather than "walk diagonally over there", you say "three blocks east, then two blocks north". Same journey — but now it is two easy moves instead of one awkward one. The parts must reassemble into the original, and that is what makes it a decomposition rather than merely two facts about the trip.

Two of them run through everything below:

The reason it earns its keep is that the parts end up handled by different machinery: the tangential half goes to the turret, the radial half to the shot solver; the rotation gives ω, the translation gives v. You cannot hand two subsystems different halves of a problem until you have split it into halves that genuinely add back up.

The word for that first one is "translation"

It appears throughout this tutorial, and it is the geometry word — nothing to do with language. Translation means sliding without turning. The whole robot shifts, every point on it moves in the same direction at the same speed, and its facing never changes. From Latin trans + latus, "carried across" — language translation carries meaning across, geometric translation carries an object across.

Rotation is its counterpart, and every possible rigid motion is some mixture of exactly the two:

That pairing is why the turret formula ends up with exactly two terms: v comes from the translation, ω from the rotation. And it's worth noting a tank drive can only translate along the way it faces — to move sideways it has to turn first. Swerve can translate in any direction while facing any direction, which is precisely why the two can be commanded, and reasoned about, separately on your robot.

TWO DIFFERENT KINDS OF "FAST" LINEAR VELOCITY v 3 m/s how fast you get somewhere ANGULAR VELOCITY ω 2 rad/s how fast you turn — going nowhere ONE ω, MANY LINEAR SPEEDS Δθ — one angle, both points short arc · v = 0.6 m/s twice the arc · v = 1.2 m/s r = 0.3 r = 0.6 ω Both sweep the SAME angle in the same time. The outer point covers twice the arc, so it moves twice as fast — and twice the speed over twice the radius is the same ω: inner 0.6 ÷ 0.3 = 2.0 rad/s outer 1.2 ÷ 0.6 = 2.0 rad/s different v, different r, same ratio — that is ω v = ω × r
Fig 4Angular velocity is shared; linear velocity isn't. Every point on the same rigid part — the chassis here — sweeps the same angle in the same time — which is what it means to share one ω — but a point twice as far from the centre must cover twice the arc to do it, so it moves twice as fast through space. The numbers underneath are the point: the outer dot is twice as fast and twice as far out, so 0.6 ÷ 0.3 and 1.2 ÷ 0.6 both come to 2.0. v and r change together, so their ratio does not — and that constant ratio is ω. Rearranging v = ω × r into ω = v ÷ r is what produces the second term of the turret formula, though watch for the trap: there the two are not locked together, so that ratio genuinely does change as you drive.
That symbol is a Greek omega, not a "w"

Worth saying, because in most sans-serif fonts ω and w are nearly indistinguishable at body-text size. Every ω in this tutorial is the Greek letter, and it always means the same thing: angular velocity.

You will also see it spelled out in code, and that inconsistency is deliberate:

A trap: OMEGA_MAX is uppercase because it is a constantSCREAMING_SNAKE_CASE is the Java convention — and not because it is a capital Ω. Different reason entirely.

And capital Ω is a different symbol, not a louder ω

In this notation, upper and lowercase Greek letters are treated as separate symbols rather than as one symbol in two sizes.

This page already contains a matching pair that proves the rule: σ means a standard deviation in the weighted metric of Part Three, while Σ means "add all of these up" in the integration formula above. Same letter, two cases, entirely different jobs. Δ (a finite change, as in Δt) versus δ (an infinitesimal one) works the same way.

Outside this note, Ω is used nowhere in the document — correctly, since nothing here is electrical. You will meet it elsewhere in FRC though: wire resistance, battery internal resistance, and brownout calculations are all measured in ohms.

Why every point shares one ω, even though they all move differently

Because they all sweep the same angle in the same time. If the robot turns 90°, then every point on it has turned 90° — the one near the axis and the one out at the corner alike. The outer point simply travelled a longer arc to get through that same 90°.

Rigidity is what forces this. The parts can't move relative to each other, so they can't get out of angular step. A minute hand is the everyday version: its tip and a point halfway along both go round once an hour — same ω — but the tip covers far more ground. Likewise everyone on a merry-go-round completes a turn together, while the riders at the edge are moving fastest.

And v is that ground covered: the linear speed of one particular point, in metres per second — arc length per second. Since arc = r × angle, dividing both sides by time gives v = r × ω directly. Same relationship as Fig 10, used in the opposite direction.

Two different things are called "v" in this tutorial

Worth separating, because they are genuinely different quantities that happen to share a letter:

In the turret maths the robot is treated as a single point that has a velocity and a spin rate. Its size, and the speeds of individual bits of it, never enter the calculation at all.

Careful — ω itself has no radius in it

v = ω × r is a conversion, not the definition of ω. Angular velocity needs no radius at all: it is simply how many radians per second something turns. Your gyro measures it directly and has no idea how large your robot is — and for a rigid body ω is the same about every reference point, which is precisely why the gyro can be bolted anywhere on the chassis and still read the same number.

The radius only appears when you ask about one specific point, and then r is the distance from the axis to that point. It is never "the radius of the object":

One ω, three different linear speeds, because three different questions were asked.

And in the turret formula, d is not the robot's radius

This is the easiest place to go wrong, because the formula contains a division by a distance and it is tempting to assume that distance is something about the robot. It isn't. There are two separate rotations in play, about two different axes:

What is rotatingAbout what axisIts rateRadius needed
the chassisthe robot's own centreωnone — read straight off the gyro
the line of sightthe targetv ÷ dd — distance to the target

The second row is the one that matters. When the robot slides sideways it is, for that instant, orbiting the target — and the radius of that orbit is how far away the target is. Fig 10 draws it literally: the arc is a circle centred on the goal, with the robot out on its rim.

So the robot's own dimensions never enter this formula anywhere. A 30 cm robot and a 90 cm robot, at the same distance and the same speed, need exactly the same turret rate.

The unit zoo

Most of the confusion around angular velocity is really confusion about units. There are three in common use and they all mean the same thing:

UnitReads asWhere you meet itScale
rad/sradians per secondall the maths, ω, gyro output1 rad/s ≈ 57 °/s
deg/sdegrees per seconddashboards and human-facing readoutsfamiliar, but never used in formulas
RPMrevolutions per minuteflywheel and motor specs4000 RPM ≈ 419 rad/s

Where does 2π come from, if the circumference is 2πr?

A fair objection, and the answer is the key to the whole unit. An angle in radians is a ratio — it is defined as the arc length divided by the radius:

angle in radians  =  arc lengthradius    so a full circle  =  2πrr  =  2π The r cancels — which is why 2π is a pure number, not a distance.

And because it cancels, 2π is the answer for a circle of any size. A coin and a stadium both contain exactly 2π radians in one turn. So 2π and 360 are simply two names for one full revolution — except that 360 is arbitrary (Babylonian, roughly the days in a year, and pleasantly divisible) while 2π falls straight out of the definition.

Fig 5One radian is one radius, laid along the rim. Take a piece of string exactly as long as the radius and bend it around the edge — the angle it spans is one radian, about 57.3°. Six of them fit, with a short gap remaining, because a full turn is 2π ≈ 6.28 of them. That leftover 0.28 is the entire reason the number isn't tidy.
And this is why the units in the formula work

Because a radian is metres ÷ metres, it is dimensionless — a pure number carrying no units at all.

Which is exactly what lets the earlier units check come out right: v ÷ d is (m/s) ÷ m = 1/s, and that is rad/s. The "rad" is free to appear from nowhere precisely because it weighs nothing. It's the same reason arc = r × θ holds only in radians — that formula is just the definition above, rearranged.

One revolution = 360° = 2π ≈ 6.28 radians. Radians are the native unit for a specific reason: arc = radius × angle is only true in radians. Work in degrees and you end up sprinkling conversion factors through every equation — which is exactly the derivation coming up in Fig 10.

Why the two terms of the formula can be added at all

Check the units on each piece:

ω is in rad/s.  ·  v ÷ d is (m/s) ÷ m = 1/s = rad/s.

Identical units, so they add. That isn't luck — dividing a linear speed by a radius converts it into an angular velocity, which is just v = ω × r rearranged. The formula is never mixing a rotation with a translation; it's adding two angular velocities that happen to arise from different causes. If your units ever fail this check, the formula is wrong.

And second — what a derivative is

You just met one. ω is a derivative — the rate at which heading changes. Since the whole tutorial runs on derivatives, and the word puts people off far more than the idea deserves, here it is plainly.

A derivative is a rate of change: how much the output moves when you nudge the input a little. Three ways of holding the same idea:

Fig 6A derivative is a steepness. Pick any point on a curve, lay a straight edge along it, and the tilt of that straight edge is the derivative there. That's the entire concept — the rest is notation. A dot, as in θ̇, means specifically "per second". The curly means "several inputs exist; wiggle this one and hold the rest still".
Both halves of this tutorial are derivatives — and in both, the derivative IS the answer

Part One. You have a formula for where the turret should point. Its derivative is how fast to drive the turret — which is exactly the feedforward. θ̇ reads as "radians of turret per second".

Part Two. The derivative is the robustness score. ∂e/∂θ reads as "centimetres of miss per degree of hood error", so a shot with a small one is a forgiving shot. When you reach the two curves in Fig 21, the fragile one is steep and the robust one is shallow — you will be looking directly at two derivatives.

In neither case is the derivative an intermediate step on the way to something else. It is the quantity you actually wanted.

Is that just algebra, then?

Reasonable thing to assume, and worth heading off, because the answer is no — though for a subtle reason.

Algebra rewrites. Differentiation transforms. Turning 2x + 6 = 10 into x = 2 is a rearrangement: same statement, different clothes, nothing lost, and you can walk it back. Turning y = x² into 2x is not that at all — those two are not equal. It's a brand-new function answering a different question about the original: not "what is its value" but "how steep is it".

Fig 7Rearranging versus transforming. The clinching detail is on the lower left: differentiation is lossy. Two different starting functions collapse to the same derivative, so you cannot work backwards to recover which one you began with. Algebra never destroys information; differentiation routinely does — which is proof enough that it isn't rearrangement.
But you're right that it looks like algebra

The procedure genuinely is symbol-shuffling: apply the power rule, apply the chain rule, out comes an answer. That impression isn't wrong.

The catch is that those rules are cached results. Somebody did the underlying limit once for xn, got n·xn−1, and wrote it down so nobody would ever have to redo it. Applying pre-computed answers is exactly why it feels mechanical — but the mechanism is a shortcut, not the definition.

You have already done the real thing

Underneath every rule is one idea: rise ÷ run, as the run shrinks towards nothing — the right-hand panel above.

Which is precisely what happens in Step 2 of the derivation further down: take a tiny slice of time Δt, work out how much the angle moved, divide by Δt. That is not a simplified stand-in for calculus. That is the definition, applied directly — the only thing missing was the name.

And the other half of calculus — integration

Worth meeting now, because both halves are already inside your shooter and it's easier to keep them apart than to untangle them later.

They are inverses of each other, and both work by chopping things into tiny slices. The only difference is what they do with the slice:

differentiate:   Δy ÷ Δt        divide by the slice
integrate:       Σ  y · Δt      multiply by the slice, then add them all up
Fig 8Slope in, area out. The same curve read two ways. Differentiation asks how steep it is at one instant; integration asks how much has accumulated up to that instant. The chain underneath is the one your robot actually uses — and note which direction the flight model runs in.
Where each of them lives in your shooter

The flight model is integration. That twenty-line loop in Part Two is numerical integration: you know the acceleration — gravity, drag, Magnus — and you want position, so you accumulate. vx += a·dt integrates acceleration into velocity; x += vx·dt integrates velocity into position. Two integrations chained. Nobody usually labels it that way, but that is exactly what it is.

The robustness metric is differentiation. Miss distance in, rate-of-change-per-degree out.

So the pipeline runs integrate, then differentiate: simulate the flight to find the miss, then take the slope of that miss to find the sensitivity.

Why integration is the hard one — and why it matters here

Differentiation is mechanical: rules exist that always work. Integration frequently has no closed-form answer at all.

Which explains something that comes up later. There is no closed-form range equation once drag is included — that integral has no elementary solution. The tidy v²·sin(2θ)/g exists only because a vacuum happens to make the integral solvable. Add air resistance and you are forced to step it numerically, which is precisely what the loop does.

One more connection worth having: integration always needs a starting value, the "+C" of school calculus. When you integrate acceleration into velocity, that constant is the initial velocity — so the empirically measured exit velocity is literally the constant of integration for the entire flight. Without it, the integration has nowhere to begin.

Start with the easy case. The robot is parked. The target is a fixed point on the field. The turret points at it. Nothing changes, nothing moves, no math required.

Now drive. Two completely separate things start ruining your aim, and it's worth being very clear that they are separate — that's the whole insight behind the formula.

CAUSE 1 — The robot spins The chassis rotates underneath the turret. TARGET heading 0° ω line to target: UNCHANGED Robot turned 40°, so the turret must turn −40° just to stay where it was. CAUSE 2 — The robot slides sideways The target's bearing — its direction — changes at fixed heading. TARGET v (sideways) line of sight (robot → target) bearing swept Heading never changed. The turret still has to sweep, because the line of sight rotated — one end is pinned to the goal, the other moved.
Fig 9Two unrelated problems. Spinning in place changes your heading but not the direction to the target. Sliding sideways changes the direction to the target but not your heading. Because they're independent, you can solve each one separately and add the answers — which is exactly what the formula does.

Cause 1: the robot's own rotation

The turret is bolted to the chassis. Its angle is measured relative to the chassis. So if the chassis rotates 40° counterclockwise and the turret motor doesn't move at all, the turret has physically swung 40° counterclockwise in the real world — and lost the target.

The fix is obvious: turn the turret backwards at exactly the rate the robot is turning. If the robot spins at ω radians per second, the turret must spin at −ω. That's the first term, and it's the easy one.

Cause 2: sideways motion, and where the division comes from

This one is less obvious and it's where the / distanceToTargetMeters in your snippet comes from.

First, split the robot's velocity into two pieces relative to the target:

Both are defined relative to the GOAL — not to the field, and not to your robot

Which means the same physical motion changes category depending on where you happen to be standing:

Nothing about your driving changed in those three lines. Your position did.

So the split shifts continuously as you drive. Travel in a straight line past a goal at constant speed and your velocity never changes — but the decomposition changes the entire way: mostly radial while approaching, purely tangential at the moment you are abreast of it (distance momentarily unchanging, bearing swinging fastest), then mostly radial again as you open up.

Fig 13 shows this happening live. Press play with the spin slider at zero and watch v swell to a peak as the robot passes the goal and fade at both ends, while the speed slider never moves. It is also why the shot solver takes radial velocity as a continuous input rather than a mode: it is not "am I approaching or strafing", it is a number that slides smoothly between the two as you drive.

Now, how fast must it sweep? Picture the sideways motion as briefly tracing a tiny arc on a circle centred on the target, of radius d. Arc length equals radius times angle, so angle equals arc length over radius:

TARGET d Δθ v⊥ arc travelled in time Δt = v⊥ · Δt arc = d · Δθ v⊥ · Δt = d · Δθ Δθ / Δt = v⊥ / d …which is exactly the bearing rate.
Fig 10Why you divide by distance. The same sideways speed sweeps a big angle when you're close and a tiny angle when you're far. Sprinting past someone standing next to you means whipping your head around; sprinting past a mountain on the horizon means barely turning at all. Same v⊥, different d.
So does the code compute an arc length? No — it cancels

The arc is scaffolding for the derivation, not part of the answer. Watch it disappear:

sideways distance in time Δt  =  v⊥ · Δt        ← the "arc"
angle swept  =  arc ÷ radius  =  (v⊥ · Δt) ÷ d
divide through by Δt          =   v⊥ ÷ d         ← Δt gone, arc gone

What the robot actually evaluates each cycle is only this — a subtraction, a length, a projection and a division:

r  = target − robotPose          // vector to the goal
d  = |r|                         // distance
v⊥ = part of the chassis velocity perpendicular to r
FF = -(omega + v⊥ / d)

No arc lengths anywhere.

Which distance is d? The flat one

Everything in that snippet happens in two dimensions, flat on the floor. Worth saying plainly, because it is easy to reach for the wrong distance and get a turret that is quietly wrong everywhere.

A robot on a floor cannot roll, pitch or fly, so its entire motion is three numbers — (vx, vy, ω), two of translation and one of rotation. That is the chassis decomposition written in coordinates, and it is the reason the turret formula has two terms and no third. WPILib calls this a ChassisSpeeds.

The turret rotates about a vertical axis, so it only ever sees that flat projection. d is therefore the horizontal distance — from the robot to the point on the floor directly beneath the goal — and not the slant range to the hoop. For a goal 2.5 m up and 4 m away across the floor, the slant range is 4.7 m; feed that in and every second term is 15% too small, so the turret consistently under-leads. v is the floor-plane component for the same reason.

The ball is the exception. The moment it leaves the shooter it is in three dimensions, which is where gravity, drag and Magnus do their work. So the two jobs live in two different planes: the turret's geometry lies flat on the floor, while the shooter's physics stands upright in the vertical slice containing the shot — which is precisely why the shot solver is handed a distance and hands back a hood angle and a flywheel speed.

And one honest wrinkle about that arc

The robot doesn't really travel along an arc — it moves in a straight line, which is a chord, not a curve. The exact angle swept is arctan(v·Δt ÷ d), and the arc formula is the small-angle approximation of it.

That approximation becomes exact only as Δt shrinks to nothing — which is precisely why this ends up a derivative rather than a finite sum. Over a genuine instant, chord and arc are indistinguishable. It is the same "shrink the slice to nothing" idea as Fig 8.

The one place a radius does show up in real code

Not in the physics — in the gear ratio between the turret motor and the turret ring. You work out the turret's angular velocity in rad/s, then convert it into motor rotations for the controller. That is a mechanical unit conversion sitting downstream of everything here, and it is the only point at which the turret's physical size matters at all.

Put the two together

Add the two effects, flip the sign because the turret has to cancel them rather than follow them, and you have the whole formula:

θ̇turret  =  −( ωrobot  +  vd ) Turn the turret backwards exactly as fast as the world appears to rotate around it.
Both terms are angular velocities — of different things

The two terms aren't an arbitrary pair. Any rigid body's motion splits into exactly two pieces — a translation of some reference point, plus a rotation about that point — and a swerve drive lets you command each independently. The formula's shape is that decomposition applied to aiming.

But there's a neater way to read it. Both terms describe something rotating:

And note which side of the equation the turret is on. Its own rotation never appears on the right — it is the thing you are solving for:

symbolwhere it comes fromwhat is rotating
ωthe chassis's rotationthe chassisinput
v ÷ dthe chassis's translation, divided by distancethe line of sightinput
θ̇turretwhat you command the motorthe turretoutput

So it is not "chassis rotation plus turret rotation". Both inputs come from the chassis — its rotation supplies ω directly, and its linear velocity supplies v, which only becomes an angular velocity once divided by d. Translation and rotation are the chassis's only two parts, which is why there is no third term to look for.

Translating the robot doesn't rotate the chassis at all, but it certainly rotates the line of sight. That is a real angular velocity; it simply belongs to an imaginary line rather than a piece of metal. So the turret is never adding "a rotation to a translation" — it is adding two angular velocities and cancelling their sum. That's the physical meaning behind the units argument from earlier: dividing by d doesn't just make the units agree, it computes how fast the line of sight swings.

The two panels of Fig 9 are exactly these two rotations, one each.

One ω, many linear velocities

Worth restating, since it's the idea underneath all of this: angular velocity belongs to the body, linear velocity belongs to a point. A spinning chassis has exactly one ω, but every point on it moves at a different speed — zero at the axis, faster the further out you go, related by v = ω × r as in Fig 4. Both describe the same motion; they answer different questions about it.

Why every point shares it — and where that stops being true

Rigidity. A solid body cannot change its own internal angles. Pick any two points on the chassis: the distance between them is fixed, so if one rotated faster than the other the angle between them would have to change — which means the metal bent. A minute hand is the everyday case: tip and midpoint both go round once an hour, because for the tip to go faster the hand would have to stretch.

The contrast makes it sharp. A whirlpool has no single ω — inner water circles faster than outer. That is differential rotation, and rigidity is exactly what forbids it. Sharing one ω is not a property of rotation; it is a property of being solid.

Which is why "the robot" is the wrong unit here. A turreted robot is two rigid bodies joined at a bearing, so they do not share an ω at all:

chassis points  →  ω
turret points   →  ω + θ̇_turret

And that is not a quibble — it is where the formula comes from. The turret's rotation in the world is the sum of the two, and you want that sum to track the bearing:

ω + θ̇_turret  =  φ̇  =  −v⊥/d
      θ̇_turret  =  −( ω + v⊥/d )

The two-term formula is precisely the statement that the turret is a separate rigid body riding on a rotating one. Gyro readings are unaffected by any of this, incidentally, because the gyro is bolted to the chassis — one body, one ω, same number wherever you mount it.

"But ω = v ÷ r — so surely it changes with where the point is?"

The most natural objection, and the answer is that v and r change together. Take a chassis spinning at 2 rad/s:

pointrv = ω·rv ÷ r
near the axis0.15 m0.30 m/s2.0
middle0.30 m0.60 m/s2.0
corner0.45 m0.90 m/s2.0

The denominator grows, but the numerator grows by exactly the same factor. The ratio staying fixed is what sharing one ω means. The reading to avoid is "r varies, so v/r varies"; the right one is "v varies in precisely the way that keeps v/r fixed" — and rigidity is what forces it to.

One detail settles it. At the axis r = 0 and v = 0, so v/r is 0/0 — undefined — yet the axis is certainly turning at 2 rad/s. So ω = v/r cannot be the definition of ω, or ω would not exist at the centre. It is a way to compute ω from one point's motion. ω belongs to the body; v = ω·r describes how each point expresses it.

And the trap worth naming: v ÷ r appears twice here, meaning different things
what is on top and bottombehaviour
ω = v ÷ r
a rigid body
a point's speed, and its distance from the axis — locked togetherratio constant · one ω
v ÷ d
the line of sight
the chassis sliding, and the distance to the goal — entirely independentratio changes constantly

In the turret formula nothing ties v to d — you can be far away and fast, or close and slow. That is exactly why the second term swings about as you drive while the first sits still. Same-looking ratio, opposite behaviour, because of what is on top and bottom.

That is precisely this line:

double turretFFRadiansPerSecond =
        -(angularVelocityRadiansPerSecond + tangentialVelocityMetersPerSecond / distanceToTargetMeters);

The same idea, phrase by phrase

Stated compactly, the whole approach is one long sentence with six pieces of jargon in it. Every one of them now has a definition, so here it is taken apart:

To compensate for motion, run the turret with a velocity feedforward angular velocity equal and opposite to the rotational speed of the robot plus the tangential velocity of the robot about the target divided by the distance to the target.
The wordsSymbolWhat it means
"to compensate for motion"Keep the turret locked on target while the robot drives and spins.
"running the turret with a velocity feedforward"θ̇ commandCommand the turret a speed, worked out in advance — not just a position for the PID to chase.
"equal (and opposite) to"the minus signCancel the motion rather than follow it.
"the rotational speed of the robot"ωTerm one — the chassis turning underneath the turret. Fig 9, left panel.
"the tangential velocity of the robot about the target"vThe sideways part of your velocity — the only part that changes the bearing. Signed: which way round the goal you are going. Fig 9, right panel.
"divided by the distance to the target"÷ dConverts that sideways component into an angular rate. This is v = ω × r rearranged. Figs 3 and 5.
"seem like a reasonable solution?"Yes — and better than reasonable. See below.

Assemble those pieces and you get −( ω + v ÷ d ) — the formula above, and the line of code shown earlier. Stripped of jargon, the question is simply: "should I spin the turret backwards at exactly the rate the world appears to rotate around it?"

Answering your first question

Yes — and it's stronger than "reasonable". This isn't an approximation or a heuristic that happens to work. Derive the turret angle as a function of robot pose and differentiate it, and this is exactly what falls out. There is no better answer to give the turret. It's the correct closed form.

What "feedforward" means, if you're new to it

A feedback controller (a PID) watches the error and reacts after the turret has already fallen behind. A feedforward is a prediction you add on top: "I know the robot is spinning at 2 rad/s, so I know the turret needs −2 rad/s before any error even appears." Feedforward handles the part you can predict; the PID mops up whatever the model got wrong. You want both — the FF alone will drift, and the PID alone will always lag.

Where the formula actually comes from

Saying the formula is "exact" is a strong claim, so here's the machinery behind it. It's a two-step recipe, and both steps are less intimidating than their names.

Step 1 — write the turret angle as a function of robot pose

Pose is just the three numbers that say where the robot is: x, y, and heading θrobot. "As a function of pose" means writing an equation whose inputs are those three numbers and whose output is the turret angle.

Doing that is pure angle bookkeeping. The turret has to point along the bearing to the target — but turret angles are measured relative to the chassis, not the field, so you subtract the robot's own heading:

field x-axis TARGET (Tx, Ty) robot's own heading θrobot φ θturret θturret = φ − θrobot where φ, the bearing, is just φ = atan2(Ty − y, Tx − x) so, entirely in terms of pose: θturret = atan2(Ty − y, Tx − x) − θrobot
Fig 11The angle bookkeeping. Three angles, all measured from the field's x-axis. φ is the bearing — which direction the target lies. θrobot is which way the chassis faces. The turret is bolted to the chassis, so the angle you actually command it is the difference between the two. atan2 is nothing more exotic than "what direction is that point from me".

Why you subtract the heading — field-centric vs robot-centric

That subtraction is worth dwelling on, because it's a frame conversion, and frames are where most sign bugs live. There are two ways to describe any direction on a field:

FIELD-CENTRIC axes are bolted to the field and never move field X field Y nose 3 m/s the same arrow, in field numbers: vx = 3.00 vy = 0.00 ROBOT-CENTRIC axes are bolted to the robot and turn with it robot X robot Y 2.30 −1.93 the same arrow, in robot numbers: vx = 2.30 vy = −1.93
Fig 12Same robot, same motion, same arrow — different numbers. Nothing physical differs between these two pictures. The robot is turned 40° and sliding downfield at 3 m/s in both. Only the axes changed, and that alone turns (3.00, 0.00) into (2.30, −1.93). Neither is more correct; they answer different questions. Mixing them up in one equation is the classic robot-code bug.
Where the numbers actually come from

The formula wants v, ω and d. Nothing on the robot measures any of them directly — each is assembled from sensors, and knowing how is what makes the failure modes predictable.

Velocity, in three steps. Every swerve module reports two things: a drive encoder giving that wheel's ground speed, and a steering encoder giving which way the module points. Speed plus direction is a small velocity vector — polar form, exactly as above. Forward kinematics then folds all four of those, together with the known geometry of where the modules sit, into three numbers for the whole chassis: (vx, vy, ω). WPILib spells this SwerveDriveKinematics.toChassisSpeeds(...), and note that ω drops out of the very same calculation — modules disagreeing slightly with one another is what rotation looks like from the wheels' point of view.

That result is robot-relative, where x means "out the front of the robot". The turret formula needs field-relative, so the last step rotates it by the gyro heading — which is precisely what getLatestMeasuredFieldRelativeChassisSpeeds() is doing in code like the snippet below, and skipping it is the bug discussed further down.

drive encoders  ─┐
steer encoders  ─┼─▶ kinematics ─▶ (vx, vy, ω) robot-relative ─▶ + gyro ─▶ field-relative v
module geometry ─┘

Distance comes from somewhere else entirely — the pose estimator, which is odometry from those same wheels fused with vision of the field markers, differenced against the goal's surveyed position.

Three things spoil it, and all three matter more the harder you drive. It is measured, not commanded — you could feed in the velocity you asked for, but the robot lags its own command, which is why the method name says Measured. Wheel slip breaks the whole chain, because encoders report wheel rotation rather than ground motion: spin the wheels and the robot confidently believes it is moving when it is not. And gyro drift corrupts the last step, rotating a perfectly good robot-relative velocity into a wrong field-relative one.

The driver's-eye version

Robot-centric is driving a car. Push the stick forward and the robot goes wherever its nose points — so if it's facing you, left and right feel reversed.

Field-centric is a top-down video game. Push the stick away from you and the robot travels away from you, whatever direction it happens to be facing. The robot works out the conversion internally. That's why nearly every swerve team drives field-centric.

Why this decides the shape of the formula

Look again at θturret = φ − θrobot. The bearing φ is field-centric — it's a direction on the field. But the turret is bolted to the chassis, so the angle you command it is robot-centric. Subtracting the heading is literally the conversion from one frame to the other. That's not a fudge factor, it's a change of address.

The same requirement shows up in the code: getLatestMeasuredFieldRelativeChassisSpeeds() is used because v has to be compared against φ, a field-frame angle. Feed it robot-relative speeds and you'd be mixing frames — the very mistake the suspected bug below is an example of.

One convenient exception: ω is the same in both frames. How fast you're spinning doesn't depend on which way you measure from, which is why the rotation term needs no conversion at all while the velocity term does.

Step 2 — differentiate it

Differentiating means finding the rate of change — the same operation as going from a formula for position-over-time to a formula for speed.

That is exactly the bridge needed here. The feedforward wants a velocity; Step 1 produced a position. Differentiating converts one into the other. Nothing else in this tutorial depends on knowing how to do it by hand, but here's the argument with no calculus at all — over a tiny slice of time Δt, two things happen:

So Δθturret = −ω·Δt − (v/d)·Δt. Divide both sides by Δt and you have the rate:

θ̇turret  =  −( ω  +  vd ) Nothing was assumed, fitted or tuned — it is bookkeeping all the way down.
The same thing with the calculus written out

Differentiate θturret = atan2(ry, rx) − θrobot, where r = T − P is the robot-to-target vector.

The second term is immediate: d/dt(θrobot) = ω, contributing −ω.

For the first, the standard result is d/dt·atan2(ry, rx) = (rxy − ryx) / d². The target is bolted to the field, so only the robot moves and ṙ = −v. Substituting:

= −(rxvy − ryvx) / d²

That numerator is the cross product r × v, which equals d · v — the length of r times the perpendicular component of v. So the whole term collapses to −d·v/d² = −v/d, and

θ̇turret = −v/d − ω = −(ω + v/d)

Note where v is defined as the component of v along rotated by +90°. Getting that rotation backwards is precisely the suspected bug discussed below.

See it move

Drive the robot past the target and watch the two terms trade off. Notice that v peaks when the robot is directly abreast of the target and falls to zero at the far ends, where the motion is almost purely radial — and notice how the total demand spikes when d gets small.

v⊥ v∥ GOAL
3.0 m/s
0.0 rad/s
Distance d
Tangential v⊥
Spin term −ω
Sweep term −v⊥/d
Turret FF total
Fig 13Interactive. Press play. Set spin to zero and the entire feedforward comes from sideways motion; it peaks as the robot passes the goal and vanishes at the extremes. Add spin and a constant offset appears regardless of where the robot is. Turn drive speed to zero and only the spin term survives.

A frame-mixing bug worth knowing about

So the formula is right. But a correct formula fed a wrong input is still a wrong turret command, and there is one mistake in computing v that is easy to make and hard to spot. It is worth walking through, because it fails quietly rather than obviously. Here is the version with the bug in it:

var targetFrameToRobot = new Translation2d(robotSpeeds.vxMetersPerSecond, robotSpeeds.vyMetersPerSecond)
        .rotateBy(robotToTargetTranslation.getAngle());
double tangentialVelocityMetersPerSecond = targetFrameToRobot.getY();

Here's the idea being attempted. The velocity is in field coordinates — x is downfield, y is to the left. You want to know how much of it is sideways relative to the target. The trick is to rotate your whole coordinate system so the target lies straight ahead on the x-axis; once you've done that, the y component is the sideways part, for free.

To rotate the world so that a direction sitting at angle φ ends up on the x-axis, you rotate by −φ. The code rotates by , which spins it the wrong way and lands the target at 2φ instead of 0.

✓ rotateBy(−φ) x direction to target v .getY() = v⊥ Target lands on +x. The y component is genuinely the sideways part. ✗ rotateBy(+φ) x target ended up at 2φ v .getY() = some unrelated number Nothing is aligned, so the y component means nothing.
Fig 14Rotating the wrong way. Written out in components, the correct quantity is −vx·sinφ + vy·cosφ; the code computes vx·sinφ + vy·cosφ. These agree only when sinφ = 0 — that is, when the target happens to be directly up or down the field. Everywhere else it's a different number, not merely a flipped sign.
Worth verifying before you trust it

Whether this is a live bug in any particular codebase hinges on the sign convention robotToTargetTranslation.getAngle() uses, and it is possible that gets negated upstream. The test: in simulation, drive a straight line past a stationary target at an angle that is not parallel to the field x-axis, and check for steady-state turret error. If the bug is real, the error grows with sideways speed and vanishes when you drive straight up the field. The fix, if needed, is .rotateBy(robotToTargetTranslation.getAngle().unaryMinus()).

So is the turret solution robust?

Worth answering carefully, because robust means two different things and they give different answers.

Is the maths sound? Yes — it's exact, as the derivation above shows. There is no fitting, no approximation, and therefore no modelling error to be fragile about.

Is it robust to imperfect inputs? Mostly — but there is one genuine structural hazard, and it's worth knowing before you trust the feedforward everywhere on the field.

Because the formula is exact, every error in its output comes from an error in its three inputs: ω, v and d. And d sits in a denominator. Differentiating the second term with respect to distance gives −v/d² — that squared d means sensitivity grows explosively as you close in.

Fig 15The one place the formula gets fragile. Two separate problems appear at close range, both from the same 1/d. The demand rises past what the turret can physically deliver, and the sensitivity to pose error rises faster still — the two error bars represent the identical ±10 cm of distance uncertainty. Out at 5 m it's negligible; at 1 m it's twenty-five times worse.
Two distinct close-range failures

Saturation. At 1 m and 3 m/s sideways, the formula demands about 3 rad/s — roughly 172 °/s of turret sweep. Many turrets simply cannot move that fast, so the feedforward asks for something impossible and tracking breaks down no matter how correct the maths is.

Amplified pose error. The same ±10 cm of distance uncertainty produces ±0.012 rad/s of feedforward error at 5 m, and ±0.30 rad/s at 1 m. Nothing changed about your vision system — only the divisor.

There is a partial consolation: up close the goal subtends a wider angle, so you can tolerate more aiming error. But that relief scales as 1/d while the error grows as 1/d², so it softens the problem without curing it.

The honest scorecard

Mathematically correct — yes, exactly so.
Robust across most of the field — yes.
Robust at close range — no; the 1/d term both saturates the turret and amplifies pose error.
Complete — no; it tracks the goal rather than the virtual goal, which is the next section.
Verified — no; the rotateBy sign above is still unconfirmed.

The redeeming feature of an exact formula is that every failure mode lives in the inputs. Better pose estimation and proper latency compensation improve it directly, and the position PID underneath catches whatever the feedforward misses — so it degrades gracefully rather than falling off a cliff. A sensible guard is to clamp the feedforward below some minimum distance and accept that shooting on the move from very close in is a different problem.

What to do about the close-range problem

Diagnosing it is only half a job. There are real fixes, and the best of them comes straight out of the formula itself.

1 · The turret is not your only actuator

Look again at what's demanded: −(ω + v/d). That's a sum — and ω, the chassis rotation rate, is something you also command. Nothing says the turret has to absorb the whole thing.

So split the work. Ask the drivetrain to rotate as far towards −v/d as it can manage, and hand the turret only what's left over:

omegaChassis = clamp(-vPerp / d,  -OMEGA_MAX,  +OMEGA_MAX);
turretFF     = -(omegaChassis + vPerp / d);      // whatever the chassis couldn't cover

Your available tracking rate becomes turret maximum + chassis maximum rather than the turret's alone. And in the limit where the drivetrain can do the whole job, the turret feedforward falls to zero — the robot simply rotates to follow the goal and the turret sits still relative to the chassis.

Fig 16The same curve, with the chassis helping. Adding the drivetrain's rotation authority to the turret's raises the ceiling and pushes the impossible zone much closer in. Beyond about 1.5 m the turret copes alone; between roughly 0.7 m and 1.5 m it needs the chassis turning with it; inside that, no combination of the two is enough and the answer has to come from the path or the strategy instead.

2 · Or delete the term altogether

Remember what v actually is: the tangential part of your velocity. Drive radially — straight towards or away from the goal — and v = 0, so the entire troublesome term disappears regardless of how close you are.

That isn't a control fix, it's a path-planning constraint: near the goal, approach along the line of sight. It costs nothing to implement and it's completely effective, which makes it the first thing to try.

3 · Cap the damage

4 · Improve the inputs

And the strategic answer, which is not a cop-out

Shooting on the move earns its keep at range, where flight times are long and stopping costs you real seconds. Up close the shot is easy and stopping costs almost nothing.

"Don't shoot on the move inside 1.5 m" is a perfectly respectable engineering decision. Knowing where a technique stops paying is part of implementing it properly — and it's much cheaper than chasing a singularity you were never going to win.

Putting it together — so how far do you actually rotate the turret?

Everything so far has been about the feedforward, which is a rate. But the obvious question is the other one: how far should the turret turn? Both halves exist in this tutorial; here they are assembled.

The first thing to unlearn is the phrasing. You never tell a turret "rotate 12°". You tell it where to point, recompute that fifty times a second, and let a controller close whatever gap remains. The "where" is the angle bookkeeping from Fig 11:

targetAngle = atan2(Ty − y, Tx − x) − robotHeading
              └──── bearing to the goal ────┘   └ chassis ┘

And the complete motor command is those two ideas added together:

// 1 · WHERE to point — a position
double targetAngle = aimPoint.minus(robotPose.getTranslation()).getAngle()
                       .minus(robotPose.getRotation()).getRadians();

// 2 · HOW FAR off we are at this instant
double error = angleWrap(targetAngle - currentTurretAngle);

// 3 · close that gap, AND keep pace with where it is heading
double output = pid.calculate(error) + turretFF;
//              └─ closes the gap ─┘   └ stops it re-opening ┘
The two halves answer different questions
PieceAnswersKind of quantity
targetAngle → PIDhow far to rotatea position
turretFFhow fast to keep rotatinga velocity

The feedforward never tells you how much to turn — it stops you falling behind while you turn. Drop it and the PID lags a moving target permanently; drop the PID and the feedforward drifts, because nothing corrects the error that accumulates. Each is useless without the other, which is why the answer to "how much do I rotate" is really "both of these, added".

Two things that will bite you in the implementation

atan2 wraps. It jumps from +180° to −180° the moment the robot crosses behind the goal. Subtract two angles across that seam and the error comes out around 360° instead of around 0°, and the turret slams the wrong way at full speed. Every FRC codebase carries an angle-wrapping helper for precisely this; the angleWrap() above is not decoration.

Turrets have hard stops. One with ±200° of travel cannot always take the short route — sometimes it has to unwind the long way round, which costs time you may not have mid-match. The usual treatment is to track accumulated rotation and pre-unwind during a quiet moment rather than discovering the limit while aiming.

One last thing about aimPoint in that snippet: it should be the virtual goal, not the real one — the goal shifted back to compensate for the ball's drift. Which is the subject of the next section, and the one thing this half of the problem cannot supply for itself.

The one thing Part One cannot do

Everything above tracks the goal. But a robot shooting on the move should not point at the goal. The ball leaves the shooter already carrying the robot's velocity, so it drifts sideways during flight. You have to aim at a virtual goal — the real goal offset backwards by however far the ball will drift.

Why the ball drifts at all

Because it inherits the robot's velocity. The ball doesn't leave carrying only the shooter's exit velocity — it leaves carrying exit velocity plus robot velocity. Throw a ball straight up on a moving train and it lands back in your hand, because it kept the train's speed the whole time. The same thing happens here, and there is no way to prevent it.

So at 3 m/s sideways with a flight lasting 0.85 s, the ball drifts 3 × 0.85 = 2.55 m before it arrives. Aim at the goal and you miss by that much.

So aim somewhere else

Offset the aim point by exactly that drift, in the opposite direction, and the drift carries the ball onto the target. One vector subtraction:

virtual goal  =  real goal  −  ( robot velocity × time of flight ) Aim at the fake point; hit the real one.
OVERHEAD VIEW — robot sliding to the right REAL GOAL VIRTUAL GOAL offset = v × TOF = 2.55 m 3 m/s aim AT the goal… …drift carries it wide aim at the FAKE point… …drift carries it home Both balls drift the same 2.55 m. Only one of them started out pointing somewhere useful. AND WHY IT HAS TO LOOP The fake point is a different distance away — so the flight time changes, so the offset changes. real goal 4.0 m ahead · sliding 3 m/s · TOF = 0.45 + 0.10·d pass d used TOF drift new d′ 14.0000.8502.5504.744 24.7440.9242.7734.867 34.8670.9372.8104.888 44.8880.9392.8174.892 corrections shrink 0.123 → 0.022 → 0.003 three passes and it has settled Then BOTH halves use the fake point: the turret aims at it · the solver solves for d′
Fig 17The virtual goal, and why it iterates. Both shots drift sideways by the same amount — that part is unavoidable. The difference is where they were pointed when they left. Offsetting the aim point by v × TOF turns the drift from an error into the thing that delivers the ball. And because the fake point sits at a different distance than the real one, the flight time it implies is different too, which is the loop drawn in Fig 3.
Two exit velocities — and only one of them is affected by driving

Relative to the robot: unaffected. The flywheel spins at its commanded RPM and grips the ball the same way whether the robot is parked or flat out. This is the one a test stand measures.

Relative to the field: entirely affected. It is the vector sum vball,field = vball,robot + vrobot — whatever the shooter gave the ball, plus whatever the robot was already doing.

That is the field-centric versus robot-centric distinction from Fig 12 all over again, and it matters because gravity, drag and the goal all live in field coordinates. The flight model integrates in the field frame, so it needs the field-frame initial velocity — which means there's an addition step that is easy to leave out:

exitVel_robot = map(rpm, hoodAngle)          // from the test stand
exitVel_field = exitVel_robot.rotateBy(robotHeading)
                             .plus(robotVelocity)   // ← the robot's motion enters HERE
simulate(exitVel_field)
Which is the virtual goal, seen from the other side

There are two ways to account for the very same fact: add the velocity to the initial conditions and re-solve, or shift the target by v × TOF and solve the stationary problem instead. Same physics, two bookkeeping styles.

And here is why shifting is exact in a vacuum: a ground robot's velocity is purely horizontal, so it never touches the vertical motion — meaning the flight time is unchanged and the landing point moves by precisely v × TOF.

The iteration above isn't a contradiction of that. For one fixed shot the flight time really is unchanged. But aiming at the virtual goal means taking a different shot, at a different distance, which carries its own flight time.

One small real term, usually ignored

If the turret is rotating as the ball leaves, the ball also picks up ωturret × rmuzzle from the muzzle sweeping sideways — the same v = ω × r as ever, applied to the turret this time. It is normally small, a few centimetres per second, but it is a genuine term and it grows with a large turret or a fast sweep. This is the one place the turret's own size does affect the shot.

The assumption hiding inside this trick

Shifting the target treats a moving shot as a stationary shot aimed somewhere else. That is exactly true in a vacuum — with no air resistance, adding a sideways velocity really does just translate the landing point, and the iteration above is the whole answer.

With drag it is only approximately true, because drag acts on the ball's total speed — which now includes your contribution. A ball moving faster overall is slowed differently, so a moving shot is not merely a displaced stationary one; it is a genuinely different trajectory.

That breakdown is the entire argument for the two-input surface later on. If drag is negligible for your game piece, this iteration is sufficient and the bigger project isn't needed.

The offset also depends on your velocity, which is itself changing — so a fully correct feedforward has a third term for how fast the aim point is moving. The two-term formula is the dominant part and correct as far as it goes; it simply isn't the complete picture.

And "how long is the ball in the air?" is a question Part One cannot answer. It has no physics in it. Look back at Fig 3: the turret in box 4 is waiting on a time-of-flight number that only the solver in box 2 can produce. Geometry has taken this as far as geometry goes.

Part Two · Choosing the shot

Job two — box 2 in Fig 3. This is where the clean answers run out, and a genuine design choice appears.

Recall what this box has to produce: given a distance, a hood angle and a flywheel speed as a matched pair, plus the time of flight the turret is waiting on. Not two independent numbers — one pair, because the two are locked to each other.

What you do today, and why it's defensible

The current approach is a lookup table. Park the robot at 3 metres, tune the hood and flywheel by hand until shots go in, write the numbers down. Repeat at 4 m, 5 m, 6 m. At runtime, measure the distance and interpolate between the two nearest rows. A separate table stores time of flight, used to compute the virtual goal offset for moving shots.

Where the table comes from in the first place

There's no clever source. You make it by shooting, on the practice field, by hand:

  1. Park the robot at a known distance from the goal.
  2. Pick a hood angle, then sweep the flywheel speed until shots go in reliably — several balls in a row, not one lucky one.
  3. Write that distance and its matching pair into the table.
  4. Move the robot, repeat. Five to ten distances is typical.
  5. For the time-of-flight map, film the shots and count frames between launch and arrival.

That's the whole method, and it's genuinely an afternoon's work. It's also why the approach earns so much trust: every row is a shot that actually went in.

One practical trick worth knowing

Key the table on what your own vision system reports, not what a tape measure says. If the camera reads 4.05 m where the tape reads 4.00 m, you want the table indexed on 4.05 — because at runtime the robot will look up using that same biased number, and the two errors cancel. Calibrating against the tape instead introduces an error that wasn't there.

And here's why this method cannot be stretched to shoot-on-the-move

Building a 1D table means standing still at six distances. Building the 2D table this approach needs would mean shooting at every combination of distance and radial velocity — driving at a controlled 1 m/s, then 2 m/s, then 3 m/s, at each of those six distances, repeatably enough to trust the result. Thirty-odd cells, each requiring a repeatable moving shot.

That is not an afternoon; it's not really achievable at all on a practice field. This is the strongest practical argument for the physics model (equations that predict the ball's flight — unpacked later in this part) — not that hand-measuring is inaccurate, but that hand-measuring a two-dimensional grid isn't something you can physically do.

That word interpolate is doing quiet work, so here's exactly what it means — the answer is that measuring d is enough because the answers were already measured and written down. You aren't deriving anything, you're blending two rows you already have.

measured: d = 3.4 m HOOD ANGLE table distance → 42° 38° 3.0 4.0 5.0 40.4° 3.4 42 + 0.4 × (38 − 42) = 40.4° FLYWHEEL SPEED table distance → 3200 3400 3.0 4.0 5.0 3280 3.4 3200 + 0.4 × (3400 − 3200) = 3280 RPM the SAME fraction 0.4 = (3.4 − 3.0) ÷ (4.0 − 3.0) drives both — which is what keeps them a matched pair
Fig 18Interpolation, in full. Work out how far between the two stored rows your measurement falls — here 40% of the way from 3.0 m to 4.0 m — then move that same 40% between the stored values. That's the whole operation. Linear simply means the blend follows a straight line between the dots rather than a curve, which is why the joined-up result is kinked at every row.
Two related questions

How is d measured at all? It isn't, directly. Vision and odometry give you the robot's pose on the field, and the goal sits at a known fixed coordinate — so d is the distance between two known points, computed from an estimate. That's precisely why it carries error — and suggestion 2 in Part Three works through what can and cannot be done about it, which turns out to be less than you might expect.

What happens once there are two inputs? With distance and radial velocity you need bilinear interpolation — blend between the four surrounding grid cells instead of two rows. Same idea, applied twice.

How inaccurate is that blend, really?

Less than you might expect, and it's worth putting a number on it rather than hand-waving — because the table's real weaknesses turn out to be somewhere else entirely.

Linear interpolation draws a straight chord between two points on a curve that actually bows, and the gap between chord and curve is bounded by (h² ÷ 8) × curvature, where h is the row spacing. Taking the very numbers from the example above — 42°, 38°, 35.5° at one-metre spacing — the curvature works out at 1.5 °/m², putting the worst-case interpolation error at 0.19°. Note that it scales with the square of spacing: halve the gaps and you quarter the error. It is the cheapest problem here to fix — you just add rows.

Error sourceTypical sizeFixed by
Interpolation between rows (1 m spacing)≈ 0.19°more rows — cheap
Tuning noise baked into each row≈ 0.5°more rows does NOT help

That second row is the important one. Hand-tuning gets you perhaps ±0.5° per entry — you find a band of settings that works and pick somewhere near the middle. And because the chord passes exactly through every measured point, each row's error is preserved permanently. Noise outweighs interpolation error by getting on for three to one, and adding rows only adds more noisy rows.

Which is an argument for fitting a curve with no physics whatsoever

A fitted curve passes near the measurements rather than through them, so it averages the noise out instead of preserving it. That is a straight accuracy gain available from the storage choice alone — no model, no test stand, no physics.

It is the single cheapest improvement discussed anywhere in this tutorial, and it's why the storage question and the data-source question deserve to be decided separately.

Three problems that aren't accuracy at all

Which reframes the complaint usefully. The table isn't badly inaccurate in the middle; it is noisy (fixable by fitting), incomplete at the edges (fixable by fitting, better by a model), and unusable for slopes (fixable only by a model). Three separate problems at three very different prices, bundled together under the single word "accuracy".

This is popular for an excellent reason: it cannot be wrong about physics, because it contains no physics. Ball compression, roller slip, how much energy the flywheel actually transfers, air drag — none of it is modelled, so none of it can be modelled incorrectly. You measured reality and wrote it down. This is usually called trusting in its "correctness", and that instinct is sound.

Where it runs out

Three limits. It's only valid where you sampled. It can't extrapolate past the ends of the table. And — the one that matters most here — it hands you exactly one answer per distance, with no way to ask whether that answer is a good one.

That third limit is the crux of the entire argument, and it takes two steps to see.

Step one — many different shots all score

At any given distance there isn't one solution — there's an entire family. Shoot high and slow (a lob), shoot low and fast (a line drive), or anything in between. All of them go in. A lookup table picks whichever one you happened to tune that day.

GOAL LOB — slow ball, steep entry, long flight MIDDLE LINE DRIVE — fast ball, shallow entry, short flight Same distance. Same goal. Three valid answers — and infinitely many in between.
Fig 19The solution family. One distance, one target, a continuum of valid shots. The question the lookup table can never ask is: which member of this family should I choose?

Why any of this matters — the shot is open-loop

Before the next step, the fact that makes it necessary: once the ball leaves the shooter, it is ballistic. Nothing on the robot can touch it. There is no controller downstream, no correction, no second chance.

Everything you can influence happens before release — and that is where all the feedback lives too.

BEFORE RELEASE — everything is correctable flywheel RPM encoder hood angle encoder robot pose vision a "ready to shoot" gate holds fire until all three are in tolerance RELEASE AFTER RELEASE — nothing is correctable no sensor, no controller, no second chance Accuracy is decided entirely on the left. Any error still present at the line is baked in for ever. You cannot fix an error you never detected — you can only pick a shot that tolerates it.
Fig 20The trigger is a one-way door. Flywheel speed, hood angle and pose are all closed-loop right up to the moment of release — and utterly open-loop after it. That asymmetry is why so much effort goes into deciding what to shoot: it is the only place effort can still change the outcome.
This is the whole argument for what follows

Because there's no feedback after release, every error still present at that instant is permanent — 2° of hood, 100 RPM, 10 cm of pose error. You will never detect it and you could not fix it if you did.

So the only defence available is to have chosen a shot that tolerates being slightly wrong. That is not a refinement on top of accuracy; for an open-loop system it is the accuracy strategy — and it's exactly what the next step is about.

Step two — but they are not equally good

Your hood will never be at exactly the commanded angle. Your flywheel will never be at exactly the commanded RPM. So the right question isn't "does this shot score when everything is perfect" — everything in Fig 19 does. It's "does this shot still score when I'm 2° and 100 RPM off?"

Some members of the family are extremely forgiving. Others fall apart. Same nominal accuracy, wildly different real-world hit rate:

FRAGILE SHOT hood angle error → miss …becomes a big miss ±2° of error… ROBUST SHOT hood angle error → …stays a make the same ±2°…
Fig 21Sensitivity is the thing to optimise. Both curves bottom out at zero miss — both are "correct" shots. But identical actuator error produces a scoring shot on the right and a brick on the left. The steepness of this curve is what the sensitivity metric measures, and it is invisible to a lookup table.

The sensitivity metric is exactly this steepness, measured in both directions at once:

S  =  (∂e/∂θ)2  +  (∂e/∂v)2 How much does the miss grow per degree of hood error, and per unit of speed error? Pick the shot where both are smallest.
Reading ∂e/∂θ if calculus is new

It's just the slope of the left-hand curve in Fig 21 — "how many centimetres of miss do I get per degree of hood error." A big number means fragile; a number near zero means the curve is flat there and small mistakes don't matter. Squaring makes both directions count as bad, and adding them combines the two error sources into one score you can minimise.

Why the table cannot compute that metric

The metric needs two derivatives. Derivatives are slopes. And here is the difficulty: your table stores where the good shots are, while robustness is entirely about the shape around them.

Every row in the table is a success — it went in. Nothing in it records what happens 2° off, because nobody ever deliberately shot 2° off and measured how far it missed. The table knows the location of the minimum and nothing at all about the terrain surrounding it.

Fig 22Points versus a landscape. Both panels show the same axes — hood angle across, launch speed up — for one fixed target distance. The table knows a handful of settings that worked. The model knows the miss distance everywhere, so it can measure the steepness at any point. The pale valley running through the right-hand panel is the solution family from Fig 19: every setting along it scores.
The marked point is real physics, not an illustration

Ignoring air resistance, a projectile's range is v²·sin(2θ)/g. Differentiate with respect to θ and you get 2v²·cos(2θ)/g — which is exactly zero at 45°. That is the classic maximum-range angle, and it is the least sensitive shot available: being a degree off in hood angle costs you almost nothing there, while the same error at 25° or 65° costs real distance.

So the robustness idea isn't an abstraction invented for this pipeline. It falls out of the simplest projectile equation there is. A real shooter adds drag, spin and an elevated goal, which moves the flattest point somewhere less tidy than 45° — and finding that point is what the model is for.

Could you just measure the shape instead?

In principle, yes. In practice the cost is brutal, for a reason that's easy to overlook: you would have to shoot deliberate misses — a grid of perturbed hood and speed settings at every distance — and measure how far each one missed, in centimetres. That is a far harder measurement than "did it go in", because it needs precise landing positions rather than a yes or no.

Five by five perturbations across six distances is already about 150 deliberately-missed shots for the stationary case alone. Add radial velocity as a second input and it runs into the thousands. The alternative is to measure the exit conditions once on a bench and compute the rest.

And no, you don't need calculus to do it

Worth saying plainly, because "derivative" makes this sound like a maths problem when it's really a simulation problem. Robustness is a derivative — "how much does the miss grow per degree of error" is a rate of change, so asking the question is already asking for one. But there are three ways to obtain that number, and only one of them involves calculus:

MethodWhat you actually doCalculus needed
SymbolicDo the algebra and get a formula for the slope.yes
Finite differenceSimulate at θ, simulate at θ + 0.5°, subtract, divide by 0.5.no — subtraction and division
Monte CarloSimulate hundreds of shots with random errors thrown in, measure the spread of the misses.no — just a standard deviation

All three answer the same question; the derivative is the name of the quantity, not the method of getting it. In practice you would use finite differences, estimating the derivatives numerically from the table.

There's also a hard reason symbolic isn't on the table: once drag is in the model there is no closed-form range equation. The tidy v²·sin(2θ)/g above exists only in a vacuum. With air resistance the trajectory has to be integrated step by step, so there is nothing to differentiate symbolically even if you wanted to.

So the real requirement is not calculus

It is this: you must be able to evaluate shots you never took.

Finite differences need e(θ + 0.5°) — an untaken shot. Monte Carlo needs hundreds of them. Symbolic needs a formula, which you only possess if you modelled it in the first place. Every route runs through the same gate.

And that is exactly what a table cannot do. It holds a handful of successes and can say nothing about the outcome of anything else. The dependency isn't on calculus — it's on simulation.

First — what "a model" actually means here

The word model gets thrown around loosely, so let's pin it down, because it's narrower and more boring than it sounds. It does not mean the real robot, a robot simulator, a CAD model, or machine learning.

"Physical model" and "physics model" are the same thing

Some write physical model; this tutorial says physics model. Same object — the flight equations below. (In ordinary English "physical model" can mean a physical object, a scale mockup. Not the meaning here.)

Watch out for something subtler though: the approach describes two models and calls both of them "model".

The word physical there is doing real work: it exists to mark the second model as not the first.

Four things in this tutorial look like tables. Only one is about the shooter.

They are easy to conflate, and conflating them is the single most common way to get lost in Part Two. Here they are side by side:

Which oneWhat it mapsBuilt byUsed
The map
about the SHOOTER
(RPM, hood angle) → exit velocity, spin measured on a test stand with a high-speed camera offline, inside the sweep
The lookup table
about the situation
distance → hood angle, RPM hand-tuned on the practice field runtime, today
The simulation grid
a working set, not a deliverable
every candidate shot → how far it missed
exists only inside the sweep
generated by the model offline, then discarded
The surface
about the situation
(distance, radial velocity) → hood angle, RPM fitted through the grid's winners runtime, in the new pipeline

Only the first is a property of your machine. Bolt the same shooter to a different robot and the map comes with it unchanged; the other three would all have to be rebuilt. That is the cleanest test for telling them apart.

And what the map is, in one line

A calibration curve for your shooter: you asked for 3200 RPM at 30°, and the ball actually left at 7.1 m/s spinning at 8 rev/s.

You cannot calculate that number. Between the wheel and the ball sit compression, slip and energy loss — a wheel surface moving at 20 m/s might launch a ball at 12, and the ratio is not even constant, shifting with hood angle and with how worn the ball is. So you measure it once and carry it forever, exactly like discovering a set of scales reads 4% light.

A model here is a page of equations you run on a laptop. Essentially all of it is this:

whereDoesItLand(exitSpeed, launchAngle, spin):
    x, y   = 0, shooterHeight
    vx, vy = exitSpeed * cos(launchAngle), exitSpeed * sin(launchAngle)

    while y > goalHeight:                              # step forward in tiny slices
        drag   = -DRAG_COEFF   * speed * (vx, vy)                # air resistance
        magnus =  MAGNUS_COEFF * spin  * perpendicular(vx, vy)   # lift from backspin
        vx += (drag.x + magnus.x)        * dt
        vy += (drag.y + magnus.y - 9.81) * dt          # gravity
        x  += vx * dt ;  y += vy * dt

    return x, elapsedTime          # where it landed, and how long it took

Step the ball forward in small time slices and watch where it goes. When people say "simulate" here, they mean running that loop — not boot up a robot simulator. Running it a hundred thousand times with different inputs is the grid sweep in the pipeline below, and it takes seconds.

What exit velocity and spin are each doing in there

They are both handed to the loop, but they enter it in quite different ways.

Mostly determinesWhich output it drives
exit velocityhow far the ball goes — the rangethe landing point
spinthe shape — hang time and how steeply it arrivesthe entry angle
And spin is nowhere near a small correction

For a hooded shooter the ball rolls along the stationary hood on its way out, which gives a spin ratio near 1 — spin fast enough that the surface speed roughly matches the ball's travel. Running the numbers for a 270 g, 9.5-inch ball leaving at 11 m/s:

spin ratiosituationMagnus forceas a share of weight
0.5heavy slip0.68 N26%
0.8typical1.02 N38%
1.0ideal rolling1.19 N45%

Somewhere between a quarter and a half of the ball's weight — the rough equivalent of cancelling a third of gravity for the duration of the flight. That is not a refinement term you can drop.

Which is why measuring only exit velocity would not do

With exit velocity alone you would get the range approximately right and the shape badly wrong. Wrong shape means wrong entry angle; wrong entry angle means the wrong acceptance window — and the acceptance window is, as Part Three works out, the single channel through which choosing a different shot buys any tolerance to pose error at all.

So the tape-and-high-speed-video measurement is not an optional extra alongside the velocity measurement. It is the half that determines the trajectory's shape.

"Flight path" and "time of flight" are not the same thing

Both appear throughout this tutorial, so: the flight path is the shape — the whole curve through the air, where the ball is at every instant. The time of flight is the duration — a single number, in seconds. Route versus four hours.

The loop above computes the path. Time of flight is then simply read off it: the moment at which that path reaches goal height. One fact extracted from the whole story.

And that framing is worth holding onto, because a single path yields three numbers, each consumed somewhere different:

Read off the pathUsed by
landing pointscoring the shot — did it go in, and by how far did it miss?
time of flightthe virtual goal offset, v × TOF — the aim point back in Part One
entry anglethe hard constraint on shallow shots, and the calibration cross-check

Which quietly favours computing over measuring. Today you obtain time of flight by filming shots and counting frames — that works, and needs no path at all, but it yields exactly one number. Compute the path and the other two arrive free from the same run. You cannot extract a landing point from a stopwatch.

The two constants — and why there are only two

Those two capitalised names are the model's only unknowns. Each bundles several messy physical properties into a single number, so you never have to know them separately:

ConstantWhat it capturesWhere its value comes from
DRAG_COEFF
how hard air fights the ball
Air density, the ball's cross-sectional area, its mass and its surface roughness — all rolled into one number. Neither is calculated. You take perhaps twenty stationary shots at known distances, then adjust the two numbers until the model's predicted landing points match where the balls actually went. Two knobs, twenty data points — very well determined.
MAGNUS_COEFF
how much lift the spin generates
The same properties, plus how effectively the ball's surface grips and drags air around with it.
Why this makes the whole approach tractable

Everything else in that loop is either measured on the test stand (exit velocity, spin) or exactly known (gravity, goal height, hood geometry). Only two quantities are genuinely unknown. That's a small enough number to pin down confidently from a modest amount of shooting — which is the difference between "build a physics model" being a weekend's work and being a research project.

Naming note: the k prefix you'll see in most write-ups (kd, km) just means "constant". They're spelled out here because d already means distance-to-target back in Part One, and the collision causes exactly the confusion it sounds like.

Three words people use interchangeably, wrongly

Model, simulation and measurement are three different things, and discussions swap between them freely. They're easy to keep straight once separated:

WordWhat it actually isWhere it happens
ModelThe equations. A description of how a ball behaves — the code block above. It's a noun: a thing that sits in a file.on a laptop
SimulationRunning that model with some numbers to see what comes out. It's a verb: the act of using the model.on a laptop
MeasurementShooting an actual ball and recording what actually happened.real hardware

Model is to simulation as a recipe is to cooking. One is the description, the other is the act of running it.

"Simulation" means two different things in this tutorial

Trajectory simulation — running the ball-flight equations above. That's what Part Two means throughout.

Robot-code simulation — running your actual robot program against a fake robot, which is how you'd test for the turret bug back in Part One.

Both mean "run it on a computer instead of on the field", but they simulate completely different things. Same word, unrelated activities.

So where does the real robot come in?

Crucially, model and measurement are not rivals — measurement is what makes the model trustworthy. Those two constants don't come from a textbook; they come from your own test stand and stationary shots. The real cycle is: measure a little → calibrate the model → simulate a lot → check against more measurements. The model doesn't replace testing. It multiplies a small amount of testing into answers about a hundred thousand shots you never took.

It's still essential — it just has a different job. The split is deliberate, and it's the cleverest part of the approach.

MEASURED — on real hardware COMPUTED — by the equations squished • the ball compresses, by some amount • the roller slips, by some amount • energy is lost, by some amount You never separate these — and never need to. Measure only what comes OUT. the ball leaves exit velocity + spin GOAL gravity air drag Magnus (from spin) • gravity — exactly known • drag and Magnus — two constants, calibrated once Clean, well-understood physics. About twenty lines of code, and it answers what-if for free.
Fig 23Where measurement stops and maths begins. The hard physics is all on the left, inside the shooter, where a squishy ball meets a spinning roller — nobody models that well, so you measure it once on a test stand. Everything on the right is a ball flying through air, which physics handles easily. "Physics model" never meant "stop testing on the real robot" — it means let the robot measure the messy part and let equations generalise the clean part.

Where do you actually run that measurement?

Stationary — and it barely matters whether that's a purpose-built stand or the robot itself. Never while driving.

The reason moving is not merely unnecessary but strictly worse: what you are measuring is the robot-frame exit velocity, and that is unaffected by driving. Moving therefore adds a quantity you already know how to compute, and then obliges you to subtract it back out — which requires knowing the robot's velocity precisely at the instant of release, with all the error that carries. Same signal, more noise, no upside.

A test standThe robot, parked
Repeatabilitybetter — fixed camera, no drivetrain drawing currentfine
Iteration speedmuch fasterslower, and it ties up the robot
Riskmismatch with the real shooternone — it is the shooter

The only thing that genuinely matters is mechanical identity. This map exists to capture compression, slip and energy transfer — so a stand that squeezes the ball even slightly differently produces numbers that are wrong in a way nothing downstream can detect. If you have a spare identical shooter, use a stand. If not, use the real robot parked. Do not build an approximate stand.

One design choice that makes the whole map robust

Key it on measured RPM, never on commanded RPM, voltage or duty cycle:

map:  (measured RPM, hood angle)  →  (exit velocity, spin)

Do that and battery state, voltage sag and motor-to-motor variation all drop out — 3200 RPM produces the same exit velocity whether the battery reads 12.6 V or 11.4 V, because the wheel is genuinely turning at 3200 either way. Key it on voltage instead and you have quietly baked the condition of one particular battery into your shooter model.

But do test on a moving robot — for a different purpose

Keep two activities firmly apart:

Calibrating while moving conflates the two, and then a failure is impossible to localise: you cannot tell a bad map from a bad drag constant from a sign error in the velocity compensation.

What "calibration" actually involves

Two calibrations, done in that order, and they are different kinds of measurement — which is worth being clear about because they get bundled under one word.

Measured once. Read a hundred thousand times.

The most common way to lose the plot here is to picture the whole procedure happening physically. It does not. Only a small, fixed amount of real shooting takes place, and it happens once:

WhatWhereHow many times
the map — RPM and hood → exit velocitybench, high-speed cameraonce · about 25 shots
the two flight constantsfield, measured landingsonce · about 20 shots
the sweep — try, perturb, scorea laptop~180,000 evaluations
the surface fita laptoponce

Roughly fifty-five physical shots in total, ever. Everything afterwards reads those fifty-five shots' worth of numbers over and over.

And why it could not be done empirically instead

Suppose you tried to run the search physically — actually shooting every candidate rather than simulating it:

180 grid cells  ×  ~200 candidate shots  ×  ~5 perturbations each
   ≈  180,000 shots

At even ten seconds a ball that is five hundred hours of continuous shooting, with a fresh battery every few minutes. It is not a preference for computers over hardware — the experiment simply cannot be run.

Which is the entire reason the model exists. It does not replace testing; it multiplies about fifty-five measured shots into answers about a hundred and eighty thousand imagined ones. The empirical part is the foundation of the whole thing — it is just small, and done once.

Step 1, the map, is measured directly. You point a high-speed camera at the muzzle and see the answer: this RPM and this hood angle produced that speed and that spin. Nothing is inferred.

Step 2, the two constants, are fitted. You cannot photograph a drag coefficient. They are inferred by asking which values would have produced the flights you actually observed:

for each candidate (kd, km):
    total = 0
    for each recorded shot:
        exitVel, spin = map(shot.rpm, shot.hood)      # needs step 1 already done
        predicted     = simulate(exitVel, spin, kd, km)
        total += (predicted - shot.actualLanding)²
    remember total

pick the (kd, km) with the smallest total

Two parameters means a plain 2D grid search is enough — sweep both across a plausible range and take the minimum, no optimiser required. And it makes plain why the map has to come first: the model cannot simulate anything without knowing where the ball started.

Measure where shots land, not whether they scored

"It went in" is one bit of information. "It landed 34 cm long" is a number you can fit against. Shooting at a wall or a tarp and marking the impact point gives far more calibration value per ball than shooting at the goal — the same asymmetry as the sensitivity discussion earlier. Around twenty shots is comfortable for two parameters, keeping a few back to validate against.

How many shots is actually enough?

Worth reframing the question slightly. Statistical significance belongs to hypothesis testing — "is there an effect at all?" — and that is not what is being asked. You are estimating parameters, so the real question is how precise they need to be, and the answer is: precise enough that their error disappears underneath the noise you cannot remove anyway.

Precision improves as the square root of the shot count, which is unforgiving. Taking a shooter with roughly ±20 cm of shot-to-shot scatter:

shotsuncertainty in the fitshare of the scatterextra balls per further cm
58.9 cm45%
106.3 cm32%2
204.5 cm22%5
403.2 cm16%15
802.2 cm11%43
1601.6 cm8%122

Twenty is the knee. By then the model's systematic error sits near 4.5 cm, comfortably beneath the ±20 cm of scatter that nothing removes. Pushing on to eighty costs sixty more balls to gain two centimetres which are invisible against that scatter — and the final column shows the price climbing 2 → 5 → 15 → 43 balls per centimetre. That column is the argument for stopping.

But coverage beats count, and it is not close

Twenty shots spread across hood angles and distances are worth more than sixty taken at similar settings. That is the degeneracy shown below — near-identical shots stack their constraints along the same valley without ever narrowing it. Given forty balls to spend, spend them on variety rather than repetition.

The bench measurement is a different problem

There you are not fitting two parameters, you are mapping a function — so what matters is covering the input space rather than statistical power. Two arrangements cost about the same:

Either serves, because you interpolate or fit through the results afterwards, which averages the remaining noise either way. Keep a few measurements out of that fit as well — the same held-back trick as everywhere else in this tutorial, and the thing that tells you whether the shape you chose actually describes your shooter.

The stopping rule that actually answers the question

Do not fix the number in advance. Fit with five shots, then ten, then fifteen, then twenty, and watch whether the constants are still moving. Still drifting at twenty means take more; settled by twelve means you were finished a while ago.

It costs nothing extra, uses data you already have, and answers an empirical question empirically rather than by rule of thumb.

And hold back about a quarter of them regardless. Five validation shots are worth more than five additional fitting shots, because they reveal whether the model generalises — something no amount of extra fitting data can ever tell you.

What if the constants drift between ball batches?

A fair worry, and it splits into two cases that want different responses.

Random variation within a batch is simply noise. It is already inside the ±20 cm scatter, it averages away across twenty shots, and there is nothing to be done about it.

Systematic drift between batches is the dangerous one — practice balls behaving differently from competition balls. Your constants are then correct for the balls you calibrated on and confidently wrong for the ones you actually compete with.

And a new batch invalidates both measurements, not just one

The map is (RPM, hood) → exit velocity, and that depends on how the ball compresses and grips. Different balls, different map. So a genuine recalibration is around forty-five balls, not twenty.

But you already own the tool for deciding whether it matters

This is what ∂e/∂v is for. You computed it for the robustness metric; now put it to a second use — converting a percentage of drift into centimetres of miss:

batch variation in exit velocity   ≈ 3% of 11 m/s   =  0.33 m/s
∂e/∂v at that shot                 ≈ 2.24 m per m/s
                                   ──────────────────────────
cost of the drift                  ≈ 74 cm of miss

And you do not have to compute that sensitivity specially: the sweep already produced it for every cell, as a by-product of scoring. Fig 27 shows it being read off four neighbouring grid cells by central difference. In the no-drag case you can also get it straight from ∂R/∂v = 2v·sin(2θ)/g, which is where the 2.24 above comes from — or empirically, by shooting two groups a hundred RPM apart and measuring the gap between their landing points.

Run your own numbers rather than these. If it lands at 5 cm, ignore the whole issue. If it lands at 70 cm, it dominates everything else in this tutorial and deserves attention before the robustness project rather than after it.

And you can optimise against it rather than merely worrying about it

Batch spread is just another source of uncertainty, so it belongs in the weighted metric alongside the others:

S  =  σθ2(∂e/∂θ)2  +  ( σv2 + σbatch2 )(∂e/∂v)2 More batch variation means a larger effective σv — and the optimiser shifts toward shots less sensitive to speed, entirely by itself.

Batch variation is not a threat to the method. It is an input to it. The machinery already handles the problem, provided you tell it the problem exists.

Three practical habits

And the point worth holding onto: without a model, batch drift simply appears as unexplained misses. With one, it appears as a measurable discrepancy you can name and put a price on. The model makes the problem visible, which is the opposite of the worry.

Fig 24The trap nobody warns you about. Both panels show the fitting error across the two constants, with the true answer at the cross. Left: every calibration shot taken at a similar hood angle — both constants reduce range, so raising one and lowering the other predicts almost the same flights. The minimum is a valley, not a point, and the fit can settle anywhere along it while looking perfectly converged. Right: the same fit with hood angles deliberately varied, which pulls the two apart into a proper bowl.
So vary your hood angles deliberately

What separates the two constants is that Magnus scales with spin and drag does not. Different hood angles produce different spin-to-speed ratios, which is precisely the lever that tells them apart. Recording the entry angle helps as well, since backspin steepens the descent far more than drag does.

Calibrating from a narrow band of similar shots is the classic way to end up with a model that fits your test data beautifully and then behaves strangely everywhere else — and unlike most mistakes in this tutorial, it leaves no obvious symptom at the time.

How you'd actually record an entry angle

Easier than the exit-velocity measurement, for a reason that is not obvious: angles need no scale calibration. Measuring a velocity requires something of known length in frame to convert pixels into metres. An angle is atan(rise ÷ run), and if both are in pixels the units simply cancel. A phone at 120–240 fps, placed square-on to the shot plane and as far back as practical with the zoom in, is the entire rig.

GOAL last 10–15 tracked frames entry angle rise run phone at 120–240 fps, square-on to the shot plane, zoomed from far back atan( rise ÷ run ) — both in pixels, so the units cancel no ruler in frame, no distance calibration, no scale of any kind
Fig 25Measuring the descent angle. Because an angle is a ratio of two lengths, the pixels-to-metres conversion that makes velocity measurement fiddly disappears entirely here. The one thing that does matter is the camera being square-on to the plane of the shot — perspective is the only real error source, and standing further back with the zoom in largely removes it.
Two ways to get the slope wrong

Don't difference two frames. At 240 fps a ball at 8 m/s advances about 3 cm per frame, so a ±1 cm tracking error across a 3 cm baseline is roughly 17° of noise — far worse than the effect you are trying to detect.

Don't fit a long straight line either. The path is curving under gravity, so a straight fit across a long baseline systematically reports the descent as shallower than it is.

Fit a short parabola across ten to fifteen points and take its derivative at the arrival point. That averages the position noise down without inheriting the curvature bias, and lands within about 2–3° with reasonable care. The open-source tool Tracker does frame-by-frame tracking and curve fitting for free if you would rather not write it.

And this is exactly how it breaks the degeneracy

The entry angle enters the fit as a second residual term:

total += (predictedLanding    - actualLanding)²
       + w * (predictedEntryAngle - actualEntryAngle)²      # w balances metres against degrees

Two constants, and now two independent observations per shot instead of one. Drag and Magnus can no longer be traded against one another, because any swap that leaves the landing point alone will change the descent angle — so the long valley in the figure above closes up into a bowl.

The crude free version: noting whether shots drop in or rattle out is ordinal rather than numeric — useless for fitting a coefficient, but a perfectly good sanity check that your model's predicted entry angles are not nonsense.

But is it necessary? No.

Worth saying plainly before the case for it, because the degeneracy problem has a cheaper fix that comes first: simply vary your hood angles. That alone breaks most of it, costs nothing and requires no extra measurement at all. Entry angle is the second lever, not the only one.

What to doEffortWhat it buys
1Vary hood angles across your calibration shotsfreethe biggest single win on conditioning
2Record where shots land, not just whether they scorednear-freeturns one bit into a number
3Measure entry angle as wellsome video workrefinement, plus a structural check

Do the first two and you have a serviceable calibration. Skip the third and nothing collapses. And keep the proportion in view: the physics model itself isn't necessary either — that is what the drag measurement in Part Three decides. Entry angle is a refinement of a refinement, and if you are still at "get shoot-on-the-move working at all", it is not where the next hour should go.

When it does earn its place

Chiefly when you are already filming for exit velocity, so the marginal cost is close to zero — or when something is wrong and you cannot tell whether the constants are mis-tuned or the model is inadequate. The last item below is the one that matters most.

The one that matters most — it can tell you the model is WRONG, not merely mis-tuned

With one observable and two free constants, the fit will essentially always succeed. There is enough freedom in two parameters to match a set of landing points even when the physics is missing something real. A good fit therefore tells you nothing about whether the model is right.

With two observables the system becomes genuinely over-determined. If no combination of kd and km can match the landing point and the descent angle at the same time, that is not a tuning failure — it is the model reporting that its structure is inadequate. Something is absent: spin decaying during flight, lift varying with speed, a shooter effect nobody modelled.

A single-observable fit can never surface that. It will hand you plausible-looking constants and a hidden problem, and you will not find out until the shots start missing in ways nobody can explain.

Fit on it, or validate against it?

Both, on different shots. Including entry angle in the residual improves conditioning; holding it back makes it an independent test the model can fail. Fit the constants on one subset and check the predicted descent angles against another — the same held-out idea as Fig 36, applied one stage earlier, at calibration rather than at the end.

The one force in there that isn't obvious — Magnus

Gravity needs no explanation and drag is just air resistance. The third term is the one people skip, and skipping it is what makes a shooter model badly wrong rather than slightly wrong.

The Magnus effect is why a spinning ball curves. It's the same physics as a baseball curveball, a topspin tennis shot dipping inside the baseline, or a backspun golf ball floating instead of dropping.

The mechanism is simpler than its reputation. A spinning ball drags a thin layer of air around with it, which makes it fling its wake off to one side. Push air one way, get pushed the other way — Newton's third law, nothing more exotic. Backspin flings air downward, so the ball gets pushed upward.

WHY IT HAPPENS backspin travel air flung DOWN behind the ball so the ball is pushed UP WHY IT MATTERS FOR THE SHOT GOAL with backspin — hangs longer, drops STEEPLY into the goal same shot, no spin — falls short and flat steep entry = won't bounce off the rim
Fig 26Backspin is lift. Left: the ball drags air around with it and throws its wake downward, so the reaction pushes it up. Right: what that buys you — the ball stays airborne longer and arrives at a much steeper entry angle, which is the difference between dropping in and rejecting off the rim.
Why this is not a detail you can skip

A flywheel-and-hood shooter grips the ball on one side only, so it cannot avoid imparting heavy backspin — that's a side effect of the mechanism, not a choice. And because the game piece is light and hollow, the Magnus force is a large fraction of its weight, not a rounding error.

So a model that leaves spin out isn't slightly off, it's the wrong shape entirely — and it will be confidently wrong, which is the worst failure mode in the table below. This is exactly why the approach measures spin with a tape marker and high-framerate video: the model needs that number, and no other method gets it.

Why splitting it in two is the cleverest part of the approach

The two-stage design confines the trust problem to one stage. The empirical stage is pure measurement — compression, slip, energy transfer — so it raises none of the "you would have to understand the physics" worry at all. Only the second stage rests on understanding.

And the second stage is the easy physics. A ball flying through air is something physics genuinely handles well; a squishy ball meeting a spinning roller is not. So the architecture hands the badly-understood half to measurement and keeps only the well-understood half as equations — which is exactly the split drawn in Fig 23 above.

Worth remembering when you reach the objection later on: "any approach that requires a physical model…" is aimed only at that second stage. Nobody disputes the first half.

Why bother, in one sentence

Those twenty lines can answer "what if the hood were 1° higher?" for a shot nobody ever took — and that question is the only way to find the flat, forgiving shots from Fig 21. No amount of measuring gets you there, because you'd have to physically take every shot you wanted to compare.

The proposed pipeline

To compute that slope you need to be able to ask what-if questions — "what if the hood were 1° higher?" A lookup table can't answer that; it only knows the shots you actually took. A physics model can answer it for free. That's the entire reason to want a model.

1 · Measure exit velocity and backspin on a test stand tape marker + high-framerate video 2 · Physics model of the flight gravity + drag + Magnus 3 · Calibrate the model against stationary shots that actually went in 4 · Sweep a huge grid of candidate shots distance × radial velocity × hood × speed 5 · Score every candidate for sensitivity flat error curve = good 6 · Keep the best shot for each distance / radial-velocity pair 7 · Fit a smooth surface to the winners for fast runtime lookup

Step 1 is the clever part of the approach and worth calling out. Instead of trying to model ball compression, roller slip and energy transfer — genuinely hard, and the usual reason physics models fail on real robots — you measure the map from (flywheel RPM, hood angle) to (exit velocity, spin) directly on a test stand. All the messy contact mechanics gets absorbed into a measurement. The physics model then only has to handle a ball flying through air, which is the part physics is actually good at.

How the robustness number is actually computed

The step is usually stated as something like: compute the robustness of every shot using the metric (derivative of distance error with respect to hood angle)² + (derivative of distance error with respect to exit velocity)², estimating the derivatives numerically from the table. Two things are worth untangling before the algorithm.

First, a typo — and then the word "table" again

Both terms say "hood angle". Stated carefully, the second one is shooter velocity. Otherwise you would simply be squaring the same quantity twice.

And "using table" does not mean the lookup table. It means the simulation grid generated in the previous step. That distinction matters, because it resolves something that otherwise looks contradictory:

Which tableWhat's in itCan you differentiate it?
The lookup table
measured on the field
A handful of settings that scored.no — misses were never recorded
The simulation grid
generated by the model
Every candidate shot and how far it missed.yes — every cell holds a value

You can't take a slope from the first because it records only successes. You can from the second because every cell — including all the bad ones — carries a computed miss distance.

The whole idea in one paragraph, before any notation

For every situation the robot could be in — some distance, some closing speed — try a great many possible shots. For each one, nudge the hood and the flywheel slightly and see how far off the ball lands. Keep the shot that is least affected by the nudge. Then fit a smooth equation through all the winners so the robot can look one up in microseconds.

The move that is easy to miss sits in the word perturbation: "optimal" is defined by what happens when you are wrong. Not the shot with the least error — every valid shot has zero error when everything is perfect. The shot with the least error after being knocked off.

At 4 m, closing at 1 m/snudge hood ±1°nudge speed ±50 RPM
Shot A — 55°, 3200 RPMlands ±8 cm off±11 cmkeep
Shot B — 35°, 4100 RPM±31 cm±19 cmdiscard

Both score perfectly when executed perfectly. Only one of them survives being executed imperfectly, and that is the entire selection rule.

Two things that sentence leaves vague — and where they get pinned down

"A perturbation" — which one, how large, in which direction? Nudge the hood upward or downward? Both variables at once? None of that is specified, and it is exactly what the full metric settles: (∂e/∂θ)² + (∂e/∂v)² folds both variables and both directions into a single number.

"Distance from the centre of the hub" is quietly load-bearing as well. Measuring distance from the centre rather than asking "did it go in" gives a continuous quantity — which is the only reason any of this can be differentiated. But it also means "optimal" bundles two things at once: a shot landing dead centre has more room to drift before it misses at all. Accuracy and margin, folded into one number.

The algorithm, for one distance and radial velocity

# 1 · fill the grid
for θ in 20°..70° step 0.5°:
    for v in 8..20 m/s step 0.1:
        e[θ][v] = simulate(θ, v).landing − goal        # signed miss, in metres

# 2 · central differences on the interior cells
dEdθ = (e[θ+Δθ][v] − e[θ−Δθ][v]) / (2·Δθ)
dEdv = (e[θ][v+Δv] − e[θ][v−Δv]) / (2·Δv)

# 3 · score every cell
S = dEdθ² + dEdv²

# 4 · choose — but ONLY among shots that actually go in
candidates = cells where |e| < tolerance
best       = argmin(S) over candidates
Fig 27A slope from four neighbours. Every cell of the generated grid holds a miss distance, so the slope at any cell comes from subtracting its neighbours — no calculus, just arithmetic on numbers you already have. The pale line is the scoring contour, where the miss is zero; the search for the most forgiving shot happens along that line, not across the whole grid.
Step 4 is the part usually left out, and it is not optional

Minimising S on its own selects a shot that is beautifully insensitive and does not go in. Flatness is only meaningful among shots that score, so this is a constrained optimisation: minimise sensitivity subject to the shot landing in the goal. In practice you walk the zero contour and take the flattest point along it.

Two refinements worth making

Use central differences, not forward ones. (right − left) / 2Δ has error proportional to Δ², while (next − this) / Δ is only proportional to Δ. Identical cost, noticeably better answer.

Then consider dropping the division entirely. Pick the step size to match the error your hardware actually has — if the hood holds ±1°, use Δθ = 1° — and use the raw difference:

Δe = e(θ + 1°, v) − e(θ, v)      # what a real 1° error costs you, in metres

Two advantages over a true derivative. It answers the question you care about directly, in centimetres of miss rather than metres-per-radian. And near the optimum, where ∂e/∂θ approaches zero and stops discriminating between candidates, a finite perturbation of realistic size still registers the curvature — which is exactly what separates two shots that both look flat to first order.

What that last step means — "fit a polynomial surface"

This phrase gets used a lot without ever being unpacked, so here it is. A lookup table and a polynomial fit do the same job — given a distance, hand back a hood angle. They differ in what they actually store.

Fig 28Same measurements, two ways to store them. The table's answer is a chain of straight segments — it passes exactly through every dot, including each dot's measurement error, and it has a kink at every one. The fit is one smooth equation that passes near the dots, averaging the noise out, and it keeps going past the last measurement. Note what each one costs to store.

So why is it called a surface?

Purely a matter of how many inputs you have.

With one input — distance — the answer graphs as a curve: a line drawn on a flat page, which is exactly Fig 28. That's your current table.

Shooting on the move adds a second input: radial velocity, how fast you're closing on or backing away from the goal. Two inputs graph as a surface — a landscape, where the horizontal position stands for a situation and the height above it is the hood angle for that situation.

It is a grid of situations, not a map of the field

The words "grid", "surface" and "landscape" all sound spatial, so this is worth stating outright: the two axes are not two directions you can drive in. If you drew this grid on the field it would mean nothing.

They are two independent numbers describing your situation — how far away you are, and how fast that distance is changing. Position and its rate of change are independent quantities even along a single line. A ball at 5 m height might be rising at 3 m/s or falling at 3 m/s: identical position, opposite velocity, entirely different future. Knowing one tells you nothing about the other, and that is the whole second dimension.

So at one distance there isn't one right answer — there is a row of them:

distanceradial velocityhoodflywheel
4.0 m−3 m/s backing away52°3450
4.0 m0 pure sideways48°3200
4.0 m+3 m/s closing44°2980

Same distance, three different shots — because radial velocity is a head start. Close at 3 m/s and the ball already carries 3 m/s toward the goal before the flywheel touches it, so it needs less help. Back away and the ball is being dragged the wrong way, so it needs more.

Which is precisely why the current table cannot do this: it has one row per distance, and there is nowhere to put the second number. Adding that column is the second dimension — not a second direction on the field, just a second thing you must know before you can answer.

But why radial velocity — and why isn't it three inputs?

A fair question, since the robot's velocity has two components and only one of them appears. The answer is that they are consumed by different subsystems, and only one survives as far as the solver.

GOAL v radial tangential TANGENTIAL → absorbed by the turret The ball drifts sideways, so you aim sideways. Pure geometry — it never changes how hard you shoot. RADIAL → reaches the shot solver Drive at the goal and the ball is already going there, so it flies further for the same RPM. A different shot. Two components in, one survives — which is why the grid has two axes, not three.
Fig 29Why the solver needs only one of the two. Once the turret has absorbed the tangential component by aiming at the virtual goal, the only inherited velocity still shaping the trajectory is the radial one. Picture the shot as happening inside a vertical plane: aiming correctly puts that plane through the virtual goal, which is exactly what cancels the sideways inheritance. What is left inside the plane is the radial part, adding to or subtracting from the ball's horizontal speed.
And what a negative radial velocity does to the shooter

Negative means receding — driving away from the goal, range opening. The turret does not care in the slightest, since v never enters θ̇turret = −(ω + v/d) and radial motion swings no bearing. The whole consequence lands on the shooter, as three effects that all push the same way:

So the shooting envelope is lopsided. Driving at the goal, all three effects reverse: the range closes, the ball gets a free boost, the flight shortens, and the shot can need less flywheel than standing still. Driving away, they compound. Back up briskly enough and the exit velocity the solver asks for exceeds what the flywheel can deliver, at which point no solution exists at all — the shot is not merely harder, it is infeasible. You can shoot while advancing at speeds you could not shoot while retreating.

Same arrow, two sets of axes — only one set holds still x y field axes — bolted to the carpet goal v v∥ v⊥ dashed grey — the goal-relative axes: r̂ toward the goal, p̂ across it MEASURED AGAINST THE FIELD set by how you drive · frozen vx vy MEASURED AGAINST THE GOAL set by where the goal is · moving v∥ radial v⊥ tangential distance d speed |v| — unchanging
2.6 m/s
22°
Fig 30 Radial and tangential are not "the x part" and "the y part". The robot drives dead straight at constant velocity, so vx and vy never budge — the top two bars are frozen for the whole run. Yet the bottom two move continuously, and v∥ even changes sign as the robot passes the goal. Nothing about the driving changed; the axes did. The faint orange and teal lines at the robot are those axes, and you can watch them swivel. Both components are built from both vx and vyv∥ = vx·r̂x + vy·r̂y and v⊥ = vx·p̂x + vy·p̂y — so neither one is ever "just x" or "just y", except by coincidence when the goal happens to line up with a field axis. Once the robot is past the goal, v∥ turns negative and its orange arrow flips to point away from the goal — that reversal is what a negative component looks like, not a second arrow. Drag the heading slider to change how you drive: the top pair jumps to new values and freezes again, while the bottom pair carries on moving regardless.
Which way the causation runs

Easy to read that animation backwards and conclude that your speed somehow emerges from the two components. It is the other way about. Speed is the input; the split is the consequence. How you drive fixes your speed, where the goal sits decides how that speed is apportioned, and the apportioning can never change the total:

√(v∥² + v⊥²)  =  |v|     — always, exactly

Pythagoras, because the two axes are perpendicular. Over the run above v swings by more than 4 m/s and reverses sign, while √(v² + v²) sits at 2.60 m/s from first frame to last.

There is a genuine interplay, though — just between the two components rather than producing the speed. Because their sum in quadrature is pinned, one can only grow if the other shrinks. (Adding in quadrature just means squaring each, adding, and taking the square root — a ⊕ b = √(a² + b²). It is how perpendicular or independent quantities combine, and its useful habit is that the largest term dominates: combining 10 with 1 gives 10.05, so the small one is very nearly free.) Picture a stick of fixed length casting shadows on two perpendicular walls: rotate the walls and the shadows trade off, while the stick is unmoved.

What the split really decides is the division of labour. Abeam the goal, v is near its maximum and the turret is working hardest while the shot solver barely notices. Driving straight in, v falls to zero, term two vanishes, and the whole problem belongs to the shot solver. Identical speed throughout; completely different demands. Which is the sharper way to say why shooting on the move is hard — not that speed is the enemy, but that speed pointed the wrong way is.

Strictly an approximation: the virtual goal sits at a slightly different bearing from the real one, so "radial" measured toward it differs a little from "radial" toward the goal itself. Second-order, and normally ignored.

And why split the work that way at all?

The most immediate reason is that each component is fixed by the mechanism that has authority over it. Sideways drift is a direction error, cured by pointing differently. A distance change is an energy error, cured by shooting differently. You cannot fix a sideways drift by shooting harder, nor a distance change by pointing sideways — so the division is dictated by the hardware, not chosen.

But the real reason is a symmetry

Gravity points straight down, and drag does not care which compass direction the ball is travelling. So the whole problem is symmetric about the vertical axis: rotate a shot about that axis and the trajectory's shape is entirely unchanged — only its direction differs.

Tangential velocity tilts the shot plane, and that is precisely such a rotation. It is therefore absorbed by a symmetry the problem already possesses — free, exact, nothing to solve. You simply point along the new plane.

Radial velocity changes things within the plane. No symmetry absorbs it, so it has to be solved for. That is why one component goes to geometry and the other to physics — not a convention, but a question of which one the symmetry can swallow.

Three practical consequences follow from that, and they all point the same way.

Why it matters
CostA third axis would take the grid from roughly 180 cells to about 1,620 — nine times the simulation, nine times the fitting data, and a 3D surface to fit — in order to re-derive something the turret already supplies exactly.
ExactnessThe turret's correction is pure geometry: exact, no calibration, no model. The grid's needs the physics model: approximate, calibration-dependent, and the part capable of being confidently wrong. Hand as much as possible to the exact mechanism and only the irreducible remainder to the approximate one.
DiagnosabilityKeep them apart and a miss carries information — left or right points at aiming, frames or latency; short or long points at the shot solution, the model or the calibration. Bundle them into one three-dimensional lookup and a miss tells you only that something is off.

So the grid is simply every combination of those two inputs:

AxisTypical rangeSampled at
distance2 – 8 m~20 values
radial velocity−3 to +3 m/snegative = backing away~9 values

Around 180 cells, each one a situation the robot might genuinely be in. Run the optimisation in every cell, keep the winning pair, then fit a surface through the results.

So where does exit velocity sit in all this?

A reasonable thing to lose track of, because it is measured carefully, discussed at length, and then never appears in the finished lookup. It lives inside the offline loop, and only there — but the loop has two levels, and the join between them is where most of the confusion sits.

OFFLINE
  for each situation (d, v_radial):          # the grid axes. Outer loop.
      for each candidate (hood, RPM):        # the inner search
          exitVel, spin = MAP(RPM, hood)     # the map knows only the shooter
          v0      = exitVel + v_radial       # (1) the situation enters here
          landing = SIMULATE(v0, spin)
          error   = landing - d              # (2) and here
          keep the flattest candidate whose error is about zero
      store that (hood, RPM) at cell (d, v_radial)

The map genuinely does not know about distance or radial velocity, and never needs to — it describes the shooter, not the situation. Those two numbers enter the simulation instead, at the points marked above: radial velocity as part of the ball's initial velocity, and distance as the thing the landing point is measured against.

The thing that trips people: the arrows seem to point opposite ways

The map takes (hood, RPM) as its input. The grid produces (hood, RPM) as its output. So how can one feed the other?

Because the grid is not calculated — it is searched. You never run the map backwards. You try candidate hood and RPM values, and the map plus the flight model tells you what each one does. The grid simply records which candidate won.

The map is the tester, not the producer. Rather like finding the key to a lock: you do not compute the key, you try keys and let the lock tell you which one fits.

One cell, worked through with real numbers

Take the cell for 4.0 m away, radial velocity zero. First the search — hunting for the RPM that lands on target at one particular hood angle:

try 2600 RPM  →  map says 5.82 m/s  →  lands 3.46 m    short
try 2800 RPM  →  map says 6.27 m/s  →  lands 4.01 m    on target
try 3000 RPM  →  map says 6.72 m/s  →  lands 4.60 m    long

The map was looked up three times — RPM in, exit velocity out. Worth being exact about what that means: the shooting experiment is not being repeated. The map was measured once, on a bench, and what happens here is the computer reading that stored measurement again. At no point did it need to know the target was 4 m away. You knew that; the map only ever answered "what does 2800 RPM give me?"

Repeat across hood angles and you are left with survivors, every one of which lands on target:

hoodRPMexit velocity
from the map
lands±1° of hood →
30°30006.72 m/s3.99 m7.8 cm off
45°28006.27 m/s4.01 m0.2 cm off
60°30006.72 m/s3.99 m8.3 cm off

All three score. 45° wins, because it barely notices a degree of hood error — so the cell (4.0 m, 0 m/s) stores hood 45°, 2800 RPM. That single pair is what the surface will later be fitted through.

And that is where "the surface" comes from — it is about twelve numbers

Worth stating plainly, because the word sounds far grander than the thing. The surface is the finished product, the only artifact that actually reaches the robot, and physically it is a dozen constants in a file.

The lineage is short:

  1. The sweep runs over roughly 180 cells.
  2. Each cell yields one winning (hood, RPM) — the 45° and 2800 above.
  3. That leaves 180 rows of (distance, radial velocity) → (hood, RPM).
  4. Fit a quadratic through those 180 rows.
  5. Ship the coefficients.
hood = c₀ + c₁·d + c₂·v + c₃·d² + c₄·d·v + c₅·v²
rpm  = k₀ + k₁·d + k₂·v + k₃·d² + k₄·d·v + k₅·v²

Six coefficients each, twelve in total. The robot substitutes its measured d and v, performs one line of arithmetic, and has its answer. The surface is simply those 180 winners, compressed.

It is called a surface because when you plot hood angle against the two inputs it forms a curved sheet over a plane — the name describes what it looks like, not what it is.

Three ingredients go into the surface, and only two of them are empirical

Worth being fussy here, because it is tempting to say the surface "comes from the exit-velocity experiment". That experiment supplies one ingredient of three.

IngredientWhere it comes fromWhat it supplies
the mapbench experiment · ~25 ballswhere the ball starts
the two constantsfield shots · ~20 ballshow strongly the air acts
the flight equationsphysics — not measuredhow the ball travels

Plus two choices that are not measurements of anything at all: which situations to solve for (the grid) and how to choose among the shots that work (the robustness metric).

bench experiment   →  the map
field shots        →  the two constants      →  SWEEP  →  winners  →  fit  →  THE SURFACE
physics equations  →  the shape of the flight

The map is an input to the sweep, not the producer of the surface. Take away any one of the three and nothing comes out the far end.

Being loose about this invites an obvious and unanswerable reply — "then why do we need the physics at all?" — because that framing has quietly dropped the part doing the work. Note too that there are two empirical experiments here rather than one: the bench measurement and the field landings measure different things and feed different parts of the machinery.

The clean version: the experiment tells you where the ball starts, the equations tell you where it goes, the constants tune how much the air interferes, and the metric picks which of the working shots is worth keeping.

Map and surface, side by side
The mapThe surface
Aboutyour shooteryour situation
Made bymeasuring, on a benchcomputing, then fitting
Takes inRPM, hood angledistance, radial velocity
Gives outexit velocity, spinhood angle, RPM
Usedoffline, inside the sweepon the robot, 50× a second

Notice that the surface outputs what the map takes in. That is not a coincidence — the sweep sits between them, feeding candidates through the map until it finds the one worth storing. And the surface is what replaces today's lookup table: same job, better storage, plus a second input.

Where each ingredient entered

Distance — the target the landing point was compared against. Never touched the map.
Radial velocity — zero in this example; otherwise added to the ball's initial velocity before simulating. Never touched the map.
The map — consulted once per candidate, purely to answer "what does this RPM produce?"

So: the map is about your machine, the grid is about your situation. They meet only inside the simulate-and-compare step, and the map never learns anything at all about where you are standing.

OUTER LOOP — for every situation in the grid: ( distance d , radial velocity vₑ ) these two are the grid axes — nothing to do with the map INNER SEARCH — for every candidate shot: ( hood , RPM ) (hood, RPM) the MAP shooter only exit velocity + spin lives only here SIMULATE landing point 1 vₑ joins the ball's initial velocity — the head start 2 error = landing − d , then score it for flatness Keep the flattest candidate that still lands on target, then store that (hood, RPM) at cell ( d , vₑ ). fit a surface through all ~180 stored winners RUNTIME — on the robot ( d , vₑ ) measured now the surface ( hood , RPM ) the very same two numbers that indexed the outer loop
Fig 31Where the situation meets the shooter. Two nested loops, and the join is easy to miss. The map never knows about distance or radial velocity — it describes the shooter alone, RPM and hood in, exit velocity out. The situation enters the simulation instead, at the two marked points: radial velocity joins the ball's initial velocity, and distance is what the landing gets compared against. Every cell of the grid is one completed inner search, which is why the runtime inputs are exactly the outer-loop variables.
Why it can be neither an axis nor an output

There is a quiet rule governing the grid. Its inputs must be things you can observe; its outputs must be things you can command.

Exit velocity is neither. You cannot ask a shooter for 14.2 m/s — you can only ask for 3200 RPM and let compression and slip decide what actually emerges, so it cannot be an output. And you cannot sense it mid-match, so it cannot be an input either.

That is precisely why the map exists. It is the translator between the quantity you can command and the quantity physics needs, and without it the flight model has no starting condition at all.

This does not mean you only shoot while driving at the goal

Easy to read it that way, but radial velocity is signed, and the axis spans all three cases:

Diagonal motion isn't a special case either. Drive at 45° to the goal and you simply have both components at once — the tangential part goes to the turret and the aim offset, the radial part indexes the grid. Nothing extra is needed.

Which is what makes the coverage complete: any velocity in the plane has exactly two components, and both are accounted for — one by the turret, one by the grid axis. For a robot driving on a flat field there is no third kind of motion. Two axes really is the entire space.

Two practical notes on that axis

Size the range to your drivetrain. If it manages 4.5 m/s and the grid only samples −3 to +3, the surface quietly fails exactly where robots most often are — at the extremes.

Pin down the sign convention before writing any of it. Positive-is-closing versus positive-is-receding is a coin flip, and getting it backwards is worse than ignoring the term altogether: the shot is then wrong by twice the correction, because it compensates hard in precisely the wrong direction.

Fig 32A surface is just a curve with one more input. Every point on the ground plane is a situation you might be in — some distance, some closing speed. The height of the mesh above it is the hood angle for that situation. The dots are computed sample shots; the mesh is the smooth equation fitted through them, so you can read off an answer at any point on the plane, not only where a dot happens to sit. A second identical surface stores flywheel speed.
The honest tradeoff

A fit buys you smoothness, tiny storage, noise-averaging, and answers outside the measured range. What it costs is the one guarantee the table had: a table cannot be wrong at a point you measured, whereas a fit can miss everywhere if you picked a shape the data doesn't actually follow — too low a degree and it can't bend enough, too high and it wiggles wildly between points. That is the same trust-versus-smoothness tradeoff as the model-versus-empirical argument, one level down.

Why "second-degree" specifically?

Degree is how many bends the equation is allowed to have. Degree 1 is a straight line and cannot bend at all; degree 2 bends once; degree 5 can wiggle four times. So the choice is really "how much freedom do I give this thing", and both extremes fail.

Fig 33Too rigid, about right, too free. Identical measurements in all three panels; only the degree changes. The straight line cannot follow a curved trend, so it is wrong in a systematic way that no extra data will fix. The degree-5 curve passes closer to every point — including each point's measurement error — and then leaves the sampled range at speed. The shaded band marks where you actually measured; everything outside it is the fit guessing.

Degree 2 is the lowest degree that can bend at all, and one bend is about as much structure as the underlying physics has across a normal operating range. In two variables it works out to exactly six terms:

hood  =  c₀ + c₁·d + c₂·v + c₃·d² + c₄·d·v + c₅·v²

That d·v cross term is the one worth noticing. It encodes the fact that how much your radial velocity matters depends on how far away you are — an interaction a degree-1 fit cannot express at all, no matter how well you tune it.

The deeper reason, particular to this problem

Near a minimum, every smooth function looks like a parabola. At an optimum the linear term vanishes by definition, so the leading behaviour left over is quadratic — that's just Taylor's theorem.

Since this entire pipeline exists to find and describe an optimum, a second-degree model is the natural local description of one. That's why quadratics turn up twice here: once as the shape of the fitted surface, and once as the reason the sensitivity metric in Fig 21 is measured with second derivatives.

But treat it as a starting guess, not a derived truth

Nothing proves degree 2 is correct for your shooter. The procedure is: fit it, then look at the residuals — what's left over after subtracting the fit. Scattered randomly means degree 2 was enough. Showing structure, such as a systematic bow in one region, means the model is too rigid.

And if it is too rigid, raising the degree is usually the worst available fix. Splitting the domain into regions, using a spline, or keeping a small residual-correction table all buy accuracy without destroying the extrapolation behaviour you adopted a fit for in the first place.

Which comes first — the model or the surface?

Worth stating plainly, because it's easy to get backwards: the surface is the last step, not a stepping stone towards the model. The model generates the data; the surface merely compresses the winning answers so the robot can look them up quickly. There is no path from a surface back to a model, and none is needed.

1 · TEST STAND measure exit velocity and backspin 2 · THE MODEL the twenty-line flight loop 3 · CALIBRATE fit the two constants to real shots 4 · SWEEP + SCORE 100k candidates, keep the flattest LAST 5 · FIT SURFACE compress the winners for runtime lookup no route backwards — and none is needed the shots you have ALREADY measured your current table — not wasted it becomes the calibration data for step 3
Fig 34One-way, and the surface is the exit. Easy to read backwards, because the approach is usually stated with the second-degree approximation before the construction of the physics model — but narrative order isn't build order. The green path is the reassuring part: the shots already measured don't get thrown away, they become the data the model is calibrated against.
The surface is a cache, not a source of truth

Because it sits at the end of a one-way chain, the surface is disposable. Recalibrate the model, change the drag constant, adjust the robustness weighting — and you regenerate the surface in seconds. Nothing is lost and nothing has to be re-measured, so committing to a surface commits you to nothing. Which also means fitting one is a genuinely low-risk thing to try first.

Two separate questions, constantly collapsed into one

It is very easy — and it happens constantly — to hear "lookup table vs. polynomial surface" and "empirical vs. physics model" as the same argument. They are not. They're two independent choices, and you make both.

A polynomial surface is only a storage format. It says nothing about where its numbers came from — you can fit one straight through hand-measured shots without a line of physics anywhere. All four combinations exist:

MEASURED shoot balls, write down what worked COMPUTED run the flight equations on a laptop STORED AS A TABLE rows of numbers STORED AS AN EQUATION a fitted curve or surface TODAY Tune by hand at 3 m, 4 m, 5 m… store the rows, interpolate between. + maximum trust — it is measured reality − kinked, can't extrapolate − cannot ask what-if Simulate a grid of shots, dump the winners straight into a table. + can optimise, since the model answers what-if − you paid for smoothness then threw it away a legitimate halfway house Fit a curve or surface directly through your measured shots. + smooth, compact, still no physics to get wrong − STILL cannot ask what-if this is the option most often doubted MODEL ROUTE Model generates and scores shots; the fit stores the winners for runtime. + optimisable AND smooth AND compact − trust now depends on the model being right the model route Robustness comes from the RIGHT-HAND COLUMN, never the bottom row. Only a model can answer "what if the hood were 1° higher?" — a shot you never took. A storage format can only hold what you already have.
Fig 35Storage format and data source are independent choices. People often speak of "a polynomial fit with no physical model" — the bottom-left cell — which only makes sense if fit and model are different things. They are. Switching to a polynomial surface alone buys smoothness and compactness; it does not unlock robustness optimisation. Only moving to the right-hand column does that.

Measured versus modelled — the real tradeoff

Measured + table
top-left cell — today
Model + fitted surface
bottom-right cell — the new pipeline
Which choice
decides this
TrustHigh — it's measured realityOnly as good as your understandingdata source
Outside sampled rangeFailsExtrapolates sensiblystorage format
ContinuityPiecewise, kinkedSmooth everywherestorage format
Can optimise robustnessNo — no gradients availableYesdata source
Effort to buildAn afternoon of shootingWeeks, plus test-stand hardwaredata source
Fails byBeing silently absentBeing confidently wrongdata source

Read the right-hand column: every property people actually argue about traces back to the data source, not the storage format. Trust, optimisability, effort, and how it fails are all decided by whether the numbers were measured or simulated. Only smoothness and extrapolation come from choosing a fit over a table — real benefits, but cheap ones, and available without any physics at all.

The obvious synthesis — a physics model with an empirically calibrated correction term — is the right instinct, and the last section sharpens it.

Why this matters most for drum shooters

A single-flywheel drum shooter has a much narrower window of shots that are accurate than a two-wheel or single-stream design. When the forgiving region is small, picking the right member of the solution family stops being a refinement and becomes the difference between scoring and not. That's the argument for why a team running one should care most.

The strongest objection — and how to answer it

There is one objection to all of this that carries real weight, and it goes roughly like this: any approach requiring a physics model gives up the great virtue of the measured table — that you need not understand the physical system very well, and can trust in its correctness regardless.

Put plainly: the table works because you measured it, not because you understand why it works — and a model only works if your understanding is right. This is the most serious argument against the whole approach, and it deserves a real answer rather than enthusiasm.

What "trust in its correctness" really means

The table is correct by construction. It is not a claim about the world that could turn out to be false; it is a record of what happened. You wrote 42° at 3 m because balls went in at 42° at 3 m. There is no physics assertion inside it to be wrong about — which is exactly why "correctness" belongs in quotation marks.

A model inverts that. Its authority rests entirely on your understanding being right. Get the drag coefficient wrong, forget that spin decays during flight, switch to a scuffed ball that grips differently, and the model keeps producing confident numbers everywhere with nothing inside it to signal they are wrong.

So the risk moves rather than shrinking. A table fails by incompleteness — no answer beyond where you sampled — and it fails visibly, at the edges. A model fails by wrongness — an answer everywhere, possibly false — and it fails invisibly, anywhere at all.

The underrated half of the objection

"We don't need to understand the physical system very well" is also an organisational point, not merely a technical one. A team's roster turns over every year. An approach that works without a resident aerodynamicist survives graduation; one that depends on somebody remembering why MAGNUS_COEFF is 0.03 may not survive the person who chose it.

That is a legitimate engineering constraint, not laziness — and it is the part of this objection most likely to be waved away in a technical discussion.

Where the objection can be answered

It rests on one assumption: that a model's correctness has to be taken on faith. It doesn't — because you already own measurements, and you can deliberately hold some of them back.

1 · YOUR MEASURED SHOTS fit with these — 14 hide these — 6 2 · CALIBRATE fit DRAG_COEFF and MAGNUS_COEFF using only the 14 the model has never seen the other 6 3 · JUDGE IT ON THE 6 IT NEVER SAW actual → predicted inside tolerance If it lands the shots it never saw, the trust is MEASURED, not assumed. And if it doesn't, you found out in the workshop rather than in a match — which is also a win. Your existing table stops being the thing the model replaces, and becomes the exam the model has to pass.
Fig 36Earning trust instead of assuming it. Standard practice anywhere models meet data: fit on part of it, judge on the rest. A model that predicts shots it was never shown has demonstrated something a lookup table cannot even be asked to demonstrate — that it generalises. The objection is right that a model's correctness can't be taken on faith. It just doesn't have to be.
What this does to the tradeoff

The question stops being "measured data versus trusted physics", which has no resolution, and becomes "does the model pass the exam your measurements set for it?" — a question with an answer you can go and get in an afternoon.

It also disposes of the fear of silent wrongness. A model that reproduces held-back shots isn't being trusted on faith; it has been tested exactly where the table said it couldn't be. And the organisational worry survives intact and should be honoured: whatever you build, the validation script is what lets next year's team re-run the check without understanding the physics themselves.

What the whole pipeline is actually for

Before taking it apart phrase by phrase, it is worth having the thesis, because the pipeline reads as a grab-bag of unrelated tasks until you see what they are all in service of.

The whole thing in one paragraph

The traditional way to pick shots is tuning until they work. That gives one answer per distance and no idea whether it is a good answer. Replace it with: measure what the shooter actually does, build a physics model of the ball's flight on top of that measurement, then use the model to try every possible shot in every possible situation and keep the ones that still score when you are slightly off. Store the winners as an equation.

Five clauses. Everything else in Part Two is detail hanging off one of them.

And the thesis underneath all five: the entire pipeline is scaffolding for a single capability — being able to evaluate a shot you never took. Every component exists to serve that, and the chain from physics down to workshop task is tighter than it first appears. Each step below is forced by the one above it.

EACH STEP IS FORCED BY THE ONE ABOVE IT A ball in flight cannot be corrected. So any error at the moment of release is permanent. So the only defence is choosing a shot that tolerates error. So you must compare shots you never actually took. So you must be able to simulate them. So the simulation needs the ball's starting velocity. And that velocity cannot be calculated — only measured. So: put the shooter on a bench and film the muzzle. a fact about the world a task for Saturday
Fig 37From a fact about physics to a task in the workshop. The map — which looks like an arbitrary implementation detail when you first meet it — sits at the bottom of a chain beginning with "you cannot steer a ball mid-air". Nothing in the middle is optional if you accept the top and want the third line.
Two things that make the passage hard to read

It is written in reverse. The destination comes first — optimal trajectories, a quadratic fit — and the prerequisites come last. The build order is the exact opposite of the reading order, which is why it feels like it starts in the middle.

And it is not really about accuracy. The shots already go in when everything is right. It is about them continuing to go in when something is slightly wrong — which, for a system with no feedback after release, is the only kind of accuracy anyone can actually engineer.

Why calibration is not optional

It is tempting to skip the two measurement steps and take the flight constants from a textbook. That fails, and fails invisibly. Published drag coefficients describe smooth rigid spheres in clean flow; a game piece is light, hollow, seamed, scuffed and spinning hard. Book values for kd on a foam ball are not merely imprecise, they are confidently wrong — the model would hand you plausible-looking numbers that were junk. Calibration is not a refinement of this pipeline. It is what makes the model mean anything at all.

The approach, phrase by phrase

Every concept in Part Two is now on the table, so the whole approach can be stated in one pass. Here it is with the technical vocabulary marked:

Use precomputed optimal trajectories rather than a linear interpolation + time of flight map. Compute the optimal trajectory by finding the one whose error given a perturbation in exit velocity or hood angle is least, for every distance / radial velocity pair, then use a second-degree approximation to recover hood angle and shooter velocity. Build the physics model by empirically measuring and mapping shooter velocity and hood angle to the ball's exit velocity, which absorbs ball compression, slip and the rest; measure the backspin with a piece of tape and a high-framerate video. Then construct a second, physical model on top of the empirical one to get the actual flight path.
The wordsWhat it meansExplained in
"precomputed optimal trajectories"Work the answers out offline on a laptop, before the match — not on the robot during it.Fig 38
"rather than linear interpolation + time of flight map"The current setup: a 1D table blended between rows, plus a separate flight-time table driving the virtual-goal loop.Fig 18, Fig 38
"error given a perturbation in exit velocity / hood angle"A perturbation is a deliberate small wiggle. Nudge the hood, nudge the speed, and see how far the ball moves. Pick the shot least disturbed by it.Fig 21
"for every distance / radial velocity pair"Repeat for every cell of the two-input grid — which is precisely what makes the result a surface rather than a curve.Fig 32
"a second-degree approximation"Fit a degree-2 (quadratic) polynomial through the winning shots. "Degree" is how much the equation is allowed to bend.Fig 28, 15
"empirically measure and map shooter velocity and hood angle to exit velocity"Test-stand calibration: put in an RPM and a hood angle, measure the speed the ball actually leaves at.Fig 23, left
"the issue of ball compression, slip, etc."The messy contact physics nobody models well — so don't model it, measure it.Fig 23
"measure the backspin using tape and high-framerate video"Tape gives a visible reference mark; a high frame rate lets you count rotations between frames. The Magnus term needs this number.Fig 26
"a second (physical) model using the empirical model"Two stages chained: the empirical stage supplies exit conditions, the physical stage flies the ball from there.Fig 23
The word doing the most work is "optimal"

It does not mean most accurate. Every candidate shot in Fig 19 is perfectly accurate when the hood and flywheel hit their targets exactly. Optimal here means most forgiving — the shot that still scores when they don't. That reframing, from "find a shot that works" to "find the shot that survives being wrong", is the entire pipeline in four words.

Unpacking the first clause — where the work happens

"Precomputed rather than linear interpolation + time of flight map" is the densest phrase in the whole approach, and it's really about when and where the thinking gets done.

TODAY — every step, live on the robot 1 · measure distance d 2 · interpolate the hood / speed table 3 · look up the time-of-flight map 4 · shift goal by v × TOF → new d′ repeat 2–3× until it settles → aim and shoot All of it inside a 20 ms loop, on the roboRIO. PRECOMPUTED — the thinking moves offline ONCE — on a laptop, before the season • sweep every (distance, radial velocity) cell • score every candidate shot for robustness • fit one surface through the winners hours of compute — but nobody is waiting on it ship the coefficients EVERY 20 ms — on the robot • measure distance and radial velocity • evaluate one polynomial → hood, speed, TOF • no loop, no iteration, nothing to converge The loop vanishes because velocity became an INPUT, rather than a correction applied after the fact.
Fig 38The same answer, computed at a different time. The loop on the left exists because of a chicken-and-egg problem: you need flight time to know the aim point, but flight time depends on distance, which depends on the aim point. So you guess, refine and repeat. On the right that question was already settled offline, for every situation, before the robot ever powered on.

The deeper change is the one printed at the bottom of the figure. Today, motion is handled as a correction — work out the stationary answer, then patch it up for the fact that you're moving. Precomputing handles motion as an input — radial velocity is one of the axes the answer was worked out over in the first place, so there is nothing left to iterate towards.

That is also exactly why the table grows from one dimension to two. The extra dimension is the thing the loop was compensating for.

In fairness to the current approach

The iteration is neither expensive nor fragile — it converges in two or three passes and costs almost nothing on a modern roboRIO. Precomputing is not about saving CPU time. The two real objections are that the loop cannot optimise for robustness, and that it quietly assumes a moving shot is just a displaced stationary shot — an assumption that drag breaks, which is the argument in Part Three.

One thing worth separating out

The usual complaint about the table-only approach is that it "leaves a lot to be desired in terms of accuracy and robustness". Those sound like one complaint but they're two, with different causes and different fixes:

Which exposes the useful thing: the change reads as one big yes-or-no, but it's really three separate questions. Two of them are just the axes of the grid from Fig 35 — the third isn't a square on that grid at all.

The questionYour optionsWhat it costsBlocked by
1 How is the answer stored? a table  /  a fitted equation a weekend nothing
2 Where do the numbers come from? measured  /  model-generated weeks, plus test-stand hardware nothing
3 What are you optimising for? any shot that scores  /  the most forgiving shot almost nothing needs Q2 = model

Questions 1 and 2 are genuinely independent — you can answer either one without touching the other. Question 3 isn't a storage or source choice at all; it's a goal, and it simply becomes available once question 2 is answered "model". Here are all three drawn on the grid you've already seen:

Q2 · change where the numbers come from — weeks + test stand Q1 · change the storage — a weekend MEASURED MODEL-GENERATED TODAY measured + table what you ship now — trusted, kinked, cannot ask what-if model + table answering Q2 alone already unlocks Q3 measured + equation smoother and smaller, but still cannot ask what-if MODEL ROUTE model + equation everything at once — and Q3 available here too Q3 optimise for robustness — possible anywhere in this column Q1 moves you DOWN. Q2 moves you RIGHT. Q3 isn't a square — it's what the right-hand column makes possible.
Fig 39Three questions, two of them axes. Moving down costs a weekend and buys smoothness. Moving right costs weeks and buys the ability to ask what-if. Q3 — optimising for robustness — is not a destination on this grid, it's a capability that switches on the moment you're in the right-hand column, and it's nearly free once you are.

So the thing everyone actually wants, Q3, is invisible until you've paid for Q2 — which is why the discussion keeps collapsing into a single "is the model worth it?". It also means the sensible order isn't the order it is usually presented in:

  1. Now: answer Q1 by fitting a surface to the table you already have. Cheap, reversible, and it commits you to nothing — the surface is a cache, as Fig 34 shows.
  2. Next: measure whether drag actually matters for your game piece. That single measurement — suggestion 4 in Part Three — is what decides whether Q2 is worth paying for.
  3. Then, only if drag matters: answer Q2, and Q3 comes almost for free with it.

The whole pipeline on one page

Everything above, assembled — and separated by when each part happens and where.

The single most useful thing to hold onto is that this runs in three phases, and they are wildly different in character. One happens in a workshop with a camera and about forty-five balls. One happens on a laptop, once, and never again. One happens on the robot fifty times a second. Confusing them is what makes the approach feel like a tangle.

The order is forced — nothing here can be done out of sequence

1. MEASURE   bench, ~25 balls           →  the map
2. MEASURE   field, ~20 balls           →  landing points
3. FIT ①     tune DRAG and MAGNUS       →  a calibrated model

4. SWEEP
   4a  build the grid    — every (cell, candidate) → its miss distance
   4b  difference it     — neighbouring cells give ∂e/∂θ and ∂e/∂v
   4c  score and select  — best candidate per cell  →  ~180 winners
       ↳ the grid itself is discarded here

5. FIT ②     quadratic through the ~180 winners  →  THE SURFACE
6. SHIP      the coefficients
That grid inside step 4 is easy to miss, and it has a name

The sweep does not try candidates one at a time and forget them. It builds a grid — every cell paired with every candidate, each entry holding that shot's miss distance — and then works on it. Roughly 180 cells by 200 candidates is about 36,000 rows, a few megabytes, nothing at all.

It has to exist because step 4b needs neighbours. A central difference is (right − left) ÷ 2Δ, so you cannot evaluate one candidate in isolation and discard it — its neighbours must be present at the same moment for a slope to be taken. This is precisely the "table" meant by the remark about numerically estimating the derivatives.

And it is not listed as a pipeline artifact because nothing survives it. The map is carried into step 4; the surface is carried out of step 5. The grid is built, used, and thrown away without ever leaving step 4 — only the ~180 winners emerge.

And the derivatives are for selecting, not for fitting

Worth separating, because "derivative" and "fit" sitting near each other suggests one feeds the other. It does not.

∂e/∂θ , ∂e/∂v   →   decide WHICH ~180 points
the fit        →   draw a curve THROUGH those points

The fit never sees a derivative. It is handed 180 rows of (distance, radial velocity) → (hood, RPM) and performs ordinary least squares. It has no notion that those particular shots were chosen for being forgiving; to the fit they are simply points.

What the derivative decides is which point each cell contributes. Cell (4.0 m, 0) has valid shots at 30°, 45°, 60° and everywhere between — all of them score. The derivative is what breaks that tie, and 45° is the row that goes forward.

So what would happen if you skipped the derivative entirely?

You would still get a surface. It would be fitted through arbitrary valid shots instead of forgiving ones — it would work, it would be worse, and nothing in the fit would tell you. The derivative's whole influence on the finished surface runs through which winners it picked.

Three different derivatives live in this pipeline — only one is yours
DerivativeWhat it measuresWho uses it
∂e/∂θ , ∂e/∂vmiss distance against actuator erroryou — the robustness metric
∂(residual²)/∂(kd, km)fit error against the constantsinside fit ①, and only with a gradient method — a grid search never forms it
∂(residual²)/∂(coefficients)fit error against the surface coefficientsinside least squares, automatically and invisibly

Only the first is ever yours to compute or reason about. The other two are internals of solvers you did not write, and whenever this tutorial says "derivative" it means the first.

Each step is blocked on the one before it. You cannot fit the constants without the map, because the model would have no starting velocity. You cannot sweep without calibrated constants, because you would be simulating the wrong physics a hundred and eighty thousand times. And you cannot fit the surface without the sweep's winners, because there would be nothing to fit through.

The two measurements sit at opposite ends of one flight

Steps 1 and 2 are both labelled "measure", which makes them look like the same activity done twice. They are not — they point cameras at opposite ends of the ball's journey and answer different questions.

1 MEASURE ① — the START how fast does it leave, and how fast is it spinning? → tells you what your SHOOTER does bench · high-speed camera · ~25 balls produces THE MAP 2 MEASURE ② — the END where did it actually land? → tells you what the AIR does field · tape measure or tarp · ~20 balls and the difference between them is what the air did
Fig 40Two measurements, opposite ends of one flight. This is also why the order cannot be swapped, and the reason is physical rather than procedural. Knowing the start and observing the end, the discrepancy between them is precisely what you attribute to drag and Magnus. Without the first measurement you could not do that at all — a ball landing short might mean heavy air resistance or a weak launch, and nothing would tell you which.
The short version

Measurement ① tells you what your shooter does. Measurement ② tells you what the air does. One is the beginning of the ball's journey, the other is the end, and the gap between what you put in and what came out is the whole of the physics you are trying to pin down.

Worth noting that ② needs far cheaper equipment — no high-speed camera at all, just a tape measure and something to mark where the ball hits.

What each measurement actually produces — and what Fit ① does with it

Both measurements produce a small table of rows. Here is what is genuinely in them.

MEASURE ① — the map · about 25 rows
RPMhood→ exit velocityspin
250030°5.6 m/s6.3 rev/s
250045°5.6 m/s6.9 rev/s
300030°6.7 m/s7.5 rev/s
300045°6.7 m/s8.2 rev/s
MEASURE ② — landing records · about 20 rows
RPM usedhood usedstood at→ landed at
280040°4.0 m4.35 m
300035°5.0 m4.80 m
Notice what is missing from the second table

Exit velocity. You never measure it in the field — all you can record there is what you commanded and what happened. That single absence is what forces the ordering, and it is the clearest way to see why.

So Fit ① walks every landing record through the same three steps:

record #7:   RPM 2800, hood 40°, landed at 4.35 m

  1  look up the MAP with (2800, 40°)                  ← measure ① enters here
        → exit velocity 6.27 m/s, spin 7.5 rev/s

  2  simulate with those, plus a GUESS at the constants
        try kd = 0.010, km = 0.004   →   predicted landing 4.51 m

  3  compare against what actually happened            ← measure ② enters here
        residual = 4.51 − 4.35 = +0.16 m

Repeat for all twenty records, square the residuals and add them up. That gives one score for one guess at the pair (kd, km). Then sweep a grid of guesses and keep whichever scored lowest.

So, precisely

What goes into Fit ①: measurement ① supplies the starting conditions for each shot, by way of the map. Measurement ② supplies the truth to compare against, in the form of actual landing points.

What is being fitted: exactly two numbersDRAG_COEFF and MAGNUS_COEFF. Everything else in the simulation is either measured or exactly known, which is why a plain grid search suffices.

And why ① must come first: measurement ② records commands, not physics. The model cannot simulate "2800 RPM" — it needs metres per second. The map is the only thing that converts one into the other, so without it your twenty landing records are unusable: you would know where the balls went, but not what was launched.

Are drag and Magnus really the only things acting on the ball?

No — but they are the two biggest uncertain ones, and that distinction is exactly why the count is two.

Acting on the ballIn the model?Carries an unknown?
Gravityyesno — 9.81 is 9.81
Dragyesyes · kd
Magnusyesyes · km
Buoyancyno
Added massno
Spin decaying during flightno
Drag varying with speedno
Sidespin curving the shot laterallyno
Ball scuffing and asymmetryno

Gravity is modelled but fitted for nothing. So of the three forces actually in the equations, only two carry an unknown — which is where "two constants" comes from.

And some of those omissions are not small

Take a typical game piece — roughly 270 g at 9.5 inches, which works out to a bulk density near 37 kg/m³, about thirty times lighter than water:

Around 5% combined, and neither appears anywhere in the model. For a light hollow ball that is not a rounding error.

But the fit quietly absorbs them

kd and km do not emerge as the true physical coefficients. They emerge as whatever values make the model match reality — which silently includes compensating for everything left out.

Spin decaying in flight weakens Magnus on average, so the fitted km lands below the true instantaneous value. Buoyancy makes the ball fall a little slower, and some of that is absorbed into kd. The model ends up wrong in its details and right on average — perfectly serviceable, provided you do not extrapolate far outside the conditions you calibrated in.

Which is precisely where the two-observable test earns its keep. If absorption is not enough — if no pair of constants can match the landing point and the descent angle at once — the model is reporting that its structure is inadequate rather than its numbers mis-tuned. That is how you would discover spin decay matters without ever having modelled it.

The one omission that cannot be absorbed

Sidespin. If the rollers grip unevenly the ball picks up a spin component that curves it sideways, and a model working only in the shot plane cannot express that at all — no value of kd or km compensates for a force pointing out of the plane.

It shows up as a persistent left-or-right bias that survives every recalibration. Worth knowing about, because the natural instinct is to chase it in the aiming code, where it is not.

Why this order is so easy to scramble: the word "fit" means two different things
Fit ① — calibrationFit ② — the surface
What is fitted2 constants12 coefficients
Fitted to~20 measured landings~180 computed winners
Fitted againstrealitythe model's own output
Purposemake the model correctmake the answer fast
Whenbefore the sweepafter the sweep

They sit on opposite sides of the sweep, and they do opposite jobs: one makes the model trustworthy, the other compresses that model's conclusions into something a roboRIO can evaluate. Merge them in your head and the ordering collapses immediately.

A short way to keep them apart: Fit ① faces reality. Fit ② faces the computer.

WORKSHOP physical ~45 balls ONCE test stand + camera sweep RPM × hood ~25 balls THE MAP RPM, hood → exit vel, spin shots at known distances landings measured ~20 balls LAPTOP computed ONCE, before the season flight equations gravity, drag, Magnus calibrate fit DRAG and MAGNUS THE SWEEP — for every (distance, radial velocity) cell: try candidate (hood, RPM) · look up the map · simulate · perturb · score · keep the flattest one that still scores THE SURFACE d, radial v → hood, RPM, TOF ship the coefficients ROBOT every 20 ms all match long pose + chassis speeds distance, radial v, tangential v read the surface → hood, RPM, TOF virtual goal shifts the target · repeat 2–3× until it settles TURRET — aim at the virtual goal, plus the two-term feedforward SHOOT no feedback after this — which is why any of it matters
Fig 41The complete pipeline. Three phases, and almost every confusion in this tutorial comes from mixing them up. Black pills are the artifacts — the two things that actually get carried from one phase to the next. The map is born in the workshop and consumed on the laptop; the surface is born on the laptop and consumed on the robot. Nothing else crosses a boundary.
The three questions this map answers

What is physical? Only the top band. About forty-five balls, once, and then never again.

What is computed? The middle band, once, on a laptop, before the season. Nobody is waiting on it, so it can take hours.

What runs during a match? Only the bottom band — read two numbers off a surface, iterate a couple of times, point the turret, fire. Everything expensive has already happened.

Where the earlier figures sit on this map

Fig 3 is the bottom band alone, drawn larger. Fig 23 is the boundary between the top and middle bands. Fig 31 zooms into the sweep box. Fig 38 contrasts the bottom band as it works today with how it would work once the middle band exists. If any of them felt disconnected, this is the frame they belong to.

Part Three · Four refinements worth making

The pipeline as described is sound. These are the four places worth pushing on — the first two make the metric honest, the third stops it optimising something dangerous, and the fourth decides whether to do any of it at all.

1 · Weight the sensitivity metric by real uncertainty

The metric as written adds a term measured in miss-per-degree to a term measured in miss-per-metre-per-second. Those units don't combine — adding them assumes your hood error and flywheel error happen to be numerically comparable, which they aren't. Weight each by how badly you actually control that axis:

S  =  σθ2(∂e/∂θ)2  +  σv2(∂e/∂v)2

σ is your measured closed-loop tracking error on each axis. This turns a heuristic into actual variance propagation — and it's a one-line change.

2 · Handle distance uncertainty — but not by adding a term

On most robots, knowing where you are is a bigger error source than hood or flywheel control. Vision latency, tag ambiguity, odometry drift. So it is tempting to add a σd2(∂e/∂d)2 term alongside the other two — it looks like the obvious way to complete the metric.

That obvious completion is wrong, and the reason is worth understanding

Work through what a distance error actually does. You believe you are at ; you are really at d; the error is δ. The sweep only keeps candidates that land at — that is the filter it applies. So every surviving candidate lands at , and every one of them misses by δ.

The miss is the same whichever candidate you picked. ∂e/∂d is therefore identical across the whole cell — a constant — and adding a constant to every score leaves the winner exactly where it was. The term is inert.

Which points at a general rule worth having: a term only earns its place in a selection metric if its value differs between candidates. Hood and flywheel sensitivities genuinely do differ, which is the entire basis of the method. Distance sensitivity does not.

None of which makes distance error harmless — it is real, often dominant, and simply cannot be reduced by choosing a different shot. So it does not belong in a metric whose only job is choosing between shots.

Where it does belong: the acceptance window

Shot choice cannot shrink the miss caused by a bad range estimate. What it can change is how large a miss the goal will still accept.

A steeply descending ball drops into the opening; a shallow one has to thread it and risks clipping the rim on the way. So the window your miss has to land inside depends on the trajectory you chose, even though the miss itself does not:

score by    total miss   vs   acceptance window ( entry angle ) …rather than by folding a distance term into the sensitivity sum.

Which makes entry angle considerably more load-bearing than suggestion 3 alone implies. It is not merely a rim-rejection constraint — it is the only channel through which choosing a different shot buys you any tolerance to pose error at all.

3 · Make entry angle a hard constraint, not part of the cost

A ball arriving too shallow bounces off the rim no matter how insensitive the trajectory is. Don't let a low entry angle be traded away against good sensitivity — filter the candidate list by minimum entry angle first, then optimise sensitivity over what survives.

4 · Lead with drag — it's the real justification for the 2D table

This one matters most when making the case for the work at all, because without it the whole project is unnecessary.

With no air drag, shooting while moving is exactly equivalent to shooting at a displaced virtual goal. The iterative "guess the flight time, shift the target, re-solve, repeat" trick is essentially exact, and you don't need a 2D table at all — you need the stationary table plus two iterations.

With meaningful drag on a light game piece, that equivalence breaks. The moving shot isn't a stationary shot at a shifted target; it's a genuinely different trajectory with a different shape and a different entry angle, because drag acts on the ball's total velocity — which now includes the robot's contribution.

The argument to lead with

The reason to build the 2D (distance, radial velocity) surface instead of iterating on the 1D table is drag. If drag is negligible for your game piece, the 2D table buys you almost nothing and the current approach is fine. Measure that first — it decides whether the entire project is worth doing.

Where this leaves you

Don't frame it as model versus empirical. That framing is what gets people stuck, and you do not have to pick.

The measured table stays ground truth. The model becomes a well-behaved interpolator with derivatives. That preserves exactly the "you can trust its correctness" property you do not want to give up, while unlocking the optimisation you do — which dissolves the tradeoff instead of picking a side.

Glossary

ActuatorAny powered mechanism that moves something — the hood motor, the flywheel. Actuator error is the gap between what you commanded and what it physically did.
Angular velocityHow fast something rotates, in radians per second. Every point on one rigid body shares the same angular velocity, however far from the centre it sits.
atan2The two-argument arctangent. Given y and x separately it returns the angle of the point (x, y), and because it sees both signs it knows which quadrant the angle is in — plain arctan cannot.
BackspinSpin where the top of the ball rotates backwards relative to travel. It creates lift via the Magnus effect and helps the ball drop into the goal rather than skid off the rim.
BearingThe compass direction from one point to another. Here, the direction from the robot to the goal.
CalibrationTuning a model's free constants until it reproduces real measurements. Here: adjusting drag and Magnus until simulated shots land where real ones did.
Central differenceA finite-difference estimate taken symmetrically — evaluate a little above and a little below, and divide by the gap. More accurate than stepping one way.
ChassisThe drivebase — the frame, wheels and motors that move the robot around. Everything else bolts on top of it.
Closed-loopA system that measures its own output and corrects itself, as opposed to open-loop, which just commands something and hopes.
ComponentThe part of a vector along one chosen direction — a single signed number, found by projecting the vector onto that direction.
DecompositionSplitting one thing into parts that add back up to it. Velocity splits into radial + tangential; motion splits into translation + rotation. The test is whether the parts reassemble into the original — Fig 29 draws that as a closed parallelogram.
DerivativeThe rate at which one quantity changes as another changes — the steepness of a curve at a point. Speed is the derivative of position; angular velocity is the derivative of angle. Written with a dot (θ̇) or as ∂e/∂θ.
Descent angleHow steeply the ball is falling when it arrives, measured from horizontal. A steep descent drops into the goal; a shallow one risks clipping the rim.
Dot productMultiply matching components and add them: v · r̂ = vx·r̂x + vy·r̂y. Against a unit vector it extracts how much of a vector lies along that direction, with a sign. All four component numbers in this tutorial — vx, vy, v∥, v⊥ — are this one operation applied to four different directions.
DragAir resistance. It slows the ball down during flight, and crucially it acts on the ball's TOTAL speed — which is why a shot taken while driving isn't just a shifted version of a stationary shot.
Drum shooterA shooter built around one wide roller. Simple and compact, but it has a narrower range of accurate shots than a two-wheel design — which is why picking the RIGHT shot matters more for one.
EncoderA sensor that counts shaft rotation. Drive encoders give each wheel's speed; steering encoders give each module's direction. They report wheel motion, not ground motion — slip fools them.
Entry angleHow steeply the ball descends into the goal. Too shallow and it rejects off the rim regardless of aim.
Exit velocityHow fast the ball is actually travelling the instant it leaves the shooter. Not the same as flywheel speed — ball compression and slip mean some energy is always lost.
ExtrapolationEstimating beyond the ends of your data, where nothing was measured. Tables cannot do it; models do it whether or not it is wise.
FeedbackMeasuring the result and feeding the error back to correct it — the mechanism inside closed-loop control. The ball in flight has none.
Feedforward (FF)A predicted control output added on top of a PID, based on what you already know is about to happen. Prevents lag instead of correcting it.
Field-relativeMeasured in the field's coordinate frame rather than the robot's, so the numbers don't change when the robot spins.
Finite differencesEstimating a derivative without calculus: evaluate at two nearby points and divide the change in output by the change in input.
Flight path vs time of flightThe flight path is the shape — the whole curve through the air. The time of flight is one number read off it, the moment the path reaches goal height. A single simulated path yields three useful numbers: the landing point, the time of flight, and the entry angle.
FlywheelA wheel spun up to thousands of RPM. The ball is fed against it and flung out; the wheel's speed determines how fast the ball leaves. Often just called \"the shooter\".
FrameA choice of axes to measure in. Field frame: x runs down the field. Robot frame: x points out the robot's nose. The same velocity has different numbers in each.
Game pieceThe object the year's game is played with — the ball. Light, hollow and seamed, which is why book aerodynamics numbers do not apply to it.
GradientThe direction and rate of steepest change of a surface — the multi-variable version of a derivative. Optimisation needs gradients to know which way to walk.
GyroA sensor that measures rotation, giving the robot its heading. Drifts slowly over a match, which corrupts anything converted through it.
HeadingThe direction the robot's nose points, measured as an angle from the field's x-axis. Changed by rotating; sliding leaves it untouched. The gyro measures it.
Held-backMeasurements deliberately left out of a fit, then used afterwards to test it. If the model predicts shots it never saw, it has earned some trust.
HoodA curved plate above the flywheel that pivots up and down to set how steeply the ball launches. Raise it for a lob, lower it for a line drive. It does not aim left or right — that's the turret.
IntegralThe running total of small pieces — the opposite of a derivative. The area under a curve.
IntegrationAdding up many small pieces to get a total — stepping a ball forward slice by slice through its flight is numerical integration.
InterpolationEstimating a value between two known ones — if the table has rows for 3 m and 4 m, reading off 3.5 m by blending them is interpolation.
KinematicsThe geometry of motion, with no forces involved. Forward kinematics folds the four module speeds and directions into one chassis velocity.
Line of sightThe imaginary straight line from the robot to the goal. Its direction is the bearing φ; its rotation rate is v⊥ ÷ d. Sliding sideways swings it even when the chassis never turns — which is the whole of the second feedforward term.
Linear velocityPlain travelling velocity — metres per second in some direction. Contrast angular velocity, which is rotation.
Lookup tableA list of measured values — here, hood angle and flywheel speed written down for each distance. At runtime you find the nearest entries and blend between them.
MagnitudeThe size of a vector with its direction thrown away. The magnitude of velocity is speed.
Magnus effectThe sideways force a spinning ball generates in flight. Backspin makes a ball hang longer and drop more steeply.
Matched pairTwo values that only make sense together. A hood angle is only correct for one particular flywheel speed, so the solver hands both back at once.
MuzzleThe point where the ball leaves the shooter — borrowed from firearms. Muzzle measurements happen before air resistance has touched the ball.
OdometryTracking your position by adding up how far the wheels have turned. Cheap and smooth, but it drifts over time as small errors accumulate.
Open-loopNo correction after the action starts. Once the ball leaves, nothing can steer it — every error present at release is permanent.
PerturbationA deliberate small wiggle applied to an input — nudge the hood a degree, nudge the speed a little — to see how much the landing point moves.
Physics modelNot the real robot and not a robot simulator — a short piece of code that steps a ball through the air using gravity, drag and spin to work out where it lands. Runs offline; the robot supplies its calibration constants.
PIDProportional-Integral-Derivative: the standard feedback controller. It watches the difference between where a mechanism is and where you want it, and pushes harder the bigger that difference is. It only reacts AFTER an error appears.
Polynomial surfaceA smooth curved sheet described by an equation, fitted through a cloud of data points so you can look up any value quickly — including between the points you measured.
PoseA robot's position AND heading together — x, y and rotation. \"Where am I and which way am I facing.\"
Pose estimatorThe code that maintains the robot's best guess of where it is, by fusing wheel odometry with camera sightings of field markers.
ProjectionTaking the part of a vector that lies along a chosen direction — the shadow it casts on that axis.
QuadraticAn expression whose highest power is a square (x²) — its graph is a parabola. Near any smooth optimum, a quadratic is the natural local description.
Quadrature (adding in)Square each, add, take the square root: √(a² + b²). How perpendicular or independent quantities combine, as opposed to plain addition for quantities pointing the same way. The largest term dominates heavily, so small contributions are usually not worth chasing. Appears here as |v| = √(v∥² + v⊥²), as d = √(rx² + ry²), and as the robustness metric S.
Radial velocityThe part moving directly toward or away from the target. Irrelevant to aiming; critical to the shot solution.
RadiansA way of measuring angles where a full circle is 2π (about 6.28) instead of 360. One radian is about 57 degrees. Used because it makes the arc-length formula come out as simply arc = radius × angle.
ResidualsWhat's left over after a model's prediction is subtracted from reality. Storing residuals lets you keep a model's smoothness while still honouring measured truth.
Rigid bodyAn object whose points never move relative to each other — it can only slide and spin as a unit. A chassis is one; a chassis plus a spinning turret is two.
Robot-relativeMeasured against axes fixed to the robot — x out the nose, y out the side. Rotate by the gyro heading to convert to field-relative.
RPMRevolutions per minute — how flywheel speed is usually reported. A few thousand RPM is typical.
Sensitivity / robustnessHow much your miss distance grows per unit of actuator or estimation error. Low sensitivity means small mistakes stay makes.
Shot solutionThe pair of numbers — hood angle and flywheel speed — that makes a given shot, together with the resulting time of flight. Always solved as a pair; neither number means anything without the other.
Shot solverThe code that turns the current situation — distance and radial velocity — into a hood angle and flywheel speed.
SimulationThe act of running a model to see what it predicts. The model is the recipe; simulating is the cooking. Note the two senses used here — simulating a ball's flight (Part Two) and simulating your robot code against a fake robot (Part One) are unrelated activities that share a word.
Simulation gridThe huge temporary table of simulated shots produced during the sweep — scored, mined for winners, then discarded. Not the lookup table.
SingularityA point where a formula stops being usable because something is divided by zero. As d approaches 0 the term v⊥/d demands an infinite turret rate.
Standard deviationThe typical spread of repeated measurements around their average — how much a single shot usually differs from the mean.
Steady-stateA constant leftover error that never goes away while conditions stay the same — the sign that your controller is systematically lagging rather than just being noisy.
SwerveA drivetrain where each wheel can both spin and steer independently, letting the robot move in any direction while pointing any direction. It's why translation and rotation can be treated separately here.
Tag ambiguityWhen a camera sees an AprilTag at an angle where two different 3D orientations look nearly identical, so the computed position can flip between two wrong answers.
Tangential velocityThe part of the robot's velocity that slides sideways across the target rather than toward or away from it. The only part that forces the turret to sweep.
Test standA fixed rig holding the shooter off the robot so you can measure exit velocity and spin precisely and repeatedly. This is where the messy contact physics gets measured instead of modelled.
The mapA measured calibration curve for the shooter: RPM and hood angle in, exit velocity and spin out. Describes your machine. Used offline, inside the sweep.
The surfaceThe shipped artifact: roughly twelve coefficients taking distance and radial velocity in and giving hood angle and RPM out. It is the sweep's winning shots compressed into an equation, and it replaces today's lookup table.
the sweepThe offline stage that tries every candidate shot in every situation on a laptop, scores each for sensitivity, and keeps the winners.
Time of flight (TOF)How long the ball is airborne. Needed to work out how far it drifts sideways while flying.
TrajectoryThe path the ball traces through the air from muzzle to landing.
TranslationSliding without turning. The whole robot shifts and every point on it moves in the same direction at the same speed, so its facing never changes. The geometric sense of the word — from Latin trans + latus, "carried across" — not the language one. Rotation is its counterpart, and any rigid motion is a combination of the two.
TurretThe powered turntable the shooter sits on. It swivels the whole shooter left and right, like turning your head. It does NOT change the launch angle — that's the hood's job.
Unit vectorAn arrow of length exactly 1 that carries only direction, no magnitude. Used to say \"this way\" without saying \"this fast\".
VectorA quantity with both a size and a direction, drawn as an arrow. Velocity is a vector; speed is just its length.
Virtual goalA fake aim point, offset from the real goal, that compensates for the ball inheriting the robot's velocity.
Vision latencyThe delay between the camera seeing something and the robot acting on it. By the time the measurement arrives the robot has already moved, so it describes where you WERE.
Wheel slipWheels spinning or skidding without matching ground motion. Encoders keep counting, so the robot confidently believes motion that is not happening.
ω (omega)Angular velocity — how fast something is rotating, in radians per second. The glyph looks almost identical to a lowercase w in many fonts, but it is the Greek letter. In code it is written out as omega / OMEGA_MAX, since identifiers are kept to plain ASCII.