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. The full script for this example is available at the end of this file. More complex examples using an actual Finite State Machine controller 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.
/** dot_product_3d.cpp
An example cpp file to demonstrate the compiled controller functionality
in the opensourceleg library.
Kevin Best
University of Michigan
October 2023
**/
struct Vector3D{
double x, y, z;
};
struct Inputs{
Vector3D vector1, vector2;
};
struct Outputs{
double result;
};
extern "C" void dot_product_3d(Inputs* inputs, Outputs* Outputs) {
Outputs->result = inputs->vector1.x * inputs->vector2.x
+ inputs->vector1.y * inputs->vector2.y
+ inputs->vector1.z * inputs->vector2.z;
}
Note: The
extern "C"
linkage-specification is important to prevent theC++
compiler from name mangling. Under the hood, our library uses aC
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 successful, 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.
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.
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.
We get the following output, showing that our compiled library successfully calculated the dot product between our two orthogonal vectors:
Full Code for This Tutorial¶
import os
from opensourceleg.control.compiled import CompiledController
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,
)
my_linalg.define_type(
"Vector3D",
[
("x", my_linalg.types.c_double),
("y", my_linalg.types.c_double),
("z", my_linalg.types.c_double),
],
)
my_linalg.define_inputs([("vector1", my_linalg.types.Vector3D), ("vector2", my_linalg.types.Vector3D)])
my_linalg.define_outputs([("result", my_linalg.types.c_double)])
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
my_linalg.inputs.vector1 = vector1
my_linalg.inputs.vector2 = vector2
outputs = my_linalg.run()
print(f"Dot product: {outputs.result}")