5.4.18 · D4Scientific Computing (Python)

Exercises — Animation — FuncAnimation

2,230 words10 min readBack to topic

This page is a graded ladder of problems for Animation — FuncAnimation. Each problem states cleanly what to do; the full solution is hidden inside a collapsible callout so you can test yourself first. Work top to bottom — the levels build on each other.

Prerequisites you may lean on: Matplotlib Figure and Axes, Artists in Matplotlib, Generators in Python, Event Loops / GUI backends, NumPy linspace and broadcasting, Saving figures and ffmpeg.


Level 1 — Recognition

L1·Q1 — What gets called per frame?

In FuncAnimation(fig, update, frames=200, interval=20, blit=True), name the function that runs once per frame, and say how many times it runs in one pass.

Recall Solution

The update function update runs once per frame. With frames=200 it runs 200 times in one pass (frames numbered 0 to 199). Why: FuncAnimation owns the timer; every timer tick = one update(frame) call = one redraw.

L1·Q2 — interval → FPS

Convert interval=40 ms to frames per second.

Recall Solution

Why the division: each frame costs interval milliseconds, and there are 1000 ms in a second, so you fit 1000/40 frames into one second.

L1·Q3 — blit return type

With blit=True, what must init_func and update return?

Recall Solution

An iterable of the changed artists, e.g. return line, (a 1-tuple). Why: blitting only redraws exactly the artists you hand back; it must be told what changed.


Level 2 — Application

L2·Q1 — Total runtime

An animation has frames=150 and interval=20 ms, repeat=False. How many seconds does one pass take?

Recall Solution

Why: time-per-frame × number-of-frames = total wall-clock time for one pass.

L2·Q2 — Choose interval for a target FPS

You want exactly 30 FPS. What interval (ms, integer) do you pass?

Recall Solution

Invert the FPS formula: You must pass an integer, so use interval=33. That gives FPS — close, but not exact. (For exact 30 FPS when saving, pass fps=30 to save, which ignores interval.)

L2·Q3 — Fill in the canonical skeleton

Complete the blanks so a dot moves along :

x = np.linspace(0, 2*np.pi, 100)
dot, = ax.plot([], [], 'ro')
def update(i):
    dot.set_data(______, ______)   # frame i selects one point
    return ______
Recall Solution
def update(i):
    dot.set_data([x[i]], [np.sin(x[i])])   # lists, because set_data wants sequences
    return dot,

Why the brackets [x[i]]: set_data expects sequences (arrays/lists) for both x and y, even for a single point. A bare scalar x[i] can raise or misbehave. Why return dot,: the trailing comma makes a 1-tuple — the iterable blit needs.


Level 3 — Analysis

L3·Q1 — The vanishing animation

This runs, the window opens, but nothing moves and then the window is blank. Why? Fix it.

def make():
    fig, ax = plt.subplots()
    line, = ax.plot([], [])
    FuncAnimation(fig, update, frames=100, interval=30, blit=True)
    plt.show()
make()
Recall Solution

Cause: FuncAnimation(...) is not stored in a variable. When make() returns, Python has no live reference to the animation object, so it is garbage-collected; its timer dies and the frames stop. Fix: keep a reference alive:

anim = FuncAnimation(fig, update, frames=100, interval=30, blit=True)
plt.show()   # anim stays alive for the whole show()

Why a reference matters: the timer that drives frames lives inside the animation object. No object → no timer → no frames.

L3·Q2 — The growing memory leak

Blitting "sometimes redraws the whole plot" and memory climbs. Diagnose:

def update(frame):
    ax.plot(x, np.sin(x - 0.1*frame))   # draw this frame
    return ax.lines
Recall Solution

Cause: ax.plot adds a new Line2D artist every frame. Old lines never get removed → the axes accumulates hundreds of lines (memory grows), and blit can't tell what actually "changed." Fix: create the line once outside update, then only mutate it:

line, = ax.plot([], [])          # created ONCE
def update(frame):
    line.set_data(x, np.sin(x - 0.1*frame))   # mutate, don't add
    return line,

Why this is the whole point of blitting: cost drops from "redraw every artist ever added" to "redraw the one moving line."

L3·Q3 — Frozen second artist

A moving line + a trailing dot: the line animates, but the dot appears frozen. Why?

def update(i):
    line.set_data(x[:i], np.sin(x[:i]))
    dot.set_data([x[i]], [np.sin(x[i])])
    return line,          # <-- look here
Recall Solution

Cause: with blit=True, matplotlib redraws exactly what you return. You returned only line,, so the dot is never re-blitted — it looks stuck. Fix: return all changed artists:

return line, dot

Why: blit's contract is "I will repaint precisely this iterable." Forget an artist and it will not update on screen.


Level 4 — Synthesis

L4·Q1 — Generator-driven, save-correct animation

Write a FuncAnimation where each frame's phase comes from a generator stepping from to just under in steps of , and which will save correctly to mp4. State the exact save_count needed and why.

Recall Solution
def frame_gen():
    t = 0.0
    while t < 6.28:
        yield t
        t += 0.04
 
def update(t):
    line.set_data(x, np.sin(x - t))
    return line,
 
anim = FuncAnimation(fig, update, frames=frame_gen,
                     init_func=init, interval=20, blit=True,
                     save_count=157)
anim.save("wave.mp4", fps=50)

How many frames? Count the values that stay below . The number of steps is Since is not strictly less than , the last emitted value is at step (value ), giving indices = 157 frames. Why save_count: a generator has unknown length; when saving, matplotlib must know how many frames to pull. save_count caps it — set it to the true frame count so nothing is dropped or padded.

L4·Q2 — Match on-screen speed to saved speed

On screen you use interval=25 ms. You then save with anim.save("out.mp4", fps=?). What fps makes the saved video play at the same speed as the live preview? What if you save with fps=50 instead?

Recall Solution

Live FPS is . To match speed, save with ==fps=40==. If you save with fps=50 instead: the same number of frames now plays per second instead of , so the video is faster (shorter duration). Why they can differ: interval only controls the live GUI timer; save(fps=...) re-spaces the same frames at seconds each, ignoring interval entirely.


Level 5 — Mastery

L5·Q1 — Duration under repeat and save

A generator yields 90 frames. Live preview uses interval=40 ms with repeat=True. You then save with anim.save("m.mp4", fps=30). (a) How long is one live pass (seconds)? (b) Does repeat=True change the saved file's length? Explain. (c) How long is the saved video (seconds)?

Recall Solution

(a) One live pass: (b) No. repeat only loops the live preview. Saving captures each frame once (up to save_count), so the file contains one pass regardless of repeat. (c) Saved video: Why (a) ≠ (c): live spacing is ms/frame (⇒ 25 FPS effective), but the file is spaced at s/frame. Same 90 frames, different playback rate ⇒ different duration.

L5·Q2 — When blit hurts, and the init contract

(a) Give one concrete situation where blit=True actually fails to update something visible even though update is correct for the moving line. (b) Why does init_func exist at all — what breaks if you omit it while using blit?

Recall Solution

(a) If part of the static scene changes (e.g. you update the axes title or tick labels or the axis limits inside update), blit won't repaint it: blit caches the background bitmap once and only re-blits the returned artists. The title/ticks live in the cached background, so they appear frozen or leave smears. (Fix: don't animate background elements under blit, or return them too / disable blit.) (b) init_func draws the clean "frame-before-0" state. Under blit, this is the frame matplotlib captures as the reusable background bitmap. Omit it and the first cached background may include leftover/empty artist state, causing ghosting or a blank first frame. It also guarantees a well-defined starting picture for repeat. Why this is the deep rule: blit's speed comes from freezing the background. Anything you want to move must (i) be a returned artist and (ii) live above that frozen background.

L5·Q3 — Exact frame count from linspace indexing

A trail uses x = np.linspace(0, 1, 51) and update(i) plots x[:i]. frames=range(0, 51). On the last frame, how many points are in the trail, and what is the largest x-value drawn?

Recall Solution

Last frame is i = 50 (since range(0,51) = 0..50). Then x[:50] takes indices 0..4950 points, largest x = x[49]. With 51 points evenly on : spacing , so Why not : x[:i] with i=50 is a half-open slice — it excludes index 50. The final point never appears unless you also allow i=51 (or slice x[:i+1]).


Recall wrap-up

Connections

  • Animation — FuncAnimation — the parent topic this drills.
  • Artists in Matplotlibset_data, why one artist is created once.
  • Generators in Python — the frame_gen in L4/L5.
  • Event Loops / GUI backends — why the reference/timer must stay alive.
  • NumPy linspace and broadcasting — the linspace indexing in L5·Q3.
  • Saving figures and ffmpegsave(fps=...) vs live interval.
  • Matplotlib Figure and Axes — the persistent canvas being updated.