Code generation at a glance

The MPC problem description is written in a file called the problem file. The data file for a specific problem is given in a different text file called the data file.

After writing these files, the next step is to actually auto-generate the C code. This is done in two easy steps:

  1. create an mpc object from the problem file.
  2. generate code for the data using the newly created mpc object and the data file.

The first step will automatically generate the C-code for the problem. The second step will generate code for the data for two cases: static memory allocation and dynamic memory allocation. In the first case, the data consists on several C files that statically allocate memory and need to be compiled. In the second case, a single data file in json format is written. This data contained in the json file that can be dynamically loaded. The first case (C-code) is useful for deployment in embedded systems, whereas the second case (json file) allows more flexibility during simulation (no need to compile the data).

A basic MPC problem

The code generation described in this section basically consist of the following steps:

  1. write the problem file with the MPC problem description,
  2. write a data file corresponding to the problem,
  3. create a muaompc object out of that problem, and
  4. create data from that object based on the data file.

The simplest problem that can be setup with the ldt module is an input constrained problem. The code for this example can be found inside the tutorial directory muaompc_root/examples/ldt/tutorial, where muaompc_root is the path to the root directory where muaompc sources were extracted.

The MPC setup description

Consider the following setup. The plant to be controlled is described by the prediction model x^+ = A x + B u, where x  \in \mathbb{R}^n, and u \in \mathbb{R}^m are the current state and input vector, respectively. The state at the next sampling time is denoted by x^+. The discrete-time system and input matrices are denoted as A and B, respectively.

The inputs are constrained to be in a box set \mathcal{C}_u = \{u \; | \; u\_lb \leq u \leq u\_ub\}.

The MPC setup is thus as follows:

\underset{\vb{u}}{\text{minimize}} & \;\;
\sum\limits_{i=0}^{N-1} (\| x_i\|^2_Q + \|u_i\|^2_R)  + \|x_{N}\|^2_P \\
\text{subject to}
& \;\; x_{i+1}=A x_i + B u_i, \;\; i = 0, \cdots, N-1 \\
& \;\; u\_lb \leq u_i \leq u\_ub, \;\; i = 0, \cdots, N-1   \\
& \;\; x_0 = \bar{x} \\

where the integer N \geq 2 is the prediction horizon. The symmetric matrices Q, R, and P are the state, input, and final state weighting matrices, respectively. The vector \bar{x} represents the system state at the current sampling time, which is given online as a parameter to the optimization problem.

The optimization variable \vb{u} \in \mathbb{R}^{Nm} is defined as the sequence \vb{u} = \{u_0 \; u_1 \; \ldots \; u_{N-1}\}. Similarly, we define the (auxiliary) state sequence \vb{x} = \{x_0\;  x_1\;  \ldots \; x_{N}\}, with \vb{x} \in \mathbb{R}^{(N+1)n}.

The MPC problem file

The MPC setup can be rather intuitively described in the problem file. In your favorite text editor write the following:

variable u[0:N-1](m);
auxs x[0:N](n);
parameters x_bar(n);
minimize sum(quad(x[i],Q)+quad(u[i], R), i=0:N-1)+quad(x[N],P);
subject to x[i+1] = A*x[i]+B*u[i], i=0:N-1;
u_lb <= u[i] <= u_ub, i=0:N-1;
x[0]=x_bar;

Save the file as myprb.prb. The resemblance of this file to the mathematical description should be apparent. Let us make a few remarks about the notation.

The problem description is based on sequences, which are defined using the format \texttt{v[a:b](m)}, where \texttt{v} is the name of the sequence, \texttt{m} is the length of each vector in the sequence, and \texttt{a} and \texttt{b} denote the index of the first and last element of the sequence. To refer to the element \texttt{i} of this sequence, we use the notation \texttt{v[i]}. In the case that a sequence consist of a single element, only the vector length need to be specified, i.e. \texttt{v(m)}. To refer to this vector no indexing is required in later parts of the problem specification, i.e. it suffices to write \texttt{v}.

The \texttt{variable} keyword identifies the optimization variable. The \texttt{parameters} keyword identify sequences that are to be specified online. The keyword \texttt{auxs} is used to specify the dimensions of the sequence \texttt{x}.

The keyword \texttt{minimize} identifies the text following it as the cost function to be minimized. Several special keywords are accepted, for example the \texttt{sum(h[i], i=a:b)} denotes the summation of the real valued functions h_{i} for $i=a,\ldots, b$. The keyword \texttt{quad(v,M)} denotes the quadratic form \|v\|_M^2.

As seen in problem file, the equality and inequality constraints follow after the keyword \texttt{subject to}. The constraints optionally accept an index variable and its range. For example, the prediction model is written as \texttt{x[i+1] = A*x[i] + B*u[i], i=0:N-1}.

With the problem file already finished, we can now write the data file.

The MPC data file

The data file must contain the numerical values for the matrices, vectors, and scalars found in the problem file. Values for the sequences defined by the keywords \texttt{variable}, \texttt{parameters} and \texttt{aux} are not required. Thus, in our example, values for \texttt{u}, \texttt{x\_bar}, and \texttt{x} do not need to be specified. The following elements need to be specified: the matrices \texttt{Q}, \texttt{R}, \texttt{P}, \texttt{A}, \texttt{B}, the vectors \texttt{u\_lb}, \texttt{u\_ub}, and the scalars \texttt{N}, \texttt{m}, \texttt{n}.

The matrices are specified using MATLAB syntax. For example, an identity matrix I \in \mathbb{R}^{2 \times 2} could be written in the following ways:

I = [1 0; 0 1]
I = [1, 0; 0, 1]

Without going into further details, let us write data file. In your favourite text editor write:

# weighting matrices
Q = [1, 0; 0, 1]
R = [1]
P = [1, 0; 0, 1]
# system matrices
A = [1.,  0.01; 0.,  0.9]
B = [1.e-04; 0.02]
# input constraints
u_lb = [-100]
u_ub = [100]
# dimensions
N = 5
n = 2
m = 1

Save this file as mydat.dat. The matrices A and B represent the discrete time model of a DC-motor. The state vector is given by x = [x_1 \; x_2]^T \in \mathbb{R}^n, where x_1 and x_2 are the rotor position and angular speed, respectively. The input is constrained to be between -100% and 100%.

For this example, we chose the weighting matrices to be identity matrices of appropriate size, i.e. P = Q = I \in \mathbb{R}^{n
\times n}, and R = 1. Clearly, the value of dimension of the state and input vector are n = 2 and m = 1. The horizon length is specified as steps through the parameter N=5.

Generating the C-code

Now that we have written the myprb.prb problem file, we proceed to create an mpc object. In the directory containing myprb.prb, launch your Python interpreter and in it type:

from muaompc import ldt

mpc = ldt.setup_mpc_problem('myprb.prb')

This will generate code specific for the problem described by myprb.prb. By itself, the code we just generated is very not useful. It only contains and abstract description of an MPC problem without any data. The next step is to generate code for data that can be used with the problem code for myprb.prb we just generated. To generate code that represents the data in mydat.dat, continue typing in your Python interpreter:

ldt.generate_mpc_data(mpc, 'mydat.dat')

And that’s it! If everything went allright, you should now see inside current directory a new folder called myprb_mpc. As an alternative to typing the above code, you can execute the file main.py found in the tutorial directory, which contains exactly that code. The tutorial directory already contains the files myprb.prb and mydat.dat. In the next section, you will learn how to use the generated C code.

Tip

If the code generation was not succesful, try passing the verbose=True input parameter to the function setup_mpc_problem. It will print extra information about the code generation procedure. For example:

mpc = ldt.setup_mpc_problem('myprb', verbose=True)

Tip

By default, the generated code uses double precision float (64-bit) for all computations. You can specify a different numeric representation via the input parameter numeric of the function setup_mpc_problem. For example, to use single precision (32-bit) floating point numbers type:

mpc = ldt.setup_mpc_problem('myprb.prb', numeric='float32')

Using the generated code

In the folder myprb_mpc you will find all the automatically generated code for the current example. To use the generated code in a control loop, the following steps are to be followed:

  1. setup a MPC controller
  2. configure the optimization algorithm
  3. set the parameters for the MPC controller
  4. solve the MPC problem
  5. apply the control input
  6. repeat from step 3

We now proceed to exemplify the use of the generated code from steps 1 to 5. We start our tutorial using the MATLAB interface, as it is simpler to explain. Later we show how it is done in pure C.

Note

The Python interface to the generated C-code will be available in future releases.

Using the generated code in MATLAB

The MATLAB interface makes it possible to directly make of the generated code and data (i.e. the MPC controller) from within MATLAB.

Once the code has been generated, the next step is to compile the MATLAB interface.

Start MATLAB, and switch to the folder myprb_mpc/src/matlab. In the MATLAB console type mpcmake, which will execute the mpcmake.m script. The last step is to add the matlab directory to the PATH environment in MATLAB. For example, assuming the MATLAB current directory is the tutorial directory muaompc_root/examples/ldt/tutorial, in the MATLAB console type:

cd myprb_mpc/src/matlab
mpcmake
cd ..
addpath matlab

Now you can use the interface which is encapsulated in a class called mpcctl which represents the MPC controller. Simply declare an instance of that class, which we usually call ctl (controller). The input parameter for the constructor of the class is the name of a json file contaning the generated data. muaompc by default saves the data in the folder myprb_mpc/data/mydat. In our example, the generated json data file is called mpcmydat.json. Continue typing in the console:

ctl = mpcctl('myprb_mpc/data/mydat/mpcmydat.json');

The next step is to configure the optimization algorithm. In this case, we have an input constrained problem. The only parameter to configure is the number of iterations of the algorithm (see section Tuning for details). For this simple case, let’s set it to 10 iterations:

ctl.conf.in_iter = 10;

Let us assume that the current state is \bar{x} = [0.1 \; -0.5]^T. The controller object has a field for the parameters defined in the problem file. The parameter x_bar can be set as follows:

ctl.parameters.x_bar = [0.1; -0.5];

We can finally solve our MPC problem for this state by calling:

ctl.solve_problem();

The solution is stored in an array ctl.u_opt, whose first m elements are commonly applied to the controlled plant. The complete MATLAB example can be found in the tutorial folder under main.m.

Using the generated code in C

The folder myprb_mpc/data/mydat already contains a template for a main file, called mpcmydatmain.c. Switch to the the folder mydat and open mpcmydatmain.c in your favourite editor. This template file shows how to solve an MPC problem using dynamic or static memory allocation. This file might look at bit daunting at first, but it just a template you can modify to fit your needs.

In the current directory you will find two main files with just the basics, that are based on the template file. The file mpcmydatmain_dynmem.c exemplifies how the dynamic memory allocation is done in C code. The file mpcmydatmain_staticmem.c exemplifies the how the static memory allocation version of the data can be used. Both files follow the same structure as the MATLAB tutorial above.

Let us take the file mpcmydatmain_staticmem.c as example.

The first thing to include is the header file of the library called mpcctl.h.

We need to have access to some of the algorithm’s variables, for example the MPC system input, the parameters, and the algorithm settings. This is done through the fields of the struct mpc_ctl structure, which we denote the controller structure. We first create an instance of this controller structure, and we set the controller by passing a pointer to the structure to the function mpcmydat_ctl_setup_ctl, which is found in mpcmydatctldata.h. For example, after including the corresponding headers, in the body of the main function we type:

struct mpc_ctl ctlst;  /* Structure for static memory allocation */
struct mpc_ctl *ctl;  /* pointer to the an allocated structure */

ctl = &ctlst;
mpcmydat_ctl_setup_ctl(ctl);

Once we controller is setup, we can continue in a similar fashion to the MATLAB case, that is first we setup the parameters, then we configure the algorithm, solver the problem:

ctl->parameters->x_bar[0] = 0.1;
ctl->parameters->x_bar[1] = -0.5;
ctl->solver->conf->in_iter = 10;
mpc_ctl_solve_problem(ctl);

Finally, the computed control input is found in the array ctl->u_opt.

Note

At the moment the user needs to know the length of the different arrays in the controller structure. This information can be infered by the user from the problem and data files. The length of the different arrays will be available in the controller structure in future releases.

To run an compile this code do the following. Copy the file mpcmydatmain_staticmem.c into the folder myprb_mpc/data/mydat and remove the main template file mpcmydatmain.c found in myprb_mpc/data/mydat (otherwise you will end up with two main functions in two different files, and the compilation will fail). In that folder you will also find example Makefiles, called mpcmydatMakefile.*, which compiles the generated code. The Makefile mpcmydatMakefile.mk compiles the code using the GNU Compiler Collection (gcc). Adapt the Makefile to your compiler if necessary.

For example, to generate, compile and run the code in Linux you would type in a console:

cd muaompc_root/examples/ldt/tutorial  # the tutorial folder
python main.py  # generates code and data
cp mpcmydatmain_staticmem.c myprb_mpc/data/mydat  # the tutorial main file
cd myprb_mpc/data/mydat
rm mpcmydatmain.c  # remove template main file
make -f mpcmydatMakefile.mk  # compile
./main  # run the controller

If everything went okay, you will see the output:

ctl->u_opt[0] = 3.056814e-02

This concludes our tutorial!

Where to go next

In the folder muaompc_root/examples/ldt/ you will find further examples.