From 18f9dbab37d6cda5bc63a495659db32f2a7d0979 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 29 Oct 2026 21:21:37 +0100 Subject: [PATCH 1/2] Add an animated figure to the docs home page. --- Makefile | 5 +++- docs/hero.py | 53 ++++++++++++++++++++++++++++++++++++++++ docs/src/assets/hero.svg | 9 +++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 docs/hero.py create mode 100644 docs/src/assets/hero.svg diff --git a/Makefile b/Makefile index 8ebc367..b682aff 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PYTHON ?= .venv/bin/python DOCS_ENV = DOCS_PYTHON=$(shell $(PYTHON) -c "import sys; print(sys.executable)") -.PHONY: help docs-install docs-dev docs-build docs-preview docs-clean +.PHONY: help docs-install docs-hero docs-dev docs-build docs-preview docs-clean help: ## list the targets @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-16s %s\n", $$1, $$2}' @@ -11,6 +11,9 @@ docs-install: ## Python docs extra and the npm packages $(PYTHON) -m pip install -e ".[docs]" cd docs && npm ci +docs-hero: ## redraw the home page figure (docs/src/assets/hero.svg) + $(PYTHON) docs/hero.py + docs-dev: ## live preview at http://localhost:4321/cunumpy/ cd docs && $(DOCS_ENV) DOCS_VALIDATE_LINKS=false npm run dev diff --git a/docs/hero.py b/docs/hero.py new file mode 100644 index 0000000..23714ac --- /dev/null +++ b/docs/hero.py @@ -0,0 +1,53 @@ +"""The figure on the docs home page: vibrating strings computed with cunumpy arrays. + +Seven periodic strings carry the wave equation, all at once in one ``(7, n)`` array of the active +backend (NumPy on the CPU, CuPy on a GPU: ``CUNUMPY_BACKEND=cupy``). The leapfrog step runs at a +Courant number of one, where it is exact on the grid, so after ``n`` steps every string is back +where it started and the animation loops without a seam. The snapshots, copied to the host with +``xp.to_numpy``, are drawn with ``plasma_plots.hero`` into ``docs/src/assets/hero.svg``. Rerun +with:: + + python docs/hero.py # or: make docs-hero +""" + +from pathlib import Path + +import numpy as np +from plasma_plots.hero import HeroFigure + +import cunumpy as xp + +strings, n, every = 7, 96, 4 +x = xp.linspace(0.0, 8.0, n, endpoint=False) +k = 2 * np.pi / 8.0 +row = xp.arange(strings, dtype=xp.float64)[:, None] +# a different mix of modes on each string; the second mode moves the other way +u = 0.12 * xp.sin(k * (row % 3 + 1) * x + row) + 0.07 * xp.cos( + k * (row % 4 + 2) * x - 2 * row +) +previous = 0.12 * xp.sin(k * (row % 3 + 1) * (x + 8.0 / n) + row) + 0.07 * xp.cos( + k * (row % 4 + 2) * (x - 8.0 / n) - 2 * row +) + +frames = [] +for step in range(n + 1): + if step % every == 0: + frames.append(xp.to_numpy(u)) + u, previous = xp.roll(u, 1, axis=1) + xp.roll(u, -1, axis=1) - previous, u + +backend = xp.get_backend() +fig = HeroFigure(xlim=(0, 8), ylim=(0, 3)) +grid = np.r_[xp.to_numpy(x), 8.0][::2] +for s in range(strings): + level = 0.35 + s * 2.3 / (strings - 1) + paths = [fig._path(grid, level + np.r_[f[s], f[s, 0]][::2]) for f in frames] + line = fig.line( + grid, + level + np.r_[frames[0][s], frames[0][s, 0]][::2], + tone="high" if s == 3 else "accent", + width=2.0 if s == 3 else 1.4, + opacity=0.95 if s == 3 else 0.55, + title=f"string {s + 1}, on {backend}", + ) + line.animate("d", paths, 12.0) +fig.save(Path(__file__).parent / "src" / "assets" / "hero.svg") diff --git a/docs/src/assets/hero.svg b/docs/src/assets/hero.svg new file mode 100644 index 0000000..6fe8521 --- /dev/null +++ b/docs/src/assets/hero.svg @@ -0,0 +1,9 @@ + +string 1, on numpy +string 2, on numpy +string 3, on numpy +string 4, on numpy +string 5, on numpy +string 6, on numpy +string 7, on numpy + From ac7a8d676f0cdb6dd6992f79f125de98a5616468 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 29 Oct 2026 21:22:28 +0100 Subject: [PATCH 2/2] Use HeroFigure.morph for the strings. --- docs/hero.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/hero.py b/docs/hero.py index 23714ac..360a2f4 100644 --- a/docs/hero.py +++ b/docs/hero.py @@ -40,14 +40,14 @@ grid = np.r_[xp.to_numpy(x), 8.0][::2] for s in range(strings): level = 0.35 + s * 2.3 / (strings - 1) - paths = [fig._path(grid, level + np.r_[f[s], f[s, 0]][::2]) for f in frames] - line = fig.line( + # the last frame is the first again; morph closes the loop itself + fig.morph( grid, - level + np.r_[frames[0][s], frames[0][s, 0]][::2], + [level + np.r_[f[s], f[s, 0]][::2] for f in frames[:-1]], + 12.0, tone="high" if s == 3 else "accent", width=2.0 if s == 3 else 1.4, opacity=0.95 if s == 3 else 0.55, title=f"string {s + 1}, on {backend}", ) - line.animate("d", paths, 12.0) fig.save(Path(__file__).parent / "src" / "assets" / "hero.svg")