In this section, we will modify the previous ping_simple example to add a custom operator into the workflow. We’ve already seen a custom operator defined in the hello_world example but skipped over some of the details.
In this example we will cover:
The example source code and run instructions can be found in the examples directory on GitHub, or under /opt/nvidia/holoscan/examples in the NGC container and the Debian package, alongside their executables.
Here is the diagram of the operators and workflow used in this example.
Compared to the previous example, we are adding a new PingMxOp operator between the PingTxOp and PingRxOp operators. This new operator takes as input an integer, multiplies it by a constant factor, and then sends the new value to PingRxOp. You can think of this custom operator as doing some data processing on an input stream before sending the result to downstream operators.
Our custom operator needs 1 input and 1 output port and can be added by calling spec.input() and spec.output() methods within the operator’s setup() method.
This requires providing the data type and name of the port as arguments (for C++ API), or just the port name (for Python API). We will see an example of this in the code snippet below. For more details, see specifying-operator-inputs-and-outputs-cpp or Specifying Operator Inputs and Outputs Python.
Operators can be made more reusable by customizing their parameters during initialization. The custom parameters can be provided either directly as arguments or accessed from the application’s YAML configuration file. We will show how to use the former in this example to customize the “multiplier” factor of our PingMxOp custom operator. Configuring operators using a YAML configuration file will be shown in a subsequent example. For more details, see Configuring App Operator Parameters.
The code snippet below shows how to define the PingMxOp class.
PingMxOp class inherits from the Operator (holoscan::ops::Operator) base class (line 7).HOLOSCAN_OPERATOR_FORWARD_ARGS macro (line 9) is syntactic sugar to help forward an operator’s constructor arguments to the Operator (holoscan::ops::Operator) base class, and is a convenient shorthand to avoid having to manually define constructors for your operator with the necessary parameters.14 and 15 respectively. The port type of both ports are int as indicated by the template argument <int>.16) with a default value of 2. This parameter is tied to the private “multiplier_” data member.compute() method, we receive the integer data from the operator’s “in” port (line 20), print its value, multiply its value by the multiplicative factor, and send the new value downstream (line 27).20, note that the data being passed between the operators has the type int.op_output.emit(value) on line 27 is equivalent to op_output.emit(value, "out") since this operator has only 1 output port. If the operator has more than 1 output port, then the port name is required.Now that the custom operator has been defined, we create the application, operators, and define the workflow.
compose() method on lines 6-8.make_operator() (line 7) as the built-in operators, and configured with a “multiplier” parameter initialized to 3 which overrides the parameter’s default value of 2 (in the setup() method).add_flow() on lines 11-12.For the C++ API, the messages that are passed between the operators are the objects of the data type at the inputs and outputs, so the value variable from lines 20 and 25 of the example above has the type int. For the Python API, the messages passed between operators can be arbitrary Python objects so no special consideration is needed since it is not restricted to the stricter parameter typing used for C++ API operators.
Let’s look at the code snippet for the built-in PingTxOp class and see if this helps to make it clearer.
int (line 6).emit() (line 11).op_input.receive<int>() has the type int.For advanced use cases, e.g., when writing C++ applications where you need interoperability between C++ native and GXF operators, you will need to use the holoscan::TensorMap type instead. See Interoperability with Gxf Operators Cpp for more details. If you are writing a Python application which needs a mixture of Python wrapped C++ operators and native Python operators, see Interoperability with Wrapped Operators Python.
Running the application should give you the following output in your terminal: