Angles, Radians & the Unit Circle

Geometry & Trigonometry

Chapter 2 · Angles, Radians & the Unit Circle

Chapter 1 showed a conceptual angle-wraparound bug — ordinary subtraction not knowing that headings repeat every 360°. This chapter builds the actual vocabulary every later chapter depends on: radians, the unit circle, and periodicity — and shows a second, genuinely deeper wraparound problem, one that connects directly back to Numerical Methods & Floating-Point Computation's own representation-error material.

Degrees vs. Radians

A radian measures an angle by arc length: one radian is the angle subtended when the arc length equals the circle's own radius. A full circle is radians, which is why π radians equals exactly half a circle: 180°.

Verified directly
math.degrees(math.pi) = 180.0 exactly, and math.radians(180) = 3.141592653589793 — Python's own stored approximation of π. Virtually every math library's trigonometric functions (sin, cos, tan) expect radians, not degrees — a genuinely common source of bugs when a value coming from user input or a design tool in degrees is passed straight into a trig function expecting radians.

The Unit Circle Definitions

For a point on a circle of radius 1 centered at the origin, at angle θ measured counterclockwise from the positive x-axis: cos(θ) is that point's x-coordinate, and sin(θ) is its y-coordinate. tan(θ) = sin(θ)/cos(θ) — the slope of the line from the origin to that point.

Anglecos(θ) — x-coordsin(θ) — y-coordQuadrant behavior
10Positive x-axis
90°01Positive y-axis
180°−10Negative x-axis
270°0−1Negative y-axis

A Real Surprise: "Exact" Trig Identities Aren't Exactly True in Floating Point

The unit circle says cos(90°)=0 and sin(180°)=0 exactly. In floating point, these are only approximately true — for the same reason Numerical Methods & Floating-Point Computation's own Chapter 2 established: π itself has no exact binary representation, so π/2 passed to cos() is never quite the true mathematical π/2.

Verified directly
cos(math.pi/2) = 6.123233995736766×10⁻¹⁷ — not exactly 0. sin(math.pi) = 1.2246467991473532×10⁻¹⁶ — also not exactly 0. Both errors are tiny, right at the scale of machine epsilon, and harmless for most purposes — but a piece of code that checks if cos(angle) == 0: to detect a right angle, following Numerical Methods & Floating-Point Computation's own Chapter 1 warning about exact equality checks, would never trigger, even at what's conceptually exactly 90°.
A dramatic consequence: tan() near its asymptote
tan(θ) is mathematically undefined at θ=90°, since division by cos(90°)=0 is undefined. In floating point, tan(math.pi/2) doesn't raise an error or return infinity — it returns 1.633123935319537×10¹⁶, a huge but perfectly ordinary finite number, because cos(π/2) as actually computed is that tiny 6.12×10⁻¹⁷ value above, not true zero. This is exactly the "dividing by something close to zero" pattern Numerical Methods & Floating-Point Computation's own conditioning chapter flagged — code near a tangent asymptote needs an explicit check, since it will silently return a huge, misleading finite number rather than failing loudly.

Periodicity, and a Second, Deeper Wraparound Problem

sin and cos repeat every ; tan repeats every π (since flipping both sin and cos in sign leaves their ratio unchanged). This means a game object's rotation angle can, in principle, be represented by infinitely many equivalent values — θ, θ+2π, θ+4π, and so on. In practice, letting an accumulating angle grow without ever wrapping it back into a bounded range is a real, measurable mistake.

Verified directly — 2,000,000 frames of unwrapped rotation vs. wrapped rotation
Simulating a spinning object that adds a small fixed increment to its rotation angle every frame, for 2,000,000 frames, without ever wrapping: the final angle reaches ≈27,400 radians. Computing sin() directly on that huge, never-wrapped value gives a relative error of ≈3.15×10⁻⁷ against a high-precision reference. Doing the mathematically identical accumulation but wrapping the angle back into [0, 2π) every single frame instead gives a relative error of just ≈4.54×10⁻¹¹nearly four orders of magnitude more accurate, for the exact same sequence of increments.
Why this happens — a direct callback to Numerical Methods & Floating-Point Computation
A double's precision is relative to its own magnitude (Numerical Methods & Floating-Point Computation Chapter 2's own sliding exponent). An angle value near 27,400 has far coarser spacing between representable values than an angle value kept near 2π≈6.28 — every increment added to the large unwrapped value loses more precision to rounding than the same increment added to a small, regularly-wrapped one. Regularly wrapping an accumulating angle isn't just tidier code — it's a genuine, measurable numerical-accuracy improvement, for free.

Where This Connects

This chapter's findingWhat it sets up
cos(π/2) and sin(π) aren't exactly zeroA concrete reason Chapter 5's rotation-matrix code needs tolerance checks, not exact-equality checks, exactly as Numerical Methods & Floating-Point Computation Chapter 1 warned generally
An unwrapped accumulating angle loses real precisionDirectly foreshadows Chapter 6's gimbal lock and Chapter 7's quaternions, both of which exist partly to keep repeated rotation accumulation numerically well-behaved
The unit circle's (cos θ, sin θ) point definitionThe literal basis for every rotation matrix built in Chapter 5

Hands-On Exercises

Exercise 1

A design tool exports a rotation of 45 degrees, but a rendering function expects radians and receives the raw value 45 unconverted. Using this chapter's own degree/radian conversion, compute what angle (in degrees) the renderer will actually display, and explain why the bug might not be obvious from a quick glance at the number 45.

📄 View solution
Exercise 2

Using this chapter's own verified cos(π/2) and tan(π/2) results, explain why a physics engine checking if abs(cos(angle)) < 1e-9: to detect "this object is at a right angle" is a better design than checking if cos(angle) == 0:, and why a physics engine should specifically guard against dividing by cos(angle) near that same angle.

📄 View solution
Exercise 3

Using this chapter's own verified 2,000,000-frame experiment, explain in your own words why wrapping an accumulating angle every frame is not just a stylistic preference but a genuine numerical-accuracy improvement — your answer should reference why a large angle value has less usable precision than a small one.

📄 View solution

Chapter 2 Quick Reference

  • Radians: a full circle is radians; π radians =180° — verified exactly via math.degrees(math.pi)=180.0
  • Unit circle: cos(θ) = x-coordinate, sin(θ) = y-coordinate, tan(θ)=sin(θ)/cos(θ), for a point at angle θ on a radius-1 circle
  • Verified: cos(π/2)≈6.12×10⁻¹⁷ and sin(π)≈1.22×10⁻¹⁶ — not exactly zero, because π itself has no exact floating-point representation; tan(π/2) returns a huge but finite number (≈1.63×10¹⁶) rather than failing
  • Periodicity: sin/cos repeat every ; tan repeats every π
  • Verified: an angle accumulated over 2,000,000 frames without wrapping has ≈3.15×10⁻⁷ relative error in its sin(); wrapping every frame reduces that to ≈4.54×10⁻¹¹ — nearly 4 orders of magnitude better, for free
  • Next chapter: Triangles — the Law of Sines and the Law of Cosines, the practical toolkit for solving a triangle from partial information