matthewplotlib.animations
Animations: a sequence of plots, and the terminal session that shows them.
There are two things in this module and they are two halves of one loop.
tstackis an animation as a value: a sequence of plots with a frame rate. It has no terminal and no clock, and like any other plot expression it composes -- slice it, map a combinator over its frames, save it as a gif.animationbuilds one straight from an array with a time axis, asimagedoes from an array without one.animateis an animation as a session: a context manager that owns the terminal for the duration of awithblock. Inside it,anim.update(plot)writes one frame, the previous-frame bookkeeping ofprint(plot - prev)disappears, frames are paced, andanim.print(...)can log a line without corrupting the plot.
Each produces the other, so there is one code path rather than two:
# push a live animation into the terminal, keeping the frames
with mp.animate(fps=20, record=True) as anim:
while running:
anim.update(compute_frame())
anim.frames.savegif("out.gif")
# pull a finished animation back out of a value
mp.tstack(*frames).play(fps=20)
Note that neither is required. Animation is a loop of print(plot - prev) and
that stays fully supported; animate is there to own the parts of it that are
about terminals rather than about plots.
Temporally stack plots into an animation.
The third stacking operation, alongside hstack (+) and vstack (/):
where those lay plots out across the screen, this one lays them out in
time. The result is a value, not an action -- nothing is printed until you
play it or savegif it.
Inputs:
- *plots : plot | tstack.
The frames, in order. Any
tstackamong them is spliced in frame by frame, sotstack(a, b)concatenates two animations andtstack(a, still)appends a frame to one. - fps : optional float.
The rate this animation is meant to play at, used as the default by
both
playandsavegif. An upper bound rather than a promise: seeanimate. Defaults to the frame rate of the firsttstackamong the inputs, so concatenating and slicing preserve it, or to 12.0 if there is none. - durations : optional sequence of float.
How long each frame was on screen, in milliseconds, if this animation
was recorded from a live run. Supplied by
animate.frames, which is the only thing that can know it, and read back bysavegif(fps="achieved"). Must have one entry per frame.
Examples:
# build one frame at a time
a = mp.tstack(*[
mp.image(field(t)) for t in np.linspace(0, 1, 60)
], fps=30)
# 60 frames, 20 rows, 40 columns
len(a), a.height, a.width
# every frame, in a titled border
a = a.map(lambda p: mp.border(p, title=" diffusion "))
# the second half, backwards
a[len(a)//2:][::-1].play()
Notes:
- Frames do not have to be the same size going in: they are padded with blank space to the largest, aligned at the top left, so an animation never changes shape while it plays. Differential redraw makes that free -- the padding is blank in every frame, so no cell of it is ever sent twice.
Apply a function to every frame, giving a new animation.
This is how an animation composes: any of the combinators in
matthewplotlib.plots can be lifted over the time axis by passing it
through here, which is usually how a static furnishing gets wrapped
around a moving interior.
a.map(lambda p: mp.border(p, title=" gen 0 ")) # a border on each
a.map(lambda p: p + legend) # a panel beside each
The frame rate and recorded durations carry over unchanged, since neither depends on what the frames contain.
For combining two animations frame by frame there is no method: zip
them and rebuild, as in
mp.tstack(*[x + y for x, y in zip(a, b)]).
Print the frames to the terminal, in order, one frame at a time.
A thin wrapper around animate: this pushes the animation's own frames
through a session, so playing a finished animation and driving a live
one go down exactly the same path.
Inputs:
- fps : optional float.
Frame rate to play at, defaulting to the animation's own
fps. - loop : bool (default False). If true, start again from the first frame instead of returning, forever. Interrupt to stop.
- stop_on_interrupt : bool (default True).
Whether Ctrl-C ends playback quietly rather than raising
KeyboardInterrupt. Unlikeanimate, this defaults to true: there is no caller loop here whose control flow could be taken over, and interrupting playback is the only way to endloop=True.
Render the frames and save them as an animated gif.
Inputs:
- filename : str. Where to save the gif. Should usually include a '.gif' extension.
- fps : optional float or the string "achieved".
Frame rate to encode. By default the animation's own
fps, which for a recording is the rate that was asked for. Pass a number to override it, or "achieved" to use the durations actually measured while recording -- faithful right down to reproducing a frame that stalled, which is honest but rarely what a showcase gif wants. Requiresdurations, so only a recorded animation can use it. - upscale : int (>=1, default 1).
Represent each pixel with a square of side-length
upscalepixels. - downscale : int (>=1, default 1).
Keep every
downscaleth pixel. Does not need to evenly divide the image height or width (think slice(0, height or width, downscale)). Applied after upscaling. - bgcolor : optional ColorLike. Default background colour. If none, a transparent background is used.
- repeat : bool (default True). If true (default), the gif loops indefinitely. If false, the gif only plays once.
Notes:
- Frames of different sizes are aligned at the top left corner and
padded with transparent pixels on the bottom and right. For different
padding, compose the frames with
blankblocks first. - A gif stores its frame delays in hundredths of a second, so the rate in the file is quantised to 10ms steps: 12 fps asks for 83ms, gets 80ms, and plays at 12.5. Delays are clamped to 10ms, the shortest a gif can express, so above 100 fps the file stops getting faster -- and above roughly 50 fps the number is fiction anyway, since many viewers refuse delays under 20ms.
An animation from an array with a time axis: image, one dimension up.
Takes exactly what image takes with a leading frame index, so a field
evolving over time goes straight to the screen without a loop in sight.
Inputs:
- ims : float[t,h,w] | float[t,h,w,rgb] | int[t,h,w] | int[t,h,w,rgb].
The frames. As for
image, floats are clipped to the unit interval and integers to 0..255, and the second and third axes are the image's rows and columns -- each pair of rows becomes one row of half-block characters, so a2nrow image isncharacter rows tall. - colormap : optional ColorMap.
Applied to a scalar array to colour it, exactly as in
image. Applied to the whole tensor in one call, which is both faster than doing it frame by frame and the only way to be sure every frame is coloured on the same scale. - fps : optional float (default 12.0). The rate the animation is meant to play at.
Examples:
# a diffusing blob, straight from the array
mp.animation(field, colormap=mp.viridis, fps=30).play()
# a video, as uint8 RGB
mp.animation(video).savegif("video.gif")
A context manager that owns the terminal while an animation runs.
Inside the block, each call to update writes one frame:
with mp.animate(fps=20) as anim:
while running:
anim.update(mp.axes(...))
which is the print(plot - prev) loop with the parts that are about
terminals rather than about plots taken over: the prev sentinel and the
assignment that maintains it, the frame clock, and separating whatever comes
next on the screen from the plot on the way out.
Everything beyond that first job is opt-in, because a context manager takes over the caller's control flow and this library would rather be a library than a framework. The plain loop remains fully supported and is what the quickstart teaches.
Inputs:
- fps : optional float.
If given, cap the frame rate:
updatesleeps off whatever is left of the previous frame's budget before writing. An upper bound, not a guarantee -- a frame that takes longer than1/fpsto compute simply takes longer, and the clock picks up from there rather than trying to catch up. If omitted,updatereturns as soon as it has written. - record : bool (default False).
If true, keep every frame, readable afterwards as
anim.frames. Opt-in because holding an entire animation in memory is a real cost and awhile Trueloop would never stop paying it. - stop_on_interrupt : bool (default False).
If true, Ctrl-C ends the animation quietly: the
withblock exits and the statements after it still run, so a recording survives to be saved. Off by default because swallowingKeyboardInterruptis precisely the kind of control flow a library should not take without being asked.
Attributes, readable during the block and after it:
- frames : tstack.
The recorded animation, with the frame timings attached. Raises unless
record=Truewas passed. - achieved_fps : float | None. The frame rate actually managed so far, or None before the second frame. Worth printing after a run that felt sluggish: asking for 20 and getting 6 is not otherwise visible.
out : a writable text stream.
anim.printas a file, for the things that want somewhere to write rather than something to call:print("step", step, file=anim.out) logging.basicConfig(stream=anim.out)Line buffered, since the plot moves out of the way a line at a time. Redirecting
sys.stdoutto it for the whole block routes every print in the program, including ones inside libraries you did not write:with mp.animate(fps=20) as anim, contextlib.redirect_stdout(anim.out): ...which works because the session captured the real stdout on the way in.
Notes:
- A plot has to fit the terminal with a row to spare for the newline that
printappends, so at mostrows - 1of it will render. If the first frame does not fit, and stdout is a terminal that can be measured, this warns once. - The cursor is left visible. It sits in the plot and blinks there, which is a fair trade for never leaving a terminal with an invisible cursor.
Write one frame, and return the string that was written.
The first call renders the whole plot; each later call repaints only the
cells that differ from the frame before it, which is the same
plot.updatestr(prev) the - operator gives, with prev looked after
here.
If the session has an fps, this is also where it waits. Everything a
frame costs is spent inside its budget rather than on top of it: the time
the caller spent computing it, because the sleep happens before the
write, and the time spent working out the diff and pushing the bytes
down the wire, because the next frame is scheduled from the moment this
one's slot opened rather than from the moment its write finished. A flat
sleep(1/fps) gets both wrong, and runs slow by their sum.
The return value is the frame's exact bytes, which is what makes the
cost of differential rendering measurable from user code -- see
examples/life.py, which plots it.
Print a line above the animation, without corrupting it.
A bare print from inside an animated loop lands in the middle of the
plot, which leaves debugging an animated program a choice between not
printing and not animating. This routes the line instead: the plot is
erased, the message takes the row the plot's first row was on, and the
plot is redrawn one row lower.
with mp.animate(fps=20) as anim:
for step in range(1000):
anim.update(vis(params))
if step % 100 == 0:
anim.print(f"step {step}: loss {loss:.4f}")
So messages pile up above the plot and scroll off the top of the screen
in the usual way, with the plot pinned below them. Takes the same
arguments as the builtin print, except that end must stay a newline:
the redraw has to start at the beginning of a line.
Costs a full repaint, rather than the differential redraw a frame gets. Printing happens at human speed, so this is not worth optimising.
The recorded frames, as a tstack, with their measured durations.
The animation's fps is the rate that was requested, so
anim.frames.savegif(...) writes a gif at the intended speed by
default, and savegif(fps="achieved") writes one at the speed the run
actually managed.
Raises ValueError unless the session was created with record=True.