Copyright (C) 2021 Jørgen S. Dokken

This file is part of DOLFINx (https://www.fenicsproject.org)

SPDX-License-Identifier:    LGPL-3.0-or-later

===============================
Using pyvista for visualization
===============================


::

  import dolfinx
  import dolfinx.io
  import dolfinx.plot
  import numpy as np
  import ufl
  from mpi4py import MPI
  
  try:
      import pyvista
  except ModuleNotFoundError:
      print("pyvista is required for this demo")
      exit(0)
  
  # If environment variable PYVISTA_OFF_SCREEN is set to true save a png
  # otherwise create interactive plot
  if pyvista.OFF_SCREEN:
      pyvista.start_xvfb(wait=0.1)
  
  # Set some global options for all plots
  transparent = False
  figsize = 800
  pyvista.rcParams["background"] = [0.5, 0.5, 0.5]
  
  
Plotting a 3D dolfinx.Function with pyvista
===========================================

::

  # Interpolate a simple scalar function in 3D
  def int_u(x):
      return x[0] + 3 * x[1] + 5 * x[2]
  
  
  mesh = dolfinx.UnitCubeMesh(MPI.COMM_WORLD, 4, 3, 5, cell_type=dolfinx.cpp.mesh.CellType.tetrahedron)
  V = dolfinx.FunctionSpace(mesh, ("CG", 1))
  u = dolfinx.Function(V)
  u.interpolate(int_u)
  
  # Extract mesh data from DOLFINx (only plot cells owned by the
  # processor) and create a pyvista UnstructuredGrid
  num_cells = mesh.topology.index_map(mesh.topology.dim).size_local
  cell_entities = np.arange(num_cells, dtype=np.int32)
  pyvista_cells, cell_types = dolfinx.plot.create_vtk_topology(mesh, mesh.topology.dim, cell_entities)
  grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, mesh.geometry.x)
  
  # Compute the function values at the vertices, this is equivalent to a
  # P1 Lagrange interpolation, and can be directly attached to the Pyvista
  # mesh. Discard complex value if running DOLFINx with complex PETSc as
  # backend
  vertex_values = u.compute_point_values()
  if np.iscomplexobj(vertex_values):
      vertex_values = vertex_values.real
  
  # Create point cloud of vertices, and add the vertex values to the cloud
  grid.point_arrays["u"] = vertex_values
  grid.set_active_scalars("u")
  
  # Create a pyvista plotter which is used to visualize the output
  plotter = pyvista.Plotter()
  plotter.add_text("Mesh and corresponding dof values",
                   position="upper_edge", font_size=14, color="black")
  
  # Some styling arguments for the colorbar
  sargs = dict(height=0.6, width=0.1, vertical=True, position_x=0.825, position_y=0.2, fmt="%1.2e",
               title_font_size=40, color="black", label_font_size=25)
  
  # Plot the mesh (as a wireframe) with the finite element function
  # visualized as the point cloud
  plotter.add_mesh(grid, style="wireframe", line_width=2, color="black")
  
  # To be able to visualize the mesh and nodes at the same time, we have
  # to copy the grid
  plotter.add_mesh(grid.copy(), style="points", render_points_as_spheres=True,
                   scalars=vertex_values, point_size=10)
  plotter.set_position([1.5, 0.5, 4])
  
  # Save as png if we are using a container with no rendering
  if pyvista.OFF_SCREEN:
      plotter.screenshot("3D_wireframe_with_nodes.png", transparent_background=transparent,
                         window_size=[figsize, figsize])
  else:
      plotter.show()
  
  # Create a new plotter, and plot the values as a surface over the mesh
  plotter = pyvista.Plotter()
  plotter.add_text("Function values over the surface of a mesh",
                   position="upper_edge", font_size=14, color="black")
  
  # Define some styling arguments for a colorbar
  sargs = dict(height=0.1, width=0.8, vertical=False, position_x=0.1,
               position_y=0.05, fmt="%1.2e",
               title_font_size=40, color="black", label_font_size=25)
  
  # Adjust camera to show the entire mesh
  plotter.set_position([-2, -2, 2.1])
  plotter.set_focus([1, 1, -0.01])
  plotter.set_viewup([0, 0, 1])
  
  # Add mesh with edges
  plotter.add_mesh(grid, show_edges=True, scalars="u", scalar_bar_args=sargs)
  if pyvista.OFF_SCREEN:
      plotter.screenshot("3D_function.png", transparent_background=transparent, window_size=[figsize, figsize])
  else:
      plotter.show()
  
Plotting a 2D dolfinx.Function with pyvista using warp by scalar
================================================================

As in the previous section, we interpolate a function into a CG
function space


::

  def int_2D(x):
      return np.sin(np.pi * x[0]) * np.sin(2 * x[1] * np.pi)
  
  
  mesh = dolfinx.UnitSquareMesh(MPI.COMM_WORLD, 12, 12, cell_type=dolfinx.cpp.mesh.CellType.quadrilateral)
  V = dolfinx.FunctionSpace(mesh, ("Lagrange", 1))
  u = dolfinx.Function(V)
  u.interpolate(int_2D)
  
  # As in the previous section, we extract the geometry and topology of
  # the mesh, and attach values to the vertices
  num_cells = mesh.topology.index_map(mesh.topology.dim).size_local
  cells = np.arange(num_cells, dtype=np.int32)
  pyvista_cells, cell_types = dolfinx.plot.create_vtk_topology(mesh, mesh.topology.dim, cells)
  grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, mesh.geometry.x)
  point_values = u.compute_point_values()
  if np.iscomplexobj(point_values):
      point_values = point_values.real
  grid.point_arrays["u"] = point_values
  
  # We set the function "u" as the active scalar for the mesh, and warp
  # the mesh in z-direction by its values
  grid.set_active_scalars("u")
  warped = grid.warp_by_scalar()
  
  # Plot mesh with scalar bar
  plotter = pyvista.Plotter()
  plotter.add_text("Warped function", position="upper_edge", font_size=14, color="black")
  sargs = dict(height=0.8, width=0.1, vertical=True, position_x=0.05,
               position_y=0.05, fmt="%1.2e",
               title_font_size=40, color="black", label_font_size=25)
  plotter.set_position([-3, 2.6, 0.3])
  plotter.set_focus([3, -1, -0.15])
  plotter.set_viewup([0, 0, 1])
  plotter.add_mesh(warped, show_edges=True, scalar_bar_args=sargs)
  if pyvista.OFF_SCREEN:
      plotter.screenshot("2D_function_warp.png", transparent_background=transparent, window_size=[figsize, figsize])
  else:
      plotter.show()
  
Plotting a 2D MeshTags and using subplots
=========================================

We continue using the mesh from the previous section, and find all
cells satisfying the condition below


::

  def in_circle(x):
      # Mark sphere with radius < sqrt(2)
      return np.array((x.T[0] - 0.5)**2 + (x.T[1] - 0.5)**2 < 0.2**2, dtype=np.int32)
  
  
  # Create a dolfinx.MeshTag for all cells. If midpoint is inside the
  # circle, it gets value 1, otherwise 0.
  num_cells = mesh.topology.index_map(mesh.topology.dim).size_local
  midpoints = dolfinx.cpp.mesh.midpoints(mesh, mesh.topology.dim, list(np.arange(num_cells, dtype=np.int32)))
  cell_tags = dolfinx.MeshTags(mesh, mesh.topology.dim, np.arange(num_cells), in_circle(midpoints))
  
  # As the dolfinx.MeshTag contains a value for every cell in the
  # geometry, we can attach it directly to the grid
  grid.cell_arrays["Marker"] = cell_tags.values
  grid.set_active_scalars("Marker")
  
  # We create a plotter consisting of two windows, and add a plot of the
  # Meshtags to the first window.
  subplotter = pyvista.Plotter(shape=(1, 2))
  subplotter.subplot(0, 0)
  subplotter.add_text("Mesh with markers", font_size=14, color="black", position="upper_edge")
  subplotter.add_mesh(grid, show_edges=True, show_scalar_bar=False)
  subplotter.view_xy()
  
  # We can also visualize subsets of data, by creating a smaller topology,
  # only consisting of thos entities that has value one in the
  # dolfinx.MeshTag
  pyvista_cells, cell_types = dolfinx.plot.create_vtk_topology(
      mesh, mesh.topology.dim, cell_tags.indices[cell_tags.values == 1])
  
  # We add this grid to the second plotter
  sub_grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, mesh.geometry.x)
  subplotter.subplot(0, 1)
  subplotter.add_text("Subset of mesh", font_size=14, color="black", position="upper_edge")
  subplotter.add_mesh(sub_grid, show_edges=True, edge_color="black")
  
  if pyvista.OFF_SCREEN:
      subplotter.screenshot("2D_markers.png", transparent_background=transparent,
                            window_size=[2 * figsize, figsize])
  else:
      subplotter.show()
  
Plotting a dolfinx.Function
===========================

In the previous sections we have considered CG-1 spaces, which have a
1-1 correspondence with the vertices of the geometry. To be able to
plot higher order function spaces, both CG and DG spaces, we have to
adjust our plotting technique.

::

  # We start by projecting a discontinuous function into a second order DG
  # space Note that we use the `cell_tags` from the previous section to
  # restrict the integration domain on the RHS.
  dx = ufl.Measure("dx", subdomain_data=cell_tags)
  V = dolfinx.FunctionSpace(mesh, ("DG", 2))
  uh = dolfinx.Function(V)
  u = ufl.TrialFunction(V)
  v = ufl.TestFunction(V)
  x = ufl.SpatialCoordinate(mesh)
  a = ufl.inner(u, v) * dx
  L = ufl.inner(x[0], v) * dx(1) + ufl.inner(0.01, v) * dx(0)
  problem = dolfinx.fem.LinearProblem(a, L, u=uh, petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
  problem.solve()
  
  # To get a topology that has a 1-1 correspondence with the degrees of
  # freedom in the function space, we call
  # `dolfinx.plot.create_vtk_topology`. We obtain the geometry for
  # the dofs owned on this process by tabulation of the dof coordinates.
  topology, cell_types = dolfinx.plot.create_vtk_topology(V)
  num_dofs_local = uh.function_space.dofmap.index_map.size_local
  geometry = uh.function_space.tabulate_dof_coordinates()[:num_dofs_local]
  
  # We discard the complex values if using PETSc in complex mode
  values = uh.vector.array.real if np.iscomplexobj(uh.vector.array) else uh.vector.array
  
  # We create a pyvista mesh from the topology and geometry, and attach
  # the coefficients of the degrees of freedom
  grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
  grid.point_arrays["DG"] = values
  grid.set_active_scalars("DG")
  
  # We would also like to visualize the underlying mesh and obtain that as
  # we have done previously
  num_cells = mesh.topology.index_map(mesh.topology.dim).size_local
  cell_entities = np.arange(num_cells, dtype=np.int32)
  pyvista_cells, cell_types = dolfinx.plot.create_vtk_topology(mesh, mesh.topology.dim, cell_entities)
  org_grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, mesh.geometry.x)
  
  
  # We visualize the data
  plotter = pyvista.Plotter()
  plotter.add_text("Second order discontinuous elements",
                   position="upper_edge", font_size=14, color="black")
  sargs = dict(height=0.1, width=0.8, vertical=False, position_x=0.1, position_y=0, color="black")
  plotter.add_mesh(grid, show_edges=False, scalar_bar_args=sargs, line_width=0)
  plotter.add_mesh(org_grid, color="white", style="wireframe", line_width=5)
  plotter.add_mesh(grid.copy(), style="points", point_size=15, render_points_as_spheres=True, line_width=0)
  plotter.view_xy()
  if pyvista.OFF_SCREEN:
      plotter.screenshot(f"DG_{MPI.COMM_WORLD.rank}.png",
                         transparent_background=transparent, window_size=[figsize, figsize])
  else:
      plotter.show()
  
  
Plotting a dolfinx.Function with vector values
===============================================

::

  # In the previous sections, we have considered how to plot scalar valued
  # functions. This section will show you how to plot vector valued
  # functions. We start by interpolating an expression into a second order
  # CG space.
  def vel(x):
      vals = np.zeros((2, x.shape[1]))
      vals[0] = np.sin(x[1])
      vals[1] = 0.1 * x[0]
      return vals
  
  
  mesh = dolfinx.UnitSquareMesh(MPI.COMM_WORLD, 6, 6, dolfinx.cpp.mesh.CellType.triangle)
  V = dolfinx.VectorFunctionSpace(mesh, ("CG", 2))
  uh = dolfinx.Function(V)
  uh.interpolate(vel)
  
  # We use the `dolfinx.plot.create_vtk_topology`
  # function, as in the previous section. However, we input a set of cell
  # entities, which can restrict the plotting to subsets of our mesh
  num_cells = mesh.topology.index_map(mesh.topology.dim).size_local
  cell_entities = np.arange(num_cells, dtype=np.int32)
  topology, cell_types = dolfinx.plot.create_vtk_topology(V, cell_entities)
  
  # As we deal with a vector function space, we need to adjust the values
  # in the underlying one dimensional array in dolfinx.Function, by
  # reshaping the data, and add an extra column to make it a 3D vector
  num_dofs_local = uh.function_space.dofmap.index_map.size_local
  geometry = uh.function_space.tabulate_dof_coordinates()[:num_dofs_local]
  values = np.zeros((V.dofmap.index_map.size_local, 3), dtype=np.float64)
  values[:, :mesh.geometry.dim] = uh.vector.array.real.reshape(V.dofmap.index_map.size_local, V.dofmap.index_map_bs)
  
  # Create a point cloud of glyphs
  function_grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
  function_grid["vectors"] = values
  function_grid.set_active_vectors("vectors")
  glyphs = function_grid.glyph(orient="vectors", factor=0.1)
  
  # To add streamlines in 2D, you need to supply the two points creating a
  # sampling line
  # NOTE: This crashes if ran in parallel, seems to be a pyvista issue
  # with pointa, pointb
  if MPI.COMM_WORLD.size == 0:
      streamlines = function_grid.streamlines(vectors="vectors", return_source=False,
                                              pointa=(0.5, 0.0, 0), pointb=(0.5, 1, 0))
  
  # Create pyvista mesh from the mesh
  pyvista_cells, cell_types = dolfinx.plot.create_vtk_topology(mesh, mesh.topology.dim, cell_entities)
  grid = pyvista.UnstructuredGrid(pyvista_cells, cell_types, mesh.geometry.x)
  
  # Add mesh, glyphs and streamlines to plotter
  plotter = pyvista.Plotter()
  plotter.add_text("Second order vector function.",
                   position="upper_edge", font_size=14, color="black")
  sargs = dict(height=0.1, width=0.8, vertical=False, position_x=0.1, position_y=0, color="black")
  
  plotter.add_mesh(grid, show_edges=True, color="black", style="wireframe")
  plotter.add_mesh(glyphs)
  if MPI.COMM_WORLD.size == 0:
      plotter.add_mesh(streamlines.tube(radius=0.001), show_scalar_bar=False)
  plotter.view_xy()
  if pyvista.OFF_SCREEN:
      plotter.screenshot(f"vectors_{MPI.COMM_WORLD.rank}.png",
                         transparent_background=transparent, window_size=[figsize, figsize])
  else:
      plotter.show()
  
Plotting a dolfinx.Function with vector values
===============================================

In this section we illustrate how to visualize streamlines in 3D


::

  def vel(x):
      vals = np.zeros((3, x.shape[1]))
      vals[0] = -(x[1] - 0.5)
      vals[1] = x[0] - 0.5
      vals[2] = 0
      return vals
  
  
  mesh = dolfinx.UnitCubeMesh(MPI.COMM_WORLD, 4, 4, 4, dolfinx.cpp.mesh.CellType.hexahedron)
  V = dolfinx.VectorFunctionSpace(mesh, ("DG", 2))
  uh = dolfinx.Function(V)
  uh.interpolate(vel)
  
  num_cells = mesh.topology.index_map(mesh.topology.dim).size_local
  cell_entities = np.arange(num_cells, dtype=np.int32)
  
  topology, cell_types = dolfinx.plot.create_vtk_topology(V, cell_entities)
  num_dofs_local = uh.function_space.dofmap.index_map.size_local
  geometry = uh.function_space.tabulate_dof_coordinates()[:num_dofs_local]
  values = np.zeros((V.dofmap.index_map.size_local, 3), dtype=np.float64)
  values[:, :mesh.geometry.dim] = uh.vector.array.real.reshape(V.dofmap.index_map.size_local, V.dofmap.index_map_bs)
  
  # Create a point cloud of glyphs
  grid = pyvista.UnstructuredGrid(topology, cell_types, geometry)
  grid["vectors"] = values
  grid.set_active_vectors("vectors")
  glyphs = grid.glyph(orient="vectors", factor=0.1)
  streamlines = grid.streamlines(vectors="vectors", return_source=False, source_radius=1, n_points=150)
  
  # Create Create plotter
  plotter = pyvista.Plotter()
  plotter.add_text("Vector function as streamlines.",
                   position="upper_edge", font_size=20, color="black")
  sargs = dict(height=0.1, width=0.8, vertical=False, position_x=0.1, position_y=0, color="black")
  plotter.add_mesh(grid, style="wireframe")
  plotter.add_mesh(glyphs)
  plotter.add_mesh(streamlines.tube(radius=0.001))
  plotter.view_xy()
  if pyvista.OFF_SCREEN:
      plotter.screenshot(f"streamlines_{MPI.COMM_WORLD.rank}.png",
                         transparent_background=transparent, window_size=[figsize, figsize])
  else:
      plotter.show()
