3.4. Basic Filter Implementation

Chapter 3.3 (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 VTK‑m. Instead, most users execute algorithms in VTK‑m 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 Chapter 3.3 (Simple Worklets), which converted the units of a pressure field from pounds per square inch (psi) to Newtons per square meter (\(\mathrm{N}/\mathrm{m}^2\)).

Filters in VTK‑m are implemented by deriving vtkm::filter::Filter.

The following example shows the declaration of our pressure unit conversion filter. VTK‑m 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.

Example 3.20 Header declaration for a simple filter.
 1namespace vtkm
 2{
 3namespace filter
 4{
 5namespace unit_conversion
 6{
 7
 8class VTKM_FILTER_UNIT_CONVERSION_EXPORT PoundsPerSquareInchToNewtonsPerSquareMeterFilter
 9  : public vtkm::filter::Filter
10{
11public:
12  VTKM_CONT PoundsPerSquareInchToNewtonsPerSquareMeterFilter();
13
14  VTKM_CONT vtkm::cont::DataSet DoExecute(const vtkm::cont::DataSet& inDataSet) override;
15};
16
17}
18}
19} // namespace vtkm::filter::unit_conversion

It is typical for a filter to have a constructor to set up its initial state. A filter will also override the vtkm::filter::Filter::DoExecute() method. The vtkm::filter::Filter::DoExecute() method takes a vtkm::cont::DataSet as input and likewise returns a vtkm::cont::DataSet containing the results of the filter operation.

virtual vtkm::cont::DataSet vtkm::filter::Filter::DoExecute(const vtkm::cont::DataSet &inData) = 0

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.

Did You Know?

A filter can also override the vtkm::filter::Filter::DoExecutePartitions(), which operates on a vtkm::cont::PartitionedDataSet. If vtkm::filter::Filter::DoExecutePartitions() is not overridden, then the filter will call vtkm::filter::Filter::DoExecute() on each of the partitions and build a new vtkm::cont::PartitionedDataSet with the outputs.

virtual vtkm::cont::PartitionedDataSet vtkm::filter::Filter::DoExecutePartitions(const vtkm::cont::PartitionedDataSet &inData)

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 Example 3.20, we will need to provide the implementation for the constructor and the 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.

Example 3.21 Constructor for a simple filter.
1VTKM_CONT PoundsPerSquareInchToNewtonsPerSquareMeterFilter::
2  PoundsPerSquareInchToNewtonsPerSquareMeterFilter()
3{
4  this->SetOutputFieldName("");
5}

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 vtkm::filter::Filter::DoExecute() method.

Example 3.22 Implementation of DoExecute for a simple filter.
 1VTKM_CONT vtkm::cont::DataSet
 2PoundsPerSquareInchToNewtonsPerSquareMeterFilter::DoExecute(
 3  const vtkm::cont::DataSet& inDataSet)
 4{
 5  vtkm::cont::Field inField = this->GetFieldFromDataSet(inDataSet);
 6
 7  vtkm::cont::UnknownArrayHandle outArray;
 8
 9  auto resolveType = [&](const auto& inputArray) {
10    // use std::decay to remove const ref from the decltype of concrete.
11    using T = typename std::decay_t<decltype(inputArray)>::ValueType;
12    vtkm::cont::ArrayHandle<T> result;
13    this->Invoke(
14      PoundsPerSquareInchToNewtonsPerSquareMeterWorklet{}, inputArray, result);
15    outArray = result;
16  };
17
18  this->CastAndCallScalarField(inField, resolveType);
19
20  std::string outFieldName = this->GetOutputFieldName();
21  if (outFieldName == "")
22  {
23    outFieldName = inField.GetName() + "_N/m^2";
24  }
25
26  return this->CreateResultField(
27    inDataSet, outFieldName, inField.GetAssociation(), outArray);
28}

The single argument to vtkm::filter::Filter::DoExecute() is a vtkm::cont::DataSet containing the data to operate on, and vtkm::filter::Filter::DoExecute() returns a derived vtkm::cont::DataSet. The filter must pull the appropriate information out of the input vtkm::cont::DataSet to operate on. This simple algorithm just operates on a single field array of the data. The vtkm::filter::Filter base class provides several methods, documented in Section 2.6.2.1 (Input Fields), 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 vtkm::filter::Filter::GetFieldFromDataSet() method as shown in Example 3.22, line 5.

inline const vtkm::cont::Field &vtkm::filter::Filter::GetFieldFromDataSet(const vtkm::cont::DataSet &input) const

Retrieve an input field from a vtkm::cont::DataSet object.

When a filter operates on fields, it should use this method to get the input fields that the use has selected with SetActiveField() and related methods.

inline const vtkm::cont::Field &vtkm::filter::Filter::GetFieldFromDataSet(vtkm::IdComponent index, const vtkm::cont::DataSet &input) const

Retrieve an input field from a vtkm::cont::DataSet object.

When a filter operates on fields, it should use this method to get the input fields that the use has selected with SetActiveField() and related methods.

One of the challenges with writing filters is determining the actual types the algorithm is operating on. The vtkm::cont::Field object pulled from the input vtkm::cont::DataSet contains a vtkm::cont::ArrayHandle (see Chapter 3.2 (Basic Array Handles)), but you do not know what the template parameters of the vtkm::cont::ArrayHandle are. There are numerous ways to extract an array of an unknown type out of a vtkm::cont::ArrayHandle (many of which will be explored later in Chapter ref{chap:UnknownArrayHandle}), but the vtkm::filter::Filter contains some convenience functions to simplify this.

In particular, this filter operates specifically on scalar fields. For this purpose, vtkm::filter::Filter provides the vtkm::filter::Filter::CastAndCallScalarField() helper method. The first argument to vtkm::filter::Filter::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. vtkm::filter::Filter::CastAndCallScalarField() will pull a vtkm::cont::ArrayHandle out of the field and call the provided functor with that object. vtkm::filter::Filter::CastAndCallScalarField() is called in Example 3.22, line 18.

template<typename Functor, typename ...Args>
inline void vtkm::filter::Filter::CastAndCallScalarField(const vtkm::cont::UnknownArrayHandle &fieldArray, Functor &&functor, Args&&... args) const

Convenience method to get the array from a filter’s input scalar field.

A field filter typically gets its input fields using the internal GetFieldFromDataSet. To use this field in a worklet, it eventually needs to be converted to an vtkm::cont::ArrayHandle. If the input field is limited to be a scalar field, then this method provides a convenient way to determine the correct array type. Like other CastAndCall methods, it takes as input a vtkm::cont::Field (or vtkm::cont::UnknownArrayHandle) and a function/functor to call with the appropriate vtkm::cont::ArrayHandle type.

template<typename Functor, typename ...Args>
inline void vtkm::filter::Filter::CastAndCallScalarField(const vtkm::cont::Field &field, Functor &&functor, Args&&... args) const

Convenience method to get the array from a filter’s input scalar field.

A field filter typically gets its input fields using the internal GetFieldFromDataSet. To use this field in a worklet, it eventually needs to be converted to an vtkm::cont::ArrayHandle. If the input field is limited to be a scalar field, then this method provides a convenient way to determine the correct array type. Like other CastAndCall methods, it takes as input a vtkm::cont::Field (or vtkm::cont::UnknownArrayHandle) and a function/functor to call with the appropriate vtkm::cont::ArrayHandle type.

Did You Know?

If your filter requires a field containing vtkm::Vec valuess of a particular size (e.g. 3), you can use the convenience method vtkm::filter::Filter::CastAndCallVecField(). vtkm::filter::Filter::CastAndCallVecField() works similarly to vtkm::filter::Filter::CastAndCallScalarField() except that it takes a template parameter specifying the size of the vtkm::Vec. For example, vtkm::filter::Filter::CastAndCallVecField<3>(inField, functor);.

As previously stated, one of the arguments to vtkm::filter::Filter::CastAndCallScalarField() is a functor that contains the routine to call with the found 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 Example 3.22 starts on line 9. 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 Example 3.22 first creates an output vtkm::cont::ArrayHandle of a compatible type (line 12), then invokes the worklet that computes the derived field (line 13), and finally captures the resulting array. Note that the vtkm::filter::Filter base class provides a vtkm::filter::Filter::Invoke() member that can be used to invoke the worklet. (See Section 3.3.5 (Invoking a Worklet) for information on invoking a worklet.) Recall that the worklet created in Chapter 3.3 (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 vtkm::filter::Filter::DoExecute() must return a vtkm::cont::DataSet, and for a simple field filter like this we want to return the same 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 (Example 3.22, line 20). 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 Example 3.21 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 vtkm::cont::DataSet using one of the vtkm::filter::Filter::CreateResult() member functions (Example 3.22, line 26). In this particular case, the filter uses vtkm::filter::Filter::CreateResultField(), which constructs a vtkm::cont::DataSet with the same structure as the input and adds the computed filter.

vtkm::cont::DataSet vtkm::filter::Filter::CreateResult(const vtkm::cont::DataSet &inDataSet) const

Create the output data set for DoExecute.

This form of CreateResult will create an output data set with the same cell structure and coordinate system as the input and pass all fields (as requested by the Filter state).

Parameters:

inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with the cell set, coordinate system, and fields of inDataSet (as selected by the FieldsToPass state of the filter).

vtkm::cont::PartitionedDataSet vtkm::filter::Filter::CreateResult(const vtkm::cont::PartitionedDataSet &input, const vtkm::cont::PartitionedDataSet &resultPartitions) const

Create the output data set for DoExecute.

This form of CreateResult will create an output PartitionedDataSet with the same partitions and pass all PartitionedDataSet fields (as requested by the Filter state).

Parameters:
  • input[in] The input data set being modified (usually the one passed into DoExecute).

  • resultPartitions[in] The output data created by the filter. Fields from the input are passed onto the return result partition as requested by the Filter state.

template<typename FieldMapper>
inline vtkm::cont::PartitionedDataSet vtkm::filter::Filter::CreateResult(const vtkm::cont::PartitionedDataSet &input, const vtkm::cont::PartitionedDataSet &resultPartitions, FieldMapper &&fieldMapper) const

Create the output data set for DoExecute.

This form of CreateResult will create an output PartitionedDataSet with the same partitions and pass all PartitionedDataSet fields (as requested by the Filter state).

Parameters:
  • input[in] The input data set being modified (usually the one passed into DoExecute).

  • resultPartitions[in] The output data created by the filter. Fields from the input are passed onto the return result partition as requested by the Filter state.

  • fieldMapper[in] A function or functor that takes a PartitionedDataSet as its first argument and a Field as its second argument. The PartitionedDataSet is the data being created and will eventually be returned by CreateResult. The Field comes from input.

template<typename FieldMapper>
inline vtkm::cont::DataSet vtkm::filter::Filter::CreateResult(const vtkm::cont::DataSet &inDataSet, const vtkm::cont::UnknownCellSet &resultCellSet, FieldMapper &&fieldMapper) const

Create the output data set for DoExecute.

This form of CreateResult will create an output data set with the given CellSet. You must also provide a field mapper function, which is a function that takes the output DataSet being created and a Field from the input and then applies any necessary transformations to the field array and adds it to the DataSet.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultCellSet[in] The CellSet of the output will be set to this.

  • fieldMapper[in] A function or functor that takes a DataSet as its first argument and a Field as its second argument. The DataSet is the data being created and will eventually be returned by CreateResult. The Field comes from inDataSet. The function should map the Field to match resultCellSet and then add the resulting field to the DataSet. If the mapping is not possible, then the function should do nothing.

vtkm::cont::DataSet vtkm::filter::Filter::CreateResultField(const vtkm::cont::DataSet &inDataSet, const vtkm::cont::Field &resultField) const

Create the output data set for DoExecute

This form of CreateResult will create an output data set with the same cell structure and coordinate system as the input and pass all fields (as requested by the Filter state). Additionally, it will add the provided field to the result.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultField[in] A Field that is added to the returned DataSet.

inline vtkm::cont::DataSet vtkm::filter::Filter::CreateResultField(const vtkm::cont::DataSet &inDataSet, const std::string &resultFieldName, vtkm::cont::Field::Association resultFieldAssociation, const vtkm::cont::UnknownArrayHandle &resultFieldArray) const

Create the output data set for DoExecute

This form of CreateResult will create an output data set with the same cell structure and coordinate system as the input and pass all fields (as requested by the Filter state). Additionally, it will add a field matching the provided specifications to the result.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultFieldName[in] The name of the field added to the returned DataSet.

  • resultFieldAssociation[in] The association of the field (e.g. point or cell) added to the returned DataSet.

  • resultFieldArray[in] An array containing the data for the field added to the returned DataSet.

inline vtkm::cont::DataSet vtkm::filter::Filter::CreateResultFieldPoint(const vtkm::cont::DataSet &inDataSet, const std::string &resultFieldName, const vtkm::cont::UnknownArrayHandle &resultFieldArray) const

Create the output data set for DoExecute

This form of CreateResult will create an output data set with the same cell structure and coordinate system as the input and pass all fields (as requested by the Filter state). Additionally, it will add a point field matching the provided specifications to the result.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultFieldName[in] The name of the field added to the returned DataSet.

  • resultFieldArray[in] An array containing the data for the field added to the returned DataSet.

inline vtkm::cont::DataSet vtkm::filter::Filter::CreateResultFieldCell(const vtkm::cont::DataSet &inDataSet, const std::string &resultFieldName, const vtkm::cont::UnknownArrayHandle &resultFieldArray) const

Create the output data set for DoExecute

This form of CreateResult will create an output data set with the same cell structure and coordinate system as the input and pass all fields (as requested by the Filter state). Additionally, it will add a cell field matching the provided specifications to the result.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultFieldName[in] The name of the field added to the returned DataSet.

  • resultFieldArray[in] An array containing the data for the field added to the returned DataSet.

template<typename FieldMapper>
inline vtkm::cont::DataSet vtkm::filter::Filter::CreateResultCoordinateSystem(const vtkm::cont::DataSet &inDataSet, const vtkm::cont::UnknownCellSet &resultCellSet, const vtkm::cont::CoordinateSystem &resultCoordSystem, FieldMapper &&fieldMapper) const

Create the output data set for DoExecute.

This form of CreateResult will create an output data set with the given CellSet and CoordinateSystem. You must also provide a field mapper function, which is a function that takes the output DataSet being created and a Field from the input and then applies any necessary transformations to the field array and adds it to the DataSet.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultCellSet[in] The CellSet of the output will be set to this.

  • resultCoordSystem[in] This CoordinateSystem will be added to the output.

  • fieldMapper[in] A function or functor that takes a DataSet as its first argument and a Field as its second argument. The DataSet is the data being created and will eventually be returned by CreateResult. The Field comes from inDataSet. The function should map the Field to match resultCellSet and then add the resulting field to the DataSet. If the mapping is not possible, then the function should do nothing.

template<typename FieldMapper>
inline vtkm::cont::DataSet vtkm::filter::Filter::CreateResultCoordinateSystem(const vtkm::cont::DataSet &inDataSet, const vtkm::cont::UnknownCellSet &resultCellSet, const std::string &coordsName, const vtkm::cont::UnknownArrayHandle &coordsData, FieldMapper &&fieldMapper) const

Create the output data set for DoExecute.

This form of CreateResult will create an output data set with the given CellSet and CoordinateSystem. You must also provide a field mapper function, which is a function that takes the output DataSet being created and a Field from the input and then applies any necessary transformations to the field array and adds it to the DataSet.

Parameters:
  • inDataSet[in] The input data set being modified (usually the one passed into DoExecute). The returned DataSet is filled with fields of inDataSet (as selected by the FieldsToPass state of the filter).

  • resultCellSet[in] The CellSet of the output will be set to this.

  • coordsName[in] The name of the coordinate system to be added to the output.

  • coordsData[in] The array containing the coordinates of the points.

  • fieldMapper[in] A function or functor that takes a DataSet as its first argument and a Field as its second argument. The DataSet is the data being created and will eventually be returned by CreateResult. The Field comes from inDataSet. The function should map the Field to match resultCellSet and then add the resulting field to the DataSet. If the mapping is not possible, then the function should do nothing.

Common Errors

The vtkm::filter::Filter::CreateResult() methods do more than just construct a new 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 vtkm::filter::Filter::DoExecute() should always return a vtkm::cont::DataSet that is created with vtkm::filter::Filter::CreateResult() or a similarly named method in the base filter class.

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 Part 4 (Advanced Development), we will see a more complete documentation of the types of filters in Chapter 4.4 (Extended Filter Implementations).