{
"cells": [
{
"cell_type": "markdown",
"id": "82fea25a",
"metadata": {},
"source": [
"# Animating footprint time series\n",
"\n",
"`fluxfootprints.footprint_animation` turns the time-resolved footprint\n",
"rasters produced by any model in this package into a **video or animated GIF**,\n",
"one frame per time step, with the timestamp drawn on each frame.\n",
"\n",
"This notebook covers:\n",
"\n",
"1. building a footprint time series from half-hourly tower data,\n",
"2. animating it at **hourly**, **daily**, and **monthly** time steps,\n",
"3. drawing the footprint over a **georeferenced basemap**,\n",
"4. exporting **MP4** as well as GIF, and\n",
"5. the styling options (color scaling, masking, source-area contours).\n",
"\n",
"## Requirements\n",
"\n",
"GIF output works with a bare install. Basemaps and MP4/WebM need the optional\n",
"extra, which pulls in [contextily](https://contextily.readthedocs.io) (map\n",
"tiles) and [imageio-ffmpeg](https://github.com/imageio/imageio-ffmpeg) (a\n",
"bundled `ffmpeg` binary):\n",
"\n",
"```bash\n",
"pip install \"fluxfootprints[animation]\"\n",
"```\n",
"\n",
"An `ffmpeg` already on your `PATH` is used in preference to the bundled copy."
]
},
{
"cell_type": "markdown",
"id": "7fa96fcb",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "13117d48",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-22T21:23:28.922589Z",
"iopub.status.busy": "2026-08-22T21:23:28.922368Z",
"iopub.status.idle": "2026-08-22T21:23:30.581108Z",
"shell.execute_reply": "2026-08-22T21:23:30.580070Z"
}
},
"outputs": [],
"source": [
"import base64\n",
"import os\n",
"import sys\n",
"from pathlib import Path\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"from IPython.display import HTML\n",
"\n",
"sys.path.insert(0, os.path.abspath(\"../../src\")) # adjust path as needed\n",
"\n",
"from fluxfootprints import (\n",
" FootprintAnimator,\n",
" animate_footprint,\n",
" build_climatology,\n",
" summarize_periods,\n",
")\n",
"from fluxfootprints.footprint_animation import ensure_ffmpeg\n",
"\n",
"OUT = Path(\"output\")\n",
"OUT.mkdir(exist_ok=True)\n",
"\n",
"\n",
"def show(path, width=460):\n",
" \"\"\"Embed an animation inline, base64-encoded.\n",
"\n",
" ``IPython.display.Image`` renders GIFs in Jupyter but the ``image/gif``\n",
" output is dropped when Sphinx builds these pages, so the animations below\n",
" are embedded as HTML instead -- that renders everywhere.\n",
" \"\"\"\n",
" path = Path(path)\n",
" data = base64.b64encode(path.read_bytes()).decode(\"ascii\")\n",
" if path.suffix == \".mp4\":\n",
" return HTML(\n",
" f''\n",
" )\n",
" mime = \"image/gif\" if path.suffix == \".gif\" else \"image/png\"\n",
" return HTML(f'')\n",
"\n",
"# Example site location (used for georeferencing further down):\n",
"# irrigated pasture in Cache Valley, Utah.\n",
"STATION_LAT, STATION_LON = 41.66, -111.89"
]
},
{
"cell_type": "markdown",
"id": "91575017",
"metadata": {},
"source": [
"## 1. Half-hourly tower data\n",
"\n",
"The animator needs a footprint time series, which in turn needs half-hourly\n",
"micrometeorology. For a real site you would read it with\n",
"`load_amf_df()`; here we synthesise six months of\n",
"plausible-looking data so the notebook runs anywhere.\n",
"\n",
"The wind direction drifts on a multi-day cycle and the turbulence follows a\n",
"diurnal cycle, so the footprint both rotates and grows/shrinks over time --\n",
"which is what makes the animation worth watching."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "11310c2f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-08-22T21:23:30.584111Z",
"iopub.status.busy": "2026-08-22T21:23:30.583446Z",
"iopub.status.idle": "2026-08-22T21:23:30.605577Z",
"shell.execute_reply": "2026-08-22T21:23:30.604802Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"
| \n", " | wind_dir | \n", "umean | \n", "ustar | \n", "ol | \n", "sigmav | \n", "zm | \n", "z0 | \n", "h | \n", "LE | \n", "
|---|---|---|---|---|---|---|---|---|---|
| datetime_start | \n", "\n", " | \n", " | \n", " | \n", " | \n", " | \n", " | \n", " | \n", " | \n", " |
| 2023-04-01 00:00:00 | \n", "149.720678 | \n", "1.902912 | \n", "0.105943 | \n", "120.0 | \n", "0.325440 | \n", "2.0 | \n", "0.05 | \n", "500.0 | \n", "32.588329 | \n", "
| 2023-04-01 00:30:00 | \n", "133.584264 | \n", "2.127531 | \n", "0.097906 | \n", "120.0 | \n", "0.351048 | \n", "2.0 | \n", "0.05 | \n", "500.0 | \n", "10.238564 | \n", "
| 2023-04-01 01:00:00 | \n", "155.069487 | \n", "2.492085 | \n", "0.169734 | \n", "120.0 | \n", "0.255680 | \n", "2.0 | \n", "0.05 | \n", "500.0 | \n", "27.798096 | \n", "
| 2023-04-01 01:30:00 | \n", "157.350850 | \n", "2.505937 | \n", "0.088282 | \n", "120.0 | \n", "0.309927 | \n", "2.0 | \n", "0.05 | \n", "500.0 | \n", "18.950612 | \n", "
| 2023-04-01 02:00:00 | \n", "122.651651 | \n", "1.591055 | \n", "0.103610 | \n", "120.0 | \n", "0.292963 | \n", "2.0 | \n", "0.05 | \n", "500.0 | \n", "21.253645 | \n", "