In this section, we look at how to create an application with a more complex workflow where operators may have multiple input/output ports that send/receive a user-defined data type.
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.
In this example, PingTxOp sends a stream of odd integers to the out1 port, and even integers to the out2 port. PingMxOp receives these values using in1 and in2 ports, multiplies them by a constant factor, then forwards them to a single port - receivers - on PingRxOp.
In the previous ping examples, the port types for our operators were integers, but the Holoscan SDK can send any arbitrary data type. In this example, we’ll see how to configure
operators for our user-defined ValueData class.
The ValueData class wraps a simple integer (line 6, 16), but could have been arbitrarily complex.
The HOLOSCAN_LOG_<LEVEL>() macros can be used for logging with fmtlib syntax (lines 7, 9 above) as demonstrated across this example. See the Logging section for more details.
After defining our custom ValueData class, we configure our operators’ ports to send/receive messages of this type, similarly to the previous example.
This is the first operator - PingTxOp - sending ValueData objects on two ports, out1 and out2:
ValueData type on lines 10 and 11 using spec.output<std::shared_ptr<ValueData>>(). Therefore, the data type for the output ports is an object to a shared pointer to a ValueData object.op_output.emit() on lines 16 and 19. The port name is required since there is more than one port on this operator.Data types of the output ports are shared pointers (std::shared_ptr), hence the call to std::make_shared<ValueData>(...) on lines 15 and 18.
We then configure the middle operator - PingMxOp - to receive that data on ports in1 and in2:
std::shared_ptr<ValueData> type on lines 8 and 9 using spec.input<std::shared_ptr<ValueData>>().op_input.receive() on lines 16 and 17 using the port names. The received values are of type std::shared_ptr<ValueData> as mentioned in the templated receive() method.PingMxOp processes the data, then sends it out on two ports, similarly to what is done by PingTxOp above.
In this workflow, PingRxOp has a single input port - receivers - that is connected to two upstream ports from PingMxOp. When an input port needs to connect to multiple upstream ports, we define it with spec.input() and set the size to IOSpec::kAnySize (or IOSpec.ANY_SIZE in Python). This allows the input port to receive data from multiple sources. The inputs are then stored in a vector, following the order they were added with add_flow().
setup() method, we define an input port receivers (line 12) with holoscan::IOSpec::kAnySize to allow any number of upstream ports to connect to it.op_input.receive<std::vector<std::shared_ptr<ValueData>>>(...).value_vector is std::vector<std::shared_ptr<ValueData>> (lines 16-17).Please see Retrieving Any Number of Inputs Cpp for more information on how to retrieve any number of inputs in C++.
The rest of the code creates the application, operators, and defines the workflow:
tx, mx, and rx are created in the application’s compose() similarly to previous examples.add_flow():
tx/out1 is connected to mx/in1, and tx/out2 is connected to mx/in2.mx/out1 and mx/out2 are both connected to rx/receivers.Running the application should give you output similar to the following in your terminal.
Depending on your log level you may see more or fewer messages. The output above was generated using the default value of INFO.
Refer to the Logging section for more details on how to set the log level.