The solution is implemented in the form of a generic, configurable controller. This generic controller is designed to solve the problem of finding optimal robot velocity commands that respect a series of constraints expressed as a pair of Jacobian matrix J and target velocity v. The Jacobian defines the mapping from the robot’s joint velocity space to the space in which the constraint is to be defined, and the target velocity is the desired velocity in said space.
Albeit highly configurable, this architecture means that the structure of an individual instance of the configurable controller is static once the controller has been loaded. The ability to switch between different behaviors is achieved by the capability of the ros_control framework infrastructure, in particular its implementation of a controller_manager component, to load, start, stop, and unload multiple controllers at runtime. The controller_manager loads and configures new controllers in separate threads from the ones used to run the update cycle of the running controllers. Implementation of ros_control robot drivers and generic control nodes leverages this structure to set up higher priority threads to update the controllers undisturbed from the potentially blocking controller initialization and configuration logic. Switching the active controllers for new ones is also designed to be executed in a non-blocking way, synchronized with the main update loop. The configurable controller proposed here adheres to this workflow by performing the potentially nondeterministic initialization of the computational graph off the critical real-time threads.
The next two subsections describe the designed format for specifying the controllers in the first place and then some relevant implementation details.
3.1. Specification Design
A generic controller that can solve optimization problems of the form presented in the previous section cannot be modeled with a rigid structure. In particular, each controller specification may present an arbitrary number of constraints. Each constraint, in turn, may require an arbitrary number of input signals of arbitrary types, some of which may require custom processing for filtering, derivation, etc. This inherently defines a graph-like structure, which is handled in different ways by the existing frameworks implementing similar characteristics to the present one.
In eTaSL, this structure is modeled as a graph of symbolic mathematical expressions. Explicitly modelling the expressions provides the ability to manipulate them to, for instance, automatically obtain derivatives. Controllers in iTaSC are built out of computational blocks exposing input/output ports to handle data flow. The components themselves and the interconnection of their ports embed the graph structure.
The present work uses the latter approach, using a computational graph structure to logically conceptualize the controller structure. Nodes in this graph represent individual computations, and edges represent the flow of information between them. The implementation uses a high-performance parallel execution model underneath, described with additional detail in
Section 3.2.1, which pairs explicit declarative definition of computational graphs, together with efficient parallel execution.
With reusability and encapsulation in mind, a hierarchical structure is used in this framework, where nodes in the graph can be atomic computations or composite subgraphs acting as nodes for outer levels of the hierarchy. For generality and unless otherwise specified, the term graph is used in the rest of this description to refer to either actual graphs of nodes or to individual nodes themselves. The controller itself is implemented as a composite graph, composed of many specialized graphs for, e.g., processing sensor data, computing the constraint Jacobians and target velocities, solving the optimization problem, etc. In order to pass data between the graphs inside a controller, each of them declares named ports, which can then be connected to one another. Instead of bundling a fixed set of graphs together with the controller, the framework also loads the individual graphs that make up a controller as plugins themselves to be dynamically loaded on demand.
Figure 1 represents a very simplified structure for a controller, where each block represents one of the aforementioned graphs in the controller structure.
Providing a collection of prebuilt, control-oriented computational blocks, together with the ability to load them and connect their ports, could suffice to describe arbitrary controller structures. In that case, a controller specification would consist of a list of graph components and another list with the connections between them. That would be a very flexible format, accommodating a very wide range of computational structures. This flexibility would, however, require users to build every controller from the ground up, defining details that a more sophisticated, albeit more restrictive, system could automate on their behalf. Additionally, in a very open-ended structure, the role of each individual component in the graph would be difficult to ascertain without deep knowledge of its implementation internals.
A specification format where individual graphs in the controller are declared with semantically specialized types is preferred by our framework. These types are parsed from the YAML text simply on the basis of conventions on each component’s input and output port types and names. For instance, blocks for constraints must declare output ports named jacobian and taskvel, with matrix and vector types. Likewise, blocks for solvers must declare a configurable number of input port pairs named jacobian_i and taskvel_i, one for each of the constraints in a given controller. These conventions inform about the role each graph plays in the controller structure and allow our framework to automatically interconnect them. The use of these specialized types results, in turn, in a more concise and expressive controller specification language.
The remainder of this section, therefore, enumerates the types of elements built into the controller specification format, providing an overview of the capabilities of the framework. First, some general concepts of the format chosen for the specification language are introduced. Then, the definition of the most basic building blocks for a controller, namely the constraints and the solver, is explained. It is followed by the description of a series of support functionalities that may be required depending on the scenario, e.g., defining custom I/O for the controller with optional filtering, defining controller-specific feature frames, or generating visual representations of the controller computational graph for debugging purposes.
3.1.1. Controller Specification Syntax
Following standard practices in the ROS pluginlib ecosystem, ros_control controller configurations for an application are declared by loading parameters to the ROS parameter server. Data is stored there using key/value mappings, where each value can consist of either a scalar, an ordered sequence, or a nested key/value mapping itself. When loading a new controller, the tooling in ros_control looks up a key matching the name of the controller and reads the necessary configuration from its associated mapping.
To support loading complex application configurations to the parameter server, the ROS ecosystem provides the tools to declare them using YAML syntax. This syntax allows one to define configuration data in a key/value format using a convenient human-readable form, as shown in Listing 1, and to directly upload that data to the ROS parameter server for applications to consume.
Listing 1. YAML syntax for a key/value data structure containing three keys, associated with a scalar value, a sequence, and a nested structure, respectively. The colon (:) symbol is used there to separate keys (displayed in black in listings) from values. Scalar values of any type are displayed in blue font except for sequences, which use indentation and a dash (-) character preceding each item (which are also shown in black). Indentation before a nested key defines its nesting level for key/value structures nested under outer keys. Comments can be added after a hash (#) character and are displayed in red text in the listings. A more exhaustive description of the YAML syntax can be found in [29]. |
![Robotics 14 00109 i001 Robotics 14 00109 i001]() |
In order to integrate seamlessly into the ROS and ros_control ecosystem, it is desirable to leverage this background knowledge that is expected from ROS developers. Therefore, the controller specification language for our proposed framework consists of a specific structure for these key/value maps in the ROS parameter server. Examples of controller specifications in the remainder of this document are presented using the same YAML syntax above, which a ROS-based application can use to configure the controllers at runtime.
The contents of the YAML specification of controllers in ros_control are typically uploaded to the parameter server by the tooling that is part of the roslaunch system, which is used to bring up ROS nodes and configure them as necessary. Syntax errors in the parsed YAML files are identified by roslaunch, and the startup of the complete system will be halted. When a syntactically correct YAML configuration contains a controller description that fails to comply with the required specification, this is detected by our controller framework at the time of configuring the controller. The ros_control infrastructure propagates this problem to the client requesting the controller to be loaded, which can then react appropriately.
3.1.2. Core Controller Definition: Constraints and Solver
Strictly speaking, there are only two required elements to define a controller in the system discussed here: a set of constraints and a solver. The constraints determine the conditions that must be fulfilled in some task-specific space, and the solver is used to find the robot joint-space command that best satisfies the given constraints.
As described in the introduction, this work currently focuses on modelling constraints at the velocity level. These take the form of a pair of elements composed of a constraint Jacobian together with a constraint-space velocity target. A constraint graph that follows the specification must, therefore, declare output ports for both of these elements. Likewise, a solver graph must declare as many pairs of Jacobian and desired-velocity input ports as constraints are defined for the given controller. An arbitrary number of constraints can be added to a controller definition, and their output is automatically connected by the framework to the inputs of the solver. An example of a minimal controller is shown in
Figure 2.
Here, a set of N constraints is each receiving state information from the robot and generating the Jacobian and target velocity data. The solver on their right receives the output of all the constraints and computes an optimal (according to its configuration) joint-velocity command, which is sent back to the robot. The configuration in Listing 2 below declares a controller matching this structure with only two constraints, named dual_arm_twist_controller.
Listing 2. dual_arm_twist_controller definition. A controller with multiple constraints. Definition of the details of each constraint is left out for clarity. |
![Robotics 14 00109 i002 Robotics 14 00109 i002]() |
The first line in Listing 2 starts a key/value map with the name of the controller, here dual_arm_twist_controller. This name is used to refer to this specific controller configuration when working with the ros_control interfaces to load, start, and stop it. The type entry, required by the ros_control, declares it to be of the configurable constraint-based controller type presented here and allows the driver to load the controller from its dynamic library. Two variants of the controller exist, each supporting different joint-command interface handle types that can be exposed by the robot hardware abstraction. The current example uses ConfigurableConstraintVelocityController, which expects joint interfaces to accept velocity commands. A corresponding version to be used for position-resolved joint interfaces can be used by defining the type to be ConfigurableConstraintPositionController. If the latter is used, the optimal joint velocities obtained from the solver are automatically integrated into a position command.
The rest of the contents in the configuration are part of the selected specification format for the configurable constraint-based controller defined in this document. In this minimal example, this consists of the two other top-level keys inside the controller configuration map. The constraint_names key lists the names of the constraints that this controller must apply—here left_arm_twist_constraint and right_arm_twist_constraint. Likewise, damped_pseudoinverse_solver is the name of the solver to use, declared with the solver_name key.
The components being assigned to specialized roles in the controller structure mean that, in this example, all data flowing between the constraints and the solver is implicitly determined. At controller initialization, every constraint’s Jacobian and target velocity output ports are automatically connected to the solver’s matching input ports, without the user needing to specify these connections manually. Also, since the framework proposed here is mainly designed to generate controllers for articulated robot structures, logic to automatically handle robot state and command information is also in place. The robot state is provided to every component that requires it by connections created automatically at controller initialization. The optimal joint velocity command found by the solver is also connected automatically to the joint command interfaces (after integrating if the underlying joints are position-driven). The state/command data flowing from/to the robot is illustrated in
Figure 2 and others by the gray blocks containing a robot icon at the left and right of the graph. Note that these icons are duplicated to improve graphical layout and figure compactness, but both represent the same robot being controlled.
Combining the output of multiple constraints into a single optimization problem for the solver requires a common structure for the joint space representation. This structure is automatically obtained by the controller from a common URDF model and used to configure two components on the graph that output the state of the robot joints and that receive the commands in every update cycle of the controller.
Details of how the constraint and solver graphs are configured are omitted in Listing 2 for clarity. The constraints and solver, referred to by name in the controller specification, are defined in a similar way to the controller itself. Listing 3 shows how a complete configuration may look.
Listing 3. dual_arm_twist_controller definition including constraint and solver details. |
![Robotics 14 00109 i003 Robotics 14 00109 i003]() |
Note that the constraints themselves are defined here in separate mappings at the same level as the controller itself, not nested inside it. This has been designed this way to allow individual graph definitions—constraints, solvers, or other types of graphs—to be reused by multiple controllers if needed. Both the constraint and solver graph descriptions contain the graph_type entry, telling the controller which graphs to load into the controller at initialization. Similar to the controller itself, in addition to the type of graph, these graph-specific mappings should contain any additional parameters required by the specific graph being loaded.
The structure of the graph configuration is similar to that of the controller because the graphs themselves are also implemented as plugins in the framework. Defining the subgraphs used to compose the controller as plugins means that users of the framework can implement and use their own graphs in the controller. However, a collection of reusable graph plugins is also implemented and provided alongside the configurable controller itself, which includes a set of common constraints and solvers.
The provided constraints are in two families: joint-space and Cartesian-space constraints. The joint-space ones can be used to define the behavior of a subset of joints in the robot, where the Jacobian is simply a selection matrix composed of suitably located ones and zeros. The provided Cartesian-space constraints can be used to control the relative velocity of any link in the robot scene with respect to any other. Both joint- and Cartesian-space constraints provided with this system include a common set of strategies to compute the target velocity in their respective constraint space:
Hold initial position/relative pose.
Move at a desired velocity (pure velocity feedforward).
Track desired position + velocity (using proportional + derivative control with the addition of a velocity feedforward component).
Since it was one of the principal use cases for which this framework was developed, Cartesian-space constraints also offer compliant versions of all three control strategies above. When using these, sensed forces are used to adjust the motion of the robot according to an admittance control law, resulting in an active compliant behavior even for rigid manipulators.
The set of solvers provided off-the-shelf is all based on the pseudoinverse-based solutions discussed before and includes the following:
3.1.3. Inputs and Outputs
The example in the previous section misses an important part of the controller, the definition of the input data sources. In its current design, certain common connections between the graphs in the controller are automatically established by the framework itself. Robot state data and timing information are fed into the constraint graphs’ input ports automatically, the constraints’ output ports are automatically connected to the solver’s input ports, and the optimal command is automatically sent to the robot by reading from the solver graph’s appropriate output port.
Often, closed-loop controllers require additional sensor information to compute the robot commands. For instance, wrench measurements from a force-torque sensor may be required as an input by a compliant constraint. Likewise, a controller may need to output more than just a joint position/velocity command to the robot. In a teleoperation setup, a force feedback signal may be computed and reflected to the user through a haptic interface.
To achieve this, this framework proposes a way to forward data in both directions: from external sources into the graph inputs, using configurable inputs, and from graph outputs to external destinations, using configurable outputs. The configurable inputs read external data from one of the available sources and forward it to one of the graphs’ input ports, whereas the configurable outputs forward data from one of the graphs’ output ports to the desired external destination.
These inputs and outputs can be configured while designing the controller and can read from and write to different communication channels using a variety of data types. The types of communication channels that can be used by the inputs and outputs are best suited for different use cases. When dealing with data streams, there are two channels that can be used as either a source or a destination for inputs or outputs, respectively:
ros_control hardware interface handles and ROS topics. Exclusively for inputs and scenarios where data does not change very often, the controller can also provide setter ROS service servers or even set the input value from constant values embedded in the controller configuration itself.
Table 1 and
Table 2 summarize the currently available formats available as part of the configurable input and output systems, respectively.
Note that the types of inputs and outputs that read from or write to ros_control handles are more limited than the other types. This reflects the limited types of hardware interfaces used by the hardware abstractions available in the community. Since joint state and command interfaces are automatically handled by the framework without using this configurable input/output system, this only leaves some very specific types of Cartesian interfaces provided by some industrial robot drivers, haptic devices, and force-torque sensors, which are all covered by the types in these tables.
An important consideration in the handling of input signals is that the data from the inputs to the controller is sampled at the start of the controller update logic and held constant for the duration of the controller update. In the case of constant values, that is a trivial statement. In the case of inputs that consume data from a ros_control hardware interface handle, this requirement is ensured by the implementation of the overall logic in a read/update/write structure, where data from the hardware handles is refreshed, then the controllers are updated before the commands are written back to the hardware. In the case of the inputs consuming information from the outer ROS system, the implementation of the input system makes use of separate threads to process the input data and only synchronizes with the rest of the control graph through mechanisms provided in the ros_control-provided realtime_tools library, which ensures lock-free read access to the underlying shared memory. This essentially means that the update logic of an individual controller will always run with the most recent data provided by its inputs, irrespective of how frequently that data is produced.
An illustrative example of using the configurable input/output system could be that of a teleoperation controller commanding the movement of a robot’s end effector to follow an external velocity (twist) command from a joystick. This example assumes the joystick exposes its Cartesian twist command using a ros_control-compatible handle. Additionally, it is also assumed that the Cartesian-space tracking constraint provides the tracking error in an output port and that the error signal must be forwarded to a ROS topic for monitoring purposes.
The resulting controller is illustrated in the schematic shown in
Figure 3. In addition to the automatic connections to the robot joint interfaces discussed before, this figure shows the input command from the joystick and the output of the error to a ROS topic.
Listing 4 shows how the configuration for such a controller would look. Here, the first lines declare the type of the controller and the constraint and solver names (their details are omitted here, but assume they are defined elsewhere). In particular, assume left_arm_twist_constraint is a type of constraint with the input/output ports described above.
Listing 4. twist_tracking_controller definition, using the configurable I/O to read an external command and broadcast the tracking error. |
![Robotics 14 00109 i004a Robotics 14 00109 i004a]() |
![Robotics 14 00109 i004b Robotics 14 00109 i004b]() |
Following the constraint and solver names,
input_names and
output_names each declare a list of configurable inputs and outputs forwarded by this controller. These lists only contain the names of the input and output port forwarding rules. Their detailed definitions, also in the controller configuration, must define the ports to forward into or to read from and the type of forwarding rule (from
Table 1 or
Table 2). Specific input or output types required additional type-specific parameters.
The definition of
left_arm_target_twist declares its
input_type to be
twist_from_ handle, which means twist data is read from a
ros_control hardware handle (other available types are shown in
Table 1). Therefore, in addition to the port into which this data is forwarded, it must also specify which
ros_control handle name to read the data from. This is provided by specifying the handle name using the
input_handle key. Finally, the
port_names property determines which graph input this data is forwarded into. Using custom notation, the dot (
.) character is used to refer to nested elements: here, an input port named
target_twist in the
left_arm_twist_constraint graph. Note that, unlike output port forwardings, a given input signal may be forwarded to more than one port in the control graph, hence the plural in
port_names.
The details of the actual_taskvel output forwarding define output_type to output vector data (since this is not technically a twist but a stacking of both left and right twist vectors) and is of the ...to_topic family. Similarly to the input_handle above, forwarding an output port into a ROS topic requires output_topic to determine the name of the topic. Here, the port_name value determines from which graph port in the controller the data must be read and sent to the configured topic: an actual_taskvel_projection port exposed by the solver component.
The implementation of the input and output port forwarding layer is decoupled from the rest of the control graph generation by implementing each configurable input/output forwarding as a specialized graph element. Custom logic is used to construct them independently from the rest of the graph and to insert them in the controller graph, where they are updated synchronously with the controller execution. This input and output forwarding capability of the controller infrastructure is particularly relevant for future reusability because it belongs to the boundary between the internal core graph structure computing the control law and other parts of the ROS ecosystem. By design, the implementation of the internal control law has been made ROS-agnostic and should be easy to port to other environments.
3.1.4. I/O Filters
One common scenario when dealing with sensor data is the need to apply different varieties of filters. For instance, a signal coming from a wrist-mounted force-torque sensor may need to go through a series of filters before being used in an admittance control constraint: an offset to compensate for temperature-based drift, followed by a compensation for the load of the robot gripper.
The input and output system introduced in the previous section also provides a means to put the data through filter chains. Filters can be arbitrary graph components and are also loaded from the same general plugin type as constraints and solver graphs. They must provide at least one input and one output port with the right names to allow the system to automatically insert them between the desired input or output and the desired port(s) in the control graph. The optional filter_names entry in the input and output definitions can contain an ordered list of the filters to be applied to the input or output signal. These filters are connected in series between the input or output graph ports and the external sources from which the forwarded data is read or written.
Figure 4 shows an example of a controller where data from a force-torque sensor is forwarded into an input port in a compliant leadthrough constraint. In this example, the data goes through two filters before being input into the constraint: first, a fixed offset is applied, and then the gravitational forces from a given load are compensated.
The example in Listing 5 shows how a controller with the structure of
Figure 4 would be configured. It shows a controller named
leadtrough_controller declaring a solver and a single constraint, the details of which are omitted. It then declares one input named
sensor_wrench, which is ultimately connected to a port named
wrench in the
leadthrough_constraint graph. In addition to the entries already shown for the configurable input in the last section, this time
filter_names is used to list the filter graphs through which the data is passed before being fed into the constraint graph. The two filter graphs, whose configurations are defined below, are automatically loaded into the controller and connected in the specified order between the force-torque signal source and the constraint’s input port, resulting in a topology like the one in
Figure 4.
Listing 5. leadtrough_controller definition, which filters the signal coming from a force-torque sensor through two filters in sequence. Constraint and solver graph definitions are omitted. |
![Robotics 14 00109 i005a Robotics 14 00109 i005a]() |
![Robotics 14 00109 i005b Robotics 14 00109 i005b]() |
3.1.5. Constraint Transformers
In order to avoid unnecessary multiplicity of constraint implementations, the current system supports a feature named constraint transformers. These are graphs to be inserted in the control graph structure, which act in a filter-like way on the outputs from the desired constraints. That is, when a transformer graph is defined for a given constraint, the Jacobian and target velocity outputs from the constraint are passed through this graph before being fed to the solver.
Possible use cases for transformer graphs are many. One of the offered transformers can be used to apply coordinate transformations to the Jacobian and/or target velocities before inputting to the solver. This allows one to effectively change the reference systems to which they refer and alters the behavior of the constraint—for instance, to track a twist command as if expressed in some static frame or as if expressed in local coordinates of a moving frame. Another use case is to implement constraint-space velocity limits to the signals before entering the solver.
One particular use case that provides great flexibility when using this type of Jacobian-based constraint formulation is the use of transformers to select specific subsets of directions in which the constraint must take effect. Note that the Jacobian matrix defining a constraint in this framework represents a list of linear equations mapping a given space, namely the joint velocity space, to some other constraint-specific space. By selecting specific rows of the matrix while discarding the rest, a constraint will only apply in certain directions of its constraint space.
For instance, when describing the relative Cartesian space velocity (twist) between two bodies, the six rows of the Jacobian matrix can be divided into two blocks representing the three components of the relative translational velocity and the three components of the relative angular velocity. By selecting the top three rows of this Jacobian, only the relative translation between the two bodies is constrained, leaving the rotation free; likewise, by selecting the bottom three rows, only the relative orientation is constrained. Other types of selections are also possible by picking different sets of rows. For instance, by selecting only one of the translational components and controlling its value to remain constant, a point in the second body is constrained in a given plane with respect to the first.
This method provides great flexibility in achieving different behaviors. Very importantly, it also allows defining controllers that constrain only the required degrees of freedom required by a given task. By doing so, tasks that would be otherwise unfeasible because of unnecessary overconstraining can become feasible. Previously feasible tasks can be augmented by secondary task objectives, which would not be possible otherwise.
Figure 5 shows the location on the control graph where this component is inserted. In this figure, a transformer labelled
position selector is inserted between the
twist constraint and the
damped solver graphs. As annotated under the arrows representing data connections, the constraint, which acts in Cartesian space, outputs a six-dimensional Jacobian and target velocity. After the selection is applied, constraints for only three of the degrees of freedom are input into the solver.
Listing 6 shows how a constraint transformer graph is included in the controller description. As in previous examples, controller configuration starts by defining the solver and a single constraint in the usual way. The constraint_transformers feature then contains a nested mapping, where entries can be added listing transformers to be applied to each constraint. An entry for a given constraint can contain a list of transformer graph names to be applied sequentially.
In this example, a single transformer graph named position_selector is defined for the twist_constraint graph. The transformer is then defined like any other graph outside of the body of the controller. In this case, the selector is configured with a selection_mask indicating which rows in the constraint must be selected for the solver. The result of selecting the first three components is that only the translational velocity is actually constrained, with the rotational velocity being left unspecified.
Listing 6. trans_vel_controller definition, applying a transformer (position_selector) to a constraint (twist_constraint). Constraint and solver graph definitions are omitted. |
![Robotics 14 00109 i006 Robotics 14 00109 i006]() |
3.1.6. Scene Model and Additional Frames
When discussing the constraint and solver formulation, the need to share a common representation of the state and command space has been mentioned. This shared representation necessarily implies a shared scene kinematic model, which relates the configuration state and command space with the motion of the different links in the robot. Since one of the objectives of this work is to provide a
ros_control-native implementation of the controller specification described here, it is assumed that a global model of the robot and the static scene around it is available in the form of a URDF description. This global model is made available to every element in the control graph through an interface, which is later described in
Section 3.2.3.
However, sometimes task-specific frames can be required, which may not belong in the URDF model, since this model is typically static and contains only the robot itself and parts of the scene that do not change. For example, a perception system can be used to detect objects that are not known beforehand and therefore are not part of the scene represented in the URDF model. Also, after the robot picks up an object, additional feature frames of said object may be attached to the robot flange to be referred to by the controllers. In both examples, these task-specific frames are not part of the robot’s kinematic model itself but are nevertheless relevant for some specific controller. A system to define controller-specific feature frames rigidly attached to arbitrary links in the URDF scene model is thus made available in this controller specification format. Listing 7 shows how this can be achieved by using the additional_frames entry, which can contain a list of feature frames listing which link they are attached to and the relative transform from them.
Listing 7. feature_frame_controller definition, declaring a frame named grasped_object attached to robot_end_effector. Note that feature_twist_constraint can then directly refer to this additional frame. Solver graph definition is omitted. |
![Robotics 14 00109 i007 Robotics 14 00109 i007]() |
Note that the task-specific frames defined using this functionality are statically defined, in a topological sense, for the lifetime of a given controller. That means that in the tree-like representation of the kinematic scene, the parent frame of these task-specific frames cannot be modified, only their relative transformation. A different scene topology requires a new controller to be defined, where each additional frame can be declared from scratch to be a child of a different parent frame.
3.1.7. Custom Graphs and Connections
In addition to every semantically meaningful graph type described above, there are cases where computational elements may not clearly fit into any of these categories. Also, graphs that belong to any of the categories above may require additional connections to the ones automatically performed by the described logic. In order to accommodate more general types of graph topologies being included in a controller, support for
custom_graphs and
custom_connections elements is also offered. The
custom_graphs element may contain a list of graph names to be added to the controller and for which no automatic connections are made. Pairs of ports labelled
source and
destination can be added to a list under
custom_connections to connect these ports. By using this functionality, arbitrary graph plugins can be inserted into the controller, and connections between any pair of ports with matching types can be made. In the example illustrated by
Figure 6, a custom trapezoidal velocity profile generator graph is displayed on the top left of the diagram. A connection is shown between it and the single constraint, representing the output of the profile being used as a target position for the robot’s end effector.
In order to add arbitrary graphs and establish connections between their ports, a configuration like the one shown in Listing 8 can be used, which would generate a controller like the one shown in
Figure 6. There, after the declaration of the constraint and solver graphs to use, the
custom_graphs entry is used to instantiate the profile generator graph. Similar to other cases, the details of which type of graph to load and the specific configuration it requires are described outside the scope of the controller configuration itself. In addition to instantiating the profile generator, the
custom_connections key is used to list connections between arbitrary pairs of ports in the graph. This specific example lists one connection from the output of the motion profile to the input of the constraint as a target.
Listing 8. profile_tracking_controller definition, tracking a pose output by a custom trapezoidal motion profile generator graph. Constraint and solver graph definitions are omitted. |
![Robotics 14 00109 i008 Robotics 14 00109 i008]() |
3.1.8. Output to DOT
Even if this tool is designed to facilitate designing controllers by providing some reusable structure, until some higher-level tooling to guide the user to do this is available, getting every detail right in the first attempt can be difficult. One functionality that can help troubleshoot or facilitate incremental building of a controller is the ability to output a DOT file describing the structure of the graph by defining a filename using the dot_output_file key. When a filename is specified using this key, the controller initialization logic automatically generates this file with the structure of the controller.
The DOT files are plain text files containing a description of the graph topology, together with optional markup for the desired visual representation of some of the elements. A diagram representation like the one in
Figure 7 can be generated in multiple supported image formats by processing the DOT file using the tools in the Graphviz [
30] toolset. These graphs display each of the atomic computational elements as rectangular blocks. Ellipses represent the ports used to define the flow of information in the graph, with solid ellipses displaying output ports and dashed ellipses displaying input ports. Subgraphs in the hierarchy are represented by rectangular shapes that contain other elements inside. Additional details about how this computational graph is implemented and executed are discussed in
Section 3.2.1 below.
For a more comprehensive example of the types of graphs that can be generated in a moderately complex controller used in practical scenarios, refer to the example discussed in
Appendix A.
3.2. Implementation
Looking at some of the details of the controller framework implementing the specification above can help understand the design choices made and the resulting behavior of the generated controllers. At its core, the controller framework requires a way to dynamically build and update arbitrarily complex computational graphs. One option to achieve this is using code generation techniques to produce a controller performing the required computations from a given specification. This approach provides maximum flexibility in structuring the resulting controller but makes it very difficult to use it dynamically at runtime. Our controller relies on dataflow, a C++ library, to dynamically compose directed acyclic graphs of interconnected computational blocks, providing typed inputs and outputs to connect them together.
Reusability of the computational blocks used in these graphs may be maximized by encapsulating computations at the smallest possible granularity level. However, exposing too granular blocks deprives them of clear high-level semantics and requires too much low-level composition work by the user. When designing this framework, providing a collection of prebuilt higher-level composable graphs has been preferred. Each of these graphs has a clear intended use in a controller structure: compute a constraint, filter a signal, etc. Furthermore, these built-in graphs are packaged and loaded dynamically as plugins, and mechanisms for the end users to provide their own graph plugins are also provided.
In addition to the runtime data transmitted amongst the blocks in the graph using their input/output data ports, each individual block in the graph may require static configuration to be read on initialization. As discussed, in the ROS ecosystem, this is typically achieved by reading the configuration from the ROS parameter server. Our infrastructure generalizes configuration loading through a ROS-independent abstract interface. By reading their configuration from this abstract interface, the composable graphs provided to build the controllers can also be used in different contexts.
Each constraint configured for a given controller ultimately outputs a Jacobian matrix relating robot joint velocities to some task space velocity. In order to be able to combine them into a single solver, it is important that every constraint shares a common representation of the robot joint-space vector. This is achieved by defining a common robot model object at the controller level to which every block in the computational graph has access. The current state of the robot at each time step is also provided to each block as an input, in an ordering coherent with that used by this model.
Each of these fundamental building blocks is described in more detail in the following sections.
3.2.1. Dataflow
The backbone of the proposed controller framework is a system, which we refer to as Dataflow, to represent computations as a directed acyclic graph of atomic operations. Nodes in these graphs contain arbitrary computations, and edges model precedence requirements, used when computations in a given node must be finished before starting the execution of another. The Taskflow header-only C++ library [
31,
32] provides infrastructure to model computations in this way. By using this library, computational graphs can be declaratively defined and handed over to an executor, which can schedule them on multiple threads using a work-stealing algorithm. The importance of the computational dependency graph being acyclic comes from the automated parallel execution scheduling used by Taskflow. In order to determine the correct order of execution of the atomic computation blocks, the graph must include no cycles.
In a general sense, there can be arbitrary factors determining computational precedence requirements. In particular, any computation that involves external side effects can dictate an order of operations. Therefore, the Taskflow library provides a general way to declare precedence relations between computations. However, the computational graphs produced by the controller framework proposed here have a specific objective: read sensor input, compute the optimal control commands, and apply these commands to the robot actuators. Since the only operation with side effects is the application of the control commands to the robot actuators, data flow dependencies between atomic computations are the only remaining source of precedence requirements. This observation guides the design of the Dataflow software layer built on top of Taskflow, which emphasizes the flow of data by explicitly modelling it via ports.
A very simple scenario, like the one shown in
Figure 1, shows a computational graph consisting of atomic computations: read from sensors, determine control targets based on it, and compute the optimal command. Data-based dependencies between the computations are represented by the arrows in the figure, where each block needs the result of the ones preceding it. This graph topology could be implemented, using Taskflow, by the pseudocode in Listing 9. The first few lines forward-declare functions performing the actual computation, highlighting the data they need as input and produce as output. Inside the
run_control method, variables to hold intermediate results are created first; then the individual computational elements, modeled as C++ lambda expressions, are added to the graph; and finally, the precedence relationships between them are explicitly declared.
Listing 9. Declaring a pipeline of tasks requires using global data and declaring precedences matching flow of this data between tasks. |
![Robotics 14 00109 i009a Robotics 14 00109 i009a]() |
![Robotics 14 00109 i009b Robotics 14 00109 i009b]() |
The
tf::Taskflow object holds the topology of the graph, composed of the computations to be executed and the required precedence relations between them. Taskflow then provides an implementation of a
tf::Executor object, which, given the graph constructed above, can update all the nodes exactly once in the desired order. This matches the expectation of the
ros_control infrastructure that every controller updates synchronously at the configured rate. Handling of long-running computations that are not suitable for deterministic execution in the duration of a control loop should be handled separately from the controller, and the results injected into the controller using the input system described in
Section 3.1.3.
Note that how the data, which must flow from one computation to the next (the sensor data and the command), is not handled by Taskflow in any way. The user of this library is responsible for defining and handling variables to hold the intermediate results of the computations. By manually using these variables to provide input to the computations and store their results, the data flow is implicitly defined. However, the precedence between the declared computations must still be explicitly declared to Taskflow, independently of the implicit data flow. This duplication requires additional work from the developer and is a source of difficult-to-detect runtime race condition errors if the explicitly declared dependencies do not match the actual implicit ordering.
To mitigate both these problems, dataflow explicitly models the flow of information between the components using input/output ports. Atomic computations in dataflow must implement the specific C++ dataflow::Task interface and explicitly declare named and typed input or output ports. The user can then assemble the computational graph by defining the tasks and the connections between their ports. In this way, the intermediate data no longer needs to be handled by the user and is held by each task instead, specifically by the output ports it defines. Furthermore, when declaring a connection between a pair of input and output ports, a precedence relationship in the internal Taskflow graph representation is automatically defined, removing the previously mentioned duplication. The connection mechanism also ensures the type correctness when connecting the ports in the graph and is capable of detecting unconnected input ports, which would result in updating the required computations with uninitialized data. The controller performs both these checks when loading a task specification into the dataflow structure and fails gracefully, making it impossible to try to activate a controller with these types of errors.
The same example used above could look like the pseudocode in Listing 10 when using dataflow. The first lines forward declare the specializations of dataflow::Task, where the ports are explicitly declared and whose associated computations are performed in a Task::update() method. Then, a run_control method equivalent to the one in Listing 9 can be found. After creating an instance of a graph and adding instances of the necessary computational blocks, the last few lines declare the connections between the ports in the tasks just once.
Listing 10. Declarative connections of task ports define precedences implicitly in dataflow. |
![Robotics 14 00109 i010a Robotics 14 00109 i010a]() |
![Robotics 14 00109 i010b Robotics 14 00109 i010b]() |
When the objective is to build general computational graphs where the precedence between tasks is uniquely determined by the data that flows between them, dataflow builds upon the foundations provided by Taskflow to allow a more expressive and less error-prone way for users to define graphs. Taskflow, on its own, can guarantee the consistency of the data used in the control graph as long as the precedence relations between the computational blocks are correctly declared by the user. The explicit modelling of the data flowing between the computational blocks is automatically transformed into execution precedence relationships declarations. Therefore, a graph built using dataflow guarantees that the data is free from parallelism-related race conditions.
3.2.2. Graph Plugins
In order to be able to dynamically build controllers using the specification format described above, the controller framework must be able to inject multiple different types of preconstructed graphs into the structure. The option to pre-determine the set of graph types that are available to use creates too tight a coupling between the infrastructure to build the controllers and the actual computational elements inside it. Also, it would make it impossible for end users to extend the controller design framework without recompiling the complete stack of controllers and constraints. The alternative adopted here is to implement the graph building blocks, which the controller design framework uses to build specific controllers as plugins. This is also an approach that fits naturally into the ros_control framework, which itself uses a plugin system to load controllers into the robot control node dynamically.
The plugin system, as defined by the class_loader and pluginlib utilities in the ROS ecosystem, allows categories of plugins to be defined by a base class interface. This base class must have an empty constructor to allow factories to be able to construct objects in a generic way and is typically accompanied by plugin category-specific initialization methods. To adhere to this format, a base class that acts as a factory for the individual type of graph included in each plugin is defined as shown in Listing 11.
Listing 11. Graph factory base class, taking the generic scene model and configuration interface components designed for this purpose. The public-facing make_graph method internally calls private, and virtual make_graph_impl, which individual graph implementations must override to construct the specific graph they provide. |
![Robotics 14 00109 i011 Robotics 14 00109 i011]() |
The built-in graphs, which are included by default in the core set of graphs, are implemented in such a way as to maximize their reusability:
A header with a function constructing the desired graph structure is first implemented. This header can be used to directly build the provided graphs into a C++ executable if the dynamic plugin and controller system described here is not necessary. This function takes the scene model but is independent of the generic configuration interface to reduce dependencies.
A graph factory class deriving from the base class in Listing 11 takes care of unpacking the required configuration parameters from the generic interface and calls the pure C++ and returns its graph. This class is registered as a dynamically loadable plugin using the ROS-agnostic macros provided with the class_loader tools. These classes can still be used as plugins in a ROS-independent environment.
A ROS package wrapper is configured to export the plugin to the ROS ecosystem using the conventions required by the pluginlib ecosystem. This facilitates the discovery of the plugin at runtime by the controller infrastructure by inspecting the configured ROS environment paths.
Since the ros_control ecosystem is inherently ROS dependent, the controller design framework discussed in this work directly or indirectly uses the three levels of structure mentioned above. However, it was deemed interesting to provide this separation to allow other uses in ROS-independent setups and particularly for a future migration to ROS 2 and ros2_control.
3.2.3. Scene Model
In the context of this framework, constraints for the robot motion are expressed by defining the relation between desired constraint-space velocities and the joint velocities through a Jacobian matrix. These are used to define bounds or desired values in said constraint space, and a solver algorithm is used to find the joint velocities that result in the desired constraint space velocities. The solver must, therefore, consume the constraints in a way where the variables to solve—joint-space velocities, in our case—are coherent between all the constraints defined. Also, the computation of the desired constraint space velocities (or their bounds) often requires knowledge of the current state of the robot. Our control framework supports this by offering every building block involved a common geometric model of the robot and its environment, as well as a snapshot of the current state of the robot. This scene model can be queried for relative Jacobians between arbitrary pairs of frames, keeping a constant representation of the robot joint space. It can also be used to compute relative poses between frames in the model, which is often required in error computations. The joint state of the robot provided by the framework to each individual computational module is always kept coherent with what the scene model expects.
The model can be initialized with multiple instances of robot models described using URDF, which is the standard format used in the ROS ecosystem. In addition to these, additional fixed frames can be defined, rigidly connected to any existing frame in the model. Furthermore, the transforms defining these additional frames can be updated dynamically in order to support injecting information from externally tracked objects and declaring constraints with respect to them and the robot links. The logic in the implementation of the scene model builds a tree-like representation of the scene by incrementally composing together the provided robot URDF models—which, by definition, must themselves adhere to a kinematic tree structure—and individual frames. The incremental construction makes sure that no duplicate frames exist in the tree, which would, in turn, lead to indetermination in the addressing of such frames from the computational elements in the controller graph.
Arbitrary modifications of the structure of the scene model can lead to nondeterministic recomputation of the internal data structures used to keep track of the scene graph. To ensure deterministic execution of the controller discussed here, the usage of the flexibility of the scene model object is explicitly limited to avoid the topology of the underlying scene graph from being modified after the controller has been initially loaded, as already discussed in
Section 3.1.6.
3.2.4. Configuration Interface
Even if first-class integration with ROS and ros_control has been one of the objectives of this work, an effort has been made to keep as much of the development using ROS-independent C++ only. This should facilitate reusing most of the functionality in this development in other environments, including an eventual port to ros2_control and ROS 2.
In addition to the runtime data flow via topics or service calls, one of the key elements of the infrastructure provided by ROS is the tooling for configuring the components of a system when initializing it. Here, a configuration is typically loaded to the ROS parameter server from either YAML files or values included directly in the launch files used to define the elements composing a runtime system. An abstract configuration_interface has been implemented for individual elements of this system to consume generic configuration values as a hierarchy of key/value pairs. To provide effortless use in a ROS scenario, a specific implementation that implements that interface while reading from the ROS parameter server is offered. Likewise, for ROS-independent setups, an implementation of said interface that internally reads directly from YAML files is also provided.