├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── examples ├── .assets │ ├── ex1.svg │ └── ex9.svg ├── ex1.ipynb ├── ex9.ipynb └── plot.ipynb ├── glvis ├── __about__.py ├── __init__.py ├── util.py ├── widget.js └── widget.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .ipynb_checkpoints 2 | glvis/nbextension 3 | js/node_modules 4 | *.saved 5 | *.mesh 6 | __pycache__ 7 | *.egg-info 8 | js/dist 9 | build 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2010-2024, Lawrence Livermore National Security, LLC 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This work was produced under the auspices of the U.S. Department of Energy by 2 | Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344. 3 | 4 | This work was prepared as an account of work sponsored by an agency of 5 | the United States Government. Neither the United States Government nor 6 | Lawrence Livermore National Security, LLC, nor any of their employees 7 | makes any warranty, expressed or implied, or assumes any legal liability 8 | or responsibility for the accuracy, completeness, or usefulness of any 9 | information, apparatus, product, or process disclosed, or represents that 10 | its use would not infringe privately owned rights. 11 | 12 | Reference herein to any specific commercial product, process, or service 13 | by trade name, trademark, manufacturer, or otherwise does not necessarily 14 | constitute or imply its endorsement, recommendation, or favoring by the 15 | United States Government or Lawrence Livermore National Security, LLC. 16 | 17 | The views and opinions of authors expressed herein do not necessarily 18 | state or reflect those of the United States Government or Lawrence 19 | Livermore National Security, LLC, and shall not be used for advertising 20 | or product endorsement purposes. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyGLVis 2 | 3 | [![badge](examples/.assets/ex1.svg)](https://colab.research.google.com/github/GLVis/pyglvis/blob/main/examples/ex1.ipynb) 4 | [![badge](examples/.assets/ex9.svg)](https://colab.research.google.com/github/GLVis/pyglvis/blob/main/examples/ex9.ipynb) 5 | 6 | 7 | \ 8 | PyGLVis is an interactive [Jupyter](https://jupyter.org/) widget for visualizing finite element meshes and functions, built on-top of the [GLVis](https://glvis.org/) library. 9 | 10 | ## 📦 Installation 11 | 12 | The GLVis Jupyter widget is installed using `pip`. To install the latest version from the repository: 13 | 14 | ```bash 15 | git clone https://github.com/GLVis/pyglvis.git 16 | cd pyglvis 17 | pip install . 18 | ``` 19 | 20 | Or, install directly from PyPi, 21 | ```bash 22 | pip install glvis 23 | ``` 24 | 25 | PyGLVis requires the Python wrapper for MFEM, [PyMFEM](https://github.com/mfem/pymfem), which can be installed with 26 | ```bash 27 | pip install mfem 28 | ``` 29 | 30 | 31 | 32 | ## 🚀 Usage 33 | 34 | ### Basic usage 35 | 36 | ```python 37 | from glvis import glvis 38 | 39 | # Create a `glvis` object 40 | g = glvis(data, width=640, height=480) 41 | 42 | # Run a cell with `g` as the last statement to display the widget 43 | g 44 | ``` 45 | 46 | The `data` object can be one of: 47 | 48 | - `Mesh`, defined in [PyMFEM](https://github.com/mfem/pymfem) 49 | - `(Mesh, GridFunction)` tuple, defined in [PyMFEM](https://github.com/mfem/pymfem) 50 | - `str`, in the format of `*.saved` files [used by MFEM and GLVis](https://mfem.org/mesh-format-v1.0/). See [examples/basic.ipynb](examples/basic.ipynb) for an example. 51 | 52 | ### Customization with key commands 53 | 54 | GLVis has many keyboard commands that can be used to customize the visualization. 55 | A few of the most common are listed below. See the [GLVis README](https://github.com/GLVis/glvis?tab=readme-ov-file#key-commands) for a full list. 56 | - `r` - reset the view 57 | - `c` - toggle the colorbar 58 | - `j` - toggle perspective 59 | - `l` - toggle the light 60 | - `g` - toggle the background color (white/black) 61 | - `a` - cycle through bounding box axes states 62 | - `m` - cycle through mesh states 63 | - `p` - cycle through color palettes 64 | - `t` - cycle through materials and lights 65 | - `0` - begin rotating around z-axis 66 | - `.` - pause rotation 67 | - `*`/`/` - zoom in/out 68 | 69 | These can be set using the `keys` argument when creating a `glvis` object. 70 | ```python 71 | glvis(data, keys='rljgac//0') 72 | ``` 73 | This combination of keys would: `r` reset the view, `l` toggle the light, `j` toggle perspective, `g` toggle the background color to black (default is white), `a` show the bounding box, `c` show the colorbar, `//` zoom out twice, and `0` begin rotating around the z-axis: 74 | 75 | ![pyglvis_preset_keys](https://github.com/GLVis/pyglvis/assets/27717785/de0e0a99-72ac-4a88-8369-708515600b09) 76 | 77 | Alternatively, keys can be typed directly into the widget after it has been created: 78 | 79 | ![pyglvis_using_keys](https://github.com/GLVis/pyglvis/assets/27717785/625f4f06-8f99-4390-94d7-4d317fd11e7f) 80 | 81 | ### Other methods 82 | 83 | Once you have a `glvis` object there are a few methods that can used to update the 84 | visualization, besides using keys: 85 | ```python 86 | # Show a new Mesh/GridFunction, resets keys 87 | g.plot(data) 88 | # Show an updated visualization with the same data, preserving keys 89 | g.update(data) 90 | # Change the image size 91 | g.set_size(width, height) 92 | # Force the widget to render. If the widget isn't the last statement in a cell it 93 | # will not be shown without this. See ex9.ipynb 94 | g.render() 95 | ``` 96 | 97 | See the [examples](examples/) directory for additional examples. To test those locally, start a Jupyter lab server with 98 | 99 | ``` 100 | jupyter lab 101 | ``` 102 | 103 | ## 🐛 Troubleshooting 104 | 105 | This widget was originally developed using the [jupyter widget cookiecutter](https://github.com/jupyter-widgets/widget-cookiecutter); however, [recent changes to the Jupyter ecosystem](https://jupyter-notebook.readthedocs.io/en/latest/migrate_to_notebook7.html#why-a-new-version) have broken a lot of functionality, leading to a rewrite using [anywidget](https://anywidget.dev/). If you encounter any problems, please consider supporting development by opening an [issue](https://github.com/GLVis/pyglvis/issues). 106 | 107 | 108 | 109 | ## 🤖 Development 110 | 111 | ### PyGLVis dependencies 112 | 113 | ```mermaid 114 | graph TD; 115 | A[mfem] --> B[pymfem]; 116 | A --> C[glvis]; 117 | C --> D[glvis-js]; 118 | Ext1[emscripten] --> D; 119 | D-.-E["glvis-js (esm)"] 120 | B & E --> G[pyglvis]; 121 | Ext2[jupyter] --> G; 122 | ``` 123 | 124 | `pyglvis` is most directly dependent on `PyMFEM` and `glvis-js`. [PyMFEM](https://github.com/mfem/pymfem) is a Python wrapper of the finite element library, `MFEM`, while `glvis-js` is a JavaScript/WebAssembly port of `glvis`. 125 | 126 | `glvis-js` is hosted on [github](https://github.com/glvis/glvis-js) and mirrored on [npm](https://www.npmjs.com/package/glvis). [esm.sh](https://esm.sh/glvis) allows `pyglvis` to pull the latest version of `glvis-js` directly from npm. This can be seen in the first line of [glvis/widget.js](glvis/widget.js): 127 | 128 | ``` 129 | import glvis from "https://esm.sh/glvis"; 130 | ``` 131 | 132 | You can specify a different version of `glvis-js` by adding `@x.y.z` to the end of this import statement, where `x.y.z` matches a version number available on `npm`, e.g. 133 | 134 | ``` 135 | import glvis from "https://esm.sh/glvis@0.6.3"; 136 | ``` 137 | 138 | 139 | ### Releasing a new version of glvis on NPM: 140 | 141 | To publish a new version of `glvis-js`, follow the instructions on the [repo](https://github.com/GLVis/glvis-js/tree/master). 142 | 143 | 144 | ### Releasing a new version of glvis on PyPI: 145 | 146 | - Update `__version__` in `glvis/__about__.py` 147 | 148 | - `git add` and `git commit` changes 149 | 150 | 151 | You will need [twine](https://pypi.org/project/twine/) to publish to PyPI, install with `pip`. 152 | 153 | ``` 154 | python -m hatch build 155 | twine upload dist/* 156 | git tag -a X.X.X -m 'comment' 157 | git push --tags 158 | ``` 159 | 160 | 161 | ## 🌐 Links 162 | - MFEM ([website](https://mfem.org/), [github](https://github.com/mfem/mfem)) 163 | - PyMFEM ([github](https://github.com/mfem/pymfem), [pypi](https://pypi.org/project/mfem/)) 164 | - GLVis ([website](https://glvis.org/), [github](https://github.com/glvis/glvis)) 165 | - glvis-js ([github](https://github.com/glvis/glvis-js), [npm](https://www.npmjs.com/package/glvis), [esm](https://esm.sh/glvis)) 166 | - pyglvis ([github](https://github.com/GLVis/pyglvis), [pypi]()) 167 | -------------------------------------------------------------------------------- /examples/.assets/ex1.svg: -------------------------------------------------------------------------------- 1 | 2 | Open in ColabOpen in Colabex1 129 | -------------------------------------------------------------------------------- /examples/.assets/ex9.svg: -------------------------------------------------------------------------------- 1 | 2 | Open in ColabOpen in Colabex9 163 | -------------------------------------------------------------------------------- /examples/ex1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Imports" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "# Pip installs pyglvis and pymfem if in a colab notebook. Can comment out if not required\n", 17 | "# PyMFEM requires numpy < 2.0.0 \n", 18 | "import importlib.metadata, os\n", 19 | "\n", 20 | "desired_numpy = \"1.26.4\"\n", 21 | "\n", 22 | "installed_numpy = importlib.metadata.version(\"numpy\")\n", 23 | "if installed_numpy != desired_numpy:\n", 24 | " print(f\"Installing NumPy {desired_numpy} (current: {installed_numpy})... \")\n", 25 | " print(\"Runtime will restart... Run again for changes to take effect.\")\n", 26 | " %pip install numpy=={desired_numpy} --prefer-binary --quiet\n", 27 | " os._exit(0)\n", 28 | "print(f\"Correct NumPy ({desired_numpy}) is installed\")\n", 29 | "\n", 30 | "%pip install --quiet glvis\n", 31 | "from glvis import glvis, GlvisData\n" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "## Visualizing from stream" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "# Load a remote data stream\n", 48 | "stream = GlvisData.ex9\n", 49 | "# Visualize the above stream (all GLVis keys and mouse commands work)\n", 50 | "glvis(stream, 300, 300)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# Another visualization instance of the same stream\n", 60 | "glvis(stream, 300, 300, keys='rljg****ttta0')" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "# Load in another stream (press Q to cycle between quadrature representations)\n", 70 | "glvis(GlvisData.quadrature_lor, keys=\"gcQ/////////\")" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "## Solving and visualizing the Laplace equation" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": { 83 | "tags": [] 84 | }, 85 | "source": [ 86 | "### MFEM Example 1\n", 87 | "\n", 88 | "Adapted from [PyMFEM/ex1.py](https://github.com/mfem/PyMFEM/blob/master/examples/ex1.py).\n", 89 | "Compare with the [original Example 1](https://github.com/mfem/mfem/blob/master/examples/ex1.cpp) in MFEM.\n", 90 | "\n", 91 | "This example code demonstrates the use of MFEM to define a simple finite element discretization of the Laplace problem\n", 92 | "\n", 93 | "\\begin{equation*}\n", 94 | "-\\Delta x = 1\n", 95 | "\\end{equation*}\n", 96 | "\n", 97 | "in a domain $\\Omega$ with homogeneous Dirichlet boundary conditions\n", 98 | "\n", 99 | "\\begin{equation*}\n", 100 | "x = 0\n", 101 | "\\end{equation*}\n", 102 | "\n", 103 | "on the boundary $\\partial \\Omega$.\n", 104 | "\n", 105 | "The problem is discretized on a computational mesh in either 2D or 3D using a finite elements space of the specified order (2 by default) resulting in the global sparse linear system\n", 106 | "\n", 107 | "\\begin{equation*}\n", 108 | "A X = B.\n", 109 | "\\end{equation*}\n", 110 | "\n", 111 | "The example highlights the use of mesh refinement, finite element grid functions, as well as linear and bilinear forms corresponding to the left-hand side and right-hand side of the\n", 112 | "discrete linear system. We also cover the explicit elimination of essential boundary conditions and using the GLVis tool for visualization." 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": { 119 | "tags": [] 120 | }, 121 | "outputs": [], 122 | "source": [ 123 | "# Requires PyMFEM, see https://github.com/mfem/PyMFEM\n", 124 | "import mfem.ser as mfem" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "# Load the mesh from a local file\n", 134 | "# meshfile = '../../mfem/data/star.mesh'\n", 135 | "# mesh = mfem.Mesh(meshfile)\n", 136 | "\n", 137 | "# Alternatively, create a simple square mesh and refine it\n", 138 | "mesh = mfem.Mesh(5, 5, \"TRIANGLE\")\n", 139 | "mesh.UniformRefinement()\n", 140 | "\n", 141 | "# Create H1 finite element function space\n", 142 | "fec = mfem.H1_FECollection(2, mesh.Dimension()) # order=2\n", 143 | "fespace = mfem.FiniteElementSpace(mesh, fec) \n", 144 | "\n", 145 | "# Determine essential degrees of freedom (the whole boundary here)\n", 146 | "ess_tdof_list = mfem.intArray()\n", 147 | "ess_bdr = mfem.intArray([1]*mesh.bdr_attributes.Size())\n", 148 | "fespace.GetEssentialTrueDofs(ess_bdr, ess_tdof_list)\n", 149 | "\n", 150 | "# Define Bilinear and Linear forms for the Laplace problem -Δu=1\n", 151 | "one = mfem.ConstantCoefficient(1.0)\n", 152 | "a = mfem.BilinearForm(fespace)\n", 153 | "a.AddDomainIntegrator(mfem.DiffusionIntegrator(one))\n", 154 | "a.Assemble()\n", 155 | "b = mfem.LinearForm(fespace)\n", 156 | "b.AddDomainIntegrator(mfem.DomainLFIntegrator(one))\n", 157 | "b.Assemble()\n", 158 | "\n", 159 | "# Create a grid function for the solution and initialize with 0\n", 160 | "x = mfem.GridFunction(fespace);\n", 161 | "x.Assign(0.0)\n", 162 | "\n", 163 | "# Form the linear system, AX=B, for the FEM discretization\n", 164 | "A = mfem.OperatorPtr()\n", 165 | "B = mfem.Vector()\n", 166 | "X = mfem.Vector()\n", 167 | "a.FormLinearSystem(ess_tdof_list, x, b, A, X, B);\n", 168 | "print(\"Size of the linear system: \" + str(A.Height()))\n", 169 | "\n", 170 | "# Solve the system using PCG solver and get the solution in x\n", 171 | "Asm = mfem.OperatorHandle2SparseMatrix(A)\n", 172 | "Msm = mfem.GSSmoother(Asm)\n", 173 | "mfem.PCG(Asm, Msm, B, X, 1, 200, 1e-12, 0.0)\n", 174 | "a.RecoverFEMSolution(X, b, x)" 175 | ] 176 | }, 177 | { 178 | "cell_type": "markdown", 179 | "metadata": {}, 180 | "source": [ 181 | "### Plot the Solution with GLVis" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": { 188 | "tags": [] 189 | }, 190 | "outputs": [], 191 | "source": [ 192 | "# Plot the mesh + solution (all GLVis keys and mouse commands work)\n", 193 | "g = glvis((mesh, x))\n", 194 | "g" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": null, 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "# Plot the mesh only\n", 204 | "glvis(mesh)" 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": null, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "# Visualization with additional GLVis keys\n", 214 | "g = glvis((mesh,x), keys='ARjlmcbp**')\n", 215 | "g" 216 | ] 217 | } 218 | ], 219 | "metadata": { 220 | "kernelspec": { 221 | "display_name": "Python 3 (ipykernel)", 222 | "language": "python", 223 | "name": "python3" 224 | }, 225 | "language_info": { 226 | "codemirror_mode": { 227 | "name": "ipython", 228 | "version": 3 229 | }, 230 | "file_extension": ".py", 231 | "mimetype": "text/x-python", 232 | "name": "python", 233 | "nbconvert_exporter": "python", 234 | "pygments_lexer": "ipython3", 235 | "version": "3.10.12" 236 | } 237 | }, 238 | "nbformat": 4, 239 | "nbformat_minor": 4 240 | } 241 | -------------------------------------------------------------------------------- /examples/ex9.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## MFEM Example 9\n", 8 | "\n", 9 | "Adapted from [PyMFEM/ex9.py]( https://github.com/mfem/PyMFEM/blob/master/examples/ex9.py).\n", 10 | "Compare with the [original Example 9](https://github.com/mfem/mfem/blob/master/examples/ex9.cpp) in MFEM.\n", 11 | "\n", 12 | "This example code solves the time-dependent advection equation\n", 13 | "\n", 14 | "\\begin{equation*}\n", 15 | "\\frac{\\partial u}{\\partial t} + v \\cdot \\nabla u = 0,\n", 16 | "\\label{advection}\\tag{1}\n", 17 | "\\end{equation*}\n", 18 | "\n", 19 | "where $v$ is a given fluid velocity, and \n", 20 | "\n", 21 | "\\begin{equation*}\n", 22 | "u_0(x)=u(0,x)\n", 23 | "\\label{advection2}\\tag{2}\n", 24 | "\\end{equation*}\n", 25 | "\n", 26 | "is a given initial condition.\n", 27 | "\n", 28 | "The example demonstrates the use of Discontinuous Galerkin (DG) bilinear forms in MFEM (face integrators), the use of explicit ODE time integrators, the definition of periodic boundary conditions through periodic meshes, as well as the use of GLVis for persistent visualization of a time-evolving solution." 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": { 35 | "tags": [] 36 | }, 37 | "outputs": [], 38 | 39 | "source": [ 40 | "# Pip installs pyglvis and pymfem if in a colab notebook. Can comment out if not required\n", 41 | "# PyMFEM requires numpy < 2.0.0 \n", 42 | "import importlib.metadata, os\n", 43 | "\n", 44 | "desired_numpy = \"1.26.4\"\n", 45 | "\n", 46 | "installed_numpy = importlib.metadata.version(\"numpy\")\n", 47 | "if installed_numpy != desired_numpy:\n", 48 | " print(f\"Installing NumPy {desired_numpy} (current: {installed_numpy})... \")\n", 49 | " print(\"Runtime will restart... Run again for changes to take effect.\")\n", 50 | " %pip install numpy=={desired_numpy} --prefer-binary --quiet\n", 51 | " os._exit(0)\n", 52 | "print(f\"Correct NumPy ({desired_numpy}) is installed\")\n", 53 | "\n", 54 | "%pip install --quiet glvis\n", 55 | "from __future__ import print_function\n", 56 | "from os.path import expanduser, join\n", 57 | "import time\n", 58 | "import numpy as np\n", 59 | "from numpy import sqrt, pi, cos, sin, hypot, arctan2\n", 60 | "from scipy.special import erfc\n", 61 | "import mfem.ser as mfem\n", 62 | "from mfem.ser import intArray\n", 63 | "from glvis import glvis, GlvisData\n" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": { 70 | "tags": [] 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "# 1. Setup problem parameters\n", 75 | "\n", 76 | "problem = 0\n", 77 | "order = 3\n", 78 | "vis_steps = 5\n", 79 | "ode_solver_type = 4\n", 80 | "sleep_interval = .05\n", 81 | "\n", 82 | "if problem == 0:\n", 83 | " # Translating hump\n", 84 | " mesh = \"periodic-hexagon.mesh\"\n", 85 | " ref_levels = 2\n", 86 | " dt = 0.01\n", 87 | " t_final = 10\n", 88 | "elif problem == 1:\n", 89 | " # Rotating power sines\n", 90 | " mesh = \"periodic-square.mesh\"\n", 91 | " ref_levels = 2\n", 92 | " dt = 0.005\n", 93 | " t_final = 9\n", 94 | "elif problem == 2:\n", 95 | " # Rotating trigs\n", 96 | " mesh = \"disc-nurbs.mesh\"\n", 97 | " ref_levels = 3\n", 98 | " dt = 0.005\n", 99 | " t_final = 9\n", 100 | "elif problem == 3:\n", 101 | " # Twisting sines\n", 102 | " mesh = \"periodic-square.mesh\"\n", 103 | " ref_levels = 4\n", 104 | " dt = 0.0025\n", 105 | " t_final = 9\n", 106 | " vis_steps = 20" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": { 113 | "tags": [] 114 | }, 115 | "outputs": [], 116 | "source": [ 117 | "# 2. Download the mesh from GitHub and read it. \n", 118 | "# We can handle geometrically periodic meshes in this code.\n", 119 | "\n", 120 | "mesh_url = f\"https://raw.githubusercontent.com/mfem/mfem/master/data/{mesh}\"\n", 121 | "mesh_str = !curl -s {mesh_url}\n", 122 | "with open(mesh, \"w\") as f:\n", 123 | " f.write(\"\\n\".join(mesh_str))\n", 124 | "mesh = mfem.Mesh(mesh, 1,1)\n", 125 | "dim = mesh.Dimension()" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "metadata": { 132 | "tags": [] 133 | }, 134 | "outputs": [], 135 | "source": [ 136 | "# 3. Define the ODE solver used for time integration. \n", 137 | "# Several explicit Runge-Kutta methods are available.\n", 138 | "\n", 139 | "ode_solver = None\n", 140 | "if ode_solver_type == 1: ode_solver = mfem.ForwardEulerSolver()\n", 141 | "elif ode_solver_type == 2: ode_solver = mfem.RK2Solver(1.0)\n", 142 | "elif ode_solver_type == 3: ode_solver = mfem.RK3SSolver()\n", 143 | "elif ode_solver_type == 4: ode_solver = mfem.RK4Solver()\n", 144 | "elif ode_solver_type == 6: ode_solver = mfem.RK6Solver()\n", 145 | "else: print(f\"Unknown ODE solver type: {ode_solver_type}\")" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": { 152 | "tags": [] 153 | }, 154 | "outputs": [], 155 | "source": [ 156 | "# 4. Refine the mesh to increase the resolution. In this example we do\n", 157 | "# 'ref_levels' of uniform refinement. If the mesh is of NURBS type,\n", 158 | "# we convert it to a (piecewise-polynomial) high-order mesh.\n", 159 | "\n", 160 | "for lev in range(ref_levels):\n", 161 | " mesh.UniformRefinement()\n", 162 | " if mesh.NURBSext:\n", 163 | " mesh.SetCurvature(max(order, 1))\n", 164 | " bb_min, bb_max = mesh.GetBoundingBox(max(order, 1))" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": null, 170 | "metadata": { 171 | "tags": [] 172 | }, 173 | "outputs": [], 174 | "source": [ 175 | "# 5. Define the discontinuous DG finite element space of the given\n", 176 | "# polynomial order on the refined mesh.\n", 177 | "\n", 178 | "fec = mfem.DG_FECollection(order, dim)\n", 179 | "fes = mfem.FiniteElementSpace(mesh, fec)\n", 180 | "\n", 181 | "print(\"Number of unknowns: \" + str(fes.GetVSize()))\n", 182 | "\n", 183 | "# Define coefficient using VecotrPyCoefficient and PyCoefficient\n", 184 | "# A user needs to define EvalValue method \n", 185 | "class velocity_coeff(mfem.VectorPyCoefficient):\n", 186 | " def EvalValue(self, x): \n", 187 | " dim = len(x)\n", 188 | " \n", 189 | " center = (bb_min + bb_max)/2.0\n", 190 | " # map to the reference [-1,1] domain \n", 191 | " X = 2 * (x - center) / (bb_max - bb_min)\n", 192 | " if problem == 0:\n", 193 | " if dim == 1: v = [1.0,]\n", 194 | " elif dim == 2: v = [sqrt(2./3.), sqrt(1./3)]\n", 195 | " elif dim == 3: v = [sqrt(3./6.), sqrt(2./6), sqrt(1./6.)]\n", 196 | " elif (problem == 1 or problem == 2):\n", 197 | " # Clockwise rotation in 2D around the origin \n", 198 | " w = pi/2\n", 199 | " if dim == 1: v = [1.0,]\n", 200 | " elif dim == 2: v = [w*X[1], - w*X[0]]\n", 201 | " elif dim == 3: v = [w*X[1], - w*X[0], 0]\n", 202 | " elif (problem == 3):\n", 203 | " # Clockwise twisting rotation in 2D around the origin\n", 204 | " w = pi/2\n", 205 | " d = max((X[0]+1.)*(1.-X[0]),0.) * max((X[1]+1.)*(1.-X[1]),0.)\n", 206 | " d = d ** 2\n", 207 | " if dim == 1: v = [1.0,]\n", 208 | " elif dim == 2: v = [d*w*X[1], - d*w*X[0]]\n", 209 | " elif dim == 3: v = [d*w*X[1], - d*w*X[0], 0]\n", 210 | " return v\n", 211 | " \n", 212 | " \n", 213 | "class u0_coeff(mfem.PyCoefficient):\n", 214 | " def EvalValue(self, x): \n", 215 | " dim = len(x)\n", 216 | " center = (bb_min + bb_max)/2.0\n", 217 | " # map to the reference [-1,1] domain \n", 218 | " X = 2 * (x - center) / (bb_max - bb_min)\n", 219 | " if (problem == 0 or problem == 1):\n", 220 | " if dim == 1:\n", 221 | " return exp(-40. * (X[0]-0.5)**2)\n", 222 | " elif (dim == 2 or dim == 3):\n", 223 | " rx = 0.45; ry = 0.25; cx = 0.; cy = -0.2; w = 10.\n", 224 | " if dim == 3:\n", 225 | " s = (1. + 0.25*cos(2 * pi * x[2]))\n", 226 | " rx = rx * s\n", 227 | " ry = ry * s\n", 228 | " return ( erfc( w * (X[0]-cx-rx)) * erfc(-w*(X[0]-cx+rx)) *\n", 229 | " erfc( w * (X[1]-cy-ry)) * erfc(-w*(X[1]-cy+ry)) )/16\n", 230 | " elif problem == 2:\n", 231 | " rho = hypot(x[0], x[1])\n", 232 | " phi = arctan2(x[1], x[0])\n", 233 | " return (sin(pi * rho) **2) * sin(3*phi)\n", 234 | " elif problem == 3:\n", 235 | " return sin(pi * X[0]) * sin(pi * X[1])\n", 236 | " \n", 237 | " return 0.0\n", 238 | "\n", 239 | "# Inflow boundary condition (zero for the problems considered in \n", 240 | "# this example)\n", 241 | "class inflow_coeff(mfem.PyCoefficient):\n", 242 | " def EvalValue(self, x):\n", 243 | " return 0" 244 | ] 245 | }, 246 | { 247 | "cell_type": "code", 248 | "execution_count": null, 249 | "metadata": { 250 | "tags": [] 251 | }, 252 | "outputs": [], 253 | "source": [ 254 | "# 6. Set up and assemble the bilinear and linear forms corresponding to \n", 255 | "# the DG discretization. The DGTraceIntegrator involves integrals over\n", 256 | "# mesh interior faces.\n", 257 | "\n", 258 | "velocity = velocity_coeff(dim)\n", 259 | "inflow = inflow_coeff()\n", 260 | "u0 = u0_coeff()\n", 261 | "\n", 262 | "m = mfem.BilinearForm(fes)\n", 263 | "m.AddDomainIntegrator(mfem.MassIntegrator())\n", 264 | "k = mfem.BilinearForm(fes)\n", 265 | "k.AddDomainIntegrator(mfem.ConvectionIntegrator(velocity, -1.0))\n", 266 | "k.AddInteriorFaceIntegrator(mfem.TransposeIntegrator(mfem.DGTraceIntegrator(velocity, 1.0, -0.5)))\n", 267 | "k.AddBdrFaceIntegrator(mfem.TransposeIntegrator(mfem.DGTraceIntegrator(velocity, 1.0, -0.5)))\n", 268 | "b = mfem.LinearForm(fes)\n", 269 | "b.AddBdrFaceIntegrator(mfem.BoundaryFlowIntegrator(inflow, velocity, -1.0, -0.5))\n", 270 | "\n", 271 | "m.Assemble()\n", 272 | "m.Finalize()\n", 273 | "skip_zeros = 0\n", 274 | "k.Assemble(skip_zeros)\n", 275 | "k.Finalize(skip_zeros)\n", 276 | "b.Assemble()\n", 277 | "\n", 278 | "# Initial conditions\n", 279 | "u = mfem.GridFunction(fes)\n", 280 | "u.ProjectCoefficient(u0)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "### Solve and Plot the Solution with GLVis" 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": null, 293 | "metadata": { 294 | "tags": [] 295 | }, 296 | "outputs": [], 297 | "source": [ 298 | "g = glvis((mesh, u), 400, 400, keys=\"Agj*****\")\n", 299 | "g.render()\n", 300 | "\n", 301 | "class FE_Evolution(mfem.PyTimeDependentOperator):\n", 302 | " def __init__(self, M, K, b):\n", 303 | " mfem.PyTimeDependentOperator.__init__(self, M.Size())\n", 304 | " self.K = K\n", 305 | " self.M = M\n", 306 | " self.b = b\n", 307 | " self.z = mfem.Vector(M.Size())\n", 308 | " self.zp = np.zeros(M.Size())\n", 309 | " self.M_prec = mfem.DSmoother()\n", 310 | " self.M_solver = mfem.CGSolver()\n", 311 | " self.M_solver.SetPreconditioner(self.M_prec)\n", 312 | " self.M_solver.SetOperator(M)\n", 313 | " self.M_solver.iterative_mode = False\n", 314 | " self.M_solver.SetRelTol(1e-9)\n", 315 | " self.M_solver.SetAbsTol(0.0)\n", 316 | " self.M_solver.SetMaxIter(100)\n", 317 | " self.M_solver.SetPrintLevel(0)\n", 318 | "\n", 319 | " def Mult(self, x, y):\n", 320 | " self.K.Mult(x, self.z)\n", 321 | " self.z += b\n", 322 | " self.M_solver.Mult(self.z, y)\n", 323 | "\n", 324 | "adv = FE_Evolution(m.SpMat(), k.SpMat(), b)\n", 325 | "\n", 326 | "ode_solver.Init(adv)\n", 327 | "t = 0.0\n", 328 | "ti = 0\n", 329 | "time.sleep(1)\n", 330 | "while True:\n", 331 | " if t > t_final - dt/2: break\n", 332 | " t, dt = ode_solver.Step(u, t, dt)\n", 333 | " ti = ti + 1\n", 334 | " if ti % vis_steps == 0:\n", 335 | " g.update((mesh, u))\n", 336 | " time.sleep(sleep_interval)\n", 337 | " print(f\"time step: {ti}, time: {t:.2e}\", end=\"\\r\")" 338 | ] 339 | } 340 | ], 341 | "metadata": { 342 | "kernelspec": { 343 | "display_name": "Python 3 (ipykernel)", 344 | "language": "python", 345 | "name": "python3" 346 | }, 347 | "language_info": { 348 | "codemirror_mode": { 349 | "name": "ipython", 350 | "version": 3 351 | }, 352 | "file_extension": ".py", 353 | "mimetype": "text/x-python", 354 | "name": "python", 355 | "nbconvert_exporter": "python", 356 | "pygments_lexer": "ipython3", 357 | "version": "3.10.12" 358 | } 359 | }, 360 | "nbformat": 4, 361 | "nbformat_minor": 4 362 | } 363 | -------------------------------------------------------------------------------- /examples/plot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Pip installs pyglvis and pymfem if in a colab notebook. Can comment out if not required\n", 10 | "# PyMFEM requires numpy < 2.0.0 \n", 11 | "import importlib.metadata, os\n", 12 | "\n", 13 | "desired_numpy = \"1.26.4\"\n", 14 | "\n", 15 | "installed_numpy = importlib.metadata.version(\"numpy\")\n", 16 | "if installed_numpy != desired_numpy:\n", 17 | " print(f\"Installing NumPy {desired_numpy} (current: {installed_numpy})... \")\n", 18 | " print(\"Runtime will restart... Run again for changes to take effect.\")\n", 19 | " %pip install numpy=={desired_numpy} --prefer-binary --quiet\n", 20 | " os._exit(0)\n", 21 | "print(f\"Correct NumPy ({desired_numpy}) is installed\")\n", 22 | "\n", 23 | "%pip install --quiet glvis\n", 24 | "from glvis import glvis, GlvisData\n" 25 | "import mfem.ser as mfem\n", 26 | "import ipywidgets as widgets\n", 27 | "from math import *" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "element_types = {\n", 37 | " \"2d\": [\"TRIANGLE\", \"QUADRILATERAL\"],\n", 38 | " \"3d\": [\"TETRAHEDRON\", \"HEXAHEDRON\"]\n", 39 | "}\n", 40 | "\n", 41 | "basis_map = {\n", 42 | " \"H1\": mfem.H1_FECollection,\n", 43 | " \"L2\": mfem.L2_FECollection\n", 44 | "}\n", 45 | "\n", 46 | "class callback_coeff(mfem.PyCoefficient):\n", 47 | " def EvalValue(self, x):\n", 48 | " return self.callback(x)\n", 49 | " \n", 50 | "class callback_vcoeff(mfem.VectorPyCoefficient):\n", 51 | " def EvalValue(self, x):\n", 52 | " return self.callback(x)\n", 53 | " \n", 54 | "# these have to be globals, if they are gc'd the runtime crashes (segfault)\n", 55 | "coeff = fec = fespace = None\n", 56 | "def generate(shape: tuple,\n", 57 | " element_type: str,\n", 58 | " order: int,\n", 59 | " mesh_order: int,\n", 60 | " surf: bool,\n", 61 | " basis: mfem.FiniteElementCollection, \n", 62 | " func: callable = None,\n", 63 | " transform: callable = None):\n", 64 | " global fec, fespace, coeff\n", 65 | " if element_type in element_types[\"2d\"]: shape = shape[:2]\n", 66 | " mesh = mfem.mesh.Mesh(*shape, element_type)\n", 67 | " if surf:\n", 68 | " mesh.SetCurvature(mesh_order, True, 3)\n", 69 | " else:\n", 70 | " mesh.SetCurvature(mesh_order)\n", 71 | " if transform is not None:\n", 72 | " mesh_coeff = callback_vcoeff(mesh.SpaceDimension())\n", 73 | " mesh_coeff.callback = transform\n", 74 | " mesh.Transform(mesh_coeff)\n", 75 | " fec = basis(order, mesh.Dimension())\n", 76 | " fespace = mfem.FiniteElementSpace(mesh, fec)\n", 77 | " if func is None: return mesh\n", 78 | " x = mfem.GridFunction(fespace)\n", 79 | " coeff = callback_coeff()\n", 80 | " coeff.callback = func\n", 81 | " x.ProjectCoefficient(coeff)\n", 82 | " return (mesh, x)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "# Widget parts\n", 92 | "nx = widgets.IntSlider(description=\"nx:\", min=1, value=8, max=50, step=1, continuous_update=False)\n", 93 | "ny = widgets.IntSlider(description=\"ny:\", min=1, value=8, max=50, step=1, continuous_update=False)\n", 94 | "nz = widgets.IntSlider(description=\"nz:\", min=1, value=8, max=50, step=1, continuous_update=False)\n", 95 | "order = widgets.BoundedIntText(description='FEM Order:', value=2, min=1, step=1, continuous_update=False, layout={\"width\": \"130px\"})\n", 96 | "mesh_order = widgets.BoundedIntText(description='Mesh Order:', value=2, min=1, step=1, continuous_update=False, layout={\"width\": \"130px\"})\n", 97 | "surf = widgets.Checkbox(description='Surface Mesh', value=False)\n", 98 | "element_type = widgets.Dropdown(\n", 99 | " value=\"TRIANGLE\",\n", 100 | " options=[e for l in element_types.values() for e in l],\n", 101 | " description='Mesh Elements:',\n", 102 | " style={'description_width': 'initial'},\n", 103 | " layout={\"width\": \"240px\"}\n", 104 | ")\n", 105 | "basis = widgets.Dropdown(\n", 106 | " # value=basis_map.values()[0],\n", 107 | " options=basis_map.keys(),\n", 108 | " description=\"FEM Basis:\",\n", 109 | " style={'description_width': 'initial'},\n", 110 | " layout={\"width\": \"130px\"}\n", 111 | ")\n", 112 | "func = widgets.Textarea(\n", 113 | " disabled=False,\n", 114 | " layout={\"height\": \"56px\", \"width\": \"240px\"}\n", 115 | ")\n", 116 | "transform = widgets.Textarea(\n", 117 | " disabled=False,\n", 118 | " layout={\"height\": \"56px\", \"width\": \"240px\"}\n", 119 | ")\n", 120 | "g = glvis(mfem.mesh.Mesh(nx.value, ny.value, element_type.value))\n", 121 | "\n", 122 | "def toggle_dim(event=None):\n", 123 | " if element_type.value in element_types[\"2d\"]:\n", 124 | " nz.disabled = True\n", 125 | " func.placeholder = \"FEM Function, e.g. x+y\"\n", 126 | " if surf == True:\n", 127 | " transform.placeholder = \"Mesh Transformation, e.g. (x,y,xy)\"\n", 128 | " else:\n", 129 | " transform.placeholder = \"Mesh Transformation, e.g. (x+y,x-y)\"\n", 130 | " else:\n", 131 | " nz.disabled = False\n", 132 | " func.placeholder = \"FEM Function, e.g. x+y+z\"\n", 133 | " transform.placeholder = \"Mesh Transformation, e.g. (x+y,x-y,z)\"\n", 134 | " \n", 135 | "# Setup handlers\n", 136 | "def show(event=None):\n", 137 | " # GridFunction callback\n", 138 | " if func.value != \"\":\n", 139 | " def gfcb(x):\n", 140 | " local = {\n", 141 | " \"x\": x[0],\n", 142 | " \"y\": x[1],\n", 143 | " \"z\": x[2] if len(x) > 2 else 0\n", 144 | " }\n", 145 | " return eval(func.value, None, local)\n", 146 | " else:\n", 147 | " gfcb = None\n", 148 | " \n", 149 | " # Mesh callback\n", 150 | " if transform.value != \"\":\n", 151 | " def mcb(x):\n", 152 | " local = {\n", 153 | " \"x\": x[0],\n", 154 | " \"y\": x[1],\n", 155 | " \"z\": x[2] if (len(x) > 2 or surf == True) else 0\n", 156 | " }\n", 157 | " return eval(transform.value, None, local)\n", 158 | " else:\n", 159 | " mcb = None\n", 160 | " \n", 161 | " g.plot(generate(\n", 162 | " shape=(nx.value, ny.value, nz.value),\n", 163 | " element_type=element_type.value,\n", 164 | " order=order.value,\n", 165 | " mesh_order=mesh_order.value,\n", 166 | " surf=surf.value,\n", 167 | " basis=basis_map[basis.value],\n", 168 | " func=gfcb,\n", 169 | " transform=mcb\n", 170 | " ))\n", 171 | "\n", 172 | "button = widgets.Button(description=\"Update\")\n", 173 | "button.on_click(show)\n", 174 | "\n", 175 | "# Figure updates on any change to nx or ny\n", 176 | "nx.observe(show, names=\"value\")\n", 177 | "ny.observe(show, names=\"value\")\n", 178 | "nz.observe(show, names=\"value\")\n", 179 | "order.observe(show, names=\"value\")\n", 180 | "mesh_order.observe(show, names=\"value\")\n", 181 | "surf.observe(show, names=\"value\")\n", 182 | "element_type.observe(show, names=\"value\")\n", 183 | "basis.observe(show, names=\"value\")\n", 184 | "element_type.observe(toggle_dim, names=\"value\")\n", 185 | "\n", 186 | "centered = widgets.Layout(display='inline-flex',\n", 187 | " align_items='center',\n", 188 | " justify_content=\"center\")\n", 189 | "\n", 190 | "# Initial state\n", 191 | "toggle_dim()\n", 192 | "\n", 193 | "# Build widget\n", 194 | "widgets.HBox([\n", 195 | " g._widget,\n", 196 | " widgets.VBox([nx, ny, nz, \n", 197 | " element_type, \n", 198 | " mesh_order, \n", 199 | " surf, transform, \n", 200 | " basis, order, func, \n", 201 | " button], layout=centered)\n", 202 | "])" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": null, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "# Sample 2D Mesh Transformation\n", 212 | "\n", 213 | "def spiral(x, y):\n", 214 | " r = 1+x+4*y\n", 215 | " phi = 4*pi*y\n", 216 | " return (r*cos(phi), r*sin(phi))\n", 217 | "\n", 218 | "# Sample 2D Surface Mesh Transformation\n", 219 | "\n", 220 | "def breather(x, y):\n", 221 | " u = 13.2*(2*x-1)\n", 222 | " v = 37.4*(2*y-1)\n", 223 | " b = 0.4\n", 224 | " r = 1 - b*b\n", 225 | " w = sqrt(r)\n", 226 | " denom = b*((w*cosh(b*u))*(w*cosh(b*u)) + (b*sin(w*v))*(b*sin(w*v)))\n", 227 | " return(-u + (2*r*cosh(b*u)*sinh(b*u)) / denom,\n", 228 | " (2*w*cosh(b*u)*(-(w*cos(v)*cos(w*v)) - sin(v)*sin(w*v))) / denom,\n", 229 | " (2*w*cosh(b*u)*(-(w*sin(v)*cos(w*v)) + cos(v)*sin(w*v))) / denom)\n", 230 | "\n", 231 | "# Sample 3D Mesh Transformation\n", 232 | "\n", 233 | "def toroid(x, y, z):\n", 234 | " ns = 3 # Number of shifts\n", 235 | " R = 1.0 # Major radius of the torus\n", 236 | " r = 0.4 # Minor radius of the torus\n", 237 | " phi = 2.0 * pi * z;\n", 238 | " theta = phi * ns / 4;\n", 239 | " u = sqrt(2) * (y - 0.5) * r;\n", 240 | " v = sqrt(2) * (x - 0.5) * r;\n", 241 | " return((R + u * cos(theta) + v * sin(theta)) * cos(phi),\n", 242 | " (R + u * cos(theta) + v * sin(theta)) * sin(phi),\n", 243 | " v * cos(theta) - u * sin(theta))\n", 244 | "\n", 245 | "# Sample 2D Function\n", 246 | "\n", 247 | "def wave2d(x,y):\n", 248 | " r = sqrt((x-0.5)**2+(y-0.5)**2)\n", 249 | " return cos(8*pi*r)*(1-r)**2\n", 250 | " \n", 251 | "# Sample 3D Function\n", 252 | " \n", 253 | "def wave3d(x, y, z):\n", 254 | " r = sqrt((x-0.5)**2+(y-0.5)**2+(z-0.5)**2)\n", 255 | " return cos(4*pi*r)*(1-r)" 256 | ] 257 | } 258 | ], 259 | "metadata": { 260 | "kernelspec": { 261 | "display_name": "Python 3 (ipykernel)", 262 | "language": "python", 263 | "name": "python3" 264 | }, 265 | "language_info": { 266 | "codemirror_mode": { 267 | "name": "ipython", 268 | "version": 3 269 | }, 270 | "file_extension": ".py", 271 | "mimetype": "text/x-python", 272 | "name": "python", 273 | "nbconvert_exporter": "python", 274 | "pygments_lexer": "ipython3", 275 | "version": "3.10.12" 276 | } 277 | }, 278 | "nbformat": 4, 279 | "nbformat_minor": 4 280 | } 281 | -------------------------------------------------------------------------------- /glvis/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.1.1" 2 | -------------------------------------------------------------------------------- /glvis/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010-2024, Lawrence Livermore National Security, LLC. Produced 2 | # at the Lawrence Livermore National Laboratory. All Rights reserved. See files 3 | # LICENSE and NOTICE for details. LLNL-CODE-443271. 4 | # 5 | # This file is part of the GLVis visualization tool and library. For more 6 | # information and source code availability see https://glvis.org. 7 | # 8 | # GLVis is free software; you can redistribute it and/or modify it under the 9 | # terms of the BSD-3 license. We welcome feedback and contributions, see file 10 | # CONTRIBUTING.md for details. 11 | 12 | from glvis.__about__ import __version__ 13 | from glvis.widget import to_stream, glvis, GlvisWidget 14 | from glvis.util import GlvisData 15 | 16 | __all__ = ["to_stream", "glvis", "GlvisWidget", "GlvisData", "__version__"] 17 | -------------------------------------------------------------------------------- /glvis/util.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | class _GlvisData(type): 4 | _keys = ( 5 | "capacitor", "distance", "ex1", "ex2", "ex27", "ex3", "ex5", "ex9", 6 | "klein_bottle", "laghos", "mesh_explorer", "mfem_logo", "minimal_surface", 7 | "mobius_strip", "navier", "quad", "quadrature_1D", "quadrature_lor", 8 | "remhos", "shaper", "shifted", "snake" 9 | ) 10 | 11 | @staticmethod 12 | def _get(key): 13 | url = f"https://github.com/GLVis/data/raw/master/streams/{key.replace('_','-')}.saved" 14 | response = requests.get(url) 15 | # success 16 | if response.status_code == 200: 17 | return response.content.decode('utf-8').rstrip("\n") 18 | else: 19 | print(f"Failed to download data from {url}") 20 | 21 | def __getattr__(self, key): 22 | if key in self._keys: 23 | return self._get(key) 24 | else: 25 | raise AttributeError(f"No data named {key}") 26 | 27 | def __dir__(self): 28 | return super().__dir__() + list(self._keys) 29 | 30 | class GlvisData(metaclass=_GlvisData): 31 | pass 32 | -------------------------------------------------------------------------------- /glvis/widget.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2010-2024, Lawrence Livermore National Security, LLC. Produced 2 | // at the Lawrence Livermore National Laboratory. All Rights reserved. See files 3 | // LICENSE and NOTICE for details. LLNL-CODE-443271. 4 | // 5 | // This file is part of the GLVis visualization tool and library. For more 6 | // information and source code availability see https://glvis.org. 7 | // 8 | // GLVis is free software; you can redistribute it and/or modify it under the 9 | // terms of the BSD-3 license. We welcome feedback and contributions, see file 10 | // CONTRIBUTING.md for details. 11 | 12 | import glvis from "https://esm.sh/gh/glvis/releases-js@gh-pages/glvis-js-0.3"; 13 | 14 | function render({ model, el }) { 15 | let div = document.createElement("div"); 16 | div.setAttribute("id", glvis.rand_id()); 17 | div.setAttribute("tabindex", "0"); 18 | el.append(div); 19 | 20 | let width = () => model.get("width"); 21 | let height = () => model.get("height"); 22 | let glv = new glvis.State(div, width(), height()); 23 | 24 | function set_size() { 25 | glv.setSize(width(), height()); 26 | } 27 | 28 | function plot() { 29 | const data = model.get("data_str"); 30 | const is_new_stream = model.get("is_new_stream"); 31 | if (is_new_stream) { 32 | glv.display(data); 33 | } else { 34 | glv.update(data); 35 | } 36 | } 37 | 38 | function handle_message(msg, buffers) { 39 | if (msg.type === "screenshot") { 40 | if (msg.use_web) { 41 | glv.saveScreenshot(msg.name); 42 | } else { 43 | glv.getPNGAsB64().then((v) => { 44 | model.send({ type: "screenshot", name: msg.name, b64: v }); 45 | }); 46 | } 47 | } 48 | } 49 | 50 | // update 51 | model.on("change:width", set_size); 52 | model.on("change:height", set_size); 53 | model.on("change:data_str", plot); 54 | model.on("msg:custom", handle_message); 55 | plot(); 56 | } 57 | 58 | export default { render }; 59 | -------------------------------------------------------------------------------- /glvis/widget.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010-2024, Lawrence Livermore National Security, LLC. Produced 2 | # at the Lawrence Livermore National Laboratory. All Rights reserved. See files 3 | # LICENSE and NOTICE for details. LLNL-CODE-443271. 4 | # 5 | # This file is part of the GLVis visualization tool and library. For more 6 | # information and source code availability see https://glvis.org. 7 | # 8 | # GLVis is free software; you can redistribute it and/or modify it under the 9 | # terms of the BSD-3 license. We welcome feedback and contributions, see file 10 | # CONTRIBUTING.md for details. 11 | 12 | import anywidget 13 | import io 14 | from IPython.display import display as ipydisplay 15 | from traitlets import Unicode, Int, Bool 16 | from typing import Union, Tuple 17 | from pathlib import Path 18 | import base64 19 | 20 | try: 21 | from mfem._ser.mesh import Mesh 22 | from mfem._ser.gridfunc import GridFunction 23 | except: 24 | Mesh = object 25 | GridFunction = object 26 | 27 | Data = Union[Tuple[Mesh, GridFunction], Mesh, str] 28 | 29 | def to_stream(data: Data) -> str: 30 | if isinstance(data, str): 31 | return data 32 | 33 | sio = io.StringIO() 34 | if isinstance(data, tuple): 35 | sio.write("solution\n") 36 | data[0].WriteToStream(sio) 37 | data[1].WriteToStream(sio) 38 | elif isinstance(data, Mesh): 39 | sio.write("mesh\n") 40 | data.WriteToStream(sio) 41 | else: 42 | raise TypeError("Unknown data type") 43 | return sio.getvalue() 44 | 45 | class _GlvisWidgetCore(anywidget.AnyWidget): 46 | """ 47 | This is the backend that inherits from AnyWidget. Because we don't want all of the AnyWidget 48 | properties/methods exposed to the user, the front-end class GlvisWidget is a composition 49 | of this object, rather than directly inheriting from AnyWidget. 50 | 51 | _esm must be specified, and is basically a giant string with all of the javascript code required 52 | It could be defined as a docstring here, but for organization and syntax highlighting, it is 53 | defined in `widget.js`, similar to the organization used by pyobsplot: 54 | https://github.com/juba/pyobsplot/blob/main/src/pyobsplot/widget.py 55 | """ 56 | _esm = anywidget._file_contents.FileContents( 57 | Path(__file__).parent / "widget.js", start_thread=False 58 | ) 59 | 60 | data_str = Unicode('').tag(sync=True) 61 | width = Int(640).tag(sync=True) 62 | height = Int(480).tag(sync=True) 63 | is_new_stream = Bool().tag(sync=True) 64 | 65 | 66 | class GlvisWidget: 67 | """ 68 | Front-end class used to interactively visualize data. 69 | """ 70 | def __init__(self, data: Data, width: int=640, height: int=480, keys=None): 71 | """ 72 | Parameters 73 | ---------- 74 | data : Union[Tuple[Mesh, GridFunction], Mesh, str] 75 | Data to be visualized. Can consist of the PyMFEM objects: (Mesh, GridFunction) or Mesh, 76 | or it can be read directly from a stream/string (see `examples/basic.ipynb`). 77 | width : int, optional 78 | Width of visualization 79 | height : int, optional 80 | Height of visualization 81 | keys : str, optional 82 | Keyboard commands to customize the visualization. Can also be typed into the widget 83 | after it is instantiated. For a full list of options, see the GLVis README: 84 | https://github.com/GLVis/glvis?tab=readme-ov-file#key-commands 85 | """ 86 | 87 | self._widget = _GlvisWidgetCore() 88 | self.set_size(width, height) 89 | self._sync(data, is_new=True, keys=keys) 90 | 91 | # Automatically renders the widget - necessary because this is a wrapper class 92 | def _repr_mimebundle_(self, **kwargs): 93 | return self._widget._repr_mimebundle_(**kwargs) 94 | 95 | def set_size(self, width: int, height: int): 96 | self._widget.width = width 97 | self._widget.height = height 98 | 99 | def plot(self, data: Data, keys=None): 100 | self._sync(data, is_new=True, keys=keys) 101 | 102 | def update(self, data: Data, keys=None): 103 | self._sync(data, is_new=False, keys=keys) 104 | 105 | def _on_msg(self, _, content, buffers): 106 | if content.get("type", "") == "screenshot": 107 | data = content.get("b64", "") 108 | name = content.get("name", "glvis.png") 109 | if not data: 110 | print(f"unable to save {name}, bad data") 111 | return 112 | with open(name, "wb") as f: 113 | f.write(base64.decodebytes(data.encode('ascii'))) 114 | 115 | def _sync(self, data: Data, is_new: bool=True, keys=None): 116 | self._widget.is_new_stream = is_new 117 | data_string = to_stream(data) 118 | if keys is not None: 119 | key_string = keys if isinstance(data, str) else f"keys {keys}" 120 | data_string += key_string 121 | self._widget.data_str = data_string 122 | 123 | def render(self): 124 | ipydisplay(self) 125 | 126 | # Constructor alias 127 | def glvis(data: Data, width: int=640, height: int=480, keys=None): 128 | return GlvisWidget(data, width, height, keys) 129 | glvis.__doc__ = GlvisWidget.__init__.__doc__ 130 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "glvis" 7 | dynamic = ["version"] 8 | description = "GLVis widget for Jupyter notebooks" 9 | readme = "README.md" 10 | license = { file = "LICENSE" } 11 | dependencies = [ 12 | "mfem>=4.5.2", 13 | "anywidget>=0.9.9", 14 | "ipywidgets>=8.0.0" # required for _repr_mimebundle_ 15 | ] 16 | 17 | [project.urls] 18 | homepage = "https://glvis.org" 19 | repostiory = "https://github.com/glvis/pyglvis" 20 | 21 | [tool.hatch.version] 22 | path = "glvis/__about__.py" 23 | 24 | [tool.hatch.build.targets.wheel] 25 | packages = ["glvis"] 26 | --------------------------------------------------------------------------------