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
47 changes: 47 additions & 0 deletions .github/workflows/notebooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Notebooks

# Execute the example notebooks end-to-end. These tests are heavy and excluded
# from the standard suite (marked `notebooks`, deselected by default via addopts
# in pyproject.toml), so they live in their own workflow.
#
# - Runs on PRs/pushes that touch the notebooks or their test harness.
# - Reusable: called from release.yml so a broken notebook blocks publishing.
# - Manually triggerable (workflow_dispatch).
on:
workflow_call:
workflow_dispatch:
push:
branches: [ main ]
paths:
- 'docs/examples/**.ipynb'
- 'tests/test_notebooks.py'
- 'pyproject.toml'
- '.github/workflows/notebooks.yml'
pull_request:
branches: [ main ]
paths:
- 'docs/examples/**.ipynb'
- 'tests/test_notebooks.py'
- 'pyproject.toml'
- '.github/workflows/notebooks.yml'

jobs:
test-notebooks:
name: Execute example notebooks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'

- name: Install package + notebook test deps
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"

- name: Execute example notebooks
run: pytest -m notebooks -v
9 changes: 8 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,17 @@ jobs:
- name: Run test suite against the installed package
run: pytest tests/ -v

# 2b. Execute the example notebooks end-to-end. These are heavy and excluded
# from the standard suite (marked `notebooks`, deselected by default via
# addopts), so they run via the dedicated reusable workflow.
test-notebooks:
name: Execute example notebooks
uses: ./.github/workflows/notebooks.yml

# 3. Publish to TestPyPI (trusted publishing — no API token).
publish-testpypi:
name: Publish to TestPyPI
needs: test-install
needs: [test-install, test-notebooks]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: testpypi
Expand Down
62 changes: 2 additions & 60 deletions docs/examples/pulse_smoothing.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -506,65 +506,7 @@
"id": "0b56bdb7-3415-4157-a2d8-abb4b96e2c9b",
"metadata": {},
"outputs": [],
"source": [
"alpha = 0.05\n",
"y_limits = [-8.5,3]\n",
"init_step = steps_2[0]\n",
"smoothed_step = -1\n",
"init_scaling = opt.history.parameters[init_step].shape[0]\n",
"smoothed_scaling = opt.history.parameters[smoothed_step].shape[0]\n",
"gates = [float(i)/init_scaling for i in range(-1,opt.history.parameters[init_step].shape[0]+1)]\n",
"smoothed_gates = [float(i)/smoothed_scaling for i in range(-1,opt.history.parameters[smoothed_step].shape[0]+1)]\n",
"\n",
"fig, axs = plt.subplots(1, 2, figsize=(10, 2.5), sharex=True)\n",
"a=0\n",
"for i, strength in enumerate(opt.history.parameters[init_step].T):\n",
" strength = [s*init_scaling for s in strength]\n",
" if opt.params.basis.labels[i] in opt.params.projected_basis.labels:\n",
" axs[a].step(gates, \n",
" np.append(np.array([0]), np.append(strength,np.array([0]))), \n",
" where='post', \n",
" label=opt.params.basis.plot_labels[i],\n",
" color=\"black\")\n",
" axs[a].fill_between(gates, \n",
" np.append(np.array([0]), np.append(strength,np.array([0]))), \n",
" step='post', \n",
" color=\"black\", \n",
" alpha=alpha)\n",
" a+=1\n",
"\n",
"a=0\n",
"for i, strength in enumerate(opt.history.parameters[smoothed_step].T):\n",
" strength = [s*smoothed_scaling for s in strength]\n",
" if opt.params.basis.labels[i] in opt.params.projected_basis.labels:\n",
" axs[a].step(smoothed_gates, \n",
" np.append(np.array([0]), np.append(strength,np.array([0]))), \n",
" where='post', \n",
" label=opt.params.basis.plot_labels[i],\n",
" color=\"green\")\n",
" axs[a].fill_between(smoothed_gates, \n",
" np.append(np.array([0]), np.append(strength,np.array([0]))), \n",
" step='post', \n",
" color=\"green\", \n",
" alpha=alpha)\n",
" a+=1\n",
"\n",
"for ax in axs:\n",
" # ax.legend(loc='upper right')\n",
" ax.grid(False)\n",
" ax.set_xlim(-0.01,gates[-1]+0.008)\n",
" # ax.set_ylim(y_limits[0],y_limits[1])\n",
"\n",
"axs[0].set_xlabel(\"Unit time, $t$\")\n",
"axs[1].set_xlabel(\"Unit time, $t$\")\n",
"\n",
"axs[0].set_ylabel(f\"$h_{{1}}(t)$\")\n",
"axs[1].set_ylabel(f\"$h_{{2}}(t)$\")\n",
"\n",
"plt.tight_layout()\n",
"os.makedirs(os.path.dirname(savefig), exist_ok=True)\n",
"plt.show()"
]
"source": "alpha = 0.05\ny_limits = [-8.5,3]\ninit_step = steps_2[0]\nsmoothed_step = -1\ninit_scaling = opt.history.parameters[init_step].shape[0]\nsmoothed_scaling = opt.history.parameters[smoothed_step].shape[0]\ngates = [float(i)/init_scaling for i in range(-1,opt.history.parameters[init_step].shape[0]+1)]\nsmoothed_gates = [float(i)/smoothed_scaling for i in range(-1,opt.history.parameters[smoothed_step].shape[0]+1)]\n\nfig, axs = plt.subplots(1, 2, figsize=(10, 2.5), sharex=True)\na=0\nfor i, strength in enumerate(opt.history.parameters[init_step].T):\n strength = [s*init_scaling for s in strength]\n if opt.params.basis.labels[i] in opt.params.projected_basis.labels:\n axs[a].step(gates, \n np.append(np.array([0]), np.append(strength,np.array([0]))), \n where='post', \n label=opt.params.basis.plot_labels[i],\n color=\"black\")\n axs[a].fill_between(gates, \n np.append(np.array([0]), np.append(strength,np.array([0]))), \n step='post', \n color=\"black\", \n alpha=alpha)\n a+=1\n\na=0\nfor i, strength in enumerate(opt.history.parameters[smoothed_step].T):\n strength = [s*smoothed_scaling for s in strength]\n if opt.params.basis.labels[i] in opt.params.projected_basis.labels:\n axs[a].step(smoothed_gates, \n np.append(np.array([0]), np.append(strength,np.array([0]))), \n where='post', \n label=opt.params.basis.plot_labels[i],\n color=\"green\")\n axs[a].fill_between(smoothed_gates, \n np.append(np.array([0]), np.append(strength,np.array([0]))), \n step='post', \n color=\"green\", \n alpha=alpha)\n a+=1\n\nfor ax in axs:\n # ax.legend(loc='upper right')\n ax.grid(False)\n ax.set_xlim(-0.01,gates[-1]+0.008)\n # ax.set_ylim(y_limits[0],y_limits[1])\n\naxs[0].set_xlabel(\"Unit time, $t$\")\naxs[1].set_xlabel(\"Unit time, $t$\")\n\naxs[0].set_ylabel(f\"$h_{{1}}(t)$\")\naxs[1].set_ylabel(f\"$h_{{2}}(t)$\")\n\nplt.tight_layout()\nplt.show()"
}
],
"metadata": {
Expand All @@ -588,4 +530,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
18 changes: 17 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,28 @@ where = ["src"]
# Default `pytest` runs only the test suite; benchmarks/ is opt-in and is run
# explicitly (`pytest benchmarks/`, requires the [benchmark] extra).
testpaths = ["tests"]
# Notebook-execution tests are heavy and excluded from the standard suite; they
# run only in the release pipeline (`pytest -m notebooks`, see release.yml).
addopts = "-m 'not notebooks'"
markers = [
"notebooks: end-to-end execution of the example notebooks (run with '-m notebooks')",
]
filterwarnings = [
"ignore:Casting complex values to real discards the imaginary part:numpy.exceptions.ComplexWarning:jax._src.lax.lax",
]

[project.optional-dependencies]
dev = ["pre-commit==3.7.0", "pytest==9.0.3", "pytest-benchmark==5.1.0", "black==24.3.0"]
dev = [
"pre-commit==3.7.0",
"pytest==9.0.3",
"pytest-benchmark==5.1.0",
"black==24.3.0",
# Notebook execution tests (tests/test_notebooks.py).
"nbconvert==7.16.6",
"nbformat==5.10.4",
"ipykernel==6.30.1",
"matplotlib==3.10.7",
]

docs = [
"matplotlib==3.10.7",
Expand Down
37 changes: 37 additions & 0 deletions tests/test_notebooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Execute the example notebooks end-to-end to guard against example rot.

Every notebook under ``docs/examples/`` is re-run from a clean state with
nbconvert's :class:`~nbconvert.preprocessors.ExecutePreprocessor`. A test
fails if any cell raises, surfacing the offending cell's traceback via
nbclient's ``CellExecutionError``.
"""

from pathlib import Path

import pytest

# Notebook deps live in the [dev] extra and are absent from the standard test
# job (which installs only `pip install -e .`). The `notebooks` marker keeps
# these tests deselected there, but collection still imports this module, so
# skip cleanly rather than erroring when the deps are missing.
nbformat = pytest.importorskip("nbformat")
ExecutePreprocessor = pytest.importorskip("nbconvert.preprocessors").ExecutePreprocessor

# docs/examples relative to the repo root (mirrors the path idiom in conftest.py).
EXAMPLES_DIR = Path(__file__).resolve().parent.parent / "docs" / "examples"
EXAMPLE_NOTEBOOKS = sorted(EXAMPLES_DIR.glob("*.ipynb"))

# Generous per-notebook ceiling so a hung cell fails rather than blocks forever.
NOTEBOOK_TIMEOUT = 900

# Heavy: excluded from the standard suite, run only in the release pipeline.
pytestmark = pytest.mark.notebooks


@pytest.mark.parametrize("notebook_path", EXAMPLE_NOTEBOOKS, ids=lambda p: p.name)
def test_example_notebook_runs(notebook_path):
"""Execute ``notebook_path`` and assert that no cell raises."""
nb = nbformat.read(notebook_path, as_version=4)
ep = ExecutePreprocessor(timeout=NOTEBOOK_TIMEOUT, kernel_name="python3")
# Run with the notebook's own directory as cwd so relative paths resolve.
ep.preprocess(nb, {"metadata": {"path": str(notebook_path.parent)}})
Loading