Compiled Controllers#

The opensourceleg.control module provides functionality for using controllers written in languages other than python via the CompiledController class. This class wraps a function from a compiled dynamic library and allows it to be called from your main python code. Using a compiled controller written in another language can be beneficial, especially when speed is a concern.

Make an Example library#

In this tutorial, we will write an example linear algebra library in c++ that provides a function to compute the dot product between two 3D vectors. This very simple function will allow us to show how to define input and output structures and call the controller. More complex examples using actual controllers in both c++ and MATLAB implementations are provided in the examples folder.

The CompiledController class assumes that you have a compiled dynamic library (with extension *.so on linux) with the function prototype myFunction(Inputs* inputs, Outputs* outputs), where Inputs and Outputs are structures holding all of the input and output data. Thus, we first need to write and compile our library.

 1/** dot_product_3d.cpp
 2An example cpp file to demonstrate the compiled controller functionality
 3in the opensourceleg library. 
 4Kevin Best
 5University of Michigan
 6October 2023
 7**/
 8
 9struct Vector3D{
10    double x, y, z;
11};
12
13struct Inputs{
14    Vector3D vector1, vector2;
15};
16
17struct Outputs{
18    double result;
19};
20
21extern "C" void dot_product_3d(Inputs* inputs, Outputs* Outputs) {
22  Outputs->result = inputs->vector1.x * inputs->vector2.x
23                    + inputs->vector1.y * inputs->vector2.y
24                    + inputs->vector1.z * inputs->vector2.z;
25}

Note

The extern "C" linkage-specification is important to prevent the C++ compiler from name mangling. Under the hood, our library uses a C style calling convention, which expects to be able to find the library functions with their standard names.

First, navigate to the directory opensourceleg/tutorials/compiled_control/. Then run make to build the library. If succesful, a new library named lin_alg.so should be created.

Load the Example Library#

Next, we need to write a python script to call our newly compiled library. First, we import the library. We also import os to get the path of the current directory.

import os

from opensourceleg.control.compiled_controller import CompiledController

Then we’ll make an instance of the CompiledController wrapper and have it load our linear algebra library. We need to pass it both the name of the library (without an extension) and the directory in which to find the library. We also give it the name of our main function as well as any initialization and cleanup functions.

my_linalg = CompiledController(
    library_name="lin_alg.so",
    library_path=os.path.dirname(__file__),
    main_function_name="dot_product_3d",
    initialization_function_name=None,
    cleanup_function_name=None,
)

Note

If your library provides initialization and cleanup functions, they will be called upon loading and cleanup, respectively. If your library does not need these functions, pass the default argument of None.

Define Custom Datatypes#

Our library uses a Vector3D structure, which we need to define so that the python code can pass the data to the library in the right format. Every structure is built using basic types from my_linalg.types, such as c_double, c_bool, c_int16, etc. We therefore can add Vector3D to the list of known types using the define_type() method, which takes two arguments: (1) a name of the new type definition, and (2) a list of tuples where the first entry is the name of the field and the second entry is the type. For example, the code to define Vector3D is

my_linalg.define_type(
"Vector3D",
    [
        ("x", my_linalg.types.c_double),
        ("y", my_linalg.types.c_double),
        ("z", my_linalg.types.c_double),
    ],
)

Now the wrapper knows how Vector3D is defined, we can use it in other type definitions, the same way as any other basic type. After all necessary types are defined, we need to define the input and output structures using the define_inputs() and define_outputs() methods. These methods are similar to define_type(), but are special because they tell the wrapper which objects to pass to and from the compiled library. We define the inputs as two Vector3D objects and the output as one double titled result.

my_linalg.define_inputs([("vector1", my_linalg.types.Vector3D),
                         ("vector2", my_linalg.types.Vector3D)])
my_linalg.define_outputs([("result", my_linalg.types.c_double)])

Populate Inputs and Test the Function#

Now that the input structure has been defined, we can write to the inputs structure at my_linalg.inputs. First, we declare two vectors and populate their fields with the appropriate values.

vector1 = my_linalg.types.Vector3D()
vector2 = my_linalg.types.Vector3D()
vector1.x = 0.6651
vector1.y = 0.7395
vector1.z = 0.1037
vector2.x = -0.7395
vector2.y = 0.6716
vector2.z = -0.0460

Then we can assign those vectors to the input structure.

my_linalg.inputs.vector1 = vector1
my_linalg.inputs.vector2 = vector2

Finally, we can run the dot product function and print the result from the output structure. As our input vectors were orthogonal, we get the expected result of zero.

outputs = my_linalg.run()

print(f"Dot product: {outputs.result}")

Dot product: 3.6549999999971154e-05

Code for this tutorial#

 1import os
 2
 3from opensourceleg.control.compiled_controller import CompiledController
 4
 5my_linalg = CompiledController(
 6    library_name="lin_alg.so",
 7    library_path=os.path.dirname(__file__),
 8    main_function_name="dot_product_3d",
 9    initialization_function_name=None,
10    cleanup_function_name=None,
11)
12
13my_linalg.define_type(
14    "Vector3D",
15    [
16        ("x", my_linalg.types.c_double),
17        ("y", my_linalg.types.c_double),
18        ("z", my_linalg.types.c_double),
19    ],
20)
21my_linalg.define_inputs(
22    [("vector1", my_linalg.types.Vector3D), ("vector2", my_linalg.types.Vector3D)]
23)
24my_linalg.define_outputs([("result", my_linalg.types.c_double)])
25
26vector1 = my_linalg.types.Vector3D()
27vector2 = my_linalg.types.Vector3D()
28
29vector1.x = 0.6651
30vector1.y = 0.7395
31vector1.z = 0.1037
32vector2.x = -0.7395
33vector2.y = 0.6716
34vector2.z = -0.0460
35
36my_linalg.inputs.vector1 = vector1  # type: ignore
37my_linalg.inputs.vector2 = vector2  # type: ignore
38
39outputs = my_linalg.run()
40
41print(f"Dot product: {outputs.result}")