From 567d927ca55e4d97127fbc4b68c6fcce2668719c Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 14:52:30 +0100 Subject: [PATCH 01/12] Add examples to documentation --- .gitignore | 1 + examples/example_1_finite_rotations.ipynb | 15 +++- ...ple_2_core_mesh_generation_functions.ipynb | 13 +++- src/beamme/core/mesh.py | 3 +- src/beamme/utils/environment.py | 5 ++ src/beamme/utils/visualization.py | 77 +++++++++++++++++++ website/README.md | 2 +- website/docs/prepare_docs.py | 24 ++++-- website/docs/source/conf.py | 39 ++++++++++ website/docs/source/index.rst | 1 + website/docs/source/index_examples.rst | 8 ++ 11 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 src/beamme/utils/visualization.py create mode 100644 website/docs/source/index_examples.rst diff --git a/.gitignore b/.gitignore index 5099893d..92feec23 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ api-documentation/* # website folder which contains automatically copied files website/docs/source/md/* +website/docs/source/examples/* ### Automatically created ignores ### diff --git a/examples/example_1_finite_rotations.ipynb b/examples/example_1_finite_rotations.ipynb index 28aec940..f990bb1b 100644 --- a/examples/example_1_finite_rotations.ipynb +++ b/examples/example_1_finite_rotations.ipynb @@ -4,15 +4,22 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Example 1: Introduction to finite rotations in BeamMe\n", - "$\n", + "$$\n", "% Define TeX macros for this document\n", "\\def\\vv#1{\\boldsymbol{#1}}\n", "\\def\\mm#1{\\boldsymbol{#1}}\n", "\\def\\R#1{\\mathbb{R}^{#1}}\n", "\\def\\SO{SO(3)}\n", "\\def\\triad{\\mm{\\Lambda}}\n", - "$\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example 1: Introduction to finite rotations in BeamMe\n", + "\n", "When working with Cosserat continua in 3D, the mathematical treatment of finite rotations is required.\n", "This example gives an overview of the finite rotation functionality in BeamMe.\n", "For a more comprehensive and theoretical overview of finite rotations, the interested reader is referred to:\n", @@ -220,7 +227,7 @@ "import vtk # We need to import vtk before pyvista for the TeX labels to work # noqa: F401, I001\n", "import pyvista as pv\n", "\n", - "pv.set_jupyter_backend(\"trame\")\n", + "pv.set_jupyter_backend(\"static\")\n", "\n", "\n", "# Utility functionality for this example\n", diff --git a/examples/example_2_core_mesh_generation_functions.ipynb b/examples/example_2_core_mesh_generation_functions.ipynb index bc56e011..641d79b7 100644 --- a/examples/example_2_core_mesh_generation_functions.ipynb +++ b/examples/example_2_core_mesh_generation_functions.ipynb @@ -4,11 +4,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Example 2: Basic mesh generation functions\n", - "$\n", + "$$\n", "% Define TeX macros for this document\n", "\\def\\vv#1{\\boldsymbol{#1}}\n", - "$\n", + "$$" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example 2: Basic mesh generation functions\n", + "\n", "This example shall showcase the core mesh generation functions provided in BeamMe." ] }, diff --git a/src/beamme/core/mesh.py b/src/beamme/core/mesh.py index 553342e5..fdb909ad 100644 --- a/src/beamme/core/mesh.py +++ b/src/beamme/core/mesh.py @@ -68,6 +68,7 @@ from beamme.utils.nodes import get_nodal_coordinates as _get_nodal_coordinates from beamme.utils.nodes import get_nodal_quaternions as _get_nodal_quaternions from beamme.utils.nodes import get_nodes_by_function as _get_nodes_by_function +from beamme.utils.visualization import show_plotter as _show_plotter class Mesh: @@ -999,7 +1000,7 @@ def display_pyvista( plotter.add_mesh(solid_grid, color="white", show_edges=True, opacity=0.5) if not _is_testing(): - plotter.show() + _show_plotter(plotter) else: return plotter diff --git a/src/beamme/utils/environment.py b/src/beamme/utils/environment.py index 4dbd05de..eebb9cf7 100644 --- a/src/beamme/utils/environment.py +++ b/src/beamme/utils/environment.py @@ -48,6 +48,11 @@ def is_mybinder(): return "BINDER_LAUNCH_HOST" in _os.environ.keys() +def is_nbsphinx(): + """Check if the current environment is running in nbsphinx.""" + return "IS_NBSPHINX" in _os.environ + + def is_testing(): """Check if the current environment is a pytest testing run.""" return "PYTEST_CURRENT_TEST" in _os.environ diff --git a/src/beamme/utils/visualization.py b/src/beamme/utils/visualization.py new file mode 100644 index 00000000..9dc0a7b2 --- /dev/null +++ b/src/beamme/utils/visualization.py @@ -0,0 +1,77 @@ +# The MIT License (MIT) +# +# Copyright (c) 2018-2025 BeamMe Authors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Helper functions for visualizations.""" + +import os as _os +import uuid as _uuid +from pathlib import Path as _Path + +import ipywidgets as _widgets +import pyvista as _pv +from IPython.display import IFrame as _IFrame +from IPython.display import display as _display + +from beamme.utils.environment import is_nbsphinx as _is_nbsphinx + + +def show_plotter(plotter: _pv.Plotter) -> None: + """Show a PyVista plotter. + + This wrapper either directly displays the plotter, default + development use for BeamMe. For the website representation of the + examples, we export static and interactive versions of the plotter + that will be embedded in the documentation. + """ + + if not _is_nbsphinx(): + plotter.show() + else: + # Path where static documents are stored for the website. + static_doc_path = _Path(_os.environ["PYVISTA_DOCS_STATIC"]) + + # Get a unique identifier for the current plotter + plotter_uid = str(_uuid.uuid4().hex) + + # Export screenshot and html representation of the plotter + plotter.export_html(static_doc_path / f"{plotter_uid}.html") + plotter.screenshot(static_doc_path / f"{plotter_uid}.png") + + # Setup the html to display on the website + static_frame_html = f""" + + """ + interactive_frame = _IFrame( + src=f"../_static/pyvista/{plotter_uid}.html", + width="100%", + height=600, + ) + tab = _widgets.Tab( + children=[ + _widgets.HTML(static_frame_html), + _widgets.HTML(interactive_frame._repr_html_()), + ] + ) + tab.set_title(0, "Static Scene") + tab.set_title(1, "Interactive Scene") + + _display(tab) diff --git a/website/README.md b/website/README.md index f4b8a956..875f784f 100644 --- a/website/README.md +++ b/website/README.md @@ -18,7 +18,7 @@ In the source directory of BeamMe simply execute python website/docs/prepare_docs.py ``` -to prepare the documents for the website build (currently only the readme gets copied into the website build directory). +to prepare the documents for the website build. Then build the website with diff --git a/website/docs/prepare_docs.py b/website/docs/prepare_docs.py index 51444d77..3ae24359 100644 --- a/website/docs/prepare_docs.py +++ b/website/docs/prepare_docs.py @@ -29,17 +29,31 @@ def prepare_docs(): """Prepare documentation for the website. - Currently, this only copies the README.md file to the documentation - directory. - """ + This function copies all relevant files for the website. - markdown_dir = Path("website/docs/source/md") + Note: We remove the target directories before copying the files to ensure + that no stale files remain. + """ # create directory which contains all the markdown files + markdown_dir = Path("website/docs/source/md") + shutil.rmtree(markdown_dir) os.makedirs(markdown_dir, exist_ok=True) # copy readme - shutil.copy("README.md", os.path.join(markdown_dir, "README.md")) + shutil.copy("README.md", markdown_dir / "README.md") + + # create directory which contains all the example files + examples_source_dir = Path("examples") + examples_target_dir = Path("website/docs/source/examples") + shutil.rmtree(examples_target_dir) + file_extensions_to_copy = {".ipynb", ".py"} + for file in examples_source_dir.rglob("*"): + if file.is_file() and file.suffix.lower() in file_extensions_to_copy: + rel_path = file.relative_to(examples_source_dir) + dest_path = examples_target_dir / rel_path + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy(file, dest_path) if __name__ == "__main__": diff --git a/website/docs/source/conf.py b/website/docs/source/conf.py index 8140e4f8..ae68cc06 100644 --- a/website/docs/source/conf.py +++ b/website/docs/source/conf.py @@ -20,6 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import os +from pathlib import Path + # general configuration project = "BeamMe" copyright = "2025, BeamMe Authors" @@ -36,6 +39,7 @@ # extensions extensions = [ "myst_parser", # to enable Markdown support + "nbsphinx", # to enable Jupyter Notebook support "sphinxcontrib.jquery", # to enable custom JavaScript (open links in new empty tab) ] @@ -49,3 +53,38 @@ # JavaScript configuration (to open links in new tabs) html_js_files = ["js/custom.js"] html_static_path = ["static"] + +# Always execute notebooks to ensure outputs are up-to-date +nbsphinx_execute = "always" + +# Prolog for nbsphinx to add a note with a link to the GitHub source and Binder +# at the top of each rendered html page +nbsphinx_prolog = r""" +{% set docname = env.doc2path(env.docname, base=False)|string %} +{% set binder_path = "/doc/tree/" ~ docname %} +{% set binder_urlpath = binder_path | urlencode %} + +.. raw:: html + +
+ This page was generated from + + {{ docname|e }} + . +
+ Interactive online version: + + Binder badge + +
+""" + +# Create the directory for static files created by pyvista plots +PYVISTA_DOCS_STATIC = Path(__file__).parent.parent / "build" / "_static" / "pyvista" +PYVISTA_DOCS_STATIC.mkdir(parents=True, exist_ok=True) +os.environ["PYVISTA_DOCS_STATIC"] = str(PYVISTA_DOCS_STATIC) + +# Set a flag that we are building the docs with nbsphinx +os.environ["IS_NBSPHINX"] = "1" diff --git a/website/docs/source/index.rst b/website/docs/source/index.rst index 4136b810..d80410b6 100644 --- a/website/docs/source/index.rst +++ b/website/docs/source/index.rst @@ -5,6 +5,7 @@ :maxdepth: 1 :hidden: + Examples API Documentation Coverage Report Github diff --git a/website/docs/source/index_examples.rst b/website/docs/source/index_examples.rst new file mode 100644 index 00000000..6cb3b5c3 --- /dev/null +++ b/website/docs/source/index_examples.rst @@ -0,0 +1,8 @@ +Examples +======== + +.. toctree:: + :maxdepth: 1 + :glob: + + examples/* From c676119599e62c48d94c1d90957972b90f802b01 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 16:38:42 +0100 Subject: [PATCH 02/12] Update how visualization is handled in the examples --- examples/example_1_finite_rotations.ipynb | 6 ---- examples/utils/example_1_utils.py | 13 ++++++-- examples/utils/general_utils.py | 40 ----------------------- src/beamme/utils/visualization.py | 5 +++ 4 files changed, 16 insertions(+), 48 deletions(-) delete mode 100644 examples/utils/general_utils.py diff --git a/examples/example_1_finite_rotations.ipynb b/examples/example_1_finite_rotations.ipynb index f990bb1b..e1d634f0 100644 --- a/examples/example_1_finite_rotations.ipynb +++ b/examples/example_1_finite_rotations.ipynb @@ -224,12 +224,6 @@ "metadata": {}, "outputs": [], "source": [ - "import vtk # We need to import vtk before pyvista for the TeX labels to work # noqa: F401, I001\n", - "import pyvista as pv\n", - "\n", - "pv.set_jupyter_backend(\"static\")\n", - "\n", - "\n", "# Utility functionality for this example\n", "from utils.example_1_utils import (\n", " PyVistaPlotter,\n", diff --git a/examples/utils/example_1_utils.py b/examples/utils/example_1_utils.py index 72bb1afc..a8bc16de 100644 --- a/examples/utils/example_1_utils.py +++ b/examples/utils/example_1_utils.py @@ -21,12 +21,17 @@ # THE SOFTWARE. """This file contains utility functions for the first example.""" +# We need to import vtk before pyvista for the TeX labels to work +import vtk # noqa: F401, I001 + import numpy as np import pyvista as pv +import sys from beamme.utils.environment import is_testing -from .general_utils import reset_print_out +# Import beamme visualization module to setup the proper rendering parameters +import beamme.utils.visualization # noqa: F401 def print_matrix(name, matrix): @@ -112,6 +117,10 @@ def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs + # PyVista changes the printout, so store the original print out + # and restore it when done. + self.stdout = sys.stdout + def __enter__(self): """Return the plotter with the given arguments.""" @@ -126,4 +135,4 @@ def __exit__(self, exc_type, exc_value, traceback): """ if not is_testing(): self.plotter.show() - reset_print_out() + sys.stdout = self.stdout diff --git a/examples/utils/general_utils.py b/examples/utils/general_utils.py deleted file mode 100644 index 2a313393..00000000 --- a/examples/utils/general_utils.py +++ /dev/null @@ -1,40 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2018-2025 BeamMe Authors -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -"""This file contains utility functions for the examples.""" - -import sys - -import pyvista as pv - -from beamme.utils.environment import is_mybinder - -# Store the default system out, so we can reset it after pyvista changes it -stdout = sys.stdout - -# If we are on mybinder, set to server side rendering -if is_mybinder(): - pv.start_xvfb() - - -def reset_print_out(): - """PyVista changes the printout, this resets it to the default.""" - sys.stdout = stdout diff --git a/src/beamme/utils/visualization.py b/src/beamme/utils/visualization.py index 9dc0a7b2..237bc413 100644 --- a/src/beamme/utils/visualization.py +++ b/src/beamme/utils/visualization.py @@ -30,8 +30,13 @@ from IPython.display import IFrame as _IFrame from IPython.display import display as _display +from beamme.utils.environment import is_mybinder as _is_mybinder from beamme.utils.environment import is_nbsphinx as _is_nbsphinx +# Start virtual framebuffer for MyBinder environments +if _is_mybinder(): + _pv.start_xvfb() + def show_plotter(plotter: _pv.Plotter) -> None: """Show a PyVista plotter. From 6f614ec531d1508e816a3748c5dfd9dd2b4c7e94 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:08:43 +0100 Subject: [PATCH 03/12] TEMP: activate website build --- .github/workflows/website.yml | 69 ++++++----------------------------- 1 file changed, 11 insertions(+), 58 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 7b566a33..507d48ce 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -1,22 +1,21 @@ name: Build and deploy website on: - # Only build and deploy the website once the documentation is built (and the test suite is completed => coverage report and badge are necessary) - workflow_run: - workflows: [Build documentation] - types: [completed] - branches: [main] - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false + schedule: + - cron: '0 06 * * *' + push: + branches: + - main + pull_request: + types: + - opened + - reopened + - synchronize + workflow_dispatch: jobs: build_website: # Only run if documentation was built successfully (if it is skipped, it is not successful) - if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -40,49 +39,3 @@ jobs: with: name: website path: website/docs/build/ - - deploy: - environment: - name: Website - # TODO Check if this url is really necessary or if it works without it - url: http://beamme-py.github.io/beamme - runs-on: ubuntu-latest - needs: build_website - permissions: - pages: write - id-token: write - contents: read - steps: - - name: Download website artifact - uses: actions/download-artifact@v4 - with: - name: website - path: ${{ github.workspace }} - - name: Download API documentation artifact - uses: dawidd6/action-download-artifact@v8 - with: - workflow: documentation.yml - name: api-documentation - path: ${{ github.workspace }}/api-documentation - branch: ${{ github.event.repository.default_branch }} - - name: Download coverage report artifact - uses: dawidd6/action-download-artifact@v8 - with: - workflow: testing_protected.yml - name: coverage-report - path: ${{ github.workspace }}/coverage-report - branch: ${{ github.event.repository.default_branch }} - - name: Download coverage badge artifact - uses: dawidd6/action-download-artifact@v8 - with: - workflow: testing_protected.yml - name: coverage-badge - path: ${{ github.workspace }}/coverage-badge - branch: ${{ github.event.repository.default_branch }} - - name: Upload all pages artifacts (website, coverage-report, coverage-badge, documentation) - uses: actions/upload-pages-artifact@v3 - id: deployment - with: - path: ${{ github.workspace }} - - name: Deploy to GitHub Pages - uses: actions/deploy-pages@v4 From 87f23a8a71ed1f61e235bee1c352ce34838358de Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:16:49 +0100 Subject: [PATCH 04/12] Only remove existing directories --- website/docs/prepare_docs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/docs/prepare_docs.py b/website/docs/prepare_docs.py index 3ae24359..01b72213 100644 --- a/website/docs/prepare_docs.py +++ b/website/docs/prepare_docs.py @@ -37,7 +37,8 @@ def prepare_docs(): # create directory which contains all the markdown files markdown_dir = Path("website/docs/source/md") - shutil.rmtree(markdown_dir) + if markdown_dir.exists(): + shutil.rmtree(markdown_dir) os.makedirs(markdown_dir, exist_ok=True) # copy readme @@ -46,7 +47,8 @@ def prepare_docs(): # create directory which contains all the example files examples_source_dir = Path("examples") examples_target_dir = Path("website/docs/source/examples") - shutil.rmtree(examples_target_dir) + if examples_target_dir.exists(): + shutil.rmtree(examples_target_dir) file_extensions_to_copy = {".ipynb", ".py"} for file in examples_source_dir.rglob("*"): if file.is_file() and file.suffix.lower() in file_extensions_to_copy: From 72b194c0b1e60f118d70d95285819a3bced5be68 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:18:25 +0100 Subject: [PATCH 05/12] Add missing package --- website/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/website/requirements.txt b/website/requirements.txt index 8a10cd84..d9857aa6 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -1,5 +1,6 @@ linkify-it-py myst-parser +nbsphinx pydata-sphinx-theme sphinx sphinxcontrib-jquery From 34e8bb624865c191ce013cd3d6414f8f5c667646 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:23:37 +0100 Subject: [PATCH 06/12] Install beamme in website --- .github/workflows/website.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 507d48ce..5e9210ee 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -28,6 +28,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r website/requirements.txt + pip install -e .[dev,fourc] - name: Prepare docs for the website run: | python website/docs/prepare_docs.py From 64236599dea9e7b33336255a735aad958844d8d9 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:27:40 +0100 Subject: [PATCH 07/12] Add libs --- .github/workflows/build_binder.yml | 21 ---- .github/workflows/check_code.yml | 30 ----- .github/workflows/check_meshpy_redirects.yml | 27 ----- .github/workflows/documentation.yml | 37 ------ .github/workflows/testing.yml | 49 -------- .github/workflows/testing_protected.yml | 120 ------------------- .github/workflows/website.yml | 1 + 7 files changed, 1 insertion(+), 284 deletions(-) delete mode 100644 .github/workflows/build_binder.yml delete mode 100644 .github/workflows/check_code.yml delete mode 100644 .github/workflows/check_meshpy_redirects.yml delete mode 100644 .github/workflows/documentation.yml delete mode 100644 .github/workflows/testing.yml delete mode 100644 .github/workflows/testing_protected.yml diff --git a/.github/workflows/build_binder.yml b/.github/workflows/build_binder.yml deleted file mode 100644 index fb6b6b29..00000000 --- a/.github/workflows/build_binder.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Trigger Binder build -on: - push: - branches: - - main - -# Allow only one concurrent build, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "build_binder" - cancel-in-progress: false - -jobs: - trigger-binder-build: - runs-on: ubuntu-latest - steps: - - uses: s-weigand/trigger-mybinder-build@v1 - with: - target-repo: beamme-py/beamme/HEAD - service-name: gh - debug: true diff --git a/.github/workflows/check_code.yml b/.github/workflows/check_code.yml deleted file mode 100644 index 0d2ee751..00000000 --- a/.github/workflows/check_code.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Code quality - -on: - schedule: - - cron: '0 06 * * *' - push: - branches: - - main - pull_request: - types: - - opened - - reopened - - synchronize - workflow_dispatch: - type: choice - -jobs: - code-check: - name: Code check - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup virtual python environment - uses: ./.github/actions/setup_virtual_python_environment - - name: Code quality checks - uses: ./.github/actions/code_check diff --git a/.github/workflows/check_meshpy_redirects.yml b/.github/workflows/check_meshpy_redirects.yml deleted file mode 100644 index 76040269..00000000 --- a/.github/workflows/check_meshpy_redirects.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Check MeshPy to BeamMe redirects - -on: - schedule: - - cron: "0 06 * * *" - -jobs: - check-redirects: - runs-on: ubuntu-latest - steps: - - name: Check GitHub repo redirect - run: | - final_url=$(curl -sL -o /dev/null -w "%{url_effective}" "https://github.com/imcs-compsim/meshpy/") - echo "Repo redirect: $final_url" - [ "$final_url" = "https://github.com/beamme-py/beamme" ] || { - echo "Repo redirect failed" - exit 1 - } - - - name: Check GitHub Pages meta redirect - run: | - redirect_url=$(curl -s "https://imcs-compsim.github.io/meshpy/" | grep -i 'http-equiv="refresh"' | sed -E 's/.*content="[0-9]+;\s*URL=([^"]*)".*/\1/I') - echo "Pages redirect: $redirect_url" - [ "$redirect_url" = "https://beamme-py.github.io/beamme/" ] || { - echo "Pages redirect failed" - exit 1 - } diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index f0abf1ef..00000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Build documentation - -on: - # Only build the documentation once the test suite has completed on the main branch - workflow_run: - workflows: [Protected test suite (can access cubit secrets)] - types: [completed] - branches: [main] - -jobs: - build_documentation: - name: Build documentation - # Only run if a push to main occurred, do not run after nightly cron job - if: ${{ github.event.workflow_run.event == 'push' }} - runs-on: ubuntu-latest - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup virtual python environment - uses: ./.github/actions/setup_virtual_python_environment - - name: Install dependencies - run: | - cd ${GITHUB_WORKSPACE} - source python-workflow-venv/bin/activate - pip install -e .[dev,fourc] - - name: Build API documentation - run: | - source python-workflow-venv/bin/activate - pdoc --math --docformat google --output-dir api-documentation src/beamme/ - - name: Upload API documentation artifact - uses: actions/upload-artifact@v4 - with: - name: api-documentation - path: api-documentation/ diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml deleted file mode 100644 index b90926bd..00000000 --- a/.github/workflows/testing.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Test suite - -on: - schedule: - - cron: '0 06 * * *' - push: - branches: - - main - pull_request: - types: - - opened - - reopened - - synchronize - workflow_dispatch: - -jobs: - beamme-testing: - name: ${{ matrix.os-version }} python${{ matrix.python-version }} - strategy: - fail-fast: false - matrix: - os-version: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.10", "3.13"] - runs-on: ${{ matrix.os-version }} - steps: - - name: Checkout PR code - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Run the test suite - uses: ./.github/actions/run_tests - with: - # Test coverage and editable install with Python 3.10, otherwise we use a - # non-editable installation and turn of coverage, because the coverage - # only works in editable mode. - install-command: >- - ${{ matrix.python-version == '3.10' && '-e .[dev,fourc]' || '.[dev,fourc]'}} - # The single space in the empty string is required, otherwise GitHub - # evaluates the if clause wrong. - additional-pytest-flags: >- - ${{ matrix.python-version == '3.10' && ' ' || '--no-cov' }} - - name: Upload test results on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: ${{ github.job }}-${{ matrix.os-version }}-python${{ matrix.python-version }}-${{ github.run_number }} - path: ${{ env.PYTEST_TMPDIR }} diff --git a/.github/workflows/testing_protected.yml b/.github/workflows/testing_protected.yml deleted file mode 100644 index 1e5c775b..00000000 --- a/.github/workflows/testing_protected.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Protected test suite (can access cubit secrets) - -on: - schedule: - - cron: '0 06 * * *' - push: - branches: - - main - pull_request_target: - types: - - opened - - reopened - - synchronize - workflow_dispatch: - -env: - CUBIT_DOWNLOAD_URL: https://f002.backblazeb2.com/file/cubit-downloads/Coreform-Cubit/Releases/Linux/Coreform-Cubit-2025.8%2B61943-Lin64.tar.gz - -jobs: - beamme-testing-all-dependencies: - name: ubuntu-latest with all dependencies - runs-on: ubuntu-latest - environment: - # Use the trusted environment only for PRs authored by an COLLABORATOR (no approval needed); all others use the untrusted environment (someone has to approve the workflow run). - # Otherwise (scheduled or merge triggers) use the trusted environment (no approval needed). - name: ${{ github.event_name == 'pull_request_target' && (github.event.pull_request.author_association == 'COLLABORATOR') && 'cubit_secrets_trusted' || (github.event_name == 'pull_request_target' && 'cubit_secrets_untrusted' || 'cubit_secrets_trusted') }} - container: - image: ghcr.io/4c-multiphysics/4c:main - defaults: - run: - shell: bash - steps: - - name: Checkout PR code - uses: actions/checkout@v4 - with: - submodules: true - # For PR runs, checkout PR code; otherwise fallback to default branch or ref - ref: ${{ github.event.pull_request.head.ref || github.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - name: Setup cubit - id: cubit - uses: ./.github/actions/cubit_setup - with: - cubit_download_url: ${{ env.CUBIT_DOWNLOAD_URL }} - cubit_email: ${{ secrets.CUBIT_EMAIL }} - cubit_password: ${{ secrets.CUBIT_PASSWORD }} - - name: Setup virtual python environment - uses: ./.github/actions/setup_virtual_python_environment - - name: Build ArborX geometric search - uses: ./.github/actions/build_arborx_geometric_search - - name: Run the test suite - uses: ./.github/actions/run_tests - with: - source-command: "source python-workflow-venv/bin/activate" - install-command: "-e .[cubitpy,dev,fourc]" - additional-pytest-flags: "--4C --ArborX --CubitPy --cov-fail-under=93" - cubit-root: ${{ steps.cubit.outputs.cubit_root }} - - name: Free cubit license - if: ${{ always() }} - uses: ./.github/actions/cubit_finalize - with: - cubit-root: ${{ steps.cubit.outputs.cubit_root }} - - name: Upload test results on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: ${{ github.job }}-${{ github.run_number }} - path: ${{ env.PYTEST_TMPDIR }} - - name: Coverage badge and report - uses: ./.github/actions/coverage - - beamme-performance-testing: - needs: beamme-testing-all-dependencies - name: performance tests - continue-on-error: true - runs-on: ubuntu-latest - environment: - # Use the trusted environment only for PRs authored by an COLLABORATOR (no approval needed); all others use the untrusted environment (someone has to approve the workflow run). - # Otherwise (scheduled or merge triggers) use the trusted environment (no approval needed). - name: ${{ github.event_name == 'pull_request_target' && (github.event.pull_request.author_association == 'COLLABORATOR') && 'cubit_secrets_trusted' || (github.event_name == 'pull_request_target' && 'cubit_secrets_untrusted' || 'cubit_secrets_trusted') }} - container: - image: ghcr.io/4c-multiphysics/4c:main - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: true - # For PR runs, checkout PR code; otherwise fallback to default branch or ref - ref: ${{ github.event.pull_request.head.ref || github.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - name: Setup cubit - id: cubit - uses: ./.github/actions/cubit_setup - with: - cubit_download_url: ${{ env.CUBIT_DOWNLOAD_URL }} - cubit_email: ${{ secrets.CUBIT_EMAIL }} - cubit_password: ${{ secrets.CUBIT_PASSWORD }} - - name: Setup virtual python environment - uses: ./.github/actions/setup_virtual_python_environment - - name: Run the test suite - uses: ./.github/actions/run_tests - with: - install-command: ".[cubitpy,dev,fourc]" - source-command: "source python-workflow-venv/bin/activate" - additional-pytest-flags: "--performance-tests --exclude-standard-tests -s --no-cov" - cubit-root: ${{ steps.cubit.outputs.cubit_root }} - - name: Free cubit license - if: ${{ always() }} - uses: ./.github/actions/cubit_finalize - with: - cubit-root: ${{ steps.cubit.outputs.cubit_root }} - - name: Upload test results on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: ${{ github.job }}-${{ github.run_number }} - path: ${{ env.PYTEST_TMPDIR }} diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 5e9210ee..237cb6df 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -26,6 +26,7 @@ jobs: python-version: '3.13' - name: Install dependencies run: | + sudo apt-get install -y libosmesa6-dev python -m pip install --upgrade pip pip install -r website/requirements.txt pip install -e .[dev,fourc] From aa2af9b26e9847b93e036359f353ed0a7791aed7 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:30:58 +0100 Subject: [PATCH 08/12] Add another librart --- website/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/website/requirements.txt b/website/requirements.txt index d9857aa6..978d1bce 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -1,6 +1,7 @@ linkify-it-py myst-parser nbsphinx +pandoc pydata-sphinx-theme sphinx sphinxcontrib-jquery From b0e74d6da4df77dfb42ae8466c3ffcd7bf6e5af6 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:34:11 +0100 Subject: [PATCH 09/12] More libs --- .github/workflows/website.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 237cb6df..30601bfb 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -27,6 +27,7 @@ jobs: - name: Install dependencies run: | sudo apt-get install -y libosmesa6-dev + sudo apt-get install -y pandoc python -m pip install --upgrade pip pip install -r website/requirements.txt pip install -e .[dev,fourc] From a342bc55efc4ce1abf8847d08053a25c9abf4bc7 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:38:37 +0100 Subject: [PATCH 10/12] Set some pyvista stuff --- .github/workflows/website.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 30601bfb..b51a4a50 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -36,6 +36,9 @@ jobs: python website/docs/prepare_docs.py - name: Build Sphinx website run: | + export PYVISTA_OFF_SCREEN=true + export PYVISTA_USE_EGL=true + export VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 sphinx-build -b html website/docs/source website/docs/build - name: Upload website artifact uses: actions/upload-artifact@v4 From 3541b431890280eaeaef3bf41e7e28b65a28564a Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:41:24 +0100 Subject: [PATCH 11/12] Emulate x-server --- .github/workflows/website.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index b51a4a50..0b47e1fc 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -28,6 +28,7 @@ jobs: run: | sudo apt-get install -y libosmesa6-dev sudo apt-get install -y pandoc + sudo apt-get install -y xvfb python -m pip install --upgrade pip pip install -r website/requirements.txt pip install -e .[dev,fourc] @@ -39,7 +40,7 @@ jobs: export PYVISTA_OFF_SCREEN=true export PYVISTA_USE_EGL=true export VTK_DEFAULT_RENDER_WINDOW_OFFSCREEN=1 - sphinx-build -b html website/docs/source website/docs/build + xvfb-run -a sphinx-build -b html website/docs/source website/docs/build - name: Upload website artifact uses: actions/upload-artifact@v4 with: From 47b3137d1144825757b045f15e933d8464680b43 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Mon, 12 Jan 2026 17:55:02 +0100 Subject: [PATCH 12/12] Fix display of example 1 image in nbsphinx --- examples/utils/example_1_utils.py | 6 ++-- src/beamme/utils/visualization.py | 54 ++++++++++++++++++------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/examples/utils/example_1_utils.py b/examples/utils/example_1_utils.py index a8bc16de..fb8eec19 100644 --- a/examples/utils/example_1_utils.py +++ b/examples/utils/example_1_utils.py @@ -29,9 +29,7 @@ import sys from beamme.utils.environment import is_testing - -# Import beamme visualization module to setup the proper rendering parameters -import beamme.utils.visualization # noqa: F401 +import beamme.utils.visualization as visualization def print_matrix(name, matrix): @@ -134,5 +132,5 @@ def __exit__(self, exc_type, exc_value, traceback): console print out). """ if not is_testing(): - self.plotter.show() + visualization.show_plotter(self.plotter, nbsphinx_export_3d_view=False) sys.stdout = self.stdout diff --git a/src/beamme/utils/visualization.py b/src/beamme/utils/visualization.py index 237bc413..c84614cf 100644 --- a/src/beamme/utils/visualization.py +++ b/src/beamme/utils/visualization.py @@ -27,6 +27,7 @@ import ipywidgets as _widgets import pyvista as _pv +from IPython.display import HTML as _HTML from IPython.display import IFrame as _IFrame from IPython.display import display as _display @@ -38,13 +39,18 @@ _pv.start_xvfb() -def show_plotter(plotter: _pv.Plotter) -> None: +def show_plotter(plotter: _pv.Plotter, *, nbsphinx_export_3d_view: bool = True) -> None: """Show a PyVista plotter. This wrapper either directly displays the plotter, default development use for BeamMe. For the website representation of the examples, we export static and interactive versions of the plotter that will be embedded in the documentation. + + Args: + plotter: The PyVista plotter to show. + nbsphinx_export_3d_view: For nbsphinx documentation, whether to + export an interactive 3D view alongside the static image. """ if not _is_nbsphinx(): @@ -56,27 +62,31 @@ def show_plotter(plotter: _pv.Plotter) -> None: # Get a unique identifier for the current plotter plotter_uid = str(_uuid.uuid4().hex) - # Export screenshot and html representation of the plotter - plotter.export_html(static_doc_path / f"{plotter_uid}.html") + # Export a screenshot of the plotter plotter.screenshot(static_doc_path / f"{plotter_uid}.png") - - # Setup the html to display on the website static_frame_html = f""" - - """ - interactive_frame = _IFrame( - src=f"../_static/pyvista/{plotter_uid}.html", - width="100%", - height=600, - ) - tab = _widgets.Tab( - children=[ - _widgets.HTML(static_frame_html), - _widgets.HTML(interactive_frame._repr_html_()), - ] - ) - tab.set_title(0, "Static Scene") - tab.set_title(1, "Interactive Scene") + + """ + + if nbsphinx_export_3d_view: + # Export a html representation of the plotter + plotter.export_html(static_doc_path / f"{plotter_uid}.html") + + interactive_frame = _IFrame( + src=f"../_static/pyvista/{plotter_uid}.html", + width="100%", + height=600, + ) + tab = _widgets.Tab( + children=[ + _widgets.HTML(static_frame_html), + _widgets.HTML(interactive_frame._repr_html_()), + ] + ) + tab.set_title(0, "Static Scene") + tab.set_title(1, "Interactive Scene") - _display(tab) + _display(tab) + else: + _display(_HTML(static_frame_html))