---
myst:
html_meta:
description: "Render a simulation as video and upload it with the simulation's channels, so they play back together."
---
# Chart animations
{.lead}
Render an animated chart of a simulation as a video, then upload it next to the simulation's channels so you can replay them together.
This example simulates a double pendulum.
The examples also need `pip install scipy matplotlib "imageio[ffmpeg]"`.
```{video} https://res.cloudinary.com/didkpxvqu/video/upload/v1729218249/double_pendulum_ozyjtj.mp4
```
```{include} /guides/_snippets/connect-the-client.md
```
## Simulate and save the channels
Solve the equations of motion once, and save the channels with absolute timestamps:
```python
from datetime import datetime, timedelta, timezone
import numpy as np
import pandas as pd
from scipy.integrate import solve_ivp
g, L = 9.81, 1.0 # both arms 1 m long, both masses 1 kg
def pendulum(t, y):
theta1, z1, theta2, z2 = y
d = theta2 - theta1
den = 2 * L - L * np.cos(d) ** 2
dz1 = (L * z1**2 * np.sin(d) * np.cos(d) + g * np.sin(theta2) * np.cos(d) + L * z2**2 * np.sin(d) - 2 * g * np.sin(theta1)) / den
dz2 = (-L * z2**2 * np.sin(d) * np.cos(d) + 2 * (g * np.sin(theta1) * np.cos(d) - L * z1**2 * np.sin(d) - g * np.sin(theta2))) / den
return [z1, dz1, z2, dz2]
t = np.linspace(0, 25, 500) # 25 s at 20 frames per second
theta1, _, theta2, _ = solve_ivp(pendulum, (0, 25), [np.pi / 2, 0, np.pi / 2, 0], t_eval=t).y
x1, y1 = L * np.sin(theta1), -L * np.cos(theta1)
x2, y2 = x1 + L * np.sin(theta2), y1 - L * np.cos(theta2)
start = datetime.now(timezone.utc)
pd.DataFrame(
{
"time": start.timestamp() + t,
"height_1": y1 + 2 * L,
"height_2": y2 + 2 * L,
"speed_2": np.hypot(np.gradient(x2, t), np.gradient(y2, t)),
}
).to_csv("pendulum.csv", index=False)
```
## Render the video
Draw one frame per time step and write them to an MP4 with [imageio](https://imageio.readthedocs.io/):
```python
import imageio.v2 as imageio
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6, 6))
with imageio.get_writer("pendulum.mp4", fps=20) as writer:
for i in range(len(t)):
ax.clear()
ax.set(xlim=(-2, 2), ylim=(-2, 2))
ax.plot([0, x1[i], x2[i]], [0, y1[i], y2[i]], "o-")
fig.canvas.draw()
writer.append_data(np.asarray(fig.canvas.buffer_rgba())[..., :3])
```
The frame rate matches the simulation's time step, so the video lasts as long as the simulation.
## Upload the channels and the video
Upload both to one dataset. The video starts at the simulation's first timestamp:
```python
dataset = client.create_dataset("Double pendulum")
dataset.add_tabular_data("pendulum.csv", timestamp_column="time", timestamp_type="epoch_seconds").poll_until_ingestion_completed()
dataset.add_video("pendulum.mp4", channel="pendulum_video", start=start).poll_until_ingestion_completed()
run = client.create_run("Double pendulum", start=start, end=start + timedelta(seconds=25))
run.add_dataset("simulation", dataset)
print(run.nominal_url)
```
If the video drifts from the channels, change its start with `update(starting_timestamp=...)` on the file from {py:meth}`~nominal.core.Dataset.list_video_files`.
See [Video ingest](/guides/video/video-ingest.md) for other options.
## Replay it in a workbook
In a workbook on the run, add a video panel for `pendulum_video` and a chart of `height_1` and `height_2`.
Playing the video moves the chart's cursor with it.
```{video} /guides/images/b2/CleanShot_2024-10-17_at_09.12.40_hoivoh.mp4
:loop:
:alt: A workbook replaying the pendulum video next to its height channels
```