YANE-Framework Tutorial 1.1.0

The Main Program

In this section we shortly explain the structure of a typical "Main Program". This program represents the center routine which interconnects the example and the routines from the NMPC library.

Libraries

The first thing to do is to define the libraries which we require, i.e.

These libraries form the head of our main program:

#include <iostream>
#include <cmath>
#include <iomanip>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>

#include <examplemodels2.h>
#include <yane.h>
#include <sqpfortran.h>

Namespaces

Having defined the libraries which we are going to use, we now have to define the namespaces in which the classes of these libraries operate. These namespaces are separated in order to guarantee implementation safety, i.e. a certain variable, method or class can be contained in two different namespaces, and have a different value and meaning. In order to avoid this error source, the namespaces are separated.

using namespace yane::MPC;
using namespace yane::SqpFortran;
using namespace yane::Utils;
using namespace std;

Fixed Variables

Within our main program, we are going to use certain variables which are crucial to the definition of the NMPC problem. Those variables are typically used several times throughout the main program in memory allocations or constructors of classes. Most importantly, we might want to change these variables easily since they are the main set-screws for our NMPC closed-loop.

Here, we chose to set the variables

#define HORIZON 50
#define H_NEW 0.1
#define INF 1.0E19
#define HORIZONSHIFT 1
#define PRECEDESTEPS 1

Definition of the NMPC Problem

Now, we are ready to define the NMPC problem using the classes from the libraries. To this end, we open up the main routine of our program and define the pointer for the state $ x $, the control $ u $ and the sampling time grid $ t $.

int main ( )
        double * x, * u, * t;

Next, we construct the two objects defining our "Inverted Pendulum on a Cart" problem by calling

        yane::Model::Model * model = new yane::ExampleModels2::InvertedPendulum();
        yane::MPC::ModelShootingData * shooting = new yane::ExampleModels2::InvertedPendulumShootingData ( ( yane::ExampleModels2::InvertedPendulum* ) model );

Using this model, we can construct a simulator. The simulator is an object that can be used to predict the behaviour of the system given an initial value and the control that shall be applied. This simulator cannot only be used within the NMPC algorithm, i.e. to predict the trajectories for different control sequences and for optimization purposes, it can also be used to simulate the closed-loop or the uncontrolled trajectories.

        yane::Model::Simulator * simulator = new yane::Model::Simulator ( model );

Now, we are going to set up the NMPC itself. According to the NMPC structure, we require

to construct an NMPC object. Since we have already constructed our example Model object, we directly turn to the other two objects. For both the differential equation manager object IOdeManager (which links the differential equation solver to the minimization routine) and the minimization routine object MinProg, we implemented several different variants in the libraries. Here is a simple configuration which we use to initialize the MPC object:

//      OdeManager * odeman = new SimpleOdeManager();
        OdeManager * odeman = new CacheOdeManager();
//      OdeManager * odeman = new SyncOdeManager();
//      OdeManager * odeman = new MultiThreadCacheOdeManager(2);
//      yane::MinProg::NLP * sqp = new BtIpopt();
        yane::MinProg::NLP * sqp = new SqpFortran();
//      yane::MinProg::NLP * sqp = new SqpNagC();

        SqpFortran * sqpSpecify = ( ( SqpFortran * ) sqp );
        sqpSpecify->setAccuracy ( 1E-4 );
        sqpSpecify->setMaxFun ( 20 );
        sqpSpecify->setMaxIterations ( 1000 );
        sqpSpecify->setLineSearchTol ( 0.1 );

        MPC * mpc = new MPC ( INF );
        mpc->reset ( odeman, sqp, model, HORIZON, PRECEDESTEPS, shooting );

For the different object only standard values are assigned on initialization, so you may want to change these values. Here, we present some changes to the minimization object:

        SqpFortran * sqpSpecify = ( ( SqpFortran * ) sqp );
        sqpSpecify->setAccuracy ( 1E-4 );
        sqpSpecify->setMaxFun ( 20 );
        sqpSpecify->setMaxIterations ( 1000 );
        sqpSpecify->setLineSearchTol ( 0.1 );

Before we can fire up the MPC itself, we need to define the optimization time grid and the control variable and supply them to the MPC object. Moreover, you may want to configure the setting of the MPC itself as well. Additionally, the initial value and the differentiation width used for the optimization should be set and Debug-Messages can be activated. Last, auxilliary variables for the output of the MPC procedure are defined which we will use within the simulator.

        mpc->allocateMemory ( t, u );
        mpc->generateTimeGrid ( t, 0.0, H_NEW );
        for ( int i = 0; i < HORIZON; i++ )
{
                model->getDefaultControl ( &u[i * model->dimensionControl() ] );
}
        mpc->initCalc ( t, u );
        mpc->resizeHorizon ( 17, H_NEW );
        mpc->setConfigShiftHorizon ( STANDARD, CURRENT_DATA );

        x = new double [ model->dimensionState() ];
        model->getDefaultState ( x );

        btmb::Utils2::OptimalDiff::calc();
        btmb::Utils2::Exception::enableDebugMessage ( true );

        double * nextControls = new double [ model->dimensionControl() * HORIZONSHIFT ];
        double * nextTimesteps = new double [ HORIZONSHIFT + 1 ];

The Feedback Loop

Now that we have set up the MPC problem and initialized it, the following loop represents the three main steps of the MPC procedure, i.e.

Within the presented code below, you see that in the first try-and-catch block we call the calculation routine of the MPC object, the heart of the MPC library, and supply the initial value $ x $. This routine computes the optimal control which is stored in the variable $ u $. In the second try-and-catch block, the internal system is shifted forward in time by SHIFT steps. The last part, the for-loop, represent the implementation of the first SHIFT control vectors to the external system.

        for ( int i = 0; i < 1000; i++ )
        {
                RTClock timer;
                try
                {
                        mpc->calc ( x );
                }
                catch ( yane::MinProg::SolverWarning e )
                {
//                      cout << e.what() << endl;
                }

                try
                {
                        mpc->shiftHorizon ( nextControls, nextTimesteps, HORIZONSHIFT, x, H_NEW );
                }
                catch ( yane::MinProg::SolverWarning e )
                {
//                      cout << e.what() << endl;
                }

                double duration = timer.elapsedSeconds() * 1000.0;

                for ( int j = 0; j < HORIZONSHIFT; j++ )
                {
                        simulator->initialize ( nextTimesteps[j], x, &nextControls[j] );
                        simulator->run ( nextTimesteps[j+1] );
                }
}

Note that for this example we initialized the internal system with the simulator object which we also use as the external system. In general, however, these two system do not coincide which can be represented very easily in this setting here.

Moreover, in the source code of the "Inverted Pendulum on a Cart" example you will find additional lines which are required to give the output of the closed-loop, i.e. to make the results readable. Within the library, however, there are also graphical routines which can be used to animate the computed results.

Closing the Main Program

Last, before closing our Main Program, we (should) delete all the constructed objects and the allocated memory:

        delete mpc;
        delete odeman;
        delete sqp;
        delete simulator;
        delete shooting;
        delete model;
        delete[] t;
        delete[] u;

Authors: