.. index:: pair: page; Engine DataPacks .. _doxid-datapacks: Engine DataPacks ================ DataPacks are simple objects which wrap around arbitrary data structures, like JSON objects or protobuf messages. They provide the necessary abstract interface, which is understood by all components of NRP-Core, while still allowing to pass data in various formats. A :ref:`DataPack ` consists of two parts: * :ref:`DataPack ` ID: which allows to uniquely indentify the object * :ref:`DataPack ` data: this is the data stored by the :ref:`DataPack `, which can be in principle of any type DataPacks are mainly used by :ref:`Transceiver functions ` to relay data between engines. Each engine type is designed to accept only datapacks of a certain type and structure. To discover which datapacks can be processed by each engine, check out the engine's documentation :ref:`here `. .. _doxid-datapacks_1datapacks_id: DataPack ID ~~~~~~~~~~~ Every datapack contains a :ref:`DataPackIdentifier `, which uniquely identifies the datapack object and allows for routing of the data between transceiver functions, engine clients and engine servers. A datapack identifier consists of three fields: * name - name of the datapack. Must be unique. * type - string representation of the :ref:`DataPack ` data type. This field will most probably will be of no concern for the users. It is set and used internally and is not in human-readable form. * engine name - name of the engine to which the datapack is bound. These fields can be accessed from the transceiver functions: .. ref-code-block:: cpp print(datapack.name) print(datapack.type) print(datapack.engine_name) .. _doxid-datapacks_1datapacks_data: DataPack data ~~~~~~~~~~~~~ :ref:`DataPack ` is a template class with a single template parameter, which specifies the type of the data contained by the :ref:`DataPack `. This :ref:`DataPack ` data can be in principle of any type. In practice there are some limitations though, since DataPacks, which are C++ objects, must be accessible from TransceiverFunctions, which are written in Python. Therefore the only :ref:`DataPack ` data types which can be actually used in NRP-core are those for which Python bindings are provided. These are commented :ref:`below `. In TransceiverFunctions, the :ref:`DataPack ` data can always be accessed using the datapack "data" attribute. .. _doxid-datapacks_1empty_datapack: Empty DataPacks ~~~~~~~~~~~~~~~ It is possible for a datapack to contain no data. This is useful for example when an Engine is asked for a certain :ref:`DataPack ` but it is not able to provide it. In this case, an Engine can return an empty datapack. This type of datapack contains only a datapack identifier and no data. Attempting to retrieve the data from an empty :ref:`DataPack ` will result in an exception. A method "isEmpty" is provided to check whether a :ref:`DataPack ` is empty or not before attempting to access its data: .. ref-code-block:: cpp if not datapack.isEmpty(): # It's safe to get the data print(datapack.data) else: # This will raise an exception print(datapack.data) .. _doxid-datapacks_1stale_datapacks: Checking if a DataPack contains the most recent data ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It may happen that certain DataPacks that are available to Preprocessing and Transceiver functions will not be updated on every simulation iteration. For example, this will be the case whenever the engines that take part in the simulation run at different frequencies. It may be wasteful to perform processing of the same data multiple times. The :ref:`DataPack ` objects have a flag, called ``isUpdated`` that allows to check if they contain the most recent data. It will be set to true only on the simulation iteration,on which the :ref:`DataPack ` has been created (for example returned from a Preprocessing Function) or received (for example from an Engine). The flag can be accessed from Preprocessing, Transceiver, and Status Functions in the following way: .. ref-code-block:: cpp if datapack.isUpdated(): # Perform some operations, because the DataPack has just been updated else: # The DataPack is 'stale', i.e. it was updated in one of the previous simulation iterations .. _doxid-datapacks_1datapacks_tfs: Role of DataPacks in TransceiverFunctions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DataPacks are both the input and output of TransceiverFunctions. When a datapack is declared as input of a TF, this datapack is always requested to the corresponding Engine when the latter is synchronized. When a datapack is returned by a TF, it is sent to the corresponding Engine after the TF is executed. For more information about the synchronization model used in NRP-core the reader can refer to these sections: * :ref:`Transceiver Functions synchronization ` * :ref:`NRP-core synchronization model ` The subsections below elaborate on the details of how to use DataPacks in TFs. .. _doxid-datapacks_1datapacks_tfs_input: DataPacks as input to transceiver functions ------------------------------------------- DataPacks can be declared as :ref:`TransceiverFunction ` inputs using the dedicated decorator. After that they can be accessed in TFs as input arguments. .. ref-code-block:: cpp # Declare datapack with "datapack_name" name from engine "engine_name" as input using the @EngineDataPack decorator # The trasceiver function must accept an argument with the same name as "keyword" in the datapack decorator @:ref:`EngineDataPack `(keyword="datapack", id=:ref:`DataPackIdentifier `("datapack_name", "engine_name")) @:ref:`TransceiverFunction `("engine_name") def transceiver_function(datapack): print(datapack.data) # Multiple input datapacks from different engines can be declared @:ref:`EngineDataPack `(keyword="datapack1", id=:ref:`DataPackIdentifier `("datapack_name1", "engine_name1")) @:ref:`EngineDataPack `(keyword="datapack2", id=:ref:`DataPackIdentifier `("datapack_name2", "engine_name2")) @:ref:`TransceiverFunction `("engine_name1") def transceiver_function(datapack1, datapack2): print(datapack1.data) print(datapack2.data) By default, all input DataPacks are passed to Transceiver Functions by value, i.e. the function receives a copy of the original :ref:`DataPack ` object. This is a safety measure implemented to prevent accidental modifications of DataPacks available to other Functions and Engines. The policy can be changed by setting the :ref:`DataPackPassingPolicy ` configuration parameter. .. _doxid-datapacks_1datapacks_tfs_output: DataPacks as output of transceiver functions -------------------------------------------- DataPacks can be returned from the transceiver function. .. ref-code-block:: cpp # NRP-Core expects transceiver functions to always return a list of datapacks @:ref:`TransceiverFunction `("engine_name") def transceiver_function(): datapack = :ref:`JsonDataPack `("datapack_name", "engine_name") return [ datapack ] # Multiple datapacks can be returned @:ref:`TransceiverFunction `("engine_name") def transceiver_function(): datapack1 = :ref:`JsonDataPack `("datapack_name1", "engine_name") datapack2 = :ref:`JsonDataPack `("datapack_name2", "engine_name") return [ datapack1, datapack2 ] .. _doxid-datapacks_1supported_datapack_types: Supported DataPack data types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As commented in the section above, DataPacks are both the input and output of TFs. Therefore, a conversion mechanism between C++ and Python is required for each supported :ref:`DataPack ` data type. The types currently supported are nlohmann::json and protobuf messages. The subsections below give details of the Python API provided for each of these types. .. _doxid-datapacks_1datapacks_json: JsonDataPack ------------ **JsonDataPack** type wraps around `nlohmann::json `__ C++ objects. The Python class wrapping the C++ json object is *NlohmannJson*, which is stored in JsonDataPack *data* attribute. **NlohmannJson** is very flexible and allows to pass most types of data between engines and transceiver functions without writing any additional code, it can contain all basic Python types. **NlohmannJson** also has partial support for numpy arrays - it's possible to use 1-dimensional arrays of integers and floats. .. _doxid-datapacks_1datapacks_json_importing: Importing and creating JsonDataPack +++++++++++++++++++++++++++++++++++ To import JsonDataPack: .. ref-code-block:: cpp from nrp_core.data.nrp_json import JsonDataPack To create a JsonDataPack object: .. ref-code-block:: cpp datapack = :ref:`JsonDataPack `("datapack_name", "engine_name") .. _doxid-datapacks_1datapacks_json_setting_getting: Getting and setting data ++++++++++++++++++++++++ Inside transceiver functions the data can be accessed like a python dictionary: .. ref-code-block:: cpp # To set the data datapack = :ref:`JsonDataPack `("datapack_name", "engine_name") datapack.data["null"] = None datapack.data["long"] = 1 datapack.data["double"] = 43.21 datapack.data["string"] = "string" datapack.data["bool"] = True datapack.data["array"] = [5, 1, 6] datapack.data["tuple"] = (1, 2, 3) datapack.data["object"] = {"key1": "value", "key2": 600} # To retrieve the data print(datapack.data["string"]) print(datapack.data["object"]) # JsonDataPack supports dict's __getitem__ and keys methods. for k in datapack.data.keys(): print(datapack.data[k]) Numpy arrays: .. ref-code-block:: cpp # Numpy integer arrays datapack.data["numpy_array_int8" ] = np.array([ 1, 2, 3], dtype="int8") datapack.data["numpy_array_int16"] = np.array([ 4, 5, 6], dtype="int16") datapack.data["numpy_array_int32"] = np.array([ 7, 8, 9], dtype="int32") datapack.data["numpy_array_int64"] = np.array([10, 11, 12], dtype="int64") # Numpy unsigned integer arrays datapack.data["numpy_array_uint8" ] = np.array([0, 1, 2], dtype="uint8") datapack.data["numpy_array_uint16"] = np.array([3, 4, 5], dtype="uint16") datapack.data["numpy_array_uint32"] = np.array([6, 7, 8], dtype="uint32") datapack.data["numpy_array_uint64"] = np.array([9, 10, 11], dtype="uint64") # Numpy floating-point arrays datapack.data["numpy_array_float32"] = np.array([0.5, 2.3, 3.55], dtype="float32") datapack.data["numpy_array_float64"] = np.array([1.5, 2.3, 3.88], dtype="float64") .. _doxid-datapacks_1datapacks_json_inspecting: Inspecting content of JsonDataPack ++++++++++++++++++++++++++++++++++ Printing the content using Python's built-in function **str** : .. ref-code-block:: cpp :ref:`str `(datapack.data) :ref:`str `(datapack.data["array"]) :ref:`str `(datapack.data["object"]) # Or print it directly: print(datapack.data) Getting a list of keys: .. ref-code-block:: cpp keys = datapack.data.keys() Getting length of the object: .. ref-code-block:: cpp length = len(datapack.data) The above will return number of keys, if data is a JSON object, or number of elements, if it's a JSON array. .. _doxid-datapacks_1datapacks_json_arrays: Using JsonDataPacks to store JSON arrays ++++++++++++++++++++++++++++++++++++++++ In all the examples above it has been assumed that JsonDataPack is storing a JSON object. Actually the data object can contain either a JSON object, a JSON array or an empty object, it depends on how it is started. After instantiation, it contains an empty object: .. ref-code-block:: cpp datapack = :ref:`JsonDataPack `("example_datapack", "example_engine") # Returned value is 'null' datapack.data.json_type() If data is appended to it, the datapack stores a JSON array: .. ref-code-block:: cpp datapack.data.append(1.55) # Returned value is 'array' datapack.data.json_type() If instead a key is assigned, it stores a JSON object: .. ref-code-block:: cpp datapack = :ref:`JsonDataPack `("example_datapack", "example_engine") datapack.data['key'] = 1.55 # Returned value is 'object' datapack.data.json_type() Please be aware that methods specific to 'object' type for accessing or setting elements will raise an error when used with an 'array' type and otherwise. .. _doxid-datapacks_1datapacks_protobuf: Protobuf DataPacks ------------------ In contrast with JsonDataPack, which can wrap any nlohmann::json C++ object, a Python wrapper class is generated for each Protobuf definition. For example, for the *Camera* message listed below (which is used by the :ref:`Gazebo Engine `), a python class *GazeboCameraDataPack* is generated. .. ref-code-block:: cpp package Gazebo; // Data coming from gazebo camera datapack message Camera { uint32 imageWidth = 1; uint32 imageHeight = 2; uint32 imageDepth = 3; bytes imageData = 4; } This class contains a *data* attribute which is of type *GazeboCamera* and gives access to the wrapped datapack data. The generated Python classes match the original Protobuf Python API as described in the `protobuf documentation `__. There are some known limitations with respect to the original Protobuf Python API which are listed below with references to the protobuf documentation: #. `Well Known Types `__ partially supported - :ref:`Generic protobuf DataPacks and wrappers for well-known protobuf types `. #. Repeated Message field not supported `ref `__ #. Map field type not supported `ref `__ #. Only basic Enum support. To set or get *Enum* fields only *int* can be used. *Enum constants* can't be accessed from python `ref `__ #. The *Message* Python wrapper only supports a subset of the methods listed `here `__. These are: 'Clear', 'ClearField', 'HasField', 'IsInitialized' and 'WhichOneof'. Finally, these Python wrappers are automatically generated in the NRP-core build process for Protobuf message definitions used by Engines shipped with NRPCore. These can be found in the folder *nrp-core-msgs/protobuf/engine_proto_defs*. See this :ref:`guide ` to know how to compile additional messages so they become afterwards available to Engines and TFs. .. _doxid-datapacks_1datapacks_protobuf_generic: Generic protobuf DataPacks and wrappers for well-known protobuf types +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ A set of generic DataPacks is provided with NRP Core. These DataPacks allow to exchange most of the data types available in protobuf, and they can be used to pass arrays of arbitrary size. The data part of the generic DataPacks has the following structure: .. ref-code-block:: cpp message Array* { repeated * array = 1; repeated uint32 shape = 2; } With the asterisk replacing concrete types. Another generic :ref:`DataPack ` type that is provided with NRP Core is the image :ref:`DataPack `, based on the NrpGenericProto::Image type: .. ref-code-block:: cpp message Image { uint32 height = 1; uint32 width = 2; uint32 step = 3; IMAGE_TYPE type = 4; bool isBigEndian = 5; bytes data = 6; } The definitions of the protobuf messages used by the DataPacks can be found in the `messages repository `__, in ``protobuf/engine_proto_defs/nrp_generic.proto``. A subset of wrappers that mimic the (`well-known protobuf types `__) is also available to use. Unfortunately, at the moment it is not possible to use the official wrappers directly, and one must use the wrappers provided with NRP Core. The messages can be found in the `messages repository `__, in ``protobuf/engine_proto_defs/wrappers.proto``. An example use of the generic DataPacks, as well as the wrappers, can be found in the ``examples/generic_proto_test`` directory. .. _doxid-datapacks_1datapacks_rosmsg: ROS msg datapacks ----------------- Similarly to Protobuf datapacks, a Python wrapper class is generated for each ROS msg definition used in NRP-Core. For example, for a message of type ``Pose`` from package ``geometry_msgs``, a python class *PoseDataPack* is generated. This class contains a *data* attribute which is of type *Pose* and gives access to the wrapped datapack data. The generated Python bindings can be found under the Python module ``nrp_core.data.nrp_ros``. For example, in the case of the ``Pose`` message: .. ref-code-block:: cpp from nrp_core.data.nrp_ros.geometry_msgs import PoseDataPack p = PoseDataPack("name","engine") type(p.data) # output is By default Python bindings are generated for all message types in the next ROS packages: * nrp_ros_msgs (package containing NRP-core ROS message definitions) * std_msgs * geometry_msgs * sensor_msgs Any message definition contained in these packages can be used in TransceiverFunctions directly. It is also possible to generate Python bindings for messages defined in other ROS packages. For more information can be found :ref:`here `. **NOTE:** At this moment there is no Engine implementation which can use ROS datapacks. The only way of interacting with other ROS nodes in NRP-Core is by using a Computational Graph in your experiment instead of Transceiver Functions to rely data between Engines. See this :ref:`page ` for more information at this regard. .. _doxid-datapacks_1datapacks_implementation: Implementation details ~~~~~~~~~~~~~~~~~~~~~~ All concrete datapack classes should be based on the :ref:`DataPack ` class. It is a template class, and the single template argument specifies the data structure type, which is going to be held by the class instances. The :ref:`DataPack ` class design is somewhat similar to that of std::unique_ptr. Whenever a datapack object is constructed, it takes ownership of the input data structure. This structure may be then accessed and modified, or the ownership may be released. The :ref:`DataPack ` class inherits from :ref:`DataPackInterface `. This class may also be instantiated, but the object will not carry any data (ie. it's an empty :ref:`DataPack `). .. _doxid-datapacks_1datapacks_implementation_empty: Empty datapacks --------------- A :ref:`DataPack ` class is considered empty when its data is released. Every instance of the base class, :ref:`DataPackInterface `, is also considered empty, because there is no data stored in it. .. _doxid-datapacks_1datapacks_implementation_python: Python interface ---------------- In order to be accessible to transceiver functions, a conversion mechanism between C++ and Python must be specified for each :ref:`DataPack ` data type. Currently NRP-core provides Python bindings for nlohmann::json and protobuf messages. In case you wished to integrate a different data type, you would have to implement Python bindings for this type and make them available to NRP-core as a Python module.