Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/scripts/check_tutorials_complete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Fail if any HowTo tutorial script looks truncated.

Several tutorial scripts were once cut off mid-generation (a long file was
re-emitted and the output was severed), losing all content below the cut and
leaving a script that ends part-way through — typically on a docstring that
promises a plot or code block which never appears. This check guards against
that regression recurring.

A tutorial is considered *complete* when it contains a recognised terminal
section marker (``__Wrap Up__`` or ``__Summary__``). A truncated script never
reaches its wrap-up, so the absence of the marker is a reliable signal that the
script lost content. Deliberate "not written yet" stub tutorials still carry a
``__Wrap Up__`` section, so they pass.

A second, cheaper guard flags any script whose final docstring block ends on a
colon (``:``) — the classic "a plot/code block follows" promise left dangling
by a mid-docstring cutoff.

Run from the repo root::

python scripts/check_tutorials_complete.py

Exit status is non-zero if any tutorial fails, listing each offender.
"""
from __future__ import annotations

import re
import sys
from pathlib import Path

TERMINAL_MARKERS = ("__wrap up__", "__summary__")


def final_docstring(text: str) -> str | None:
"""Return the last triple-quoted block if it sits at the end of the file."""
blocks = list(re.finditer(r'"""(.*?)"""', text, re.DOTALL))
if not blocks:
return None
last = blocks[-1]
if text[last.end():].strip() == "":
return last.group(1)
return None


def check(path: Path) -> str | None:
"""Return a failure reason for a truncated-looking tutorial, else None."""
text = path.read_text(encoding="utf-8")
if not any(marker in text.lower() for marker in TERMINAL_MARKERS):
return "no __Wrap Up__/__Summary__ terminal section (looks truncated)"
trailing = final_docstring(text)
if trailing is not None and trailing.strip().endswith(":"):
return "final docstring ends on ':' (dangling promise of a following block)"
return None


def main(root: str) -> int:
scripts_dir = Path(root) / "scripts"
files = sorted(scripts_dir.rglob("tutorial_*.py"))

failures = [(f, reason) for f in files if (reason := check(f)) is not None]

print(f"Checked {len(files)} tutorial scripts under {scripts_dir}.")
if failures:
print(f"\n{len(failures)} tutorial(s) look incomplete / truncated:\n")
for f, reason in failures:
print(f" [FAIL] {f.relative_to(root)} — {reason}")
print(
"\nEach tutorial must end with a `__Wrap Up__` (or `__Summary__`) "
"section. If a script is genuinely truncated, restore its lost "
"content; if it is complete, add the terminal section."
)
return 1

print("All tutorial scripts have a terminal section — none look truncated.")
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1] if len(sys.argv) > 1 else "."))
20 changes: 20 additions & 0 deletions .github/workflows/tutorials_complete.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Tutorials Complete

# Guards against tutorial scripts being truncated / cut off mid-generation (a
# long file re-emitted with the output severed, losing every section below the
# cut). Each tutorial_*.py must end with a `__Wrap Up__` (or `__Summary__`)
# terminal section; the check is pure-stdlib and needs no library install.
# See scripts/check_tutorials_complete.py.

on: [push, pull_request]

jobs:
tutorials-complete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Check tutorials are not truncated
run: python .github/scripts/check_tutorials_complete.py .
Original file line number Diff line number Diff line change
Expand Up @@ -1019,7 +1019,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"Finish."
"__Wrap Up__\n",
"\n",
"This tutorial showed how to inspect the results of a model-fit: the maximum log likelihood instance, the full set of\n",
"samples, parameter estimates with errors at a given confidence, and how to output quantities to a LaTeX table. These\n",
"tools are the foundation for interpreting every model-fit you perform with **PyAutoFit**."
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,10 +533,31 @@
"# The mean field object also contains a dictionary of the s.d./variance**0.5.\n",
"# \"\"\"\n",
"# print(f\"Centre SD/sqrt(variance) = {mean_field.scale[centre_shared_prior]}\")\n",
"# print()\n"
"# print()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"__Wrap Up__\n",
"\n",
"This tutorial introduced Expectation Propagation (EP), which fits a graphical model by passing messages between its\n",
"factors rather than sampling the full joint parameter space at once. This makes inference scalable to graphs with many\n",
"datasets and shared parameters.\n",
"\n",
"The commented-out code above shows optional further inspection of the `MeanField` result (per-parameter means,\n",
"variances and standard deviations) which you can uncomment to explore."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [],
"outputs": [],
"execution_count": null
}
],
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,31 @@
"# ),\n",
"# ep_history=af.EPHistory(kl_tol=1.0),\n",
"# max_steps=5,\n",
"# )\n"
"# )"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"__Wrap Up__\n",
"\n",
"This optional tutorial showed how a hierarchical model can be fitted using Expectation Propagation (EP). The\n",
"`factor_graph.optimise` call above is commented out because a full EP fit is computationally expensive; uncomment it\n",
"to run the fit yourself.\n",
"\n",
"For the concepts behind hierarchical models and EP, see the main chapter 3 tutorials\n",
"(`tutorial_4_hierarchical_model` and `tutorial_5_expectation_propagation`)."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [],
"outputs": [],
"execution_count": null
}
],
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,9 +524,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"__Wrap Up__\n",
"\n",
"We can compare these values to those inferred in `tutorial_4_hierarchical_model`, which fits all datasets and the\n",
"hierarchical values of the parent Gaussian simultaneously.,\n",
" \n",
"hierarchical values of the parent Gaussian simultaneously.\n",
"\n",
"The errors for the fit performed in this tutorial are much larger. This is because of how in a graphical model\n",
"the \"datasets talk to one another\", which is described fully in that tutorials subsection \"Benefits of Graphical Model\"."
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -694,5 +694,9 @@ def model_data_from(self, xvalues: np.ndarray):
print(latex)

"""
Finish.
__Wrap Up__

This tutorial showed how to inspect the results of a model-fit: the maximum log likelihood instance, the full set of
samples, parameter estimates with errors at a given confidence, and how to output quantities to a LaTeX table. These
tools are the foundation for interpreting every model-fit you perform with **PyAutoFit**.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,14 @@ def log_likelihood_function(self, instance, xp=np):
# """
# print(f"Centre SD/sqrt(variance) = {mean_field.scale[centre_shared_prior]}")
# print()

"""
__Wrap Up__

This tutorial introduced Expectation Propagation (EP), which fits a graphical model by passing messages between its
factors rather than sampling the full joint parameter space at once. This makes inference scalable to graphs with many
datasets and shared parameters.

The commented-out code above shows optional further inspection of the `MeanField` result (per-parameter means,
variances and standard deviations) which you can uncomment to explore.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- **Model Fit**: Run the EP fit of the hierarchical model.
"""

# from autoconf import setup_notebook; setup_notebook()
# from autoconf import setup_notebook; setup_notebook()

import numpy as np
from os import path
Expand Down Expand Up @@ -228,3 +228,14 @@
# ep_history=af.EPHistory(kl_tol=1.0),
# max_steps=5,
# )

"""
__Wrap Up__

This optional tutorial showed how a hierarchical model can be fitted using Expectation Propagation (EP). The
`factor_graph.optimise` call above is commented out because a full EP fit is computationally expensive; uncomment it
to run the fit yourself.

For the concepts behind hierarchical models and EP, see the main chapter 3 tutorials
(`tutorial_4_hierarchical_model` and `tutorial_5_expectation_propagation`).
"""
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
- **Analysis + Search**: Create the analysis and search for the parent distribution fit.
"""

# from autoconf import setup_notebook; setup_notebook()
# from autoconf import setup_notebook; setup_notebook()

import numpy as np
from os import path
Expand Down Expand Up @@ -332,9 +332,11 @@ def probability_from_values(self, values: np.ndarray) -> float:
print()

"""
__Wrap Up__

We can compare these values to those inferred in `tutorial_4_hierarchical_model`, which fits all datasets and the
hierarchical values of the parent Gaussian simultaneously.,
hierarchical values of the parent Gaussian simultaneously.

The errors for the fit performed in this tutorial are much larger. This is because of how in a graphical model
the "datasets talk to one another", which is described fully in that tutorials subsection "Benefits of Graphical Model".
"""
Loading