diff --git a/Makefile b/Makefile index e97ce11..0ecac3a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ NOTEBOOKS = cd docs && $(DOCS_ENV) npx starlight-notebooks DOCS_ENV = DOCS_PYTHON=$(shell $(PYTHON) -c "import sys; print(sys.executable)") FORCE ?= -.PHONY: help install test lint readme readme-check docs-install docs-notebooks docs-dev docs-build docs-preview docs-clean +.PHONY: help install test lint readme readme-check docs-install docs-notebooks 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}' @@ -38,6 +38,9 @@ docs-notebooks: ## execute the changed tutorials and convert all of them (FORCE $(NOTEBOOKS) execute $(if $(FORCE),--force,) $(NOTEBOOKS) convert +docs-hero: ## redraw the home page figure (docs/src/assets/hero.svg) + $(PYTHON) docs/hero.py + docs-dev: ## live preview at http://localhost:4321/patchwork/ $(NOTEBOOKS) convert 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..13260fb --- /dev/null +++ b/docs/hero.py @@ -0,0 +1,90 @@ +"""The animated figure at the top of the docs home page: ``docs/src/assets/hero.svg``. + +patchwork solves a Poisson problem on a hierarchy of nested patches: a sharp, narrow charge +on the right, refined by three levels of patches around it, and a broad charge of the other +sign to its left. The figure shows the patch outlines, which appear one level at a time, and +the contours of the composite potential, which run smoothly across the patch edges; a few dots +drift along them. The solve takes about a second. + +Rerun with ``make docs-hero`` or ``python docs/hero.py`` (needs ``plasma_plots.hero``). +""" + +from pathlib import Path + +import contourpy +import numpy as np +from plasma_plots.hero import HeroFigure +from scipy.interpolate import RegularGridInterpolator + +from patchwork import AMRHierarchy, solve_poisson + +OUTPUT = Path(__file__).parent / "src" / "assets" / "hero.svg" +XLIM, YLIM = (0.0, 8.0), (0.0, 3.0) +SHARP, BROAD = (5.9, 1.5), (3.4, 1.4) + +mesh = AMRHierarchy(base_shape=(64, 24), bounds=(XLIM, YLIM)) +# boxes in cells of their own level (spacing 1/8, 1/16, 1/32, 1/64 of a unit) +mesh.add_patch(level=1, box=((76, 112), (8, 40))) +mesh.add_patch(level=2, box=((172, 204), (32, 64))) +mesh.add_patch(level=3, box=((360, 396), (80, 112))) +mesh.allocate_field("rhs", halo=1) +for patch in mesh: + x, y = patch.local_cell_centers(mesh) + sharp = np.exp(-((x - SHARP[0]) ** 2 + (y - SHARP[1]) ** 2) / (2 * 0.05**2)) + broad = np.exp(-((x - BROAD[0]) ** 2 + (y - BROAD[1]) ** 2) / (2 * 0.5**2)) + patch.field("rhs").local[...] = 60.0 * sharp - 1.2 * broad +solve_poisson(mesh, "rhs", potential="phi", boundary_value=0.0) + +# sample the composite potential on a uniform grid, each point from the finest patch holding it +gx = np.linspace(*XLIM, 641) +gy = np.linspace(*YLIM, 241) +X, Y = np.meshgrid(gx, gy, indexing="ij") +phi = np.full(X.shape, np.nan) +for patch in sorted(mesh, key=lambda p: p.level): + (x0, x1), (y0, y1) = patch.physical_bounds(mesh) + hx, hy = mesh.spacing(patch.level) + cx = np.arange(x0 + hx / 2, x1, hx) + cy = np.arange(y0 + hy / 2, y1, hy) + inside = (X >= x0) & (X <= x1) & (Y >= y0) & (Y <= y1) + values = mesh.gather(patch, "phi") + interp = RegularGridInterpolator( + (cx, cy), values, bounds_error=False, fill_value=None + ) + phi[inside] = interp(np.column_stack([X[inside], Y[inside]])) + +fig = HeroFigure(xlim=XLIM, ylim=YLIM) +# the patch outlines, appearing one level at a time over a 12 s loop +CYCLE = 12.0 +for patch in mesh: + if patch.level == 0: + continue + (x0, x1), (y0, y1) = patch.physical_bounds(mesh) + box = fig.rect(x0, y0, x1, y1, tone="low", opacity=0.35, outline="high") + box.attrs["stroke-width"] = "1.5" + box.animate( + "opacity", + [0, 0] + [0] * (patch.level - 1) + [1] * (9 - patch.level) + [0], + CYCLE, + ) + +levels = np.concatenate( + [ + -np.geomspace(0.02, 0.25, 4)[::-1], + np.geomspace(0.01, 0.5, 6), + [phi.max() * 0.8, phi.max() * 0.6, phi.max() * 0.45], + ] +) +generator = contourpy.contour_generator(gx, gy, phi.T) +lines = [] +for level in levels: + tone = "accent" if level > 0 else "muted" + for seg in generator.lines(level): + seg = np.vstack([seg[:: max(1, len(seg) // 160)], seg[-1:]]) + if len(seg) < 8: + continue + lines.append(fig.line(seg[:, 0], seg[:, 1], tone=tone, width=1.3, opacity=0.7)) + +for i, line in enumerate(lines[::3]): + fig.tracer(line, duration=8 + (i % 5) * 2, offset=1.3 * i, r=3, tone="high") + +print(f"Saved {fig.save(OUTPUT)}") diff --git a/docs/src/assets/hero.svg b/docs/src/assets/hero.svg new file mode 100644 index 0000000..c63d3b6 --- /dev/null +++ b/docs/src/assets/hero.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + +