Composite Data Writers as VTKPythonAlgorithm Subclasses
The functionality using decorated VTKPythonAlgorithmBase
classes as ParaView plugins has a composite support option for the smdomain
input property that is incredibly simple to use with filter algorithms yet can be tricky to use for writer algorithms.
This snippet provides a simple base class for a user to inherit functionality which is a modification of PVGeo’s BaseWriter
algorithm found here. In this example I demonstrate the use of this base class through creating a writer algorithm that will save out XYZ+attribute data of the cell centers and cell data for any given input datasets.
Note that we must use the composite_data_supported=True
flag for the @smproxy.writer(...)
declaration as well as append allowable input types within the algorithms FillInputPortInformation
method.
This solution handles altering the given filename to save out each object in the composite dataset separately by saving each block out in PerformWriteOut
method that is repeatedly called by RequestData
explicitly.
# This is module to import. It provides VTKPythonAlgorithmBase, the base class
# for all python-based vtkAlgorithm subclasses in VTK and decorators used to
# 'register' the algorithm with ParaView along with information about UI.
from paraview.util.vtkAlgorithm import *
from vtk.util.vtkAlgorithm import VTKPythonAlgorithmBase
import vtk
import numpy as np
from vtk.numpy_interface import dataset_adapter as dsa
class WriterBase(VTKPythonAlgorithmBase):
"""This is a writer base class to add convienace methods to the
``VTKPythonAlgorithmBase`` for writer algorithms and was originally
implemented in `PVGeo`_ by `Bane Sullivan`_.
.. _PVGeo: http://pvgeo.org
.. _Bane Sullivan: http://banesullivan.com
For more information on what functionality is available, check out the VTK
Docs for the `vtkAlgorithm`_ and then check out the following blog posts:
* `vtkPythonAlgorithm is great`_
* A VTK pipeline primer `(part 1)`_, `(part 2)`_, and `(part 3)`_
* `ParaView Python Docs`_
.. _vtkAlgorithm: https://www.vtk.org/doc/nightly/html/classvtkAlgorithm.html
.. _vtkPythonAlgorithm is great: https://blog.kitware.com/vtkpythonalgorithm-is-great/
.. _(part 1): https://blog.kitware.com/a-vtk-pipeline-primer-part-1/
.. _(part 2): https://blog.kitware.com/a-vtk-pipeline-primer-part-2/
.. _(part 3): https://blog.kitware.com/a-vtk-pipeline-primer-part-3/
.. _ParaView Python Docs: https://www.paraview.org/ParaView/Doc/Nightly/www/py-doc/paraview.util.vtkAlgorithm.html
"""
def __init__(self, nInputPorts=1, inputType='vtkPolyData', **kwargs):
VTKPythonAlgorithmBase.__init__(self, nInputPorts=nInputPorts,
inputType=inputType,
nOutputPorts=0)
self.__filename = kwargs.get('filename', None)
self.__fmt = '%.9e'
# For composite datasets: not always used
self.__blockfilenames = None
self.__composite = False
def FillInputPortInformation(self, port, info):
"""Allows us to save composite datasets as well.
NOTE: I only care about ``vtkMultiBlockDataSet``s but you could hack
this method and ``RequestData`` to handle ``vtkCompositeDataSet``s for
a general use case.
"""
info.Set(self.INPUT_REQUIRED_DATA_TYPE(), self.InputType)
info.Append(self.INPUT_REQUIRED_DATA_TYPE(), 'vtkMultiBlockDataSet') # 'vtkCompositeDataSet'
return 1
def SetFileName(self, fname):
"""Specify the filename for the output.
This will be appended if saving composite datasets.
"""
if not isinstance(fname, str):
raise RuntimeError('File name must be string. Only single file is supported.')
if self.__filename != fname:
self.__filename = fname
self.Modified()
def GetFileName(self):
"""Get the set filename."""
return self.__filename
def Write(self, inputDataObject=None):
"""A Python focused conveinance method to perform the write out."""
if inputDataObject:
self.SetInputDataObject(inputDataObject)
self.Modified()
self.Update()
def PerformWriteOut(self, inputDataObject, filename):
"""This method must be implemented. This is automatically called by
``RequestData`` for single inputs or composite inputs."""
raise NotImplementedError('PerformWriteOut must be implemented!')
def Apply(self, inputDataObject):
"""A convienace method if using these algorithms in a Python environment.
"""
self.SetInputDataObject(inputDataObject)
self.Modified()
self.Update()
def SetFormat(self, fmt):
"""Use to set the ASCII format for the writer default is ``'%.9e'``
"""
if self.__fmt != fmt and isinstance(fmt, str):
self.__fmt = fmt
self.Modified()
def GetFormat(self):
return self.__fmt
#### Following methods are for composite datasets ####
def UseComposite(self):
"""True if input dataset is a composite dataset"""
return self.__composite
def SetBlockFileNames(self, n):
"""Gets a list of filenames based on user input filename and creates a
numbered list of filenames for the reader to save out. Assumes the
filename has an extension set already.
"""
number = n
count = 0
while (number > 0):
number = number // 10
count = count + 1
count = '%d' % count
identifier = '_%.' + count + 'd'
blocknum = [identifier % i for i in range(n)]
# Check the file extension:
ext = self.GetFileName().split('.')[-1]
basename = self.GetFileName().replace('.%s' % ext, '')
self.__blockfilenames = [basename + '%s.%s' % (blocknum[i], ext) for i in range(n)]
return self.__blockfilenames
def GetBlockFileName(self, idx):
"""Get the filename for a specific block if composite dataset.
"""
return self.__blockfilenames[idx]
def RequestData(self, request, inInfoVec, outInfoVec):
"""Subclasses must implement a ``PerformWriteOut`` method that takes an
input data object and a filename. This method will automatically handle
composite data sets. DO NOT OVERWRITE THIS.
"""
inp = self.GetInputData(inInfoVec, 0, 0)
if isinstance(inp, vtk.vtkCompositeDataSet):
self.__composite = True
# Handle composite datasets. NOTE: This only handles vtkMultiBlockDataSet
if self.__composite:
num = inp.GetNumberOfBlocks()
self.SetBlockFileNames(num)
for i in range(num):
data = inp.GetBlock(i)
if data.IsTypeOf(self.InputType):
self.PerformWriteOut(data, self.GetBlockFileName(i))
else:
warnings.warn('Input block %d of type(%s) not saveable by writer.' % (i, type(data)))
# Handle single input dataset
else:
self.PerformWriteOut(inp, self.GetFileName())
return 1
################################################################################
## Now lets use ``WriterBase`` to make a writer algorithm that ParaView can use
class WriteCellCenterData(WriterBase):
"""This writer will save a file of the XYZ points for an input dataset's
cell centers and its cell data. Use in tandom with ParaView's native CSV
writer which saves the PointData. This class was originally
implemented in `PVGeo`_ by `Bane Sullivan`_.
.. _PVGeo: http://pvgeo.org
.. _Bane Sullivan: http://banesullivan.com
"""
def __init__(self):
WriterBase.__init__(self, inputType='vtkDataSet')
self.__delimiter = ','
def PerformWriteOut(self, inputDataObject, filename):
# Find cell centers
filt = vtk.vtkCellCenters()
filt.SetInputDataObject(inputDataObject)
filt.Update()
centers = dsa.WrapDataObject(filt.GetOutput(0)).Points
# Get CellData
wpdi = dsa.WrapDataObject(inputDataObject)
celldata = wpdi.CellData
keys = celldata.keys()
# Save out using numpy
arr = np.zeros((len(centers), 3 + len(keys)))
arr[:,0:3] = centers
for i, name in enumerate(keys):
arr[:,i+3] = celldata[name]
# Now write out the data
# Clean data titles to make sure they do not contain the delimiter
repl = '_' if self.__delimiter != '_' else '-'
for i, name in enumerate(keys):
keys[i] = name.replace(self.__delimiter, repl)
header = ('%s' % self.__delimiter).join(['X', 'Y', 'Z'] + keys)
np.savetxt(filename, arr,
header=header,
delimiter=self.__delimiter,
fmt=self.GetFormat(),
comments='')
# Success for pipeline
return 1
def SetDelimiter(self, delimiter):
"""The string delimiter to use"""
if self.__delimiter != delimiter:
self.__delimiter = delimiter
self.Modified()
################################################################################
## Now wrap that writer for use in ParaView!
@smproxy.writer(extensions="dat", file_description="Cell Centers and Cell Data", support_reload=False)
@smproperty.input(name="Input", port_index=0)
@smdomain.datatype(dataTypes=["vtkDataSet"], composite_data_supported=True)
class PVWriteCellCenterData(WriteCellCenterData):
"""The ``WriteCellCenterData`` class wrapped for use as a plugin in ParaView.
Be sure that the ``composite_data_supported`` flag is set to ``True``.
"""
def __init__(self):
WriteCellCenterData.__init__(self)
@smproperty.stringvector(name="FileName", panel_visibility="never")
@smdomain.filelist()
def SetFileName(self, fname):
"""Specify filename for the file to write."""
WriteCellCenterData.SetFileName(self, fname)
@smproperty.stringvector(name="Format", default_values='%.9e')
def SetFormat(self, fmt):
"""Use to set the ASCII format for the writer default is ``'%.9e'``"""
WriteCellCenterData.SetFormat(self, fmt)
@smproperty.stringvector(name="Delimiter", default_values=',')
def SetDelimiter(self, delimiter):
"""The string delimiter to use"""
WriteCellCenterData.SetDelimiter(self, delimiter)