diff --git a/.github/scripts/check_tutorials_complete.py b/.github/scripts/check_tutorials_complete.py new file mode 100644 index 0000000..670b8dd --- /dev/null +++ b/.github/scripts/check_tutorials_complete.py @@ -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 ".")) diff --git a/.github/workflows/tutorials_complete.yml b/.github/workflows/tutorials_complete.yml new file mode 100644 index 0000000..a3fa780 --- /dev/null +++ b/.github/workflows/tutorials_complete.yml @@ -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 . diff --git a/notebooks/chapter_1_introduction/tutorial_5_results_and_samples.ipynb b/notebooks/chapter_1_introduction/tutorial_5_results_and_samples.ipynb index 0838255..2d29445 100644 --- a/notebooks/chapter_1_introduction/tutorial_5_results_and_samples.ipynb +++ b/notebooks/chapter_1_introduction/tutorial_5_results_and_samples.ipynb @@ -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**." ] }, { diff --git a/notebooks/chapter_3_graphical_models/tutorial_5_expectation_propagation.ipynb b/notebooks/chapter_3_graphical_models/tutorial_5_expectation_propagation.ipynb index 2b61581..29bd6a2 100644 --- a/notebooks/chapter_3_graphical_models/tutorial_5_expectation_propagation.ipynb +++ b/notebooks/chapter_3_graphical_models/tutorial_5_expectation_propagation.ipynb @@ -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": { diff --git a/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.ipynb b/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.ipynb index 4927a25..2c61f45 100644 --- a/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.ipynb +++ b/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.ipynb @@ -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": { diff --git a/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.ipynb b/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.ipynb index 7e3ba13..013d247 100644 --- a/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.ipynb +++ b/notebooks/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.ipynb @@ -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\"." ] diff --git a/scripts/chapter_1_introduction/tutorial_5_results_and_samples.py b/scripts/chapter_1_introduction/tutorial_5_results_and_samples.py index 50a39d9..467c515 100644 --- a/scripts/chapter_1_introduction/tutorial_5_results_and_samples.py +++ b/scripts/chapter_1_introduction/tutorial_5_results_and_samples.py @@ -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**. """ diff --git a/scripts/chapter_3_graphical_models/tutorial_5_expectation_propagation.py b/scripts/chapter_3_graphical_models/tutorial_5_expectation_propagation.py index ff0a73a..f190e54 100644 --- a/scripts/chapter_3_graphical_models/tutorial_5_expectation_propagation.py +++ b/scripts/chapter_3_graphical_models/tutorial_5_expectation_propagation.py @@ -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. +""" diff --git a/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.py b/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.py index d1e7201..e32c2b9 100644 --- a/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.py +++ b/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_ep.py @@ -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 @@ -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`). +""" diff --git a/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.py b/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.py index fefcc16..91e8ef9 100644 --- a/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.py +++ b/scripts/chapter_3_graphical_models/tutorial_optional_hierarchical_individual.py @@ -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 @@ -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". """