4.1. Create your Onika Operator

This page explains the basic steps to create a new operator in Onika.

A minimal operator is made of four steps:

4.1.1. Inherit from OperatorNode

Every operator must inherit from onika::scg::OperatorNode and implement execute(). The documentation() method is also recommended, because it provides a human-readable description of what the operator does.

#include <onika/scg/operator.h>
#include <onika/scg/operator_factory.h>
#include <onika/scg/operator_slot.h>

class MyOperator : public onika::scg::OperatorNode
{
public:
  inline std::string documentation() const override final
  {
    return "Example operator that processes data.";
  }

  inline void execute() override final
  {
    // operator body
  }
};

The execute() method is the core of the operator. It is called when the node is executed in the simulation graph.

4.1.2. Add slots with ADD_SLOT

Slots define the interface of the operator. They are declared with the ADD_SLOT macro and expose members that can be accessed directly from the operator implementation.

A typical example is:

class MyOperator : public onika::scg::OperatorNode
{
  ADD_SLOT( std::string , input_data , INPUT , "default" );
  ADD_SLOT( std::string , output_data , OUTPUT );
  ADD_SLOT( double , value , INPUT_OUTPUT , 0.0 );
  ADD_SLOT( bool , enabled , OPTIONAL , true );
  ADD_SLOT( std::string , internal_cache , PRIVATE );

public:
  inline std::string documentation() const final
  {
    return "Example operator with several slot types.";
  }

  inline void execute() final
  {
    // use input_data, output_data, enabled, internal_cache here
  }
};

The meaning of the main slot kinds is the following:

  • INPUT: the slot is consumed by the operator. It is typically connected from an upstream operator.

  • OUTPUT: the slot produces data for downstream operators.

  • INPUT_OUTPUT: the slot can be used both as an input and as an output. It is useful when data is read and updated in place.

The ADD_SLOT macro can also be enriched with additional modifiers:

  • REQUIRED: this is not a default value; it is a marker indicating that the slot must be connected or provided by the configuration. If it is missing, the operator is considered incomplete and the execution fails.

  • OPTIONAL: the slot is not mandatory. It can remain unconnected, and a default value can be provided if needed.

  • PRIVATE: the slot is internal to the operator and is not meant to be connected in the graph. It is hidden from the regular dataflow interface. Because it is not part of the normal graph contract, you should not assume that its underlying storage is already allocated or initialized before the operator runs.

    Note

    If you want to know whether a value was actually provided, you can test the slot with has_value() and check the resulting boolean.

Several forms are possible depending on the needs of the operator:

ADD_SLOT( std::string , input_data , INPUT );
ADD_SLOT( std::string , output_data , OUTPUT );
ADD_SLOT( std::string , state , INPUT_OUTPUT , "initial" );
ADD_SLOT( std::string , prefix , INPUT , "" );
ADD_SLOT( bool , enabled , INPUT , OPTIONAL , true );
ADD_SLOT( bool , must_exist , INPUT , REQUIRED );
ADD_SLOT( bool , verbose , INPUT , OPTIONAL , false , DocString{"Enable verbose output"} );
ADD_SLOT( std::string , message , INPUT , "hello" , DocString{"Message to display"} );
ADD_SLOT( SomeInternalType , cache , PRIVATE );

In practice:

  • a default value can be given directly after the direction;

  • REQUIRED and OPTIONAL are markers that modify the slot contract;

  • DocString{...} can be added to give a description that appears in the operator interface.

  • PRIVATE is used for internal state that should not be exposed in the graph.

Once a slot is declared, it can be accessed from the operator implementation through the generated member. For example, a slot named value can be used as follows:

ADD_SLOT( double , value , INPUT_OUTPUT , 0.0 );
...
double& value_ref = *value;

This gives direct access to the underlying value stored in the slot.

4.1.3. Register the operator

Once the operator class is implemented, it must be registered in the factory so that Onika can instantiate it from the graph configuration.

ONIKA_AUTORUN_INIT(my_operator)
{
  OperatorNodeFactory::instance()->register_factory(
    "my_operator",
    make_compatible_operator< MyOperator >
  );
}

The string passed to register_factory() is the name used to reference the operator in the configuration. For example, a YAML graph can instantiate it with:

operators:
  - type: my_operator
    value: 1.0

This is the final step that makes your operator available to the execution engine.

4.1.4. Mark the operator as a sink when needed

is_sink() is not about side effects. Overriding it and returning true simply forces the operator to be a sink in the DAG, i.e. a node with no downstream connection that Onika should keep instead of pruning.

inline bool is_sink() const override final
{
  return true;
}

This tells Onika that the operator is terminal-like from the graph point of view. In practice, it is useful for operators that should not be removed just because they have no downstream output connection.

Warning

Forcing an operator to is_sink is not common practice. An operator marked as is_sink should not have any OUTPUT slot.

If you do not override this method, the default behavior is to behave like a regular batch operator.