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 Viskores. Instead, most users execute algorithms in Viskores 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 Viskores are implemented by deriving viskores::filter::Filter.

The following example shows the declaration of our pressure unit conversion filter. Viskores filters are divided into libraries. In this example, we are assuming this filter is being compiled in a library named viskores::filter::unit_conversion. By convention, the source files would be placed in a directory named viskores/filter/unit_conversion.

Example 3.24 Header declaration for a simple filter.
 1namespace viskores
 2{
 3namespace filter
 4{
 5namespace unit_conversion
 6{
 7
 8class VISKORES_FILTER_UNIT_CONVERSION_EXPORT
 9  PoundsPerSquareInchToNewtonsPerSquareMeterFilter : public viskores::filter::Filter
10{
11public:
12  VISKORES_CONT PoundsPerSquareInchToNewtonsPerSquareMeterFilter();
13
14  VISKORES_CONT viskores::cont::DataSet DoExecute(
15    const viskores::cont::DataSet& inDataSet) override;
16};
17
18}
19}
20} // namespace viskores::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 viskores::filter::Filter::DoExecute() method. The viskores::filter::Filter::DoExecute() method takes a viskores::cont::DataSet as input and likewise returns a viskores::cont::DataSet containing the results of the filter operation.

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

Note that the declaration of the PoundsPerSquareInchToNewtonsPerSquareMeterFilter contains the export macro VISKORES_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 viskores::filter::unit_conversion. For this library, CMake creates a header file named viskores/filter/unit_conversion.h that declares macros like VISKORES_FILTER_UNIT_CONVERSION_EXPORT.

Did You Know?

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

virtual viskores::cont::PartitionedDataSet viskores::filter::Filter::DoExecutePartitions(const viskores::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.24, we will need to provide the implementation for the constructor and the viskores::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.25 Constructor for a simple filter.
1VISKORES_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 viskores::filter::Filter::DoExecute() method.

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

The single argument to viskores::filter::Filter::DoExecute() is a viskores::cont::DataSet containing the data to operate on, and viskores::filter::Filter::DoExecute() returns a derived viskores::cont::DataSet. The filter must pull the appropriate information out of the input viskores::cont::DataSet to operate on. This simple algorithm just operates on a single field array of the data. The viskores::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 viskores::filter::Filter::GetFieldFromDataSet() method as shown in Example 3.26, line 5.

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

Retrieve an input field from a viskores::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 viskores::cont::Field &viskores::filter::Filter::GetFieldFromDataSet(viskores::IdComponent index, const viskores::cont::DataSet &input) const

Retrieve an input field from a viskores::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 viskores::cont::Field object pulled from the input viskores::cont::DataSet contains a viskores::cont::ArrayHandle (see Chapter 3.2 (Basic Array Handles)), but you do not know what the template parameters of the viskores::cont::ArrayHandle are. There are numerous ways to extract an array of an unknown type out of a viskores::cont::ArrayHandle, many of which will be explored later in Chapter 3.5 (Unknown Array Handles), but the viskores::filter::Filter contains some convenience functions to simplify this.

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

template<typename Functor, typename ...Args>
inline void viskores::filter::Filter::CastAndCallScalarField(const viskores::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 viskores::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 viskores::cont::Field (or viskores::cont::UnknownArrayHandle) and a function/functor to call with the appropriate viskores::cont::ArrayHandle type.

template<typename Functor, typename ...Args>
inline void viskores::filter::Filter::CastAndCallScalarField(const viskores::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 viskores::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 viskores::cont::Field (or viskores::cont::UnknownArrayHandle) and a function/functor to call with the appropriate viskores::cont::ArrayHandle type.

Did You Know?

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

As previously stated, one of the arguments to viskores::filter::Filter::CastAndCallScalarField() is a functor that contains the routine to call with the found viskores::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.26 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.26 first creates an output viskores::cont::ArrayHandle of a compatible type (line 13), then invokes the worklet that computes the derived field (line 14), and finally captures the resulting array. Note that the viskores::filter::Filter base class provides a viskores::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 viskores::filter::Filter::DoExecute() must return a viskores::cont::DataSet, and for a simple field filter like this we want to return the same viskores::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.26, line 21). 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.25 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 viskores::cont::DataSet using one of the viskores::filter::Filter::CreateResult() member functions (Example 3.26, line 27). In this particular case, the filter uses viskores::filter::Filter::CreateResultField(), which constructs a viskores::cont::DataSet with the same structure as the input and adds the computed filter.

viskores::cont::DataSet viskores::filter::Filter::CreateResult(const viskores::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).

viskores::cont::PartitionedDataSet viskores::filter::Filter::CreateResult(const viskores::cont::PartitionedDataSet &input, const viskores::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 viskores::cont::PartitionedDataSet viskores::filter::Filter::CreateResult(const viskores::cont::PartitionedDataSet &input, const viskores::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 viskores::cont::DataSet viskores::filter::Filter::CreateResult(const viskores::cont::DataSet &inDataSet, const viskores::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.

viskores::cont::DataSet viskores::filter::Filter::CreateResultField(const viskores::cont::DataSet &inDataSet, const viskores::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 viskores::cont::DataSet viskores::filter::Filter::CreateResultField(const viskores::cont::DataSet &inDataSet, const std::string &resultFieldName, viskores::cont::Field::Association resultFieldAssociation, const viskores::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 viskores::cont::DataSet viskores::filter::Filter::CreateResultFieldPoint(const viskores::cont::DataSet &inDataSet, const std::string &resultFieldName, const viskores::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 viskores::cont::DataSet viskores::filter::Filter::CreateResultFieldCell(const viskores::cont::DataSet &inDataSet, const std::string &resultFieldName, const viskores::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 viskores::cont::DataSet viskores::filter::Filter::CreateResultCoordinateSystem(const viskores::cont::DataSet &inDataSet, const viskores::cont::UnknownCellSet &resultCellSet, const viskores::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 viskores::cont::DataSet viskores::filter::Filter::CreateResultCoordinateSystem(const viskores::cont::DataSet &inDataSet, const viskores::cont::UnknownCellSet &resultCellSet, const std::string &coordsName, const viskores::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 viskores::filter::Filter::CreateResult() methods do more than just construct a new viskores::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 viskores::filter::Filter::DoExecute() should always return a viskores::cont::DataSet that is created with viskores::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).