diff --git a/.github/workflows/open_in_cloud.yml b/.github/workflows/open_in_cloud.yml new file mode 100644 index 00000000..3bb244fe --- /dev/null +++ b/.github/workflows/open_in_cloud.yml @@ -0,0 +1,130 @@ +name: "Update notebooks for cloud environments" + +on: + push: + branches: + - "**" + - "!gh-pages" + - "!open-in-colab" + - "!open-in-kaggle" + pull_request: + branches: + - main + schedule: + - cron: "0 4 * * MON" + workflow_dispatch: + inputs: + branch: + description: "Branch on viskex repository" + type: string + +jobs: + open_in_colab: + strategy: + matrix: + include: + - backend: dolfinx + fem_on_colab_packages: | + gmsh@current%dolfinx.io + fenicsx==real@current$dolfinx + notebook_pattern: | + "**/tutorial_*_dolfinx.ipynb" + - backend: firedrake + fem_on_colab_packages: | + firedrake==real@current + notebook_pattern: | + "**/tutorial_*_firedrake.ipynb" + fail-fast: false + uses: fem-on-colab/open-in-colab-workflow/.github/workflows/workflow_call.yml@main + with: + work_directory: open_in_colab + notebook_pattern: ${{ matrix.notebook_pattern }} + notebook_preparation: | + python3 -m pip install --no-dependencies git+https://github.com/multiphenics/nbvalx.git + git clone https://github.com/viskex/viskex.git + cd viskex + if [ -n "${{ (inputs || github.event.inputs).branch }}" ]; then + git checkout "${{ (inputs || github.event.inputs).branch }}" + fi + NO_TESTS_COLLECTED=5 + python3 -m pytest --ipynb-action=create-notebooks --tag-collapse --work-dir=.ipynb_colab tutorials || (($?==$NO_TESTS_COLLECTED)) + find tutorials -type d -name .ipynb_colab -exec rsync -avz --remove-source-files --include="*.ipynb" --exclude="*" {}/ {}/.. \; + NOTEBOOKS_TO_INCLUDE="" + while read -r PATTERN; do + NOTEBOOKS_TO_INCLUDE="${NOTEBOOKS_TO_INCLUDE} --include=$(echo ${PATTERN} | sed 's|\"||g')" + done <<< $(printf "%s" "${{ matrix.notebook_pattern }}") + rsync -avz --delete --include "*/" ${NOTEBOOKS_TO_INCLUDE} --exclude="*" tutorials ../open_in_colab/ + fem_on_colab_packages: ${{ matrix.fem_on_colab_packages }} + pip_packages: | + viskex@https://github.com/viskex/viskex.git@current + test_script: | + apt install -y -qq libgl1-mesa-glx xvfb + export DISPLAY=":99" + Xvfb $DISPLAY -screen 0 1024x768x24 > /dev/null 2>&1 & + NOTEBOOKS_TO_TEST="" + while read -r PATTERN; do + NOTEBOOKS_TO_TEST="${NOTEBOOKS_TO_TEST} $(find open_in_colab -wholename $(echo ${PATTERN} | sed 's|\"||g'))" + done <<< $(printf "%s" "${{ matrix.notebook_pattern }}") + python3 -m pytest --nbval ${NOTEBOOKS_TO_TEST} + publish_on: github@viskex/viskex.github.io@open-in-colab + publish_if_repository: viskex/viskex.github.io + + open_in_kaggle: + strategy: + matrix: + include: + - backend: dolfinx + fem_on_kaggle_packages: | + gmsh@current%dolfinx.io + fenicsx==real@current$dolfinx + notebook_pattern: | + "**/tutorial_*_dolfinx.ipynb" + - backend: firedrake + fem_on_kaggle_packages: | + firedrake==real@current + notebook_pattern: | + "**/tutorial_*_firedrake.ipynb" + fail-fast: false + uses: fem-on-kaggle/open-in-kaggle-workflow/.github/workflows/workflow_call.yml@main + with: + work_directory: open_in_kaggle + notebook_pattern: ${{ matrix.notebook_pattern }} + notebook_preparation: | + python3 -m pip install --no-dependencies git+https://github.com/multiphenics/nbvalx.git + git clone https://github.com/viskex/viskex.git + cd viskex + if [ -n "${{ (inputs || github.event.inputs).branch }}" ]; then + git checkout "${{ (inputs || github.event.inputs).branch }}" + fi + NO_TESTS_COLLECTED=5 + python3 -m pytest --ipynb-action=create-notebooks --tag-collapse --work-dir=.ipynb_kaggle tutorials || (($?==$NO_TESTS_COLLECTED)) + find tutorials -type d -name .ipynb_kaggle -exec rsync -avz --remove-source-files --include="*.ipynb" --exclude="*" {}/ {}/.. \; + NOTEBOOKS_TO_INCLUDE="" + while read -r PATTERN; do + NOTEBOOKS_TO_INCLUDE="${NOTEBOOKS_TO_INCLUDE} --include=$(echo ${PATTERN} | sed 's|\"||g')" + done <<< $(printf "%s" "${{ matrix.notebook_pattern }}") + rsync -avz --delete --include "*/" ${NOTEBOOKS_TO_INCLUDE} --exclude="*" tutorials ../open_in_kaggle/ + fem_on_kaggle_packages: ${{ matrix.fem_on_kaggle_packages }} + pip_packages: | + viskex@https://github.com/viskex/viskex.git@current + test_script: | + apt install -y -qq libgl1-mesa-glx xvfb + export DISPLAY=":99" + Xvfb $DISPLAY -screen 0 1024x768x24 > /dev/null 2>&1 & + NOTEBOOKS_TO_TEST="" + while read -r PATTERN; do + NOTEBOOKS_TO_TEST="${NOTEBOOKS_TO_TEST} $(find open_in_kaggle -wholename $(echo ${PATTERN} | sed 's|\"||g'))" + done <<< $(printf "%s" "${{ matrix.notebook_pattern }}") + python3 -m pytest --nbval ${NOTEBOOKS_TO_TEST} + publish_on: github@viskex/viskex.github.io@open-in-kaggle + publish_if_repository: viskex/viskex.github.io + + warn: + runs-on: ubuntu-latest + if: github.repository == 'viskex/viskex.github.io' && github.ref == 'refs/heads/main' && github.event_name == 'schedule' + steps: + - name: Warn if scheduled workflow is about to be disabled + uses: fem-on-colab/warn-workflow-about-to-be-disabled-action@main + with: + workflow-filename: open_in_cloud.yml + days-elapsed: 50 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 00000000..2700f57c --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,215 @@ +name: "Website update" + +on: + push: + branches: + - "**" + - "!gh-pages" + pull_request: + branches: + - main + schedule: + - cron: "0 3 * * MON" + workflow_dispatch: + inputs: + branch: + description: "Branch on viskex repository" + type: string + reset_github_pages: + description: "Reset the gh-pages branch (yes or no; default: no). Remember to set the gh-pages branch back in Settings -> Pages!" + type: string + +jobs: + convert_notebooks: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - backend: dolfinx + container: dolfinx/dolfinx:nightly + setup_container: | + export DEBIAN_FRONTEND="noninteractive" + apt -y -qq update + apt install -y -qq libgl1-mesa-glx xvfb + notebook_pattern: | + "**/tutorial_*_dolfinx.ipynb" + - backend: firedrake + container: firedrakeproject/firedrake + setup_container: | + export DEBIAN_FRONTEND="noninteractive" + apt -y -qq update + apt install -y -qq libgl1-mesa-glx xorg xvfb + echo "/home/firedrake/firedrake/bin" >> $GITHUB_PATH + notebook_pattern: | + "**/tutorial_*_firedrake.ipynb" + fail-fast: false + container: + image: ${{ matrix.container }} + options: --user root + steps: + - name: Determine which branch to checkout when cloning source repository + id: source_branch + run: | + if [ -n "${{ (inputs || github.event.inputs).branch }}" ]; then + echo "branch=${{ (inputs || github.event.inputs).branch }}" >> ${GITHUB_OUTPUT} + else + echo "branch=main" >> ${GITHUB_OUTPUT} + fi + shell: bash + - name: Clone source repository on the previously computed branch + uses: actions/checkout@v3 + with: + repository: viskex/viskex + ref: ${{ steps.source_branch.outputs.branch }} + fetch-depth: 0 + - name: Clone website repository on current branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + path: _website + - name: Setup container + run: | + ${{ matrix.setup_container }} + python3 -m pip -q install nbconvert + - name: Install viskex + run: python3 -m pip install .[tutorials] + - name: Install nbconvert + run: python3 -m pip -q install nbconvert + - name: Copy jupyter template from website repository + run: | + jupyter --paths --json > /tmp/jupyter-paths + JUPYTER_SHARE=$(python3 -c 'import json; data = json.loads(open("/tmp/jupyter-paths", "r").read()); print(data["data"][-1])') + mkdir -p ${JUPYTER_SHARE}/nbconvert/templates + cp -rf _website/share/jupyter/nbconvert/templates/html/viskex ${JUPYTER_SHARE}/nbconvert/templates/ + rm /tmp/jupyter-paths + - name: Convert notebooks to html + run: | + NOTEBOOKS_TO_RUN=() + while read -r PATTERN; do + NOTEBOOKS_TO_RUN+=($(find tutorials -wholename $(echo ${PATTERN} | sed 's|\"||g'))) + done <<< $(printf "%s" "${{ matrix.notebook_pattern }}") + mkdir -p _build/html + export DISPLAY=":99" + Xvfb $DISPLAY -screen 0 1024x768x24 > /dev/null 2>&1 & + for NOTEBOOK in "${NOTEBOOKS_TO_RUN[@]}"; do + NOTEBOOK_DIRNAME=$(dirname "${NOTEBOOK}") + NOTEBOOK_OUTPUT_DIRNAME=${GITHUB_WORKSPACE}/_build/html/${NOTEBOOK_DIRNAME} + NOTEBOOK_BASENAME=$(basename "${NOTEBOOK}") + NOTEBOOK_OUTPUT_BASENAME=${NOTEBOOK_BASENAME/.ipynb/.html} + pushd ${NOTEBOOK_DIRNAME} + VISKEX_PYVISTA_BACKEND="panel" jupyter nbconvert --to html --template viskex --execute --output-dir ${NOTEBOOK_OUTPUT_DIRNAME} ${NOTEBOOK_BASENAME} + popd + pushd ${NOTEBOOK_OUTPUT_DIRNAME} + sed -i "s|\"https://colab.research.google.com\"|\"https://colab.research.google.com/github/viskex/viskex.github.io/blob/open-in-colab/${NOTEBOOK_DIRNAME}/${NOTEBOOK_BASENAME}\"|g" ${NOTEBOOK_OUTPUT_BASENAME} + sed -i "s|\"https://kaggle.com\"|\"https://kaggle.com/kernels/welcome?src=https://github.com/viskex/viskex.github.io/blob/open-in-kaggle/${NOTEBOOK_DIRNAME}/${NOTEBOOK_BASENAME}\"|g" ${NOTEBOOK_OUTPUT_BASENAME} + popd + done + shell: bash + - name: Store converted notebooks as artifacts + uses: actions/upload-artifact@v3 + with: + name: converted-notebooks-${{ matrix.backend }} + path: _build/html + retention-days: 1 + + publish_notebooks: + runs-on: ubuntu-latest + needs: [convert_notebooks] + steps: + - name: Clone website repository on current branch + uses: actions/checkout@v3 + - name: Clone website repository on gh-pages branch + uses: actions/checkout@v3 + with: + ref: gh-pages + fetch-depth: 0 + path: _build/html + - name: Download converted notebooks from artifacts + uses: actions/download-artifact@v3 + with: + path: _build/html + - name: Flatten the artifacts hierarchy + run: | + rsync -avh --remove-source-files _build/html/converted-notebooks-*/ _build/html/ + find _build/html/ -type d -empty -delete + - name: Upload release file to website + if: github.repository == 'viskex/viskex.github.io' && github.ref == 'refs/heads/main' + run: | + SHA_SHORT=$(git rev-parse --short HEAD) + pushd _build/html + git config user.name "GitHub Actions" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add . + git pull origin gh-pages + [ -n "$(git status --porcelain=v1 2>/dev/null)" ] && git commit -m "deploy notebooks: ${SHA_SHORT}" + git push origin gh-pages + popd + shell: bash + + website: + runs-on: ubuntu-latest + needs: [publish_notebooks] + steps: + - name: Clone website repository on current branch + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Clone website repository on gh-pages branch + uses: actions/checkout@v3 + with: + ref: gh-pages + fetch-depth: 0 + path: _build/html + - name: Install dependencies + run: | + pip3 -q install sphinx-material + - name: Generate sphinx website + run: | + rm -rf _build/html/* && make html + - name: Fix permissions + run: | + sudo chown $USER _build -R + - name: Clean up old Github pages branch + if: github.repository == 'viskex/viskex.github.io' && github.event.inputs.reset_github_pages == 'yes' + run: | + if git ls-remote origin | grep -sw gh-pages 2>&1 >/dev/null; then git push origin --delete gh-pages; fi + - name: Check that no tutorials have been deleted + run: | + pushd _build/html + if [[ $(git ls-files --deleted tutorials | wc -l) -gt 0 ]]; then + echo "The following tutorials have been deleted:" + git ls-files --deleted tutorials + exit 1 + fi + popd + - name: Deploy to GitHub pages + if: github.repository == 'viskex/viskex.github.io' && github.ref == 'refs/heads/main' + run: | + SHA_SHORT=$(git rev-parse --short HEAD) + pushd _build/html + git config user.name "GitHub Actions" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add . + git pull origin gh-pages + [ -n "$(git status --porcelain=v1 2>/dev/null)" ] && git commit -m "deploy website: ${SHA_SHORT}" + git push origin gh-pages + popd + shell: bash + - name: Deploy to GitHub artifacts + if: github.repository == 'viskex/viskex.github.io' && github.ref != 'refs/heads/main' + uses: actions/upload-artifact@v3 + with: + name: website + path: _build/html + retention-days: 1 + + warn: + runs-on: ubuntu-latest + if: github.repository == 'viskex/viskex.github.io' && github.ref == 'refs/heads/main' && github.event_name == 'schedule' + steps: + - uses: actions/checkout@v3 + - name: Warn if scheduled workflow is about to be disabled + uses: fem-on-colab/warn-workflow-about-to-be-disabled-action@main + with: + workflow-filename: website.yml + days-elapsed: 50 diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/_ext/ext.py b/_ext/ext.py new file mode 100644 index 00000000..9bdff070 --- /dev/null +++ b/_ext/ext.py @@ -0,0 +1,191 @@ +import os +import subprocess +from docutils import nodes +from docutils.parsers.rst import Directive +from tutorials import tutorials +import sphinx_material + +class Tutorials(Directive): + + def run(self): + output = list() + # General text + intro = f""" +
+viskex is accompanied by a few tutorials, that can be run on JupyterLab through a local installation of the library, or on cloud computing platforms such as Google Colab and Kaggle. +
+""" + output.append(nodes.raw(text=intro, format="html")) + # Tutorials + for num in tutorials.keys(): + data = tutorials[num] + steps = data["steps"] + buttons = "" + for step_description in steps: + step_files = steps[step_description] + if len(step_files) == 1: + buttons += self._button(step_description, step_files) + else: + buttons += self._dropdown(step_description, step_files) + + card_num = self._card( + num=num, + title=data["title"], + description=data["description"], + buttons=buttons + ) + output.append(nodes.raw(text=card_num, format="html")) + return output + + @staticmethod + def _card(num, title, description, buttons): + return f""" +{description}
+