|
1 | 1 | """Render the README animations and the animated docs logo from real learner runs. |
2 | 2 |
|
3 | 3 | Writes docs/_static/readme/{learner1d,learner2d,learnerND}.gif and |
4 | | -docs/_static/logo_docs.webm. Needs ffmpeg. |
| 4 | +docs/_static/logo_docs.webm. Needs ffmpeg, and Chrome or Chromium for kaleido |
| 5 | +(set BROWSER_PATH if it is not found). |
5 | 6 |
|
6 | 7 | Usage: python docs/make_animations.py [learner1d learner2d learnerND logo] |
7 | 8 | """ |
|
21 | 22 | import matplotlib.pyplot as plt # noqa: E402 |
22 | 23 | import matplotlib.tri as mtri # noqa: E402 |
23 | 24 | import numpy as np # noqa: E402 |
24 | | -from matplotlib.colors import to_rgba # noqa: E402 |
25 | | -from mpl_toolkits.mplot3d.art3d import Poly3DCollection # noqa: E402 |
26 | 25 | from PIL import Image, ImageDraw # noqa: E402 |
27 | 26 |
|
28 | 27 | import adaptive # noqa: E402 |
|
31 | 30 |
|
32 | 31 | STATIC = Path(__file__).parent / "_static" |
33 | 32 | DARK, ACCENT, NEW = "#2c3e6b", "#21918c", "#e4572e" |
34 | | -BLUE, YELLOW = "#3b528b", "#fde725" |
35 | | -NPOINTS_3D = 6000 |
36 | 33 | plt.rcParams.update( |
37 | 34 | { |
38 | 35 | "font.family": "DejaVu Sans", |
@@ -172,65 +169,50 @@ def draw(fig, i): |
172 | 169 |
|
173 | 170 |
|
174 | 171 | def learnerND(target: Path) -> None: |
175 | | - learner = adaptive.LearnerND(shell, bounds=[(-1, 1)] * 3) |
176 | | - adaptive.runner.simple(learner, npoints_goal=NPOINTS_3D) |
177 | | - triangles = _isosurface(learner) |
178 | | - centers = triangles.mean(axis=1) |
179 | | - inner = np.linalg.norm(centers, axis=1) < 0.75 |
180 | | - # Cut an x > 0, y > 0 wedge out of the outer surface to show the inner one. |
181 | | - keep = inner | ~((centers[:, 0] > 0) & (centers[:, 1] > 0)) |
182 | | - triangles, inner = triangles[keep], inner[keep] |
183 | | - base = np.array([to_rgba(BLUE), to_rgba(YELLOW)])[inner.astype(int)] |
184 | | - nframes = 90 |
185 | | - |
186 | | - def draw(fig, i): |
187 | | - azim = 45 + 55 * np.sin(2 * np.pi * i / nframes) # keeps the cut in view |
188 | | - colors = base * _lambert(triangles, azim - 30, 25 + 20)[:, None] |
189 | | - colors[:, 3] = 1 |
190 | | - ax = fig.add_subplot(projection="3d") |
191 | | - surface = Poly3DCollection( |
192 | | - triangles, facecolors=colors, edgecolors=colors, linewidths=0.3 |
193 | | - ) |
194 | | - ax.add_collection3d(surface) |
195 | | - ax.view_init(elev=25, azim=azim) |
196 | | - for set_lim in (ax.set_xlim, ax.set_ylim, ax.set_zlim): |
197 | | - set_lim(-1, 1) |
198 | | - ax.set_box_aspect((1, 1, 1), zoom=1.25) |
199 | | - ax.set_axis_off() |
200 | | - fig.text( |
201 | | - 0.5, 0.04, f"LearnerND, {learner.npoints} points", |
202 | | - ha="center", color="#555555", fontsize=12, |
203 | | - ) # fmt: skip |
204 | | - fig.subplots_adjust(left=0, right=1, top=1, bottom=0) |
| 172 | + """Rotating version of the LearnerND plot on the "Algorithms and examples" page.""" |
| 173 | + import plotly.graph_objects as go |
| 174 | + import plotly.io as pio |
205 | 175 |
|
206 | | - _render_gif(draw, nframes, (4.5, 4.5), target, hold=0, dpi=90) |
207 | | - |
208 | | - |
209 | | -def _lambert(triangles: np.ndarray, azim: float, elev: float) -> np.ndarray: |
210 | | - """Brightness of each triangle lit from the direction (azim, elev) in degrees.""" |
211 | | - azim, elev = np.radians(azim), np.radians(elev) |
212 | | - light = np.array( |
213 | | - [np.cos(elev) * np.cos(azim), np.cos(elev) * np.sin(azim), np.sin(elev)] |
214 | | - ) |
215 | | - normals = np.cross( |
216 | | - triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0] |
217 | | - ) |
218 | | - normals /= np.linalg.norm(normals, axis=1, keepdims=True) |
219 | | - return 0.45 + 0.55 * np.clip(normals @ light, 0, 1) |
220 | | - |
221 | | - |
222 | | -def _isosurface(learner: adaptive.LearnerND) -> np.ndarray: |
223 | | - """Triangles of the learner's f = 0.5 isosurface, with outward normals.""" |
224 | | - vertices, faces = learner._get_iso(level=0.5) |
225 | | - triangles = np.array(vertices)[np.array(faces)] |
226 | | - # Wind every triangle with its normal pointing away from the center, |
227 | | - # so the shading is consistent. |
228 | | - normals = np.cross( |
229 | | - triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0] |
| 176 | + points, values = sampled( |
| 177 | + adaptive.LearnerND(shell, bounds=[(-1, 1)] * 3), npoints_goal=5000 |
230 | 178 | ) |
231 | | - inward = np.einsum("ij,ij->i", normals, triangles.mean(axis=1)) < 0 |
232 | | - triangles[inward] = triangles[inward][:, ::-1] |
233 | | - return triangles |
| 179 | + # Cut away the x > 0, y > 0 quarter to show the shell in cross-section. |
| 180 | + keep = ~((points[:, 0] > 0) & (points[:, 1] > 0)) |
| 181 | + marker = { |
| 182 | + "size": 5, |
| 183 | + "color": values[keep], |
| 184 | + "colorscale": "Viridis", |
| 185 | + "line": {"color": "rgb(50,50,50)", "width": 0.5}, |
| 186 | + } # same style as LearnerND.plot_3D, with bigger markers |
| 187 | + x, y, z = points[keep].T |
| 188 | + fig = go.Figure(go.Scatter3d(x=x, y=y, z=z, mode="markers", marker=marker)) |
| 189 | + hidden = {"visible": False} |
| 190 | + fig.update_layout( |
| 191 | + scene={ |
| 192 | + "xaxis": hidden, "yaxis": hidden, "zaxis": hidden, "aspectmode": "cube", |
| 193 | + "camera": {"projection": {"type": "orthographic"}}, |
| 194 | + }, |
| 195 | + margin={"l": 0, "r": 0, "t": 0, "b": 0}, |
| 196 | + showlegend=False, |
| 197 | + annotations=[ |
| 198 | + { |
| 199 | + "text": f"LearnerND, {len(points)} points", |
| 200 | + "x": 0.5, "y": 0.02, "xref": "paper", "yref": "paper", |
| 201 | + "showarrow": False, "font": {"size": 15, "color": "#555555"}, |
| 202 | + } |
| 203 | + ], |
| 204 | + ) # fmt: skip |
| 205 | + nframes = 90 |
| 206 | + figures = [] |
| 207 | + for i in range(nframes): |
| 208 | + angle = np.pi / 4 + 2 * np.pi * i / nframes # start looking into the cut |
| 209 | + eye = {"x": 2 * np.cos(angle), "y": 2 * np.sin(angle), "z": 0.25} |
| 210 | + figures.append(go.Figure(fig).update_layout(scene_camera_eye=eye)) |
| 211 | + with tempfile.TemporaryDirectory() as folder: |
| 212 | + files = [Path(folder) / f"{i:04d}.png" for i in range(nframes)] |
| 213 | + pio.write_images(figures, files, width=400, height=400) |
| 214 | + to_gif(Path(folder), target, hold=0) |
| 215 | + print(f"{target.relative_to(STATIC.parent)}: {target.stat().st_size / 1e6:.2f} MB") |
234 | 216 |
|
235 | 217 |
|
236 | 218 | def _render_gif(draw, nframes, figsize, target, hold=2.5, dpi=100, fps=15): |
|
0 commit comments