vtk-m/docs/users-guide/basic-filter-impl.rst

119 lines
11 KiB
ReStructuredText

==============================
Basic Filter Implementation
==============================
.. index:: filter; implementation
:chapref:`simple-worklets:Simple Worklets` introduced the concept of a worklet and demonstrated how to create and run one to execute an algorithm on a device.
Although worklets provide a powerful mechanism for designing heavily threaded visualization algorithms, invoking them requires quite a bit of knowledge of the workings of |VTKm|.
Instead, most users execute algorithms in |VTKm| using filters.
Thus, to expose algorithms implemented with worklets to general users, we need to implement a filter to encapsulate the worklets.
In this chapter we will create a filter that encapsulates the worklet algorithm presented in :chapref:`simple-worklets:Simple Worklets`, which converted the units of a pressure field from pounds per square inch (psi) to Newtons per square meter (:math:`\mathrm{N}/\mathrm{m}^2`).
Filters in |VTKm| are implemented by deriving one of the filter base classes provided in ``vtkm::filter``.
There are multiple base filter classes that we can choose from.
These different classes are documented later in Chapter \ref{chap:FilterTypeReference}.
For this example we will use the :class:`vtkm::filter::FilterField` base class, which is used to derive one field from another field.
.. todo:: Fix above reference.
The following example shows the declaration of our pressure unit conversion filter.
By convention, this declaration would be placed in a header file with a ``.h`` extension.
|VTKm| filters are divided into libraries.
In this example, we are assuming this filter is being compiled in a library named ``vtkm::filter::unit_conversion``.
By convention, the source files would be placed in a directory named ``vtkm/filter/unit_conversion``.
.. load-example:: SimpleField
:file: GuideExampleSimpleAlgorithm.cxx
:caption: Header declaration for a simple filter.
It is typical for a filter to have a constructor to set up its initial state.
A filter will also override the :func:`vtkm::filter::Filter::DoExecute` method.
The :func:`vtkm::filter::Filter::DoExecute` method takes a :class:`vtkm::cont::DataSet` as input and likewise returns a :class:`vtkm::cont::DataSet` containing the results of the filter operation.
Note that the declaration of the ``PoundsPerSquareInchToNewtonsPerSquareMeterFilter`` contains the export macro ``VTKM_FILTER_UNIT_CONVERSION_EXPORT``.
This is a macro generated by CMake to handle the appropriate modifies for exporting a class from a library.
Remember that this code is to be placed in a library named ``vtkm::filter::unit_conversion``.
For this library, CMake creates a header file named ``vtkm/filter/unit_conversion.h`` that declares macros like ``VTKM_FILTER_UNIT_CONVERSION_EXPORT``.
.. didyouknow::
A filter can also override the :func:`vtkm::filter::Filter::DoExecutePartitions`, which operates on a :class:`vtkm::cont::PartitionedDataSet`.
If :func:`vtkm::filter::Filter::DoExecutePartitions` is not overridden, then the filter will call :func:`vtkm::filter::Filter::DoExecute` on each of the partitions and build a new :class:`vtkm::cont::PartitionedDataSet` with the outputs.
Once the filter class is declared in the ``.h`` file, the filter implementation is by convention given in a separate ``.cxx`` file.
Given the definition of our filter in :numref:`ex:SimpleField`, we will need to provide the implementation for the constructor and the :func:`vtkm::filter::Filter::DoExecute` method.
The constructor is quite simple.
It initializes the name of the output field name, which is managed by the superclass.
.. load-example:: SimpleFieldConstructor
:file: GuideExampleSimpleAlgorithm.cxx
:caption: Constructor for a simple filter.
In this case, we are setting the output field name to the empty string.
This is not to mean that the default name of the output field should be the empty string, which is not a good idea.
Rather, as we will see later, we will use the empty string to flag an output name that should be derived from the input name.
The meat of the filter implementation is located in the :func:`vtkm::filter::Filter::DoExecute` method.
.. load-example:: SimpleFieldDoExecute
:file: GuideExampleSimpleAlgorithm.cxx
:caption: Implementation of ``DoExecute`` for a simple filter.
The single argument to :func:`vtkm::filter::Filter::DoExecute` is a :class:`vtkm::cont::DataSet` containing the data to operate on, and :func:`vtkm::filter::Filter::DoExecute` returns a derived :class:`vtkm::cont::DataSet`.
The filter must pull the appropriate information out of the input :class:`vtkm::cont::DataSet` to operate on.
This simple algorithm just operates on a single field array of the data.
The :class:`vtkm::filter::FilterField` base class provides several methods to allow filter users to select the active field to operate on.
The filter implementation can get the appropriate field to operate on using the :func:`vtkm::filter::FilterField::GetFieldFromDataSet` method provided by :class:`vtkm::filter::FilterField` as shown in :exlineref:`ex:SimpleFieldDoExecute:InputField`.
One of the challenges with writing filters is determining the actual types the algorithm is operating on.
The :class:`vtkm::cont::Field` object pulled from the input :class:`vtkm::cont::DataSet` contains a :class:`vtkm::cont::ArrayHandle` (see :chapref:`basic-array-handles:Basic Array Handles`), but you do not know what the template parameters of the :class:`vtkm::cont::ArrayHandle` are.
There are numerous ways to extract an array of an unknown type out of a :class:`vtkm::cont::ArrayHandle` (many of which will be explored later in Chapter \ref{chap:UnknownArrayHandle}), but the :class:`vtkm::filter::FilterField` contains some convenience functions to simplify this.
.. todo:: Fix above reference.
In particular, this filter operates specifically on scalar fields.
For this purpose, :class:`vtkm::filter::FilterField` provides the :func:`vtkm::filter::FilterField::CastAndCallScalarField` helper method.
The first argument to :func:`vtkm::filter::FilterField::CastAndCallScalarField` is the field containing the data to operate on.
The second argument is a functor that will operate on the array once it is identified.
:func:`vtkm::filter::FilterField::CastAndCallScalarField` will pull a :class:`vtkm::cont::ArrayHandle` out of the field and call the provided functor with that object.
:func:`vtkm::filter::FilterField::CastAndCallScalarField` is called in :exlineref:`ex:SimpleFieldDoExecute:CastAndCall`.
.. didyouknow::
If your filter requires a field containing ``vtkm::Vec`s of a particular size (e.g. 3), you can use the convenience method :func:`vtkm::filter::FilterField::CastAndCallVecField`.
:func:`vtkm::filter::FilterField::CastAndCallVecField` works similarly to :func:`vtkm::filter::FilterField::CastAndCallScalarField` except that it takes a template parameter specifying the size of the ``vtkm::Vec`.
For example, ``vtkm::filter::FilterField::CastAndCallVecField<3>(inField, functor);``.
As previously stated, one of the arguments to :func:`vtkm::filter::FilterField::CastAndCallScalarField` is a functor that contains the routine to call with the found :class:`vtkm::cont::ArrayHandle`.
A functor can be created as its own ``class`` or ``struct``, but a more convenient method is to use a C++ lambda.
A lambda is an unnamed function defined inline with the code.
The lambda in :numref:`ex:SimpleFieldDoExecute` starts on :exlineref:`line {line}<ex:SimpleFieldDoExecute:Lambda>`.
Apart from being more convenient than creating a named class, lambda functions offer another important feature.
Lambda functions can "capture" variables in the current scope.
They can therefore access things like local variables and the ``this`` reference to the method's class (even accessing private members).
The callback to the lambda function in :numref:`ex:SimpleFieldDoExecute` first creates an output :class:`vtkm::cont::ArrayHandle` of a compatible type (:exlineref:`line {line}<ex:SimpleFieldDoExecute:CreateOutputArray>`), then invokes the worklet that computes the derived field (:exlineref:`line {line}<ex:SimpleFieldDoExecute:Invoke>`), and finally captures the resulting array.
Note that the :class:`vtkm::filter::Filter` base class provides a :func:`vtkm::filter::Filter::Invoke` member that can be used to invoke the worklet.
(See :secref:`simple-worklets:Invoking a Worklet` for information on invoking a worklet.)
Recall that the worklet created in :chapref:`simple-worklets:Simple Worklets` takes two parameters: an input array and an output array, which are shown in this invocation.
With the output data created, the filter has to build the output structure to return.
All implementations of :func:`vtkm::filter::Filter::DoExecute` must return a :class:`vtkm::cont::DataSet`, and for a simple field filter like this we want to return the same :class:`vtkm::cont::DataSet` as the input with the output field added.
The output field needs a name, and we get the appropriate name from the superclass (:exlineref:`ex:SimpleFieldDoExecute:OutputName`).
However, we would like a special case where if the user does not specify an output field name we construct one based on the input field name.
Recall from :numref:`ex:SimpleFieldConstructor` that by default we set the output field name to the empty string.
Thus, our filter checks for this empty string, and if it is encountered, it builds a field name by appending "_N/M^2" to it.
Finally, our filter constructs the output :class:`vtkm::cont::DataSet` using one of the :func:`vtkm::filter::Filter::CreateResult` member functions (:exlineref:`ex:SimpleFieldDoExecute:CreateResult`).
In this particular case, the filter uses :func:`vtkm::filter::FilterField::CreateResultField`, which constructs a :class:`vtkm::cont::DataSet` with the same structure as the input and adds the computed filter.
.. commonerrors::
The :func:`vtkm::filter::Filter::CreateResult` methods do more than just construct a new :class:`vtkm::cont::DataSet`.
They also set up the structure of the data and pass fields as specified by the state of the filter object.
Thus, implementations of :func:`vtkm::filter::Filter::DoExecute` should always return a :class:`vtkm::cont::DataSet` that is created with :func:`vtkm::filter::Filter::CreateResult` or a similarly named method in the base filter classes.
This chapter has just provided a brief introduction to creating filters.
There are several more filter superclasses to help express algorithms of different types.
After some more worklet concepts to implement more complex algorithms are introduced in :partref:`part-advanced:Advanced Development`, we will see a more complete documentation of the types of filters in Chapter \ref{chap:FilterTypeReference}.
.. todo:: Fix above reference.