Visualizing quadrature functions as point clouds

Visualizing quadrature functions as point clouds#

Author: Henrik N.T. Finsberg

SPDX-License-Identifier: MIT

Quadrature functions are not possible to visualize directly in ParaView, as they are not defined on a mesh. However, we can visualize them as point clouds. In this example we will show how you can use scifem to save your quadrature fuctions as XDMF files, which can be loaded into ParaView for visualization.

Note

This demo requires a backend for writing HDF5 files. Please install scifem with either scifem[adios2] or scifem[h5py].

First, we import the necessary modules.

import logging
from mpi4py import MPI
import numpy as np
import basix
import dolfinx
import scifem
# First initialize logging
logging.basicConfig(level=logging.INFO)

Now let’s create a quadrature function on a unit square mesh. We will use the basix.ufl module to create the quadrature element.

mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10, dolfinx.mesh.CellType.triangle, dtype=np.float64)
el = basix.ufl.quadrature_element(
    scheme="default", degree=3, cell=mesh.basix_cell(), value_shape=()
)
V = dolfinx.fem.functionspace(mesh, el)
u = dolfinx.fem.Function(V)
u.interpolate(lambda x: np.sin(np.pi * x[0]) * np.sin(np.pi * x[1]))

One option that already exists with the current DOLFINx/Pyvista API is to plot them as points

import pyvista
plotter = pyvista.Plotter()
plotter.add_points(
    V.tabulate_dof_coordinates(),
    scalars=u.x.array,
    render_points_as_spheres=True,
    point_size=20,
    show_scalar_bar=False,
)
if not pyvista.OFF_SCREEN:
    plotter.show()
2026-08-05 06:37:42.901 (   0.565s) [    7FBAF9CDA140]vtkXOpenGLRenderWindow.:1460  WARN| bad X server connection. DISPLAY=:99.0
INFO:trame_server.utils.namespace:Translator(prefix=None)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
File /dolfinx-env/lib/python3.12/site-packages/aiohttp/web_urldispatcher.py:546, in StaticResource.__init__(self, prefix, directory, name, expect_handler, chunk_size, show_index, follow_symlinks, append_version)
    545 try:
--> 546     directory = Path(directory).expanduser().resolve(strict=True)
    547 except FileNotFoundError as error:

File /usr/lib/python3.12/pathlib.py:1242, in Path.resolve(self, strict)
   1241 try:
-> 1242     s = self._flavour.realpath(self, strict=strict)
   1243 except OSError as e:

File <frozen posixpath>:432, in realpath(filename, strict)

File <frozen posixpath>:477, in _joinrealpath(path, rest, strict, seen)

FileNotFoundError: [Errno 2] No such file or directory: '/dolfinx-env/lib/python3.12/site-packages/trame_client/module/vue3-www'

The above exception was the direct cause of the following exception:

ValueError                                Traceback (most recent call last)
Cell In[5], line 10
      6     point_size=20,
      7     show_scalar_bar=False,
      8 )
      9 if not pyvista.OFF_SCREEN:
---> 10     plotter.show()

File /dolfinx-env/lib/python3.12/site-packages/pyvista/_deprecate_positional_args.py:243, in _deprecate_positional_args.<locals>._inner_deprecate_positional_args.<locals>.inner_f(*args, **kwargs)
    239             warn_external(msg, PyVistaDeprecationWarning)
    241         warn_positional_args()
--> 243 return f(*args, **kwargs)

File /dolfinx-env/lib/python3.12/site-packages/pyvista/plotting/plotter.py:8231, in Plotter.show(self, title, window_size, interactive, auto_close, interactive_update, full_screen, screenshot, return_img, cpos, jupyter_backend, return_viewer, return_cpos, before_close_callback, store_image_depth, **kwargs)
   8228         jupyter_backend = self._theme.jupyter_backend
   8230     if jupyter_backend is None or jupyter_backend.lower() != 'none':
-> 8231         jupyter_disp = handle_plotter(self, backend=jupyter_backend, **jupyter_kwargs)
   8233 self.render()
   8235 # initial double render needed for certain passes when offscreen

File /dolfinx-env/lib/python3.12/site-packages/pyvista/jupyter/notebook.py:74, in handle_plotter(plotter, backend, screenshot, **kwargs)
     71     if backend in ['server', 'client', 'trame', 'html']:
     72         from pyvista.trame.jupyter import show_trame  # noqa: PLC0415
---> 74         return show_trame(plotter, mode=backend, **kwargs)
     76 except ImportError as e:
     77     # Trame was explicitly requested but not available
     78     _ensure_entry_points()

File /dolfinx-env/lib/python3.12/site-packages/pyvista/trame/jupyter.py:394, in show_trame(plotter, mode, name, server_proxy_enabled, server_proxy_prefix, jupyter_extension_enabled, collapse_menu, add_menu, add_menu_items, default_server_rendering, handler, **kwargs)
    391 kwargs.setdefault('height', dh)
    393 if mode == 'html':
--> 394     return EmbeddableWidget(plotter, **kwargs)
    396 if jupyter_extension_enabled is None:
    397     jupyter_extension_enabled = pv.global_theme.trame.jupyter_extension_enabled

File /dolfinx-env/lib/python3.12/site-packages/pyvista/trame/jupyter.py:137, in EmbeddableWidget.__init__(self, plotter, width, height, **kwargs)
    135     msg = 'Please install `ipywidgets`.'
    136     raise ImportError(msg)
--> 137 scene = plotter.export_html(filename=None)
    138 src = scene.getvalue().replace('"', '&quot;')
    139 # eventually we could maybe expose this, but for now make sure we're at least
    140 # consistent with matplotlib's color (light gray)

File /dolfinx-env/lib/python3.12/site-packages/pyvista/plotting/plotter.py:784, in BasePlotter.export_html(self, filename)
    781     msg = 'Please install trame dependencies: pip install "pyvista[jupyter]"'
    782     raise ImportError(msg)
--> 784 data = self.export_vtksz(filename=None)
    785 buffer = io.StringIO()
    786 write_html(data, buffer)

File /dolfinx-env/lib/python3.12/site-packages/pyvista/plotting/plotter.py:837, in BasePlotter.export_vtksz(self, filename, format)
    835 server = get_server(pv.global_theme.trame.jupyter_server_name)
    836 if not server.running:
--> 837     elegantly_launch(pv.global_theme.trame.jupyter_server_name)
    839 view = PyVistaLocalView(self, trame_server=server)
    841 content = view.export(format=format)

File /dolfinx-env/lib/python3.12/site-packages/pyvista/trame/jupyter.py:487, in elegantly_launch(*args, **kwargs)
    484 # Basically monkey patches asyncio to support this
    485 nest_asyncio2.apply()
--> 487 return asyncio.run(launch_it())

File /dolfinx-env/lib/python3.12/site-packages/nest_asyncio2.py:115, in run(main, debug, loop_factory)
    113 task = asyncio.ensure_future(main, loop=loop)
    114 try:
--> 115     return loop.run_until_complete(task)
    116 finally:
    117     if not task.done():

File /dolfinx-env/lib/python3.12/site-packages/nest_asyncio2.py:230, in _patch_loop.<locals>.run_until_complete(self, future)
    227 if self._ready:
    228     self._run_once()
--> 230 return f.result()

File /usr/lib/python3.12/asyncio/futures.py:203, in Future.result(self)
    201 self.__log_traceback = False
    202 if self._exception is not None:
--> 203     raise self._exception.with_traceback(self._exception_tb)
    204 return self._result

File /usr/lib/python3.12/asyncio/tasks.py:314, in Task.__step_run_and_handle_result(***failed resolving arguments***)
    310 try:
    311     if exc is None:
    312         # We use the `send` method directly, because coroutines
    313         # don't have `__iter__` and `__next__` methods.
--> 314         result = coro.send(None)
    315     else:
    316         result = coro.throw(exc)

File /dolfinx-env/lib/python3.12/site-packages/pyvista/trame/jupyter.py:482, in elegantly_launch.<locals>.launch_it()
    481 async def launch_it():
--> 482     await launch_server(*args, **kwargs).ready

File /dolfinx-env/lib/python3.12/site-packages/pyvista/trame/jupyter.py:215, in launch_server(server, port, host, wslink_backend, **kwargs)
    213 if server._running_stage == 0:
    214     server.controller.on_server_ready.add(on_ready)
--> 215     server.start(
    216         exec_mode='task',
    217         host=host,
    218         port=port,
    219         open_browser=False,
    220         show_connection_info=False,
    221         disable_logging=True,
    222         timeout=0,
    223         backend=wslink_backend,
    224     )
    225 # else, server is already running or launching
    226 return server

File /dolfinx-env/lib/python3.12/site-packages/trame_server/core.py:725, in Server.start(self, port, thread, open_browser, show_connection_info, disable_logging, backend, follow_symlinks, exec_mode, timeout, host, **kwargs)
    722 CoreServer.configure(options)
    724 self._running_stage = 1
--> 725 task = CoreServer.server_start(
    726     options,
    727     **{  # Do a proper merging/override
    728         **kwargs,
    729         "disableLogging": disable_logging,
    730         "backend": backend,
    731         "exec_mode": exec_mode,
    732     },
    733 )
    735 # Manage exit life cycle unless coroutine
    736 if exec_mode == "main":

File /dolfinx-env/lib/python3.12/site-packages/trame_server/protocol.py:52, in CoreServer.server_start(options, disableLogging, backend, exec_mode, **kwargs)
     43 @staticmethod
     44 def server_start(
     45     options,
   (...)     50 ):
     51     # NOTE: **kwargs to wslink's start_webserver are currently unused
---> 52     return server.start_webserver(
     53         options=options,
     54         protocol=CoreServer,
     55         disableLogging=disableLogging,
     56         backend=backend,
     57         exec_mode=exec_mode,
     58         **kwargs,
     59     )

File /dolfinx-env/lib/python3.12/site-packages/wslink/server.py:257, in start_webserver(options, protocol, disableLogging, backend, exec_mode, **_)
    254     server_config["handle_signals"] = not options.nosignalhandlers
    256 # Create the webserver and start it
--> 257 ws_server = create_webserver(server_config, backend=backend)
    259 # Once we have python 3.7 minimum, we can start the server with asyncio.run()
    260 # asyncio.run(ws_server.start())
    261 
    262 # Until then, we can start the server this way
    263 try:

File /dolfinx-env/lib/python3.12/site-packages/wslink/server.py:168, in create_webserver(server_config, backend)
    167 def create_webserver(server_config, backend="aiohttp"):
--> 168     return backends.create_webserver(server_config, backend=backend)

File /dolfinx-env/lib/python3.12/site-packages/wslink/backends/__init__.py:5, in create_webserver(server_config, backend)
      2 if backend == "aiohttp":
      3     from .aiohttp import create_webserver  # noqa: PLC0415
----> 5     return create_webserver(server_config)
      7 if backend == "generic":
      8     from .generic import create_webserver  # noqa: PLC0415

File /dolfinx-env/lib/python3.12/site-packages/wslink/backends/aiohttp/__init__.py:218, in create_webserver(server_config)
    215     return ReverseWebAppServer(server_config)
    217 # Normal web server
--> 218 return WebAppServer(server_config)

File /dolfinx-env/lib/python3.12/site-packages/wslink/backends/aiohttp/__init__.py:118, in WebAppServer.__init__(self, server_config)
    116     # Resolve / => index.html
    117     self.app.router.add_route("GET", "/", _root_handler)
--> 118     self.app.add_routes(routes)
    120 self.app[STATE_KEY] = {}

File /dolfinx-env/lib/python3.12/site-packages/aiohttp/web_app.py:379, in Application.add_routes(self, routes)
    378 def add_routes(self, routes: Iterable[AbstractRouteDef]) -> list[AbstractRoute]:
--> 379     return self.router.add_routes(routes)

File /dolfinx-env/lib/python3.12/site-packages/aiohttp/web_urldispatcher.py:1259, in UrlDispatcher.add_routes(self, routes)
   1257 registered_routes = []
   1258 for route_def in routes:
-> 1259     registered_routes.extend(route_def.register(self))
   1260 return registered_routes

File /dolfinx-env/lib/python3.12/site-packages/aiohttp/web_routedef.py:87, in StaticDef.register(self, router)
     86 def register(self, router: UrlDispatcher) -> list[AbstractRoute]:
---> 87     resource = router.add_static(self.prefix, self.path, **self.kwargs)
     88     routes = resource.get_info().get("routes", {})
     89     return list(routes.values())

File /dolfinx-env/lib/python3.12/site-packages/aiohttp/web_urldispatcher.py:1183, in UrlDispatcher.add_static(self, prefix, path, name, expect_handler, chunk_size, show_index, follow_symlinks, append_version)
   1181 if prefix.endswith("/"):
   1182     prefix = prefix[:-1]
-> 1183 resource = StaticResource(
   1184     prefix,
   1185     path,
   1186     name=name,
   1187     expect_handler=expect_handler,
   1188     chunk_size=chunk_size,
   1189     show_index=show_index,
   1190     follow_symlinks=follow_symlinks,
   1191     append_version=append_version,
   1192 )
   1193 self.register_resource(resource)
   1194 return resource

File /dolfinx-env/lib/python3.12/site-packages/aiohttp/web_urldispatcher.py:548, in StaticResource.__init__(self, prefix, directory, name, expect_handler, chunk_size, show_index, follow_symlinks, append_version)
    546     directory = Path(directory).expanduser().resolve(strict=True)
    547 except FileNotFoundError as error:
--> 548     raise ValueError(f"'{directory}' does not exist") from error
    549 if not directory.is_dir():
    550     raise ValueError(f"'{directory}' is not a directory")

ValueError: '/dolfinx-env/lib/python3.12/site-packages/trame_client/module/vue3-www' does not exist

Using scifem, we can write the point cloud data to an XDMFFile that can be opened with Paraview.

with scifem.xdmf.XDMFFile("point_cloud.xdmf", [u]) as xdmf:
    xdmf.write(0.0)

The point cloud can now be loaded into ParaView for visualization, by selecting “Point Gaussian” as the representation. Point cloud in ParaView

We can write any dolfinx.fem.FunctionSpace that has support for tabulate_dof_coordinates to a point cloud. For example, we can create a higher order Lagrange space, and write two functions to file.

Q = dolfinx.fem.functionspace(mesh, ("Lagrange", 3, (2, )))
q_1 = dolfinx.fem.Function(Q, name="q_sin")
q_1.interpolate(lambda x: (np.sin(np.pi * x[0]),  np.sin(np.pi * x[1])))
q_2 = dolfinx.fem.Function(Q, name="q_cos")
q_2.interpolate(lambda x: (np.cos(np.pi * x[0]),  np.cos(np.pi * x[1])))

We write these two functions to file as illustrated above

with scifem.xdmf.XDMFFile("point_cloud_lagrange.xdmf", [q_1, q_2]) as xdmf:
    xdmf.write(0.0)

yielding the following point clouds in ParaView after applying glyphs. Point cloud in ParaView

If you have time dependent data, you can write multiple time steps to the same file using e.g

with scifem.xdmf.XDMFFile("point_cloud_lagrange.xdmf", [q_1, q_2]) as xdmf:
    xdmf.write(0.0)
    q_1.interpolate(lambda x: (-x[0], x[1]))
    q_2.interpolate(lambda x: (x[0], -x[1]))
    xdmf.write(0.3)

If you need to keep the file open for longer, you can use the following syntax

xdmf = scifem.xdmf.XDMFFile("point_cloud_lagrange.xdmf", [q_1, q_2])
xdmf.write(0.0)
q_1.interpolate(lambda x: (-x[0], x[1]))
q_2.interpolate(lambda x: (x[0], -x[1]))
xdmf.write(0.3)
xdmf.close()