# Generic mesh readers under ParaView via the new Python interface

7 分钟阅读

Recently I discoved the Python VTKPythonAlgorithmBase interface introduced in the ParaView 5.6 version for defining user plugins with pure Python files. These plugins can then be loaded through Tools / Manage Plugins / Load New under ParaView and can be served as custom sources, readers, writers and filters.

Previously before 5.6 only plugins defined by a XML file (or compiled library files) can be loaded. For generating such XML files, I used a Python package pvpyfilter. With the newly introduced native VTKPythonAlgorithmBase interface, it is no longer necessary.

In this post, I will illustrate the development of such Python plugins via the example of a generic mesh format reader, with the help of meshio. The code source of this plugin is available on Github.

## meshio

meshio is a Python library that reads and writes various frequently-used mesh formats: XDMF, VTK, gmsh, Abaqus…Personnally since 2018 I’m actively involved in this project especially for the MED format. The finite element result produced by code_aster is a MED file and in general requires the rather fatty salome platform for visualization and post-processing. With the MED interface of meshio, these results files can now be converted to a more generic format readable directly by ParaView, like XDMF or VTU. Directly Python processing is also possible without the use of salome environment.

The API of meshio is minimalist. For reading or writing a mesh file foo, simply use

mesh = meshio.read(foo)  # read mesh from foo
meshio.write(foo, mesh)  # write mesh to foo


The actual mesh format used by foo is either automatically deduced from its extension, or in case of ambiguity, can be specified by the file_format argument

mesh = meshio.read(foo, file_format="...")
meshio.write(foo, mesh, file_format="...")


The mesh information is contained in a Mesh instance returned by meshio.read. The most useful attributes are defined as follows

mesh.points  # node coordinates
mesh.cells  # cell connectivities
mesh.point_data  # nodal fields
mesh.cell_data  # cell fields


For cells, mesh.cells is a dictionary that defines the connectivity information for each geomerical type of cells. In the case of a 2d mesh composed of triangles and quads, we will have

mesh.cells = {
"triangle": ...,  # a (n_triangles, 3) numpy array
}


Each line of these two numpy arrays specifies the node id’s of a particular cell: 3 points for triangles and 4 for quad’s. The id’s correspond to the node coordinates of the mesh defined in mesh.points.

The point data is a dictionary of numpy arrays with the keys as field names, for instance

mesh.point_data = {
"Displacement": ...,  # a (n_points, 3) numpy array
}


defines a 3d (with 3 spatial components) displacement field defined on each node of the mesh. As for cell data, an additional dictionary level is necessary to specify the cell type

mesh.cell_data = {
"triangle":  {
"Displacement": ...,  # a (n_triangles, 3) numpy array
},
"Displacement": ...,  # a (n_quads, 3) numpy array
},
}


In this case, a 3d displacement is defined on each triangular and quadrilateral cell.

## VTKPythonAlgorithmBase interface for defining ParaView plugins

For a more comprehensive introduction, interested readers can consult section 12.4 of the ParaView Guide and the PythonAlgorithmExamples.py file provided by ParaView.

Essentially, to develop a user ParaView plugin, we just need to define a subclass of VTKPythonAlgorithmBase in your Python file. In my generic mesh reader case, we have

from paraview.util.vtkAlgorithm import VTKPythonAlgorithmBase
def __init__(self):
VTKPythonAlgorithmBase.__init__(
self, nInputPorts=0, nOutputPorts=1, outputType="vtkUnstructuredGrid"
)
...


If you are familiar with ParaView or VTK, you know that post-processing of the input data is organized through pipeline. Each operation can be characterized by the number of inputs and the number of outputs. A “source” or “reader” do not require inputs, but produce some outputs; a “writer” take an or several inputs but do not produce additional data; a “filter” performs some operation to inputs and generates the results. This input / output meta-informaton is needed to initialize the class. In this case, as a generic mesh format reader, no inputs are required (so you do not need to select an existing object in ParaView before using this plugin to open a file), and it only returns 1 object as a vtkUnstructuredGrid, an unstructured mesh defined by points coordinates and cell connectivities (as meshio reads and writes).

Until now, only VTK objects are involved (VTKPythonAlgorithmBase is in fact defined by VTK). To declare this class as an effective reader usable under ParaView, it has to be decorated by ParaView functions.

from paraview.util.vtkAlgorithm import smdomain, smhint, smproperty, smproxy
extensions=meshio_supported_ext,  # [msh, med, inp, fem, ...]
file_description="meshio-supported files",
)
...


By doing so, the plugin will be registered as a reader and when you open files, the addtional file formats will be available as “meshio-supported files” in the dialog.

### Data generation

The mesh reading and output to ParaView pipeline is performed in the RequestData method of VTKPythonAlgorithmBase. In my case, it can be summarized as follows

from vtkmodules.numpy_interface import dataset_adapter as dsa
from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid
def RequestData(self, request, inInfo, outInfo):
# Use meshio to read the mesh

# Produce ParaView output from mesh
output = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo))
output.SetPoints(points)  # point coordinates
output.SetCells(cell_types, cell_offsets, cell_conn)  # cell connectivities

output.PointData.append(array, name)  # point data
output.CellData.append(array, name)  # cell data
output.FieldData.append(array, name)  # field data


The amazing part of this Python approach is that the meshio library can be directly used to do the dirtiest work (through meshio.read) of actually reading the mesh file. Later on we only need to carefully wrap the mesh object into the ParaView-desired output outInfo. The connection between ParaView VTK objects and pure Python is achieved by vtkmodules.numpy_interface.dataset_adapter. By doing

output = dsa.WrapDataObject(vtkUnstructuredGrid.GetData(outInfo))


The numpy-friendly object output can be used to construct the mesh for output. Numpy arrays can be directly used when defining node coordinates via SetPoints, cell connectivities via SetCells, etc.

Some GUI elements can be added to the plugin to provide additional parameters to the plugin. These parameters can be continuous (integers, floats) or discrete in nature (a list of choices). In this example, as suggested by one of the Github user, I added a file format (string) selector to for users to explicitly indicate the mesh format of the input file. It can be defined as additional methods inside the class

@smproperty.stringvector(name="StringInfo", information_only="1")
def GetStrings(self):
return meshio_input_filetypes

@smproperty.stringvector(name="FileFormat", number_of_elements="1")
@smdomain.xml(
"""<StringListDomain name="list">
<RequiredProperties>
<Property name="StringInfo" function="StringInfo"/>
</RequiredProperties>
</StringListDomain>
"""
)
def SetFileFormat(self, file_format):
...


In this case, the GUI (string list) must be defined through raw XML format. In other cases (floats, integers…), simple decorators can be used, see the documentation.

## Conclusions

The Python VTKPythonAlgorithmBase interface provides a powerful and easy-to-maintain approach of defining ParaView plugins. Since it is pure Python, additional technical Python libraries can be directly used to process the data. It can also be easily distributed to other users, since only one source file is involved.