util.vtkAlgorithm Module

This module provides utility functions that can be used to expose a VTKPythonAlgorithmBase derived Pythonic vtkAlgorithm subclass in ParaView.

Introduction

VTK provides a convenient Python-friendly mechanism to add new algorithms, such as data sources, filters, readers, and writers via vtkPythonAlgorithm. In this approach, developers can write subclasses of VTKPythonAlgorithmBase and then implement the vtkAlgorithm API in Python.

from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase
class ContourShrink(VTKPythonAlgorithmBase):
  def __init__(self):
      VTKPythonAlgorithmBase.__init__(self)

  def RequestData(self, request, inInfo, outInfo):
      inp = vtk.vtkDataSet.GetData(inInfo[0])
      opt = vtk.vtkPolyData.GetData(outInfo)

      cf = vtk.vtkContourFilter()
      cf.SetInputData(inp)
      cf.SetValue(0, 200)

      sf = vtk.vtkShrinkPolyData()
      sf.SetInputConnection(cf.GetOutputPort())
      sf.Update()

      opt.ShallowCopy(sf.GetOutput())
      return 1

Such VTKPythonAlgorithmBase-based algorithms act as any other vtkAlgorithm and can be used directly in VTK pipelines. To use them in ParaView, however, one has to make ParaView aware of the capabilities of the algorithm. For vtkAlgorithm subclasses in C++, this is done by writing an XML description of the algorithm. For Python-based algorithms, this module provides us a mechanism to build the ParaView-specific XML via decorators.

from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase
from paraview.util.vtkAlgorithm import smproxy, smproperty, smdomain

@smproxy.filter(label="Contour Shrink")
@smproperty.input(name="Input")
@smdomain.datatype(dataTypes=["vtkDataSet"], composite_data_supported=False)
class ContourShrink(VTKPythonAlgorithmBase):
  def __init__(self):
      VTKPythonAlgorithmBase.__init__(self)

  def RequestData(self, request, inInfo, outInfo):
      ...
      return 1

Note

ParaView’s XML description provides a wide gamut of features. The decorators available currently only expose a small subset of them. The plan is to have complete feature parity in the future. If you encounter a missing capability, please raise an issue or better yet, a merge-request to address the same.

Decorator Basics

There are four main classes currently available: smproxy, smproperty, smdomain, and smhint. Each provides a set of decorators to declare a proxy, property, domain or a hint, respectively.

All available decorators exhibit the following characteristics:

  1. With the exception of decorators defined in smproxy, all decorators are intended for decorating classes or their methods. smproxy decorators are only intended for classes.

  2. Decorators take keyword arguments (positional arguments are discouraged). While there may be exceptions, the general pattern is that a decorator corresponds to an XML node in the ParaView XML configuration. Any attributes on the XML node itself can be specified as keyword arguments to the decorator. The decorator may explicitly define parameters that affect the generated node or to explicit limit the arguments provided.

  3. Decorators can be chained. The order of chaining is significant and follows the direction of nesting in the XML e.g. to add a domain to a property, one would nest decorators as follows:

 @smproperty.intvector(name="Radius", default_values=[0])
 @smdomain.intrange(min=0, max=10)
 @smhint.xml("<SomeHint/>")
 def SetRadius(self, radius):
     ...


Note, since domains can't have nested hints, the hint specified after the
`smdomain.intrange` invocation is intended for the `smproperty` and not the
`smdomain`.

When chaining `smproperty` decorators under a `smproxy`, the order in which
the properties are shown in ParaView UI is reverse of the order in the
Python code e.g. the in following snippets the UI will show "PolyDataInput"
above "TableInput".
@smproxy.filter(...)
@smproperty.input(name="TableInput", port_index=1, ...)
@smdomain.datatype(dataTypes=["vtkTable"])
@smproperty.input(name="PolyDataInput", port_index=0, ...)
@smdomain.datatype(dataTypes=["vtkPolyData"])
class MyFilter(VTKPythonAlgorithmBase):
   ...

smproxy Decorators

To make VTKPythonAlgorithmBase subclass available in ParaView as a data-source, filter, reader, or writer, one must use of the decorators in the smproxy class.

Common decorator parameters

The following keyword parameters are supported by all smproxy decorators:

  1. name: if present, provides the name to use of the Proxy. This is the name that ParaView uses to refer to your class. If none specified, the name is deduced from the class name.

  2. label: if present, provides the label to use in the UI. If missing, the label is deduced from the name.

  3. class: if present, is the fully qualified class name ParaView should use to instantiate this object. Except for exceptional situation, it’s best to let ParaView deduce this argument.

  4. group: if present, is the proxy-group under which the definition must be placed. This is often not needed since the decorators set a good default based on the role of the proxy.

As mentioned before, any keyword arguments passed any decorator that is not processed by the decorator simply gets added to the generated XML node as attribute.

smproxy.source

This decorator can be used to declare data sources. Data sources don’t have any inputs, but provide 1 or more outputs. While technically a reader is a source, smproxy.reader is preferred way of adding readers.

smproxy.filter

This decorator can be used to declare filters that have one or more inputs and one or more outputs. The inputs themselves must be declared using smproperty.input decorators.

smproxy.reader

This decorator is used to declare reader. This is same as smproxy.source, except it adds the appropriate hints to make ParaView aware that this is a reader.

It takes the following keyword parameters, in addition to the standard ones:

  1. file_description: (required) a user friendly text to use in the File Open dialog.

  2. extensions: a string or list of strings which define the supported extensions (without the leading .).

  3. filename_patterns: wild card patten.

Either extensions or filename_patterns must be provided.

smproxy.writer

This decorator is used to declare a writer. Similar to smproxy.reader, one provides file_description and extensions keyword parameters to indicate which file extensions the writer supports.

smproperty Decorators

Decorators in smproperty are used to add properties to the proxies declared using smproxy decorators. These can be added either to class methods or to class objects. Multiple smproperty decorators may be chained when adding to class objects.

Common decorator parameters

The following keyword parameters are supported by all smproperty decorators:

  1. name: if present, provides the name to use of the Property. If missing, the name will be deduced from the object being decorated which will rarely be correct when decorating class objects. Hence, this parameter is optional when decorating class methods, but required when decorating class objects.

  2. label: if present, provides the label to use in the UI. If missing, the label is deduced from the name.

  3. command: if present, is the method name. If missing, ParaView can deduce it based on the method being decorated. Hence, as before, optional when decorating class methods, but generally required when decorating class objects (with the exception of smproperty.input).

smproperty.xml

This is a catch-all decorator. One can use this to add arbitrary XML to the proxy definition.

@smproperty.xml(xmlstr="<IntVectorProperty name=..> ....  </IntVectorProperty>")
def MyMethod(self, ...):
    ...

smproperty.intvector, smproperty.doublevector, smproperty.idtypevector, smproperty.stringvector

These are used to declare various types of vector properties. The arguments are simply the XML attributes.

smproperty.proxy

This decorator can be used for adding a ProxyProperty, i.e. a method that takes another VTK object as argument.

smproperty.input

This decorator can be used to add inputs to a vtkAlgorithm. Since vtkAlgorithm provides the appropriate API to set an input, often one doesn’t have custom methods in Python in the VTKPythonAlgorithmBase subclass. Hence, this decorator is often simply added to the class definition rather than a particular method.

@smproxy.filter(...)
@smproperty.input(name="TableInput", port_index=0, multiple_input=True)
@smdomain.datatype(dataTypes=["vtkTable"], composite_data_supported=False)
class MyFilter(VTKPythonAlgorithmBase):
   ...

smproperty.dataarrayselection

This a convenience decorator that can be used to decorate a method that returns a vtkDataArraySelection object often used to array selection in data sources and readers. The decorator adds appropriate ParaView ServerManager XML components to expose the array selection to the user.

smdomain Decorators

These decorators are used to add domains to smproperty. Domains often guide the UI that ParaView generated for the specific property. The current set of available domains, includes smdomain.intrange, smdomain.doublerange, smdomain.filelist, and smdomain.datatype. More will be added as needed. Additionally, one can use the smdomain.xml catch-all decorator to add XML definition for the domain.

smhint Decorators

These add elements under <Hints/> for smproxy and/or smproperty elements. Currently smhint.filechooser and the generic smhint.xml are provided with more to be added as needed.

Examples

PythonAlgorithmExamples.py provides a working example that demonstrates how several of the decorators described here may be used.

../../../../Examples/Plugins/PythonAlgorithm/PythonAlgorithmExamples.py
"""This module demonstrates various ways of adding
VTKPythonAlgorithmBase subclasses as filters, sources, readers,
and writers in ParaView"""


# 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 *

#------------------------------------------------------------------------------
# A source example.
#------------------------------------------------------------------------------
@smproxy.source(name="PythonSuperquadricSource",
       label="Python-based Superquadric Source Example")
class PythonSuperquadricSource(VTKPythonAlgorithmBase):
    """This is dummy VTKPythonAlgorithmBase subclass that
    simply puts out a Superquadric poly data using a vtkSuperquadricSource
    internally"""
    def __init__(self):
        VTKPythonAlgorithmBase.__init__(self,
                nInputPorts=0,
                nOutputPorts=1,
                outputType='vtkPolyData')
        from vtkmodules.vtkFiltersSources import vtkSuperquadricSource
        self._realAlgorithm = vtkSuperquadricSource()

    def RequestData(self, request, inInfo, outInfo):
        from vtkmodules.vtkCommonDataModel import vtkPolyData
        self._realAlgorithm.Update()
        output = vtkPolyData.GetData(outInfo, 0)
        output.ShallowCopy(self._realAlgorithm.GetOutput())
        return 1

    # for anything too complex or not yet supported, you can explicitly
    # provide the XML for the method.
    @smproperty.xml("""
        <DoubleVectorProperty name="Center"
            number_of_elements="3"
            default_values="0 0 0"
            command="SetCenter">
            <DoubleRangeDomain name="range" />
            <Documentation>Set center of the superquadric</Documentation>
        </DoubleVectorProperty>""")
    def SetCenter(self, x, y, z):
        self._realAlgorithm.SetCenter(x,y,z)
        self.Modified()

    # In most cases, one can simply use available decorators.
    @smproperty.doublevector(name="Scale", default_values=["1", "1", "1"])
    @smdomain.doublerange()
    def SetScale(self, x, y, z):
        self._realAlgorithm.SetScale(x,y,z)
        self.Modified()

    @smproperty.intvector(name="ThetaResolution", number_of_elements="1", default_values="16")
    def SetThetaResolution(self, x):
        self._realAlgorithm.SetThetaResolution(x)
        self.Modified()

    @smproperty.intvector(name="PhiResolution", default_values="16")
    @smdomain.intrange(min=0, max=1000)
    def SetPhiResolution(self, x):
        self._realAlgorithm.SetPhiResolution(x)
        self.Modified()

    @smproperty.doublevector(name="Thickness", number_of_elements="1", default_values="0.3333")
    @smdomain.doublerange(min=1e-24, max=1.0)
    def SetThickness(self, x):
        self._realAlgorithm.SetThickness(x)
        self.Modified()

    # "ValueRangeInfo" and "Value" demonstrate how one can have a slider in the
    # UI for a property with its range fetched at runtime. For int values,
    # use `intvector` and `IntRangeDomain` instead of the double variants used
    # below.
    @smproperty.doublevector(name="ValueRangeInfo", information_only="1")
    def GetValueRange(self):
        print("getting range: (0, 100)")
        return (0, 100)

    @smproperty.doublevector(name="Value", default_values=["0.0"])
    @smdomain.xml(\
        """<DoubleRangeDomain name="range" default_mode="mid">
                <RequiredProperties>
                    <Property name="ValueRangeInfo" function="RangeInfo" />
                </RequiredProperties>
           </DoubleRangeDomain>
        """)
    def SetValue(self, val):
        print("settings value:", val)

    # "StringInfo" and "String" demonstrate how one can add a selection widget
    # that lets user choose a string from the list of strings.
    @smproperty.stringvector(name="StringInfo", information_only="1")
    def GetStrings(self):
        return ["one", "two", "three"]

    @smproperty.stringvector(name="String", number_of_elements="1")
    @smdomain.xml(\
        """<StringListDomain name="list">
                <RequiredProperties>
                    <Property name="StringInfo" function="StringInfo"/>
                </RequiredProperties>
            </StringListDomain>
        """)
    def SetString(self, value):
        print("Setting ", value)




#------------------------------------------------------------------------------
# A reader example.
#------------------------------------------------------------------------------
def createModifiedCallback(anobject):
    import weakref
    weakref_obj = weakref.ref(anobject)
    anobject = None
    def _markmodified(*args, **kwars):
        o = weakref_obj()
        if o is not None:
            o.Modified()
    return _markmodified

# To add a reader, we can use the following decorators
#   @smproxy.source(name="PythonCSVReader", label="Python-based CSV Reader")
#   @smhint.xml("""<ReaderFactory extensions="csv" file_description="Numpy CSV files" />""")
# or directly use the "@reader" decorator.
@smproxy.reader(name="PythonCSVReader", label="Python-based CSV Reader",
                extensions="csv", file_description="CSV files")
class PythonCSVReader(VTKPythonAlgorithmBase):
    """A reader that reads a CSV file. If the CSV has a "time" column, then
    the data is treated as a temporal dataset"""
    def __init__(self):
        VTKPythonAlgorithmBase.__init__(self, nInputPorts=0, nOutputPorts=1, outputType='vtkTable')
        self._filename = None
        self._ndata = None
        self._timesteps = None

        from vtkmodules.vtkCommonCore import vtkDataArraySelection
        self._arrayselection = vtkDataArraySelection()
        self._arrayselection.AddObserver("ModifiedEvent", createModifiedCallback(self))

    def _get_raw_data(self, requested_time=None):
        if self._ndata is not None:
            if requested_time is not None:
                return self._ndata[self._ndata["time"]==requested_time]
            return self._ndata

        if self._filename is None:
            # Note, exceptions are totally fine!
            raise RuntimeError("No filename specified")

        import numpy
        self._ndata = numpy.genfromtxt(self._filename, dtype=None, names=True, delimiter=',', autostrip=True)
        self._timesteps = None
        if "time" in self._ndata.dtype.names:
            self._timesteps = numpy.sort(numpy.unique(self._ndata["time"]))

        for aname in self._ndata.dtype.names:
            # note, this doesn't change MTime on the array selection, which is
            # good!
            self._arrayselection.AddArray(aname)
        return self._get_raw_data(requested_time)

    def _get_timesteps(self):
        self._get_raw_data()
        return self._timesteps.tolist() if self._timesteps is not None else None

    def _get_update_time(self, outInfo):
        executive = self.GetExecutive()
        timesteps = self._get_timesteps()
        if timesteps is None or len(timesteps) == 0:
            return None
        elif outInfo.Has(executive.UPDATE_TIME_STEP()) and len(timesteps) > 0:
            utime = outInfo.Get(executive.UPDATE_TIME_STEP())
            dtime = timesteps[0]
            for atime in timesteps:
                if atime > utime:
                    return dtime
                else:
                    dtime = atime
            return dtime
        else:
            assert(len(timesteps) > 0)
            return timesteps[0]

    def _get_array_selection(self):
        return self._arrayselection

    @smproperty.stringvector(name="FileName")
    @smdomain.filelist()
    @smhint.filechooser(extensions="csv", file_description="Numpy CSV files")
    def SetFileName(self, name):
        """Specify filename for the file to read."""
        if self._filename != name:
            self._filename = name
            self._ndata = None
            self._timesteps = None
            self.Modified()

    @smproperty.doublevector(name="TimestepValues", information_only="1", si_class="vtkSITimeStepsProperty")
    def GetTimestepValues(self):
        return self._get_timesteps()

    # Array selection API is typical with readers in VTK
    # This is intended to allow ability for users to choose which arrays to
    # load. To expose that in ParaView, simply use the
    # smproperty.dataarrayselection().
    # This method **must** return a `vtkDataArraySelection` instance.
    @smproperty.dataarrayselection(name="Arrays")
    def GetDataArraySelection(self):
        return self._get_array_selection()

    def RequestInformation(self, request, inInfoVec, outInfoVec):
        executive = self.GetExecutive()
        outInfo = outInfoVec.GetInformationObject(0)
        outInfo.Remove(executive.TIME_STEPS())
        outInfo.Remove(executive.TIME_RANGE())

        timesteps = self._get_timesteps()
        if timesteps is not None:
            for t in timesteps:
                outInfo.Append(executive.TIME_STEPS(), t)
            outInfo.Append(executive.TIME_RANGE(), timesteps[0])
            outInfo.Append(executive.TIME_RANGE(), timesteps[-1])
        return 1

    def RequestData(self, request, inInfoVec, outInfoVec):
        from vtkmodules.vtkCommonDataModel import vtkTable
        from vtkmodules.numpy_interface import dataset_adapter as dsa

        data_time = self._get_update_time(outInfoVec.GetInformationObject(0))
        raw_data = self._get_raw_data(data_time)
        output = dsa.WrapDataObject(vtkTable.GetData(outInfoVec, 0))
        for name in raw_data.dtype.names:
            if self._arrayselection.ArrayIsEnabled(name):
                output.RowData.append(raw_data[name], name)

        if data_time is not None:
            output.GetInformation().Set(output.DATA_TIME_STEP(), data_time)
        return 1

#------------------------------------------------------------------------------
# A writer example.
#------------------------------------------------------------------------------
@smproxy.writer(extensions="npz", file_description="NumPy Compressed Arrays", support_reload=False)
@smproperty.input(name="Input", port_index=0)
@smdomain.datatype(dataTypes=["vtkTable"], composite_data_supported=False)
class NumpyWriter(VTKPythonAlgorithmBase):
    def __init__(self):
        VTKPythonAlgorithmBase.__init__(self, nInputPorts=1, nOutputPorts=0, inputType='vtkTable')
        self._filename = None

    @smproperty.stringvector(name="FileName", panel_visibility="never")
    @smdomain.filelist()
    def SetFileName(self, fname):
        """Specify filename for the file to write."""
        if self._filename != fname:
            self._filename = fname
            self.Modified()

    def RequestData(self, request, inInfoVec, outInfoVec):
        from vtkmodules.vtkCommonDataModel import vtkTable
        from vtkmodules.numpy_interface import dataset_adapter as dsa

        table = dsa.WrapDataObject(vtkTable.GetData(inInfoVec[0], 0))
        kwargs = {}
        for aname in table.RowData.keys():
            kwargs[aname] = table.RowData[aname]

        import numpy
        numpy.savez_compressed(self._filename, **kwargs)
        return 1

    def Write(self):
        self.Modified()
        self.Update()

#------------------------------------------------------------------------------
# A filter example.
#------------------------------------------------------------------------------
@smproxy.filter()
@smproperty.input(name="InputTable", port_index=1)
@smdomain.datatype(dataTypes=["vtkTable"], composite_data_supported=False)
@smproperty.input(name="InputDataset", port_index=0)
@smdomain.datatype(dataTypes=["vtkDataSet"], composite_data_supported=False)
class ExampleTwoInputFilter(VTKPythonAlgorithmBase):
    def __init__(self):
        VTKPythonAlgorithmBase.__init__(self, nInputPorts=2, nOutputPorts=1, outputType="vtkPolyData")

    def FillInputPortInformation(self, port, info):
        if port == 0:
            info.Set(self.INPUT_REQUIRED_DATA_TYPE(), "vtkDataSet")
        else:
            info.Set(self.INPUT_REQUIRED_DATA_TYPE(), "vtkTable")
        return 1

    def RequestData(self, request, inInfoVec, outInfoVec):
        from vtkmodules.vtkCommonDataModel import vtkTable, vtkDataSet, vtkPolyData
        input0 = vtkDataSet.GetData(inInfoVec[0], 0)
        input1 = vtkDataSet.GetData(inInfoVec[1], 0)
        output = vtkPolyData.GetData(outInfoVec, 0)
        # do work
        print("Pretend work done!")
        return 1

@smproxy.filter()
@smproperty.input(name="Input")
@smdomain.datatype(dataTypes=["vtkDataSet"], composite_data_supported=False)
class PreserveInputTypeFilter(VTKPythonAlgorithmBase):
    """
    Example filter demonstrating how to write a filter that preserves the input
    dataset type.
    """
    def __init__(self):
        super().__init__(nInputPorts=1, nOutputPorts=1, outputType="vtkDataSet")

    def RequestDataObject(self, request, inInfo, outInfo):
        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()
            outInfo.GetInformationObject(0).Set(outData.DATA_OBJECT(), outData)
        return super().RequestDataObject(request, inInfo, outInfo)

    def RequestData(self, request, inInfo, outInfo):
        inData = self.GetInputData(inInfo, 0, 0)
        outData = self.GetOutputData(outInfo, 0)
        print("input type =", inData.GetClassName())
        print("output type =", outData.GetClassName())
        assert outData.IsA(inData.GetClassName())
        return 1


def test_PythonSuperquadricSource():
    src = PythonSuperquadricSource()
    src.Update()

    npts = src.GetOutputDataObject(0).GetNumberOfPoints()
    assert npts > 0

    src.SetThetaResolution(50)
    src.SetPhiResolution(50)
    src.Update()
    assert src.GetOutputDataObject(0).GetNumberOfPoints() > npts

def test_PythonCSVReader(fname):
    reader = PythonCSVReader()
    reader.SetFileName(fname)
    reader.Update()
    assert reader.GetOutputDataObject(0).GetNumberOfRows() > 0

if __name__ == "__main__":
    #test_PythonSuperquadricSource()
    #test_PythonCSVReader("/tmp/data.csv")

    from paraview.detail.pythonalgorithm import get_plugin_xmls
    from xml.dom.minidom import parseString
    for xml in get_plugin_xmls(globals()):
        dom = parseString(xml)
        print(dom.toprettyxml(" ","\n"))