What a derivative measures¶
A secant becomes a tangent as two points on a curve come together. The tangent's changing slope traces the derivative's graph.
examples/derivative.py
"""What a derivative is: the slope of a curve at one point.
Nudge x by dx and f changes by df; the ratio df/dx is the slope of the secant through the two
points, rise over run. Let dx shrink toward 0 and the secant turns into the tangent, whose slope
is the derivative: f′(x) = lim (f(x + dx) − f(x))/dx as dx → 0. Slide x along the curve and the
tangent's slope is positive where the curve rises, 0 where it is flat and negative where it
falls. Plotted against x, the slopes draw a curve of their own: the graph of f′.
"""
from collections.abc import Callable
from itertools import pairwise
import numpy as np
import manimgx as m
WALL = 17.6 # the tangent slides and f′ is traced
X_MIN, X_MAX = 0.3, 9.3 # the curve's stretch of x; the axes run from 0 to 9.6
Y_MAX = 2.1 # f's axes run from −Y_MAX to Y_MAX
X0 = 3.6 # where the secant becomes the tangent
DX0, DX1 = 1.6, 0.01 # the nudge, from wide to tiny
FLAT = 0.15 # slopes this close to 0 fade to white: flat
DF_COLOR = m.PURPLE_B # df, the rise; dx, the run, is yellow
SHIFT = 2.0 # how far f's axes move left to make room for f′'s readout
def f(x: float) -> float:
"""A wave on a slope: rising, flat and falling stretches."""
return float(np.sin(x - 4.8) + (x - 4.8) / 3)
def quotient(x: float, dx: float) -> float:
"""The secant's slope: (f(x + dx) − f(x)) / dx."""
return (f(x + dx) - f(x)) / dx
def slope(x: float) -> float:
"""The tangent's slope, the derivative, by a central difference."""
eps = 1e-6
return (f(x + eps) - f(x - eps)) / (2 * eps)
def sign_color(s: float) -> m.ManimColor:
"""Green for a rising slope, red for a falling one, white where it is flat."""
return m.interpolate_color(
m.WHITE, m.GREEN if s > 0 else m.RED, min(abs(s) / FLAT, 1.0)
)
def zeros(func: Callable[[float], float], lo: float, hi: float) -> list[float]:
"""Where `func` changes sign between lo and hi, by bisection on a fine grid."""
xs = np.linspace(lo, hi, 400)
ys = [func(float(x)) for x in xs]
found = []
for a, b, ya, yb in zip(xs[:-1], xs[1:], ys[:-1], ys[1:], strict=True):
if ya * yb < 0:
a, b = float(a), float(b)
for _ in range(40):
mid = (a + b) / 2
if func(a) * func(mid) <= 0:
b = mid
else:
a = mid
found.append((a + b) / 2)
return found
def clip(
p: np.ndarray, q: np.ndarray, low: np.ndarray, high: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""The part of the segment pq inside the box from `low` to `high` (Liang–Barsky)."""
t0, t1 = 0.0, 1.0
d = q - p
for k in range(2):
if abs(d[k]) < 1e-12:
continue
a, b = (low[k] - p[k]) / d[k], (high[k] - p[k]) / d[k]
t0, t1 = max(t0, min(a, b)), min(t1, max(a, b))
return p + t0 * d, p + max(t1, t0) * d
def axes(y_range: list[float], y_length: float) -> m.Axes:
return m.Axes(
x_range=[0, X_MAX + 0.3, 1],
y_range=y_range,
x_length=9.4,
y_length=y_length,
tips=False,
axis_config={"stroke_width": 3, "color": m.GREY_B},
)
def colored_quotient(tex: str) -> m.MathTex:
"""A formula with the rise f(x + dx) − f(x) in df's color and the run dx in yellow."""
formula = m.MathTex(tex, font_size=60)
for part in formula.get_parts_by_tex("dx", substring=False):
part.set_color(m.YELLOW)
formula.get_parts_by_tex("f(x+dx) - f(x)").set_color(DF_COLOR)
return formula
class DerivativeSlope(m.Scene):
def construct(self) -> None:
top = axes([-Y_MAX, Y_MAX, 1], 3.4).move_to(1.9 * m.UP)
curve = top.plot(f, x_range=[X_MIN, X_MAX], color=m.BLUE, stroke_width=7)
f_label = m.MathTex("f(x)", color=m.BLUE, font_size=52)
f_label.next_to(curve.get_end(), m.RIGHT, buff=0.2)
graph = m.VGroup(top, curve, f_label)
x = m.ValueTracker(X0)
log_dx = m.ValueTracker(float(np.log(DX0)))
def dx() -> float:
return float(np.exp(log_dx.get_value()))
def point(t: float) -> np.ndarray:
return top.c2p(t, f(t))
def secant() -> m.Line:
s = quotient(x.get_value(), dx())
p, q = point(x.get_value()), point(x.get_value() + dx())
direction = top.c2p(1, s) - top.c2p(0, 0)
direction = direction / np.linalg.norm(direction)
mid = (p + q) / 2
half = np.linalg.norm(q - p) / 2 + 1.5
start, end = clip(
mid - half * direction,
mid + half * direction,
top.c2p(0, -Y_MAX),
top.c2p(X_MAX, Y_MAX), # not past the curve's end, where its label is
)
return m.Line(start, end, color=sign_color(s), stroke_width=7)
def legs() -> m.VGroup:
x0, h = x.get_value(), dx()
p, q = point(x0), point(x0 + h)
corner = top.c2p(x0 + h, f(x0))
run = m.Line(p, corner, color=m.YELLOW, stroke_width=5)
rise = m.Line(corner, q, color=DF_COLOR, stroke_width=5)
shown = float(np.clip((h - 0.3) / 0.5, 0, 1))
run_label = m.MathTex("dx", color=m.YELLOW, font_size=44)
run_label.next_to(run, m.DOWN, buff=0.12).set_opacity(shown)
rise_label = m.MathTex("df", color=DF_COLOR, font_size=44)
rise_label.next_to(rise, m.RIGHT, buff=0.12).set_opacity(shown)
return m.VGroup(run, rise, run_label, rise_label)
def far_dot() -> m.Dot:
return m.Dot(point(x.get_value() + dx()), radius=0.08, color=m.WHITE)
def near_dot() -> m.Dot:
return m.Dot(point(x.get_value()), radius=0.09, color=m.WHITE)
line = m.always_redraw(secant)
triangle = m.always_redraw(legs)
q_dot = m.always_redraw(far_dot)
p_dot = m.always_redraw(near_dot)
# the quotient, and its value: the secant's slope, then the tangent's
quotient_tex = colored_quotient(
r"{{ \frac{df}{dx} }} {{=}} \frac{ {{f(x+dx) - f(x)}} }{ {{dx}} } {{=}}"
)
ratio = quotient_tex[0] # its glyphs: d, f, d, x, the bar
m.VGroup(*ratio[0:2]).set_color(DF_COLOR)
m.VGroup(*ratio[2:4]).set_color(m.YELLOW)
quotient_tex.move_to(1.9 * m.DOWN + 0.8 * m.LEFT)
number = m.DecimalNumber(0, num_decimal_places=2, font_size=60)
tangent_phase = [False]
def show_value(mob: m.Mobject) -> None:
x0 = x.get_value()
s = slope(x0) if tangent_phase[0] else quotient(x0, dx())
number.set_value(s)
number.set_color(sign_color(s))
show_value(number)
number.next_to(quotient_tex, m.RIGHT, buff=0.25)
number.add_updater(show_value)
dx_label = m.MathTex("dx =", color=m.YELLOW, font_size=44)
dx_label.move_to(3.3 * m.DOWN + 0.6 * m.LEFT)
dx_number = m.DecimalNumber(
DX0, num_decimal_places=3, font_size=44, color=m.YELLOW
)
dx_number.next_to(dx_label, m.RIGHT, buff=0.2)
dx_number.add_updater(lambda d: d.set_value(dx()))
dx_readout = m.VGroup(dx_label, dx_number)
# 0–3 s: the curve; two points on it, the secant through them, the run dx and
# the rise df
self.add(top)
self.play(m.Create(curve), m.FadeIn(f_label), run_time=1.6)
self.play(
m.FadeIn(p_dot),
m.FadeIn(q_dot),
m.Create(line),
m.FadeIn(triangle),
run_time=1.2,
)
self.play(
m.Write(quotient_tex), m.FadeIn(number), m.FadeIn(dx_readout), run_time=1.5
)
self.wait(0.4)
# 5–9 s: dx shrinks toward 0; the secant turns into the tangent
self.play(log_dx.animate.set_value(float(np.log(DX1))), run_time=4)
self.remove(triangle, q_dot)
limit_tex = colored_quotient(
r"{{f'(x)}} {{=}} \lim_{ {{dx}} \to 0}"
r" \frac{ {{f(x+dx) - f(x)}} }{ {{dx}} } {{=}}"
)
limit_tex.move_to(quotient_tex).align_to(quotient_tex, m.RIGHT)
self.play(
m.TransformMatchingTex(quotient_tex, limit_tex),
m.FadeOut(dx_readout),
run_time=2,
)
tangent_phase[0] = True
self.wait(0.4)
# 11–16 s: the limit is f′(x); room for its own graph, below
prime, equals = limit_tex[0], limit_tex[1]
rest = m.VGroup(*limit_tex[2:])
self.remove(limit_tex)
self.add(prime, equals, rest)
number.remove_updater(show_value) # held still while it moves
self.play(
m.FadeOut(rest),
number.animate.next_to(equals, m.RIGHT, buff=0.25),
run_time=1,
)
bottom = axes([-1.1, 1.6, 1], 3.0).move_to(np.array([-SHIFT, -2.2, 0]))
readout = m.VGroup(prime, equals, number)
self.play(
graph.animate.shift(SHIFT * m.LEFT),
readout.animate.scale(56 / 60).move_to(
bottom.get_right() + 0.45 * m.RIGHT, aligned_edge=m.LEFT
),
run_time=2,
)
number.add_updater(show_value)
lo_hi = [0.0, 0.0] # the stretch of x traced so far
def traced() -> m.VGroup:
x0 = x.get_value()
lo_hi[0], lo_hi[1] = min(lo_hi[0], x0), max(lo_hi[1], x0)
lo, hi = lo_hi
pieces = m.VGroup()
if hi - lo < 1e-3:
return pieces
cuts = [lo, *zeros(slope, lo, hi), hi]
for a, b in pairwise(cuts):
if b - a < 1e-4:
continue
pieces.add(
bottom.plot(
slope,
x_range=[a, b, (b - a) / max(2, int(40 * (b - a)))],
color=m.GREEN if slope((a + b) / 2) > 0 else m.RED,
stroke_width=7,
)
)
return pieces
def slope_dot() -> m.Dot:
s = slope(x.get_value())
return m.Dot(bottom.c2p(x.get_value(), s), radius=0.09, color=sign_color(s))
def drop() -> m.DashedLine:
s = slope(x.get_value())
return m.DashedLine(
point(x.get_value()),
bottom.c2p(x.get_value(), s),
color=m.GREY_B,
stroke_width=2.5,
dash_length=0.08,
)
self.play(m.Create(bottom), x.animate.set_value(X_MIN), run_time=1.8)
dot = m.always_redraw(slope_dot)
connector = m.always_redraw(drop)
self.play(m.FadeIn(dot), m.Create(connector), run_time=0.5)
lo_hi[0] = lo_hi[1] = x.get_value()
self.add(m.always_redraw(traced))
# 16–24 s: slide x along the curve; the tangent's slope, as a height, draws f′
self.play(x.animate.set_value(X_MAX), run_time=7.5)
hilltop = zeros(slope, X0, X_MAX)[0]
self.play(x.animate.set_value(hilltop), run_time=2)
self.wait(1.5)
if __name__ == "__main__":
DerivativeSlope().render("derivative.mp4")