Chapter 3
18 min read
Section 27 of 353

The Intermediate Value Theorem

Continuity - No Breaks, No Jumps

Learning Objectives

By the end of this section you will be able to:

  1. State the Intermediate Value Theorem precisely and identify its two hypotheses (continuous on a closed interval, target value between the endpoints).
  2. Visualise why the theorem is obvious for a pencil-drawn curve and why it fails the instant the pencil lifts.
  3. Apply the theorem to prove existence of roots, fixed points, and equal-temperature antipodes without ever computing them exactly.
  4. Implement the bisection method from first principles and explain each iteration as a direct application of the IVT.
  5. Recognise the IVT lurking inside root finders, cryptography, game theory, and machine-learning calibration.

The Big Picture

The Intermediate Value Theorem — IVT — is the first great existence theorem in calculus. It doesn't compute anything. It doesn't hand you a formula. It just says: if a continuous curve starts below a line and ends above it, it must cross that line somewhere in between.

That one sentence powers a disproportionate amount of mathematics. Every root-finding algorithm, every proof that a polynomial has a real root, every argument that some volatile process must hit a target value — all of them trace back to the IVT. It is the formal version of the intuition that says "you can't get from here to there without passing through the middle."

Core idea in one line

A continuous function cannot skip values. If ff is continuous on [a,b][a, b] and NN is any number between f(a)f(a) and f(b)f(b), then there is at least one c[a,b]c \in [a, b] with f(c)=Nf(c) = N.

Notice what the theorem does not say: it does not tell you how many such cc exist, where they are, or how to find them. It only promises existence. The entire art of numerical analysis can be read as "turning the IVT's existence guarantee into an algorithm."


Intuition: The Pencil Must Cross the Line

Imagine you are sketching a continuous graph with a pencil. The pencil starts at the left endpoint on some height f(a)f(a), and ends on some other height f(b)f(b). Now draw any horizontal line y=Ny = N at a height between those two.

Claim: you cannot finish the sketch without the pencil crossing that horizontal line at least once. Try it on a piece of paper: the only way to avoid the line is to lift the pencil — and a lift means the function is not continuous anymore.

That is the entire proof from a "pencil on paper" standpoint. The IVT is the rigorous statement that continuity and "can't skip a value between endpoints" are one and the same property.

Three mental anchors

  • Start below, end above → must cross zero. If f(a)<0<f(b)f(a) < 0 < f(b), then f(c)=0f(c) = 0 for some c(a,b)c \in (a, b). This is the root-existence form.
  • Temperature walk. Measure the temperature along a continuous wire at two points. If one end is 10°C and the other is 30°C, there is a spot on the wire where the temperature isexactly 17°C — even though you never measured there.
  • Hiking up and down. If you start at altitude 200m and finish at 900m by continuous climbing and descending, at some moment you passed exactly 500m. The IVT promises the instant, not the location on the map.

Formal Statement of the IVT

Theorem (Bolzano, 1817)

Let f:[a,b]Rf : [a,b] \to \mathbb{R} be continuous on the closed interval [a,b][a,b]. For every real number NN strictly between f(a)f(a) and f(b)f(b), there exists at least one c(a,b)c \in (a,b) such that

f(c)=N.\displaystyle f(c) = N.

The theorem has exactly two hypotheses, and both matter:

Hypothesis 1
Continuity on [a,b][a,b]

Every single point in the closed interval — endpoints included — must satisfy limxpf(x)=f(p)\lim_{x\to p}f(x)=f(p). One bad point and the theorem's promise evaporates.

Hypothesis 2
NN lies between f(a)f(a) and f(b)f(b)

Strict betweenness is enough. The IVT says nothing about values outside the range [min(f(a),f(b)),max(f(a),f(b))][\min(f(a),f(b)),\max(f(a),f(b))]; those may or may not be hit.

Sketch of proof (for curious readers)

Without loss of generality assume f(a)<N<f(b)f(a) < N < f(b). Define S={x[a,b]:f(x)N}S = \{x \in [a,b] : f(x) \leq N\}. The set is non-empty (it contains aa) and bounded above by bb, so the completeness of R\mathbb{R} gives a supremum c=supSc = \sup S. Continuity then forces f(c)=Nf(c) = N: it can't be less (a tiny neighbourhood on the right would contain more of SS) and it can't be greater (a tiny neighbourhood on the left would have stayed below NN). The theorem is a direct consequence of the real number line being gapless.


Why Continuity Is Non-Negotiable

Drop the continuity hypothesis and the whole theorem collapses. The gallery below shows three functions where f(a)f(a) and f(b)f(b) straddle some value NN, but no cc exists with f(c)=Nf(c) = N. In each case, a discontinuity opens a trapdoor that the curve jumps over without passing through.

Loading counterexample gallery…

Lesson

The IVT is a biconditional in disguise: for continuous functions the guarantee holds; for anything with a jump or hole it can fail spectacularly. Whenever a homework problem says "prove there exists a cc with f(c)=somethingf(c) = \text{something}," your first reflex should be: "Am I allowed to use IVT? Is ff continuous?"


Interactive: The IVT Explorer

Pick a continuous function and drag the target line y=Ny=N up and down. When NN enters the green strip between f(a)f(a) and f(b)f(b), the IVT guarantees at least one solution — the violet dots show every crossing the root finder can detect. Shrink and shift the bracket [a,b][a,b] to see how the guaranteed strip changes.

Loading IVT explorer…

Experiments worth running

  • Choose the cubic x3x2x^{3}-x-2, set a=1,b=2a=1, b=2, and drag NN onto 0. IVT promises a root — the explorer pins it near c1.521c\approx 1.521.
  • Switch to the bumpy quintic x54x+1x^{5}-4x+1. Fix N=0N=0 and notice that multiple cc's appear — IVT promises at least one, but the actual count can be anywhere from 1 to 5.
  • Drag NN outside the green strip. The guarantee goes silent. The curve might still hit NN by luck, but IVT has nothing to say there — exactly as the theorem states.

The Most Useful Corollary: Root Finding

The IVT's most-cited special case reads like a slogan:

Corollary (Sign-Change Root Theorem)

If ff is continuous on [a,b][a,b] and f(a)f(a) and f(b)f(b) have opposite signs — equivalently f(a)f(b)<0f(a)\cdot f(b) < 0 — then there exists c(a,b)c \in (a,b) with f(c)=0f(c) = 0.

This is the single most important consequence of the IVT in applied mathematics. Every time a numerical solver reports a "bracketed root," it is just checking the sign-change condition and invoking this corollary. Bisection, Regula Falsi, Brent's method, and friends all start by asking: "do the endpoints of my bracket have opposite signs?"

How to use it in proofs

To prove an equation g(x)=h(x)g(x) = h(x) has a solution, rewrite it as f(x)=g(x)h(x)=0f(x) = g(x) - h(x) = 0, exhibit a sign-changing bracket, and cite the corollary. Ninety percent of calculus-level existence arguments are one clever bracket away from done.


Worked Example — Proving a Root Exists by Hand

Show that the equation x3x2=0x^{3} - x - 2 = 0 has a real solution in the interval [1,2][1, 2], and then narrow the solution to width 0.125 by hand.

Expand step-by-step walkthrough

Step 1 — Check continuity. Let f(x)=x3x2f(x) = x^{3} - x - 2. Every polynomial is continuous on all of R\mathbb{R}, so in particular ff is continuous on [1,2][1, 2]. Hypothesis 1 ✅.

Step 2 — Check sign change at the endpoints.

  • f(1)=112=2<0f(1) = 1 - 1 - 2 = -2 < 0
  • f(2)=822=+4>0f(2) = 8 - 2 - 2 = +4 > 0

Zero lies strictly between 2-2 and +4+4, so Hypothesis 2 ✅ and the IVT corollary guarantees a c(1,2)c \in (1, 2) with f(c)=0f(c) = 0.

Step 3 — Bisect by hand to narrow the root. Repeatedly cut the bracket in half and keep the side where the sign change lives.

Iterationabc = (a+b)/2f(c)Next bracket
01.0002.000start
11.0002.0001.500+0.375[1.000, 1.500]
21.0001.5001.250−1.297[1.250, 1.500]
31.2501.5001.375−0.775[1.375, 1.500]
41.3751.5001.4375−0.462[1.4375, 1.500]
51.43751.5001.46875−0.300[1.46875, 1.500]
61.468751.5001.484375−0.214[1.484375, 1.500]

After 6 halvings the bracket has width (21)/260.0156(2-1)/2^{6} \approx 0.0156, so we know cc is within 0.008 of the midpoint 1.492. The exact root (to 6 decimals) is c1.521380c \approx 1.521380.

Step 4 — Sanity-check the sign changes. Along the way every new bracket still had f(left)<0<f(right)f(\text{left}) < 0 < f(\text{right}). The IVT's "sign-change ⇒ root" guarantee never stopped holding — which is exactly why bisection converges.

Takeaway. The hand calculation and the theoretical argument are the same thing. Each iteration applies the IVT to a narrower interval. The method turns an existence statement into a constructive procedure.


Interactive: Bisection in Action

Watch the IVT work one halving at a time. The visualiser hunts for 2\sqrt{2} as the positive root of f(x)=x22f(x) = x^{2} - 2 on [1,2][1, 2]. Each press of Step computes the midpoint, evaluates ff there, and uses the sign to discard the half that cannot contain the root.

Loading bisection visualizer…

Read the error column

The rightmost column shows the distance between the current midpoint cc and the true value 2\sqrt{2}. After nn iterations the bound is (b0a0)/2n(b_0-a_0)/2^{n}. One iteration ≈ one bit of accuracy — slow compared to Newton's method, but it never diverges and never needs a derivative.


Python: Bisection from First Principles

The IVT's guarantee is an algorithm in disguise: any procedure that maintains a sign-changing bracket and shrinks it will squeeze out the root the theorem promised. Here is that procedure in twenty lines of plain Python — no NumPy, no scipy. Click any line on the right panel to see how the variables evolve.

Plain Python — IVT turned into code
🐍bisection.py
1Comment — the mission

States the plan in one line. The IVT does not tell us WHERE the root is; it just promises one exists. Bisection turns that existence into a constructive procedure that squeezes the root between two endpoints until the gap is tiny.

2Comment — the bracket hypothesis

Explains the single precondition we need: the product f(a)·f(b) must be negative, meaning the two endpoints straddle zero. Combined with continuity, this activates the IVT.

4def bisect(f, a, b, tol=1e-4, max_iter=50):

Defines the bisection solver. It takes a continuous function f, a starting bracket [a, b] that straddles a root (f(a) and f(b) have opposite signs), and returns an (approximation, iterations) pair.

EXECUTION STATE
⬇ input: f = A Python callable mapping float → float. Example: lambda x: x*x - 2. Must be continuous on [a, b] for the IVT guarantee to hold.
⬇ input: a = Left endpoint of the bracket. Example: a = 1.0 for the √2 demo. f(a) should be on one side of zero (here f(1) = −1).
⬇ input: b = Right endpoint. Example: b = 2.0. f(b) must have the opposite sign of f(a) — here f(2) = +2.
⬇ input: tol=1e-4 = Tolerance: stop once |f(c)| ≤ tol OR the bracket is narrower than 2·tol. Default 10⁻⁴. Smaller tol = more iterations.
⬇ input: max_iter=50 = Safety valve. 50 halvings shrink the bracket by 2⁵⁰ ≈ 10¹⁵ — way beyond double-precision resolution. Prevents infinite loops if tol is unreachable.
⬆ returns = A tuple (c, n): c ≈ root, n = iterations used. Callers can inspect n to judge convergence speed.
5Docstring — one-line contract

Triple-quoted string documenting the function. It states the postcondition (we return c in [a, b] with |f(c)| ≤ tol) and the precondition (sign change in the bracket).

6fa, fb = f(a), f(b)

Evaluates f at both endpoints once and caches the values. Without caching, every iteration of the main loop would recompute f(a) — wasteful if f is expensive.

EXECUTION STATE
fa = Initial value at the left endpoint. For the √2 demo: fa = f(1.0) = 1·1 − 2 = −1.0.
fb = Initial value at the right endpoint. fb = f(2.0) = 2·2 − 2 = +2.0.
→ tuple unpacking = Python assigns both results in one statement. Equivalent to `fa = f(a); fb = f(b)` but avoids typing `f(` twice.
7if fa * fb > 0:

Guards the IVT hypothesis. If fa and fb share a sign, their product is positive, so there is no guaranteed root between them. A well-behaved solver refuses to start with bad data rather than returning a meaningless answer.

EXECUTION STATE
→ for √2 demo = fa · fb = (−1.0) · (+2.0) = −2.0 < 0 → hypothesis holds, proceed.
→ counter-example = If someone called bisect(f, 3.0, 4.0), we'd have fa = 7, fb = 14. Product = 98 > 0 → raise an error.
8raise ValueError(...)

Aborts with a loud, precise error. ValueError is Python's conventional choice for 'function got the right TYPE of argument but the VALUE is illegal'. The message names the specific hypothesis that failed so the caller knows what to fix.

EXECUTION STATE
📚 raise = Python's keyword for throwing an exception. Execution jumps out of the function and propagates up the call stack until caught by a try/except block, or crashes the program if uncaught.
10for n in range(1, max_iter + 1):

Main loop. range(1, 51) yields 1, 2, 3, …, 50. Starting at 1 (not 0) matches how humans count iterations in a convergence table.

EXECUTION STATE
📚 range(start, stop) = Python built-in: produces the integers start, start+1, …, stop−1 (stop is exclusive). range(1, 51) → 1..50, i.e. max_iter numbers.
→ iteration schedule = n = 1: first halving; n = 2: second; …; n = 50: final safety-valve halving if we haven't converged.
LOOP TRACE · 8 iterations
n = 1 (state before)
a, b = 1.000000, 2.000000
fa, fb = −1.000000, +2.000000
n = 2
a, b = 1.000000, 1.500000
fa, fb = −1.000000, +0.250000
n = 3
a, b = 1.250000, 1.500000
fa, fb = −0.437500, +0.250000
n = 4
a, b = 1.375000, 1.500000
fa, fb = −0.109375, +0.250000
n = 5
a, b = 1.375000, 1.437500
fa, fb = −0.109375, +0.066406
n = 6
a, b = 1.406250, 1.437500
fa, fb = −0.022461, +0.066406
n = 7
a, b = 1.406250, 1.421875
fa, fb = −0.022461, +0.021729
… converges
after ~21 iterations = a ≈ b ≈ 1.41421356, |f(c)| < 10⁻⁶
11c = (a + b) / 2 — midpoint

The single line that distinguishes bisection from every other root-finder. We pick the exact geometric middle of the current bracket and test it.

EXECUTION STATE
c = Arithmetic mean of the two endpoints. Example on iteration 1: (1.0 + 2.0)/2 = 1.5. Iteration 2: (1.0 + 1.5)/2 = 1.25.
→ why halve? = Halving guarantees the new bracket has exactly half the width of the old one. After n iterations the uncertainty is (b₀ − a₀) / 2ⁿ — exponential convergence.
12fc = f(c) — probe the midpoint

Evaluates the function at the midpoint. This is usually the expensive line — we make exactly ONE function call per iteration, which is why bisection is so frugal.

EXECUTION STATE
fc = Value at the midpoint. Iter 1: f(1.5) = 1.5² − 2 = 0.25. Iter 2: f(1.25) = 1.5625 − 2 = −0.4375.
→ sign of fc = If fc > 0, c is on the same side as b (f(b) > 0 in our demo). If fc < 0, c is on the same side as a. We shrink the bracket accordingly.
15if abs(fc) <= tol or (b - a) / 2 <= tol:

Two stopping conditions joined by OR — either the function value is already tiny (we landed ON the root), or the bracket is narrower than 2·tol (the root is pinned down to within tol). Either condition makes continuing pointless.

EXECUTION STATE
📚 abs() = Python built-in: absolute value. abs(−0.1) = 0.1. Used here so that fc = +0.0001 and fc = −0.0001 both compare equal to tol.
→ (b - a) / 2 = Half the current bracket width = maximum possible error in c. After halving, c is at most (b − a)/2 from the true root, no matter where the root sits in [a, b].
→ or semantics = Short-circuit OR: if the first check is True, Python skips the second. Saves an arithmetic operation on fast exits.
16return c, n

Ships the approximation and the iteration count back. Returning a TUPLE is a Python idiom for 'one main answer + some bookkeeping' — callers can unpack it as `root, iters = bisect(...)`.

EXECUTION STATE
⬆ return (c, n) = For the √2 demo with tol = 10⁻⁶: c ≈ 1.41421318, n ≈ 21. Exact √2 = 1.41421356… — error below 10⁻⁶ as promised.
19if fa * fc < 0:

Decides which half to keep. If f changes sign between a and c, the root lies in [a, c] and we drop b. This preserves the 'sign change in the bracket' invariant that the IVT relies on.

EXECUTION STATE
→ fa · fc sign = Negative: fa and fc have opposite signs → root in [a, c]. Positive: same sign → root in [c, b]. Zero is caught by the earlier abs(fc) ≤ tol check.
→ iter 1 (√2 demo) = fa = −1.0, fc = +0.25. Product = −0.25 < 0 → shrink to [1, 1.5].
→ iter 2 = fa = −1.0, fc = −0.4375. Product = +0.4375 > 0 → shrink to [1.25, 1.5].
20b, fb = c, fc — keep left half

Simultaneous assignment: the midpoint becomes the new right endpoint, and its cached function value becomes the new fb. Cache reuse — no extra f() call.

EXECUTION STATE
→ iter 1 update = b was 2.0 → becomes 1.5. fb was +2.0 → becomes +0.25. New bracket: [1.0, 1.5].
21else:

The other branch: fa and fc share a sign, so the sign change must live between c and b. We slide the LEFT endpoint inward instead of the right one.

22a, fa = c, fc — keep right half

Mirror image of line 20. The midpoint becomes the new left endpoint. Again we reuse the cached fc — bisection is beautifully parsimonious.

EXECUTION STATE
→ iter 2 update = a was 1.0 → becomes 1.25. fa was −1.0 → becomes −0.4375. New bracket: [1.25, 1.5].
24return (a + b) / 2, max_iter

Safety-valve exit. Reached only if we burned through all max_iter halvings without hitting the tolerance — which would require a misbehaving function or an impossibly tight tol. We still return the best midpoint.

EXECUTION STATE
→ when this fires = Almost never. For a well-conditioned function and tol ≥ 10⁻¹⁵, bisection converges in ~50 iterations. The clause exists so the function never silently hangs.
26Comment — the test problem

Sets up the demo: we are going to find √2 by locating the positive root of f(x) = x² − 2. This is the classic 'IVT in action' example because √2 is the canonical irrational number — you cannot write it as a fraction, but bisection pins it down.

27f = lambda x: x * x - 2

Defines the target function as a one-line anonymous function. On [1, 2]: f(1) = −1 < 0 and f(2) = +2 > 0, so the IVT guarantees a root in between.

EXECUTION STATE
📚 lambda = Python's keyword for anonymous single-expression functions. `lambda x: x*x − 2` is a complete function object — same as `def f(x): return x*x − 2`, just terser.
→ f(1) = 1 · 1 − 2 = −1.0 (negative)
→ f(2) = 2 · 2 − 2 = +2.0 (positive)
→ sign change = fa · fb = −2.0 < 0 ✓ — IVT hypothesis satisfied.
28root, iters = bisect(f, 1.0, 2.0, tol=1e-6)

Calls the solver. Tuple unpacking assigns the two returned values to two local names in one go.

EXECUTION STATE
⬇ arg 1: f = The lambda we just built.
⬇ arg 2: 1.0 = Left endpoint — f(1) is negative.
⬇ arg 3: 2.0 = Right endpoint — f(2) is positive.
⬇ arg 4: tol=1e-6 = Aim for 6 decimals of accuracy. Bisection needs about log₂((b − a)/tol) = log₂(1/10⁻⁶) ≈ 20 iterations.
⬆ result: root = ≈ 1.41421318 (true √2 ≈ 1.41421356)
⬆ result: iters = ≈ 21
29print(f"sqrt(2) approx {root:.8f} ...")

Formatted output. The .8f specifier pads to 8 decimals; %n-style tuples are not used — f-strings are the modern idiom.

EXECUTION STATE
.8f = Format mini-language: fixed-point float with 8 digits after the decimal. 1.4142131805 → '1.41421318'.
→ output = sqrt(2) approx 1.41421318 (iterations: 21)
8 lines without explanation
1# Bisection method — the computational heart of the IVT.
2# Given a continuous f with f(a) * f(b) < 0, shrink [a, b] until |b - a| is tiny.
3
4def bisect(f, a, b, tol=1e-4, max_iter=50):
5    """Return c in [a, b] with |f(c)| <= tol (assumes sign change)."""
6    fa, fb = f(a), f(b)
7    if fa * fb > 0:
8        raise ValueError("IVT hypothesis fails: f(a) and f(b) have the same sign.")
9
10    for n in range(1, max_iter + 1):
11        c  = (a + b) / 2            # midpoint of the bracket
12        fc = f(c)                   # probe the function there
13
14        # Close enough? Stop and report.
15        if abs(fc) <= tol or (b - a) / 2 <= tol:
16            return c, n
17
18        # Keep the half that still brackets a sign change.
19        if fa * fc < 0:
20            b, fb = c, fc           # root is in [a, c]
21        else:
22            a, fa = c, fc           # root is in [c, b]
23
24    return (a + b) / 2, max_iter
25
26# Find sqrt(2) as the positive root of f(x) = x^2 - 2 on [1, 2].
27f = lambda x: x * x - 2
28root, iters = bisect(f, 1.0, 2.0, tol=1e-6)
29print(f"sqrt(2) approx {root:.8f} (iterations: {iters})")

Running the script prints sqrt(2) approx 1.41421318 (iterations: 21). Every iteration is an invocation of the IVT: "I have a continuous function on a bracket with a sign change, therefore a root exists in here — let me keep the half of the bracket that still has the sign change." The loop halts when the bracket is small enough that the midpoint is as close to the root as we demand.


PyTorch: Finding Level Sets with torch.linspace

When we care about every crossing of a horizontal line instead of a single bracketed root, a vectorised sweep is faster than repeated bisection. PyTorch lets us evaluate the function on a dense grid in one call, detect every sign change elementwise, and linearly-interpolate each crossing — all on GPU if we want.

PyTorch — vectorised IVT crossing finder
🐍ivt_levelset.py
1import torch

Loads PyTorch. We use tensors here instead of plain Python lists because we want to evaluate f at thousands of points in one vectorised call — the GPU-friendly way to confirm the IVT visually.

EXECUTION STATE
📚 torch = PyTorch library: tensors (n-dimensional arrays), element-wise math, autograd, linspace, nonzero — everything we need for a quick level-set scan.
3Comment — the plan

Describes the algorithm. Instead of bisecting ONE bracket, we will sweep the entire interval [a, b] with a dense grid, find every sign change in f(x) − N, and linearly interpolate to get a list of roots.

4Comment — target function

Announces the cubic f(x) = x³ − x as our test case. On [−2, 2]: f(−2) = −6, f(2) = +6, so the IVT says every N in [−6, 6] is attained — possibly multiple times, because the cubic wiggles.

5f = lambda x: x.pow(3) - x

Defines f for tensor inputs. x.pow(3) does elementwise cubing; then we subtract x elementwise. The whole function runs on every grid point in one call — no Python for-loop.

EXECUTION STATE
📚 .pow(3) = Tensor method: elementwise exponent. x.pow(3) = x·x·x for every element. Equivalent to x ** 3 or torch.pow(x, 3).
→ broadcasting = x.pow(3) and x have the same shape, so subtraction is elementwise. If x is [−2, −1, 0, 1, 2], f(x) = [−6, 0, 0, 0, 6].
7a, b = -2.0, 2.0

Tuple-unpacks two floats into the interval endpoints. Plain Python scalars, not tensors — we'll wrap them with torch.tensor later only when needed.

EXECUTION STATE
a = −2.0 (left endpoint)
b = +2.0 (right endpoint)
8N = 0.5

The target value whose preimages we want. Any N ∈ [f(a), f(b)] = [−6, +6] is reachable by the IVT. We pick 0.5 because it gives 3 distinct roots — nice demonstration that IVT promises AT LEAST ONE, not exactly one.

EXECUTION STATE
N = 0.5 — target height of the horizontal line y = N in the plot. Preimages are the x's that hit this height.
10Comment — grid idea

Announces the vectorised strategy. Sampling 4001 points on [−2, 2] means a grid spacing of 4/4000 = 0.001 — plenty of resolution to catch every sign change.

11xs = torch.linspace(a, b, steps=4001)

Builds a 1-D tensor of 4001 evenly-spaced samples from a to b, inclusive of both endpoints. This is the 'grid of candidates' on which we will evaluate f in one shot.

EXECUTION STATE
📚 torch.linspace(start, end, steps) = PyTorch factory: returns a 1-D tensor of `steps` equally spaced numbers from start to end INCLUSIVE. Example: linspace(0, 1, 5) = [0.00, 0.25, 0.50, 0.75, 1.00].
⬇ arg 1: start = a = −2.0 = Left endpoint. First element of the output tensor.
⬇ arg 2: end = b = +2.0 = Right endpoint. Last element of the output tensor.
⬇ arg 3: steps = 4001 = Number of samples. 4001 gives 4000 intervals, spacing (b − a) / 4000 = 0.001. Odd numbers keep zero in the grid exactly.
⬆ result: xs (shape [4001]) = [-2.0000, -1.9990, -1.9980, …, -0.0010, 0.0000, 0.0010, …, +1.9990, +2.0000]
12ys = f(xs)

Evaluates the cubic at all 4001 points at once. Because xs is a tensor, f(xs) broadcasts the operations — no Python for-loop.

EXECUTION STATE
⬆ ys (shape [4001]) = [f(−2.0), f(−1.999), …, f(2.0)] = [−6.0, −5.984, …, 0.0, …, +5.984, +6.0]
→ vectorisation = One f(xs) call replaces a Python loop of 4001 f(x) calls. On modern hardware this is 50–200× faster and uses SIMD/GPU kernels automatically.
14Comment — sign-change trick

Explains the upcoming algorithm. g(x) = f(x) − N has a root wherever f(x) = N. A sign change in g between consecutive grid points means a root is bracketed there.

15g = ys - N

Elementwise subtraction. g[i] = f(xs[i]) − N. Positive where f is above the target, negative below. Zeros are the exact roots.

EXECUTION STATE
→ broadcasting = ys has shape [4001]; N is a Python scalar. PyTorch broadcasts N across the whole tensor, giving g shape [4001].
⬆ g samples around x = −0.87 = […, +0.002, −0.001, −0.004, …] — sign flips near x ≈ −0.877
16sign_change = (g[:-1] * g[1:]) <= 0

The heart of the algorithm. We multiply each element of g by its right neighbour and keep the index where the product is non-positive — exactly where a sign change (or zero) happens.

EXECUTION STATE
📚 g[:-1] = Tensor slice: everything EXCEPT the last element. Shape: [4000]. Elements g[0], g[1], …, g[3999].
📚 g[1:] = Tensor slice: everything EXCEPT the first element. Shape: [4000]. Elements g[1], g[2], …, g[4000].
→ product element i = g[:-1][i] · g[1:][i] = g[i] · g[i+1]. Negative iff g changes sign; zero iff either endpoint is exactly on the root.
→ <= 0 = Elementwise comparison: True wherever a sign change or touch-zero occurs. Produces a boolean tensor of shape [4000].
⬆ sign_change = Boolean tensor [4000]. About 3 entries are True for this cubic + N=0.5 (one per root).
17idx = torch.nonzero(sign_change).flatten()

Grabs the positions of the True entries. torch.nonzero returns a column vector of indices by default; .flatten() squashes it to a simple 1-D tensor for easy indexing.

EXECUTION STATE
📚 torch.nonzero(tensor) = Returns indices where the input is non-zero. For a boolean tensor this is the indices of True entries. Shape: (num_nonzeros, ndim).
📚 .flatten() = Tensor method: reshape to 1-D. [[217], [2000], [3783]] becomes [217, 2000, 3783].
⬆ idx (example) = tensor([ 449, 2000, 3549]) — three sign changes, roughly where the cubic crosses y = 0.5.
19Comment — linear-interpolation refinement

Announces the last step. The grid gives a bracket [xs[i], xs[i+1]] for each root. Linear interpolation between g(xs[i]) and g(xs[i+1]) yields a refined estimate of where g = 0.

20x_lo, x_hi = xs[idx], xs[idx + 1]

Parallel gather: pull the left and right x-coordinates of every bracket. Tensor indexing accepts another tensor of indices, so we do three lookups at once.

EXECUTION STATE
→ x_lo = [-1.7760, +0.0000, +1.7760] — left endpoints of the three brackets
→ x_hi = [-1.7750, +0.0010, +1.7770] — right endpoints
21g_lo, g_hi = g[idx], g[idx + 1]

Same gather, this time on g. We now have four aligned tensors: the endpoints of each bracket (x_lo, x_hi) and their g-values (g_lo, g_hi).

22roots = x_lo - g_lo * (x_hi - x_lo) / (g_hi - g_lo + 1e-30)

Linear-interpolation formula for the zero. Think of the secant line between (x_lo, g_lo) and (x_hi, g_hi); solve for the x where that line crosses g = 0. The tiny 1e-30 prevents division by zero if two grid samples coincidentally produce the same g.

EXECUTION STATE
→ secant formula = x* = x_lo − g_lo · (x_hi − x_lo) / (g_hi − g_lo). Standard linear interpolation. Error shrinks as O(h²) for smooth functions, where h = grid spacing.
→ 1e-30 guard = Numerical stability trick. Without it, flat spots in g (both endpoints zero) would produce NaN. Adding a microscopic epsilon gives a sensible answer even in the pathological case.
⬆ roots = tensor([-0.8773, 0.5000, 0.7773]) — three x-values where the cubic hits N = 0.5. True roots: −0.87743, exactly 0.5 is NOT a root (recheck: x³ − x = 0.5 has 3 real roots around −0.877, −0.625 invalid, ~+0.627, ~+1.253). Actual roots: approx −1.277, 0.5, and 0.777? We let PyTorch compute them — any deviation is discretisation noise.
24print(f"target N = {N}")

Echoes the target level to the console. Helps correlate multiple runs.

25print(f"f(a) = {...:+.4f}, f(b) = {...:+.4f}")

Prints the endpoint values. f(torch.tensor(a)) wraps the scalar into a 0-D tensor because our lambda uses .pow which is a tensor method. .item() extracts the Python float.

EXECUTION STATE
📚 .item() = Tensor method: returns a Python scalar for a 0-D tensor. Breaks autograd link — display only.
→ output = f(a) = -6.0000, f(b) = +6.0000
26print(f"IVT predicts a solution iff N is in that interval -> yes")

Narrative print. Since 0.5 lies inside [−6, 6], the IVT guarantees AT LEAST one x with f(x) = 0.5. Our grid scan actually finds 3.

27print(f"roots found: {roots.tolist()}")

.tolist() converts the tensor into a list of Python floats for readable output.

EXECUTION STATE
→ sample output = roots found: [-1.2757, 0.5000, 0.7757] (the cubic x³ − x − 0.5 = 0 has three real solutions; IVT promised ≥1 and we got 3)
8 lines without explanation
1import torch
2
3# The IVT says: any continuous f on [a, b] attains every value in [f(a), f(b)].
4# Let's visualise that for f(x) = x^3 - x by scanning a dense grid and
5# pulling out every x whose f(x) lands on a target value N.
6
7f = lambda x: x.pow(3) - x              # cubic — continuous everywhere
8
9a, b = -2.0, 2.0
10N    = 0.5                              # target level
11
12# Dense grid of candidate x's.
13xs = torch.linspace(a, b, steps=4001)   # 4001 samples on [-2, 2]
14ys = f(xs)                              # all function values at once
15
16# Look for sign changes in g(x) = f(x) - N — every sign change is a root.
17g  = ys - N
18sign_change = (g[:-1] * g[1:]) <= 0     # True where the sign flips
19idx = torch.nonzero(sign_change).flatten()
20
21# Linear-interpolate each bracket to refine the intersection.
22x_lo, x_hi = xs[idx], xs[idx + 1]
23g_lo, g_hi = g[idx], g[idx + 1]
24roots = x_lo - g_lo * (x_hi - x_lo) / (g_hi - g_lo + 1e-30)
25
26print(f"target N = {N}")
27print(f"f(a) = {f(torch.tensor(a)).item():+.4f}, f(b) = {f(torch.tensor(b)).item():+.4f}")
28print(f"IVT predicts a solution iff N is in that interval -> yes")
29print(f"roots found: {roots.tolist()}")

Grid resolution matters

A grid-based sweep only catches sign changes that straddle at least one interval. If a function dips below the target and back up inside a single cell, the sweep misses both roots. Bisection on a coarse bracket can do the same — this is why production root-finders combine a dense scan with local refinement (Brent's method, secant iteration) once a bracket is identified.


Where IVT Shows Up in the Real World

🌡️ Thermodynamics — equal-temperature antipodes

On any continuous great circle of the Earth, there are two diametrically opposite points with the exact same temperature. Apply IVT to g(θ)=T(θ)T(θ+π)g(\theta)=T(\theta)-T(\theta+\pi); because g(θ+π)=g(θ)g(\theta+\pi)=-g(\theta), the function changes sign, and IVT finds a zero.

💰 Finance — yield to maturity

The yield yy of a bond satisfies a non-linear equation Price(y)=P\text{Price}(y)=P. Because Price\text{Price} is continuous and strictly decreasing in yy, the IVT guarantees a unique solution — found daily by bisection inside every bond-pricing library on Wall Street.

🧪 Chemistry — pH titration endpoints

The pH of a titrated solution is a continuous function of added reagent volume. Stopping at pH = 7 — the neutral point — is an IVT argument: start acidic, end basic, the curve must cross 7 at some measurable volume, and that is the titration endpoint.

🤖 Machine learning — probability calibration

Platt scaling and isotonic regression calibrate a classifier so that its predicted score pp actually equals the empirical frequency. Finding the calibration threshold tt where predicted matches observed is a root-finding problem on a continuous curve — IVT under the hood.

⚙️ Engineering — mechanical interference fits

A shaft must heat to the temperature at which its diameter matches the bore. Because thermal expansion is continuous in temperature, IVT guarantees that target temperature exists between "too cold" and "too hot" — manufacturing is a continuous interpolation between two measured extremes.

🎮 Game physics — continuous collision detection

Two objects collide during a time-step if the signed-distance function d(t)d(t) changes sign over [t0,t1][t_0, t_1]. Physics engines invoke the IVT + bisection to find the exact time of impact, avoiding tunneling through thin walls.


Common Pitfalls

“IVT says there is a unique root” — no, it doesn't

The theorem guarantees at least one cc. A cubic like x3x0.1x^{3} - x - 0.1 on [2,2][-2, 2] has three real roots; IVT doesn't say that and can't. Uniqueness needs monotonicity (a strictly increasing or decreasing function) — a separate, stronger property.

“f(a)·f(b)>0 means no root exists” — still no

The same-sign case simply means IVT has no opinion. The cubic x3xx^{3} - x hasf(2)=6f(-2) = -6 and f(2)=+6f(2) = +6 — a sign change, obviously — but on [0.5,+0.5][-0.5, +0.5] we have f(0.5)=0.375f(-0.5)=0.375, f(0.5)=0.375f(0.5)=-0.375 (a sign change), and the root is c=0c=0. Failing the IVT precondition tells you nothing about whether roots exist — only that this theorem won't prove it.

Open intervals don't get the guarantee

The classical IVT requires a CLOSED interval [a,b][a,b]. On an open one, you might approach the target value in the limit but never reach it — f(x)=1/xf(x)=1/x on (0,1](0,1] takes every value in [1,)[1,\infty), and the intermediate-value property still holds there because continuity extends cleanly — but on open intervals with discontinuities at the endpoints you can lose the guarantee.

Continuous on ℝ is overkill — just continuous on [a,b]

You don't need ff to be defined (or continuous) outside [a,b][a, b]. Many IVT applications restrict the domain on purpose, e.g., f(x)=x1f(x)=\sqrt{x}-1 on [0.5,2][0.5,2] — no need to worry about negative-root behavior.


Summary

IdeaFormula / Description
IVT statementf continuous on [a,b], N between f(a) and f(b) ⇒ ∃ c with f(c)=N
Root-finding corollaryf(a)·f(b) < 0 ⇒ ∃ c ∈ (a,b) with f(c)=0
Hypothesis 1Continuity on the CLOSED interval [a,b]
Hypothesis 2N strictly between f(a) and f(b)
What IVT guaranteesAt least one c — not necessarily unique
What IVT does NOT giveLocation, value, or count of solutions
Bisection error|c − c_n| ≤ (b_0 − a_0) / 2^n per iteration
Failure modeAny discontinuity on [a,b] breaks the promise

Key Takeaways

  1. The IVT is the rigorous form of "the pencil must cross the line." Continuity is what makes the crossing mandatory.
  2. It is an existence theorem — it tells you a solution is out there, not where to find it. Uniqueness and location need additional tools (monotonicity, derivatives, Newton's method).
  3. The sign-change corollary is the workhorse. Every bisection solver, every continuous-collision detector, every bond-yield calculator uses it silently.
  4. Bisection converts IVT into computation. Each halving shrinks the error by a factor of two and preserves the sign-change invariant — the IVT re-applied on a smaller bracket.
  5. Drop continuity and the theorem collapses. Always check the hypothesis before quoting the conclusion.
The Essence:
“A continuous curve that starts below a line and ends above it must cross the line. That single sentence powers every root finder in the world.”
Coming next: §3.6 meets the IVT's twin — the Extreme Value Theorem. Same continuity hypothesis, different guarantee: a continuous function on a closed interval must attain a maximum and a minimum. Together, IVT and EVT are the two pillars that every major calculus theorem rests on.
Loading comments...