diff --git a/.github/workflows/demo-notebook.yml b/.github/workflows/demo-notebook.yml new file mode 100644 index 0000000..a948f73 --- /dev/null +++ b/.github/workflows/demo-notebook.yml @@ -0,0 +1,55 @@ +name: demo notebook + +# Builds examples/demo.py (a `# %%` cell file) into examples/demo.ipynb with jupytext, so it can be +# opened in Colab. On pushes to main the notebook is force-pushed to the orphan `demo` branch, which +# the README's "Open in Colab" badge points at; on pull requests it is only built, as a check. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install jupytext nbformat + - name: Convert the cell files to notebooks + run: | + set -e + for f in examples/*.py; do + case "$f" in examples/make_gif.py) continue;; esac + jupytext --to ipynb "$f" + done + ls -la examples/*.ipynb + - name: Publish to the demo branch + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + run: | + set -e + mkdir -p /tmp/demo/examples + cp examples/*.ipynb /tmp/demo/examples/ + cp README.md LICENSE /tmp/demo/ + cd /tmp/demo + git init -q -b demo + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + cat > README.md <<'EOF' + # liveplot demo notebooks + + Generated by CI from `examples/*.py` on `main`; do not edit here. + Open in Colab: https://colab.research.google.com/github/ARENA-education/liveplot/blob/demo/examples/demo.ipynb + EOF + git add -A + git commit -q -m "Build demo notebooks from main ${GITHUB_SHA::7}" + git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + git push -q --force origin demo + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..fb57020 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,17 @@ +name: tests + +on: + push: + branches: [main] + pull_request: + +jobs: + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: python -m pip install -e ".[test]" tqdm ipython numpy + - run: python -m pytest -q tests diff --git a/README.md b/README.md index c26ec53..da92886 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Live training curves in Jupyter, Colab and the VS Code / Cursor interactive window, with a tqdm bar underneath, at (almost) no cost to the training loop. +[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ARENA-education/liveplot/blob/demo/examples/demo.ipynb) [![tests](https://github.com/ARENA-education/liveplot/actions/workflows/tests.yml/badge.svg)](https://github.com/ARENA-education/liveplot/actions/workflows/tests.yml) + +Try it in Colab with the badge above: that notebook is [`examples/demo.py`](examples/demo.py), a cell-by-cell tour of the features, which CI converts with jupytext and publishes to the `demo` branch on every push to `main`. + ![training loss every step, eval loss and accuracy every 50 steps, on one panel with two y-axes](docs/demo.gif) The GIF is cell 2 of [`examples/demo.py`](examples/demo.py), a tour of the features in a cell-separated file: open it in the VS Code / Cursor interactive window and run it cell by cell. That cell trains a tiny numpy classifier, logging the training loss every step and the eval loss and accuracy every 50 steps, laid out as `"loss | eval_loss eval_acc"`. The GIF itself was recorded by the library: `examples/make_gif.py` runs the same loop as a plain script with `record="docs/demo.gif"`. diff --git a/examples/demo.py b/examples/demo.py index 740c132..782f541 100644 --- a/examples/demo.py +++ b/examples/demo.py @@ -1,12 +1,25 @@ # %% """ liveplot tour. A cell-separated file: run it cell by cell in the VS Code / Cursor interactive -window (or paste the cells into a notebook) to watch each plot update live. Running it as a plain -script also works, it just prints instead of drawing (except the recording cell, which renders). - - pip install git+https://github.com/ARENA-education/liveplot.git +window, open it in Colab (the README has the link: CI turns this file into a notebook on the +`demo` branch), or run it as a plain script, which prints instead of drawing (except the +recording cell, which renders). """ +# Setup: on Colab, install liveplot; anywhere else it is already installed. +import subprocess +import sys + +try: + import google.colab # noqa: F401 + + IN_COLAB = True +except ImportError: + IN_COLAB = False +if IN_COLAB: + subprocess.run([sys.executable, "-m", "pip", "install", "-q", "git+https://github.com/ARENA-education/liveplot.git"], check=True) + +# %% import math import random import time diff --git a/tests/test_liveplot.py b/tests/test_liveplot.py index e057b75..e38653c 100644 --- a/tests/test_liveplot.py +++ b/tests/test_liveplot.py @@ -181,6 +181,7 @@ def test_interrupt_inside_with_block_is_clean(fake_notebook): assert len(p.data["loss"][0]) == n_warmup + 31 and fake_notebook.frames, "final frame drawn, data kept" +@pytest.mark.skipif(sys.platform == "win32", reason="os.kill(pid, SIGINT) terminates the process on Windows") def test_render_child_ignores_sigint(fake_notebook): """Jupyter's interrupt goes to the whole process group; the renderer must shrug it off.""" p = LivePlot(refresh_seconds=0.1)