Skip to content

Commit b27e7d6

Browse files
Add example gallery
1 parent 1fc678c commit b27e7d6

File tree

5 files changed

+355
-1
lines changed

5 files changed

+355
-1
lines changed

doc/conf.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import sys
2222
import pytensor
2323

24+
sys.path.insert(0, os.path.abspath(os.path.join("..", "scripts")))
25+
2426
# General configuration
2527
# ---------------------
2628

@@ -34,7 +36,9 @@
3436
"sphinx.ext.linkcode",
3537
"sphinx.ext.mathjax",
3638
"sphinx_design",
37-
"sphinx.ext.intersphinx"
39+
"sphinx.ext.intersphinx",
40+
"myst_nb",
41+
"generate_gallery",
3842
]
3943

4044
intersphinx_mapping = {
@@ -295,3 +299,30 @@ def find_source():
295299

296300
# If false, no module index is generated.
297301
# latex_use_modindex = True
302+
303+
304+
# -- MyST config -------------------------------------------------
305+
myst_enable_extensions = [
306+
"colon_fence",
307+
"deflist",
308+
"dollarmath",
309+
"amsmath",
310+
"substitution",
311+
]
312+
myst_dmath_double_inline = True
313+
314+
myst_substitutions = {
315+
"pip_dependencies": "{{ extra_dependencies }}",
316+
"conda_dependencies": "{{ extra_dependencies }}",
317+
"extra_install_notes": "",
318+
}
319+
320+
nb_execution_mode = "off"
321+
nbsphinx_execute = "never"
322+
nbsphinx_allow_errors = True
323+
324+
325+
# -- Bibtex config -------------------------------------------------
326+
bibtex_bibfiles = ["references.bib"]
327+
bibtex_default_style = "unsrt"
328+
bibtex_reference_style = "author_year"

doc/gallery/introduction/what_is_pytensor.ipynb

Lines changed: 134 additions & 0 deletions
Large diffs are not rendered by default.

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Community
8080
introduction
8181
user_guide
8282
API <library/index>
83+
Examples <gallery/gallery>
8384
Contributing <dev_start_guide>
8485

8586
.. _Theano: https://github.com/Theano/Theano

environment.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ dependencies:
4343
- ipython
4444
- pymc-sphinx-theme
4545
- sphinx-design
46+
- myst-nb
47+
- matplotlib
48+
- watermark
49+
4650
# code style
4751
- ruff
4852
# developer tools

scripts/generate_gallery.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""
2+
Sphinx plugin to run generate a gallery for notebooks
3+
4+
Modified from the pymc project, which modified the seaborn project, which modified the mpld3 project.
5+
"""
6+
7+
import base64
8+
import json
9+
import os
10+
import shutil
11+
from pathlib import Path
12+
13+
import matplotlib
14+
15+
16+
matplotlib.use("Agg")
17+
import matplotlib.pyplot as plt
18+
import sphinx
19+
from matplotlib import image
20+
21+
22+
logger = sphinx.util.logging.getLogger(__name__)
23+
24+
DOC_SRC = Path(__file__).resolve().parent
25+
# DEFAULT_IMG_LOC = os.path.join(os.path.dirname(DOC_SRC), "_static", "PyMC.png")
26+
27+
DEFAULT_IMG_LOC = None
28+
external_nbs = {}
29+
30+
HEAD = """
31+
Example Gallery
32+
===============
33+
34+
.. toctree::
35+
:hidden:
36+
37+
"""
38+
39+
SECTION_TEMPLATE = """
40+
.. _{section_id}:
41+
42+
{section_title}
43+
{underlines}
44+
45+
.. grid:: 1 2 3 3
46+
:gutter: 4
47+
48+
"""
49+
50+
ITEM_TEMPLATE = """
51+
.. grid-item-card:: :doc:`{doc_name}`
52+
:img-top: {image}
53+
:link: {doc_reference}
54+
:link-type: {link_type}
55+
:shadow: none
56+
"""
57+
58+
folder_title_map = {
59+
"introduction": "Introduction",
60+
}
61+
62+
63+
def create_thumbnail(infile, width=275, height=275, cx=0.5, cy=0.5, border=4):
64+
"""Overwrites `infile` with a new file of the given size"""
65+
im = image.imread(infile)
66+
rows, cols = im.shape[:2]
67+
size = min(rows, cols)
68+
if size == cols:
69+
xslice = slice(0, size)
70+
ymin = min(max(0, int(cx * rows - size // 2)), rows - size)
71+
yslice = slice(ymin, ymin + size)
72+
else:
73+
yslice = slice(0, size)
74+
xmin = min(max(0, int(cx * cols - size // 2)), cols - size)
75+
xslice = slice(xmin, xmin + size)
76+
thumb = im[yslice, xslice]
77+
thumb[:border, :, :3] = thumb[-border:, :, :3] = 0
78+
thumb[:, :border, :3] = thumb[:, -border:, :3] = 0
79+
80+
dpi = 100
81+
fig = plt.figure(figsize=(width / dpi, height / dpi), dpi=dpi)
82+
83+
ax = fig.add_axes([0, 0, 1, 1], aspect="auto", frameon=False, xticks=[], yticks=[])
84+
ax.imshow(thumb, aspect="auto", resample=True, interpolation="bilinear")
85+
fig.savefig(infile, dpi=dpi)
86+
plt.close(fig)
87+
return fig
88+
89+
90+
class NotebookGenerator:
91+
"""Tools for generating an example page from a file"""
92+
93+
def __init__(self, filename, root_dir, folder):
94+
self.folder = folder
95+
96+
self.basename = Path(filename).name
97+
self.stripped_name = Path(filename).stem
98+
self.image_dir = Path(root_dir) / "_thumbnails" / folder
99+
self.png_path = self.image_dir / f"{self.stripped_name}.png"
100+
101+
with filename.open(encoding="utf-8") as fid:
102+
self.json_source = json.load(fid)
103+
self.default_image_loc = DEFAULT_IMG_LOC
104+
105+
def extract_preview_pic(self):
106+
"""By default, just uses the last image in the notebook."""
107+
pic = None
108+
for cell in self.json_source["cells"]:
109+
for output in cell.get("outputs", []):
110+
if "image/png" in output.get("data", []):
111+
pic = output["data"]["image/png"]
112+
if pic is not None:
113+
return base64.b64decode(pic)
114+
return None
115+
116+
def gen_previews(self):
117+
preview = self.extract_preview_pic()
118+
if preview is not None:
119+
with self.png_path.open("wb") as buff:
120+
buff.write(preview)
121+
else:
122+
logger.warning(
123+
f"Didn't find any pictures in {self.basename}",
124+
type="thumbnail_extractor",
125+
)
126+
shutil.copy(self.default_image_loc, self.png_path)
127+
create_thumbnail(self.png_path)
128+
129+
130+
def main(app):
131+
logger.info("Starting thumbnail extractor.")
132+
133+
working_dir = Path.getcwd()
134+
os.chdir(app.builder.srcdir)
135+
136+
file = [HEAD]
137+
138+
for folder, title in folder_title_map.items():
139+
file.append(
140+
SECTION_TEMPLATE.format(
141+
section_title=title, section_id=folder, underlines="-" * len(title)
142+
)
143+
)
144+
145+
thumbnail_dir = Path("..") / "_thumbnails" / folder
146+
if not thumbnail_dir.exists():
147+
Path.mkdir(thumbnail_dir, parents=True)
148+
149+
if folder in external_nbs.keys():
150+
file += [
151+
ITEM_TEMPLATE.format(
152+
doc_name=descr["doc_name"],
153+
image=descr["image"],
154+
doc_reference=descr["doc_reference"],
155+
link_type=descr["link_type"],
156+
)
157+
for descr in external_nbs[folder]
158+
]
159+
160+
nb_paths = sorted(Path.glob(f"gallery/{folder}/*.ipynb"))
161+
162+
for nb_path in nb_paths:
163+
nbg = NotebookGenerator(
164+
filename=nb_path, root_dir=Path(".."), folder=folder
165+
)
166+
nbg.gen_previews()
167+
168+
file.append(
169+
ITEM_TEMPLATE.format(
170+
doc_name=Path(folder) / nbg.stripped_name,
171+
image="/" + str(nbg.png_path),
172+
doc_reference=Path(folder) / nbg.stripped_name,
173+
link_type="doc",
174+
)
175+
)
176+
177+
with Path("gallery", "gallery.rst").open("w", encoding="utf-8") as f:
178+
f.write("\n".join(file))
179+
180+
os.chdir(working_dir)
181+
182+
183+
def setup(app):
184+
app.connect("builder-inited", main)

0 commit comments

Comments
 (0)