diff --git a/docs/mesh_doctor.rst b/docs/mesh_doctor.rst index 5d8540e7..55df3e59 100644 --- a/docs/mesh_doctor.rst +++ b/docs/mesh_doctor.rst @@ -121,4 +121,84 @@ The ``supported_elements`` check will validate that no unsupported element is in It will also verify that the ``VTK_POLYHEDRON`` cells can effectively get converted into a supported type of element. .. command-output:: python mesh_doctor.py supported_elements --help - :cwd: ../geosx_mesh_doctor + :cwd: ../../../coreComponents/python/modules/geosx_mesh_doctor + +``Using mesh_doctor in paraview`` +"""""""""""""""""""""""""""""""""" + +Using mesh_doctor as a programmable filter +____________________________________________ + +To use ``mesh_doctor`` in Paraview as a python programmable filter, a python package install is required first in Paraview python resolved +path. Paraview is storing its python ressources under its *lib/pythonX.X* depending on the paraview version, *e.g* Paraview 5.11 is working +with python 3.9. As a results the following command will install ``mesh_doctor`` package into Paraview resolved path. + +.. command-output:: python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps --upgrade mesh_doctor + +.. note:: + ``pip`` is installing the ``mesh_doctor`` package from the test.pypi repo, which is intended to test package deployment. + Once stabilized and ``mesh_doctor`` uploaded onto the main package repo, this should be dropped out. + +.. note:: + The packaged version of ``mesh_doctor`` has been amended so that `element_volumes.check` takes directly a vtk mesh as input instead of a file name as originally as the loading is taken care of by Paraview + +Once the installation done, the installation directory should contain ``mesh_doctor`` package content, *i.e.* ``checks`` and ``parsing``. +Then launching ``Paraview`` and loading our *mesh.vtu*, as an example, we will design a *Programmable python filter* relying on *element_volumes* from +``mesh_doctor``. Add such a filter pipelined after the mesh reader, in the script section paste the following, + +.. code-block:: python + :linenos: + + mesh = inputs[0].VTKObject + tol = 1.2e-6 + + from checks import element_volumes + import vtk + + res = element_volumes.__check(mesh, element_volumes.Options(tol)) + #print(res) + ids = vtk.vtkIdTypeArray() + ids.SetNumberOfComponents(1) + for cell_index, volume in res.element_volumes: + ids.InsertNextValue(cell_index) + + selectionNode = vtk.vtkSelectionNode() + selectionNode.SetFieldType(vtk.vtkSelectionNode.CELL) + selectionNode.SetContentType(vtk.vtkSelectionNode.INDICES) + selectionNode.SetSelectionList(ids) + selection = vtk.vtkSelection() + selection.AddNode(selectionNode) + extracted = vtk.vtkExtractSelection() + extracted.SetInputDataObject(0, mesh) + extracted.SetInputData(1, selection) + extracted.Update() + print("There are {} cells under {} m3 vol".format(extracted.GetOutput().GetNumberOfCells(), tol)) + output.ShallowCopy(extracted.GetOutput()) + +Here we rely on ``pyvtk`` interface more than on Paraview adaptation, for legacy and reusability reasons. This is the reason +for the full ``import vtk`` instead of ``from paraview import vtk``, the `vtkSelectionNode` being fully wrapped in paraview +and not accessible otherwise. + +On line 7, we leverage ``mesh_doctor`` package to provide us with pairs of `(index,volumes)` of cells with volumes lower +than tolerance `tol`. As input of *Programmable Python Filter* is wrapped in a `dataset_adapter.UnstructuredGrid`, we rely on +the copy of the inital VTKObject `inputs[0].VTKObject` to ensure consistency with our ``pyvtk`` workflow. + +What follows is ``pyvtk`` steps in oder to convert into input struct and extract from the original mesh this list of cells. +Eventually, the `extracted` selection is shallow-copied to the output and then accessible in ``Paraview``. An helper print +is left and should be reported in *Output Message* of ``Paraview`` (and in launching terminal if exist). + +Using mesh_doctor as a paraview plugins +____________________________________________ + +Another way of leveraging ``mesh_doctor`` in ``Paraview`` is to wrap it in a python plugin that would be loadable through the +``Paraview`` interface under **Tools | Manage Plugins/Extensions** and **Load New** looking for ``mesh_doctor-pvplugin.py``. +(see `Paraview How To `_ for more details). + +Starting by local installation to get ``Paraview`` to resolve ``mesh_doctor`` import. + +.. command-output:: python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps --upgrade mesh_doctor + + +The file ``mesh_doctor-pvplugin.py`` is located under the ``geosx_mesh_doctor`` module in GEOS. Once the plugin loaded and a mesh opened, +it should appear in filter list as *Mesh Doctor(GEOS)*. It displays a parameter value box allowing the user to enter the volume he wants as +threshold to select cells based on ``element_volumes`` capability. Once applied, it extracts selected set of cells as a new unstructured grid. diff --git a/geosx_mesh_doctor/mesh_doctor-pvplugin.py b/geosx_mesh_doctor/mesh_doctor-pvplugin.py new file mode 100644 index 00000000..c5a98010 --- /dev/null +++ b/geosx_mesh_doctor/mesh_doctor-pvplugin.py @@ -0,0 +1,99 @@ +import numpy as np +import functools + +from paraview.util.vtkAlgorithm import VTKPythonAlgorithmBase, smproxy, smproperty, smdomain +from paraview.vtk import vtkIdTypeArray, vtkSelectionNode, vtkSelection, vtkCollection, vtkInformation, vtkDataObject +from paraview.vtk.util import numpy_support +from vtkmodules.util import vtkConstants + +from checks import element_volumes, non_conformal + + +# #decorator +def extract_mesh( attr_key ): + + def mesh_decorator( func ): + """Make a selected set from a list of points/face/cells""" + + @functools.wraps( func ) + def wrapper_extract_mesh( self, **kwargs ): + res = func( self, **kwargs ) + inData = self.GetInputData( kwargs[ 'inInfo' ], 0, 0 ) + mesh = inData.NewInstance() + mesh.DeepCopy( inData ) + maskArray = np.full( ( mesh.GetNumberOfCells(), ), 0 ) + for ix, _ in getattr( res, attr_key ): + maskArray[ ix ] = 1 + + print( f'There are {np.sum(maskArray)} cells under {self.opt} m3 vol' ) + insidedness = numpy_support.numpy_to_vtk( maskArray, deep=1, array_type=vtkConstants.VTK_SIGNED_CHAR ) + insidedness.SetName( attr_key ) + mesh.GetAttributes( vtkDataObject.CELL ).AddArray( insidedness ) + outData = self.GetOutputData( kwargs[ 'outInfo' ], 0 ) + kwargs[ 'outInfo' ].GetInformationObject( 0 ).Set( outData.DATA_OBJECT(), mesh ) + + return res + + return wrapper_extract_mesh + + return mesh_decorator + + +class BaseFilter( VTKPythonAlgorithmBase ): + """ + Base Class refactoring filter construction + """ + + def __init__( self ): + super().__init__( outputType='vtkUnstructuredGrid' ) + + def RequestData( self, request: vtkInformation, inInfo: vtkInformation, outInfo: vtkInformation ): + inData = self.GetInputData( inInfo, 0, 0 ) + outData = self.GetOutputData( outInfo, 0 ) + assert inData is not None + if outData is None or ( not outData.IsA( inData.GetClassName() ) ): + outData = inData.NewInstance() + self._Process( inInfo=inInfo, outInfo=outInfo ) + + print( "1> There are {} cells under {} vol".format( outData.GetNumberOfCells(), self.opt ) ) + return 1 + + +@smproxy.filter( name="Mesh Doctor(GEOS) - Element Volume Filter" ) +@smproperty.input( name="Input" ) +@smdomain.datatype( dataTypes=[ "vtkUnstructuredGrid" ], composite_data_supported=False ) +class ElementVolumeFilter( BaseFilter ): + + def __init__( self ): + super().__init__() + self.opt = element_volumes.Options( 0 ) + + @extract_mesh( attr_key='element_volumes' ) + def _Process( self, inInfo: vtkInformation, outInfo: vtkInformation ): + return element_volumes.check( self.GetInputData( inInfo, 0, 0 ), self.opt ) + + @smproperty.doublevector( name="Vol Threshold", default_values=[ "0.0" ] ) + def SetValue( self, val: float ): + self.opt = element_volumes.Options( min_volume=val ) + print( "Settings value:", self.opt ) + self.Modified() + + +@smproxy.filter( name="Mesh Doctor(GEOS) - NonConformal" ) +@smproperty.input( name="Input" ) +@smdomain.datatype( dataTypes=[ "vtkUnstructuredGrid" ], composite_data_supported=False ) +class NonConformalFilter( BaseFilter ): + + def __init__( self ): + super().__init__() + self.opt = non_conformal.Options( 0, 0, 0 ) + + @extract_mesh( attr_key='non_conformal_cells' ) + def _Process( self, inInfo: vtkInformation, outInfo: vtkInformation ): + return non_conformal.check( self.GetInputData( inInfo, 0, 0 ), self.opt ) + + @smproperty.doublevector( name="angle/point/face tol", default_values=[ "0.0", "0.0", "0.0" ] ) + def SetValue( self, angle: float, point: float, face: float ): + self.opt = non_conformal.Options( angle_tolerance=angle, point_tolerance=point, face_tolerance=face ) + print( "Settings value:", self.opt ) + self.Modified()