# Structuring your project¶

In this guide we show you a suggested structure for your MPC or MHE project.

In general, we advice to use the provided templates from our GitHub repository
as a starting point. We will explain the structure following the `CSTR`

example.
Simple projects can also be developed as presented in our introductory Jupyter Notebooks (MPC, MHE)

We split our MHE / MPC configuration into five separate files:

`template_model.py` |
Define the dynamic model |

`template_mpc.py` |
Configure the MPC controller |

`template_simulator.py` |
Configure the DAE/ODE/discrete simulator |

`template_estimator.py` |
Configure the estimator (MHE / EKF / state-feedback) |

`main.py` |
Obtain all configured modules and run the loop. |

The files all include a single function and return the configured `do_mpc.model.Model`

,
`do_mpc.controller.MPC`

, `do_mpc.simulator.Simulator`

or `do_mpc.estimator.MHE`

objects, when called from a central `main.py`

script.

## template_model¶

The **do-mpc** model class is at the core of all other components and contains the
mathematical description of the investigated dynamical system in the form of
ordinary differential equations (ODE) or differential algebraic equations (DAE).

The `template_model.py`

file will be structured as follows:

```
def template_model():
# Obtain an instance of the do-mpc model class
# and select time discretization:
model_type = 'continuous' # either 'discrete' or 'continuous'
model = do_mpc.model.Model(model_type)
# Introduce new states, inputs and other variables to the model, e.g.:
C_b = model.set_variable(var_type='_x', var_name='C_b', shape=(1,1))
...
Q_dot = model.set_variable(var_type='_u', var_name='Q_dot')
...
# Set right-hand-side of ODE for all introduced states (_x).
# Names are inherited from the state definition.
model.set_rhs('C_b', ...)
# Setup model:
model.setup()
return model
```

## template_mpc¶

With the configured model, it is possible to configure and setup the MPC controller. Note that the optimal control problem (OCP) is always given in the following form:

The configuration of the `do_mpc.controller.MPC`

class in `template_mpc.py`

can be done as follows:

```
def template_mpc(model):
# Obtain an instance of the do-mpc MPC class
# and initiate it with the model:
mpc = do_mpc.controller.MPC(model)
# Set parameters:
setup_mpc = {
'n_horizon': 20,
'n_robust': 1,
't_step': 0.005,
...
}
mpc.set_param(**setup_mpc)
# Configure objective function:
mterm = (_x['C_b'] - 0.6)**2 # Setpoint tracking
lterm = (_x['C_b'] - 0.6)**2 # Setpoint tracking
mpc.set_objective(mterm=mterm, lterm=lterm)
mpc.set_rterm(F=0.1, Q_dot = 1e-3) # Scaling for quad. cost.
# State and input bounds:
mpc.bounds['lower', '_x', 'C_b'] = 0.1
mpc.bounds['upper', '_x', 'C_b'] = 2.0
...
mpc.setup()
return mpc
```

## template_simulator¶

In many cases a developed control approach is first tested on a simulated system.
**do-mpc** responds to this need with the `simulator`

class.
The `simulator`

uses state-of-the-art DAE solvers, e.g. Sundials CVODE to solve the DAE equations defined in the supplied `model`

.
This will often be the same model as defined for the `optimizer`

but it is also possible to use a more complex model of the same system.

The simulator is configured and setup with the supplied `model`

in the `template_simulator.py`

file,
which is structured as follows:

```
def template_simulator(model):
# Obtain an instance of the do-mpc simulator class
# and initiate it with the model:
simulator = do_mpc.simulator.Simulator(model)
# Set parameter(s):
simulator.set_param(t_step = 0.005)
# Optional: Set function for parameters and time-varying parameters.
# Setup simulator:
simulator.setup()
return simulator
```

## template_estimator¶

In the case that a dedicated estimator is required, another python file should be added to the project. Configuration and setup of the moving horizon estimator (MHE) will be structured as follows:

```
def template(mhe):
# Obtain an instance of the do-mpc MHE class
# and initiate it with the model.
# Optionally pass a list of parameters to be estimated.
mhe = do_mpc.estimator.MHE(model)
# Set parameters:
setup_mhe = {
'n_horizon': 10,
't_step': 0.1,
'meas_from_data': True,
}
mhe.set_param(**setup_mhe)
# Set custom objective function
# based on:
y_meas = mhe._y_meas
y_calc = mhe._y_calc
# and (for the arrival cost):
x_0 = mhe._x
x_prev = mhe._x_prev
...
mhe.set_objective(...)
# Set bounds for states, parameters, etc.
mhe.bounds[...] = ...
# [Optional] Set measurement function.
# Measurements are read from data object by default.
mhe.setup()
return mhe
```

Note that the cost function for the MHE can be freely configured using the available variables. Generally, we suggest to choose the typical MHE formulation:

The measurement function must be defined in the model definition and typically contains the inputs. Inputs are not treated separately as in some other formulations.

## main script¶

All previously defined functions are called from a single `main.py`

file, e.g.:

```
from template_model import template_model
from template_mpc import template_mpc
from template_simulator import template_simulator
model = template_model()
mpc = template_mpc(model)
simulator = template_simulator(model)
estimator = do_mpc.estimator.StateFeedback(model)
```

Simple configurations, as for the `do_mpc.estimator.StateFeedback`

class above are often directly implemented in the `main.py`

file.

### Initial state & guess¶

Afterwards we set the initial state (true state) for the simulator.
Note that in proper investigations we usually have a different initial state
for the `simulator`

(true state) and e.g. the estimator.

```
# Set the initial state of simulator:
C_a_0 = 0.8
...
x0 = np.array([C_a_0, ...]).reshape(-1,1)
simulator.x0 = x0
```

We can set the initial guessed state for the MHE by modifying its attribute
similarly as for the simulator shown above. The MPC initial guess is given when
calling the function `do_mpc.controller.MPC.make_step()`

for the first time.

### Graphics configuration¶

Visualization the estimation and control results is key to evaluating performance
and identifying potential problems. **do-mpc** has a powerful graphics library based on
Matplotlib for quick and customizable graphics.
After creating a blank class instance and initiating a figure object with:

```
# Initialize graphic:
graphics = do_mpc.graphics.Graphics()
fig, ax = plt.subplots(5, sharex=True)
```

we need to configure where and what to plot, with the `graphics.Graphics.add_line()`

method:

```
graphics.add_line(var_type='_x', var_name='C_a', axis=ax[0])
# Fully customizable:
ax[0].set_ylabel('c [mol/l]')
ax[0].set_ylim(...)
...
```

Note that we are not plotting anything just yet.

### closed-loop¶

As shown in Diagram Project structure, after obtaining the different **do-mpc**
objects they can be used in the *main loop*. In code form the loop looks like this:

```
for k in range(N_iterations):
u0 = mpc.make_step(x0)
y_next = simulator.make_step(u0)
x0 = estimator.make_step(y_next)
```

Instead of running for a fixed number of iterations, we can also start an infinite loop with:

```
while True:
...
```

or have some checks active:

```
while mpc._x0['C_b'] <= 0.8:
...
```

During or after the loop, we are using the previously configured `graphics`

class.
Open-loop predictions can be plotted at each sampling time:

```
for k in range(N_iterations):
u0 = mpc.make_step(x0)
y_next = simulator.make_step(u0)
x0 = estimator.make_step(y_next)
graphics.reset_axes()
graphics.plot_results(mpc.data, linewidth=3)
graphics.plot_predictions(mpc.data, linestyle='--', linewidth=1)
plt.show()
input('next step')
```

Furthermore, we can obtain a visualization of the full closed-loop trajectory after the loop:

```
graphics.plot_results(mpc.data)
```