Concept 04: Inverse Trig & 4-Quadrant Heading with atan2
▶ Interactive Demo: atan vs. atan2 Explorer
Drag the target through all four quadrants. Watch
atan(y/x)andatan2(y, x)agree in Quadrant I, then part company by exactly 180° — and watchatanfail outright when x reaches 0.
1. The Real-World Problem: Getting the Angle Back
Concept 01 took a wheel pointed 30° off down-field, driven at 4.0 m/s, and split it into 3.464 m/s down-field and 2.000 m/s sideways. One angle and one speed in; two components out.
This concept runs that arrow backwards. A path follower does not think in wheel angles — it computes where the robot should be heading and hands you exactly those two numbers: so many m/s down-field, so many m/s sideways. But the wheel has two physical motors, one that spins it and one that points it, so it needs a speed and an angle. The speed is Module 1’s distance formula and costs nothing:
speed = √( 3.464² + 2.000² ) = √( 12.000 + 4.000 ) = √16.000 = 4.000 m/s
The angle is the problem, and it is the whole of this concept.
The same question has a second face you will meet more often. Your robot sits at (2.0, 1.0) and a scoring target at (5.0, 5.0); subtracting, as Module 1 does for distance, gives (3.0, 4.0) — three meters down-field, four to the left. Which way does the turret point? Again: two components known, one angle wanted.
That is the inverse of everything Concept 01 did, and the tool for it has a name most people get wrong the first three times.
2. Building the Math: Inverting a Function That Repeats
Step 1: What inverting a trig function has to mean
sin 30° = 0.500. Running that backwards means asking: what is the angle whose sine is 0.500?
Look at what the sine function actually does over a long stretch of angles. Concept 01 established that sin θ is the y-coordinate of the point at angle θ on the unit circle, so as θ sweeps round and round, the sine rises to 1, falls to −1 and comes back, forever. Draw a horizontal line at height 0.500 and it does not cross the curve once. It crosses infinitely often.
Reading off the crossings: 30°, 150°, 390°, 510°, and going the other way −210°, −330°, and on forever in both directions. Two families, 30° + 360k and 150° + 360k, for every whole number k.
So the honest answer to “the angle whose sine is 0.5” is there is no such angle; there are infinitely many. But a function must return exactly one value for each input — that is what makes it a function. The inverse therefore cannot answer the question as asked. It answers a narrower one: of all the angles whose sine is 0.5, the one lying inside a window we agree on in advance. That agreed window is the principal range, and picking it is not a detail — it is the entire reason the inverse exists at all.
Math!
sin⁻¹ x = arcsin x = asin x (three spellings, one function)Read out loud as “the angle whose sine is x” — “arcsine of x” is the shorter version. The
⁻¹marks a function inverse, not a reciprocal:sin⁻¹(0.5)is 30°, whereas1 / sin(0.5)is a completely different and unrelated number. This clash of notation is unfortunate and permanent. Java sidesteps it entirely and calls themMath.asin,Math.acosandMath.atan.
Step 2: Choosing the three windows
A window has to satisfy two demands. It must be narrow enough that the function hits each output only once inside it, or we are back to two answers. And it must be wide enough to reach every possible output, or some legal inputs would have no answer at all.
- Sine. From −90° to +90° the sine climbs steadily from −1 up to +1, passing through each height exactly once — the purple stretch in the figure. Narrower and you lose values; wider and 150° sneaks in beside 30°. So
asinreturns an angle in [−90°, +90°]. - Cosine. The same window fails for cosine:
cos(−60°) = cos(+60°) = 0.500, two answers already. Cosine is symmetric about 0°, so any window containing 0° in its interior is doomed. Slide it: from 0° to 180° cosine falls steadily from +1 to −1, once through. Soacosreturns an angle in [0°, 180°]. - Tangent. Concept 01 showed
tan θis the slope of the line at angle θ, and that it blows up at ±90°. Between those two poles the slope sweeps from arbitrarily steep downhill to arbitrarily steep uphill, taking every real number exactly once. Soatanaccepts any number and returns an angle in (−90°, +90°), endpoints excluded because no finite slope is vertical.
Math!
asin : [−1, +1] → [ −90°, +90° ] acos : [−1, +1] → [ 0°, 180° ] atan : any number → ( −90°, +90° )Read the first line out loud as “arcsine takes a number between −1 and 1 and returns an angle between −90 and +90 degrees.” Square brackets include the endpoint, round brackets exclude it — so
asin(1) = 90°exactly, whileatanapproaches ±90° and never arrives. Java’s versions return radians:Math.asin(0.5)is0.5236, not30. Wrap them inMath.toDegreesat the boundary, exactly as Concept 01 insisted.
Hold on to one consequence, because everything that goes wrong from here comes out of it: atan can only ever return an angle in the right half of the plane, quadrants I and IV. Its entire output range is −90° to +90°. Nothing you feed it will make it point left.
Step 3: The inverse tangent, and why it looks like it works
Back to the wheel. We have the two legs, so the ratio we want is opposite over adjacent — sideways over down-field — and Concept 01’s SOH-CAH-TOA says that ratio is the tangent:
tan θ = sideways / down-field = 2.000 / 3.464 = 0.5774
θ = atan(0.5774) = 30.0°
Correct. And this is the trap, because it is correct every single time you try it on a wheel pushing forwards and to the left, which is what you naturally try first.
Step 4: The first failure — two opposite directions, one answer
Take the turret problem, and give it two targets. Both are 5 m away, using the 3-4-5 triangle from Geometry Concept 01:
- Target A at
(x, y) = (3, 4)— ahead and to the left. - Target B at
(x, y) = (−3, −4)— behind and to the right. The exact opposite direction.
Form the ratio each time:
Target A: y / x = (+4) / (+3) = +1.3333
Target B: y / x = (−4) / (−3) = +1.3333
The same number. Not close — identical, because the two minus signs cancel in the division. So atan cannot help but return the same angle for both:
atan(+1.3333) = 53.13° for A — correct
atan(+1.3333) = 53.13° for B — the true heading is −126.87°
A robot aiming with atan would fire at empty space exactly 180° from the target. Not off by a bit, not noisy: reversed. Drive a swerve module with it and the wheel pushes precisely backwards.
And notice this is not a defect in atan. atan was handed the number +1.3333 and returned the only angle in its range whose tangent is +1.3333. The information was destroyed before the call. There were four possible sign pairs going in — (+,+), (−,+), (−,−), (+,−) — and the division emits only two possible ratio signs, positive and negative. Two directions per ratio, and the ratio itself does not remember which. Restated geometrically: negating both x and y rotates the target 180° about the robot and leaves y/x unchanged, so no function of y/x alone can ever distinguish a direction from its opposite.
Step 5: The second failure — x = 0
Now point the wheel straight sideways: down-field 0.0, sideways 4.0. Nothing exotic — that is a pure strafe, one of the most-commanded motions there is. The ratio is
y / x = 4.0 / 0.0 undefined
Division by zero. In Java, double arithmetic will not throw here — it quietly produces Infinity, and Math.atan(Infinity) happens to return exactly +90°, which is the right answer. That accident is worse than a crash, because it teaches you the code is fine. Three things it hides:
- If the components arrive as integers — raw encoder counts, pixel offsets from a camera —
dy / dxthrowsArithmeticExceptionand the robot code dies mid-match. - If the components are both zero,
0.0 / 0.0isNaN.Math.atan(NaN)isNaN, andNaNfails every comparison silently, so your clamps and safety checks all pass it straight through into a motor command. - You are computing the commonest sideways heading on the field by relying on infinity arithmetic to bail you out. That is not a design; it is a coincidence you inherited.
Step 6: atan2 — stop dividing
Both failures have one cause: we destroyed information by dividing before we asked the question. The fix is to stop dividing and hand the function both components separately, so it can look at their signs itself.
That function is atan2.
Math!
θ = atan2(y, x)Read out loud as “theta equals a-tan-two of y and x”. Its defining property is exactly the undoing of Concept 01: for any point other than the origin, with
r = √(x² + y²),atan2(y, x)is the one angle θ in the range(−180°, +180°]satisfyingx = r · cos θ and y = r · sin θConcept 01 went θ →
(x, y). This goes(x, y)→ θ. The half-open range means −180° is not a legal answer and +180° is; pointing straight backwards reports+180°.
You can build it yourself out of atan, and doing so is the point — there is no magic inside. Two steps:
1. Get the size of the turn, ignoring direction. Feed atan the ratio of the magnitudes:
a = atan( |y| / |x| ) always between 0° and 90°
This is Concept 01’s reference angle — the acute angle between the ray and the horizontal axis.
2. Place it in the correct quadrant using the two signs. The signs are still in your hand, because you never divided them away:
sign of x sign of y quadrant θ = atan2(y, x) for (±3, ±4), a = 53.13°
----------------------------------------------------------------------------
+ + I a +53.13°
− + II 180° − a +126.87°
− − III a − 180° −126.87°
+ − IV − a −53.13°
Four distinct answers from the same reference angle, all four targets 5 m away, and no two of them confusable.
The axes need no special pleading — they fall straight out of the same rule. x > 0, y = 0 gives 0°; x = 0, y > 0 gives +90°; x < 0, y = 0 gives +180°; x = 0, y < 0 gives −90°. The pure strafe that broke Step 5 is atan2(4.0, 0.0) = +90.0°, computed with no division and no special case, because the magnitude ratio |y| / |x| is only reached when x is nonzero — the sign table handles the axes on its own.
This is Concept 01’s Step 5 running in reverse. There, you found the reference angle and read the signs off the picture to evaluate cos 150°. Here you read the signs off the components to recover the angle. Same table, opposite direction.
Step 7: The argument order is backwards, and it is a real bug
atan2 takes y first. The reason is that it is standing in for tan θ = y / x, and the numerator is written first — but almost nobody guesses it, because every coordinate pair you have ever written puts x first.
Swapping the arguments does not crash and does not produce anything obviously silly. It mirrors the heading about the 45° diagonal, because for a first-quadrant point the two reference angles are complementary:
atan2(4, 3) = 53.13° atan2(3, 4) = 36.87° 53.13 + 36.87 = 90
Which means a robot tested by driving it diagonally forward-left — at or near 45°, the natural thing to try — behaves perfectly, because 45° is a fixed point of the mirror. The bug only surfaces later, off the diagonal, under a match load. Write the order out in a comment every time.
Worse, WPILib’s Rotation2d constructor takes (x, y) — the opposite order — so a single Java file can legitimately contain both conventions. Both are correct. Neither is guessable.
Step 8: atan2(0, 0)
The origin has no direction. There is genuinely no angle to return, because every angle satisfies 0 = r cos θ when r = 0. Java’s Math.atan2(0.0, 0.0) returns 0.0 by convention rather than erroring — which means a zero velocity command silently reads as “point straight down-field”, and the wheels snap to 0° for no reason at the end of every path.
The fix is a magnitude check before the call, not after:
if √(x² + y²) < ε keep the previous commanded angle
else θ = atan2(y, x)
Holding the last angle is the right behavior for a swerve module: a stopped wheel should stay where it is pointed, ready for the next command.
Step 9: The loop closes
Feed the opening problem back in. atan2(2.000, 3.464) = 30.0°, and the speed was already 4.000 m/s — exactly the pair Concept 01 started from. The two concepts are inverses, and now you can go both ways.
For the turret: the difference (3.0, 4.0) gives atan2(4.0, 3.0) = +53.13° measured in the field frame. Concept 03 warned that a turret does not live in the field frame — if the robot is itself facing 20°, the turret must go to 53.13° − 20° = 33.13° relative to the chassis. Subtracting two angles can push the result outside ±180°, and repairing that is Concept 05’s job.
3. Solving It in Code (Java & WPILib)
First Principles (Java)
// The inverse of Concept 01: two components in, angle and speed out.
double downfieldMps = 3.4641; // meters per second
double sidewaysMps = 2.0000; // meters per second
double speedMps = Math.hypot(downfieldMps, sidewaysMps); // 4.0000 m/s
double angleDeg = Math.toDegrees(Math.atan2(sidewaysMps, downfieldMps));
System.out.printf("speed %.4f angle %.2f deg%n", speedMps, angleDeg);
// speed 4.0000 angle 30.00 deg <- exactly what Concept 01 started from
Building atan2 by hand from atan, so nothing is hidden — this is the sign table from Step 6, line for line:
/** Same contract as Math.atan2: returns radians in (-PI, PI]. */
static double atan2FromScratch(double y, double x) {
if (x == 0.0 && y == 0.0) return 0.0; // no direction exists
if (x == 0.0) return (y > 0) ? Math.PI / 2 : -Math.PI / 2;
if (y == 0.0) return (x > 0) ? 0.0 : Math.PI;
double a = Math.atan(Math.abs(y) / Math.abs(x)); // reference angle, 0..PI/2
if (x > 0 && y > 0) return a; // quadrant I
if (x < 0 && y > 0) return Math.PI - a; // quadrant II
if (x < 0 && y < 0) return a - Math.PI; // quadrant III
return -a; // quadrant IV
}
Run both against the four targets and watch atan collapse the plane in half:
double[] xs = { 3.0, -3.0, -3.0, 3.0, 0.0, 0.0 };
double[] ys = { 4.0, 4.0, -4.0, -4.0, 4.0, 0.0 };
for (int i = 0; i < xs.length; i++) {
double x = xs[i], y = ys[i];
double naive = Math.toDegrees(Math.atan(y / x)); // one argument
double good = Math.toDegrees(Math.atan2(y, x)); // two arguments
double mine = Math.toDegrees(atan2FromScratch(y, x));
System.out.printf("(%+.0f,%+.0f) atan %8.2f atan2 %8.2f mine %8.2f%n",
x, y, naive, good, mine);
}
// (+3,+4) atan 53.13 atan2 53.13 mine 53.13
// (-3,+4) atan -53.13 atan2 126.87 mine 126.87 <- 180 deg out
// (-3,-4) atan 53.13 atan2 -126.87 mine -126.87 <- 180 deg out
// (+3,-4) atan -53.13 atan2 -53.13 mine -53.13
// (+0,+4) atan 90.00 atan2 90.00 mine 90.00 <- 4.0/0.0 = Infinity
// (+0,+0) atan NaN atan2 0.00 mine 0.00 <- 0.0/0.0 = NaN
The hand-built version agrees with Math.atan2 on every row, which is the point: atan2 is atan plus a sign table, and you have just written the sign table.
In a Robot Project (Java & WPILib)
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Translation2d;
// The Rotation2d(x, y) constructor calls atan2 internally and normalizes the
// pair to length 1. Note the argument order: (x, y), NOT atan2's (y, x).
Rotation2d wheelAngle = new Rotation2d(3.4641, 2.0000);
double deg = wheelAngle.getDegrees(); // 30.00 <- same as above
// Bearing to a target, straight from a position difference.
Translation2d robot = new Translation2d(2.0, 1.0);
Translation2d target = new Translation2d(5.0, 5.0);
Translation2d delta = target.minus(robot); // (3.0, 4.0)
double range = delta.getNorm(); // 5.00 m
Rotation2d bearing = delta.getAngle(); // 53.13 deg, field frame
// Turret command is relative to the chassis, so subtract the robot's heading.
Rotation2d turret = bearing.minus(robotPose.getRotation()); // 33.13 deg at heading 20
// The (0, 0) guard from Step 8, which no library will apply for you.
// lastTurret is a field, holding whatever was commanded on the previous loop.
if (delta.getNorm() > 1e-6) {
lastTurret = turret;
}
turretSubsystem.setSetpoint(lastTurret);
Both tiers produce the same numbers — 30.00° for the wheel, 53.13° and 5.00 m for the target — because Rotation2d(x, y) and Translation2d.getAngle() are Math.atan2 with a different argument order and a normalization bolted on.
4. Bridge to Real Systems
Every heading a robot computes
atan2 is the single most-called trig function in a drivetrain. SwerveDriveKinematics turns a chassis velocity into four module states, and the last thing it does for each wheel is convert that wheel’s two velocity components into a Rotation2d — an atan2 per module, fifty times a second. Vision code turns an AprilTag’s position relative to the camera into a bearing. Translation2d.getAngle(), Rotation2d(x, y), Transform2d.getTranslation().getAngle() — every one of them is this function wearing a WPILib name.
The reason the library never stores a bare angle is Step 5 and Step 8 combined. Rotation2d holds the (cos, sin) pair, calls atan2 once at construction, and hands back the angle only when something asks. No division, no Infinity, no quadrant to lose.
Cartesian to polar, and the phase of a signal
Outside robotics, the same operation is called converting Cartesian coordinates to polar: a point (x, y) becomes a magnitude r = √(x² + y²) and an angle θ = atan2(y, x). The magnitude is Module 1’s distance formula; the angle is this concept. That pair shows up wherever two orthogonal measurements have to become “how big, and which way”.
Signal processing leans on it hardest. A discrete Fourier transform reports each frequency bin as two numbers, a real part and an imaginary part, and the two questions asked of a bin are how much of this frequency is present and where in its cycle did it start. The first is the magnitude; the second is the phase, computed as atan2(imaginary, real) — which is exactly what NumPy’s numpy.angle calls. The failure mode is identical to the turret’s: use the ratio instead of the pair and a signal is reported half a cycle out of step, which in radar interferometry or beamforming puts a target on the wrong side of the antenna.
5. Checkpoints & Exploration Prompts
Checkpoint 1
The robot is at field position (6.0, 2.0) and an AprilTag is at (2.0, 5.0). Find the range and the field-frame bearing to the tag. Then say what atan(dy / dx) would have returned, and how far wrong the turret would end up.
Solution:
- Difference.
dx = 2.0 − 6.0 = −4.0,dy = 5.0 − 2.0 = +3.0. Behind the robot’s down-field axis and to the left. - Range.
√((−4.0)² + (3.0)²) = √(16.0 + 9.0) = √25.0 = 5.0m — a 3-4-5 triangle again. - Reference angle.
a = atan(|3.0| / |−4.0|) = atan(0.7500) = 36.87°. - Quadrant.
x < 0,y > 0, so Quadrant II, and the table saysθ = 180° − a = 180° − 36.87° = 143.13°. - What atan gives.
dy / dx = 3.0 / (−4.0) = −0.7500, andatan(−0.7500) = −36.87°.atancan only answer in the right half-plane, so it reports a heading forward and to the right. - How wrong.
143.13° − (−36.87°) = 180.00°— exactly reversed, as it must be, since(−4, +3)and(+4, −3)share the ratio−0.75.
Checkpoint 2
A wheel is commanded to down-field = 0.0, sideways = −2.5 m/s — a pure strafe to the right. Work out what atan(y / x) does in Java, what atan2 gives, and what happens one loop later when the command becomes (0.0, 0.0).
Solution:
- The naive ratio.
−2.5 / 0.0is not an exception for Javadoubles; it evaluates to−Infinity.Math.atan(Double.NEGATIVE_INFINITY)returns−π/2, soMath.toDegreesgives−90.0°— which is the correct heading, obtained entirely by luck. Change the components toints and the same expression throwsArithmeticException. - atan2.
Math.atan2(−2.5, 0.0) = −π/2 = −90.0°. Step 6’s axis rule, no division performed, no coincidence involved. - The stop command.
0.0 / 0.0isNaN, so the naive path producesNaNdegrees, andNaNfails every comparison — including anyif (angle > limit)guard — so it reaches the motor controller unchallenged. - atan2 at the origin.
Math.atan2(0.0, 0.0)returns0.0, so the wheel would snap from −90° round to 0° the instant the robot stops. Correct behavior is Step 8’s guard:if (Math.hypot(x, y) < 1e-6) keep the previous angle, leaving the wheel at −90°.
Deep Dive 1
atan2 is not the only way to recover an angle from components. Try building the same thing out of acos instead: given (x, y) and r = √(x² + y²), the cosine of the heading is x / r, so acos(x / r) is a candidate. Work out for which points it is correct and for which it is wrong, using the fact that acos returns 0° to 180°. Then find the one-line sign patch that repairs it, and test your repaired version against Math.atan2 on the four targets (±3, ±4). Finally, argue which of the two constructions you would trust when r is very small, and why.
Deep Dive 2
Step 7 claimed a swapped atan2(x, y) reflects the heading about the 45° line, so a 45° test passes. Find every heading that survives the swap unchanged — there is one more besides 45°, and the sign table tells you where. Then design the smallest set of test targets that is guaranteed to catch a swapped call, and explain why testing “straight ahead” alone is not enough on its own to be convincing. For extra bite: Microsoft Excel’s ATAN2 takes its arguments as (x, y), the reverse of Java, Python, C and JavaScript. Work out what happens to a scouting spreadsheet that copies a heading formula out of robot code, and which cells would look right.