Source code for do_mpc.approximateMPC._ampc

#
#   This file is part of do-mpc
#
#   do-mpc: An environment for the easy, modular and efficient implementation of
#        robust nonlinear model predictive control
#
#   Copyright (c) 2014-2019 Sergio Lucia, Alexandru Tatulea-Codrean
#                        TU Dortmund. All rights reserved
#
#   do-mpc is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Lesser General Public License as
#   published by the Free Software Foundation, either version 3
#   of the License, or (at your option) any later version.
#
#   do-mpc is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with do-mpc.  If not, see <http://www.gnu.org/licenses/>.

# Imports
import warnings
import torch
import numpy as np
import casadi as ca
from ._ampcsettings import ApproximateMPCSettings


# Feedforward NN
[docs] class FeedforwardNN(torch.nn.Module): """Feedforward Neural Network. .. versionadded:: >v4.6.0 Args: n_in (int): Number of input neurons. n_out (int): Number of output neurons. n_hidden_layers (int): Number of hidden layers. n_neurons (int): Number of neurons in hidden layers. act_fn (str): Activation function. output_act_fn (str): Output activation function. """ def __init__(self, n_in, n_out, n_hidden_layers, n_neurons, act_fn, output_act_fn): super().__init__() assert n_hidden_layers >= 0, "Number of hidden layers must be >= 0." self.n_in = n_in self.n_out = n_out self.n_layers = n_hidden_layers + 1 self.n_neurons = n_neurons self.act_fn = act_fn self.output_act_fn = output_act_fn self.layers = torch.nn.ModuleList() for i in range(self.n_layers): if i == 0: self.layers.append(torch.nn.Linear(n_in, n_neurons)) self.layers.append(self._get_activation_layer(act_fn)) elif i == self.n_layers - 1: self.layers.append(torch.nn.Linear(n_neurons, n_out)) if output_act_fn != "linear": self.layers.append(self._get_activation_layer(output_act_fn)) else: self.layers.append(torch.nn.Linear(n_neurons, n_neurons)) self.layers.append(self._get_activation_layer(act_fn)) def _get_activation_layer(self, act_fn): """Gets the pytorch activation layer. Args: act_fn (str): Activation function. Returns: torch.nn.Module: Activation layer. """ if act_fn == "relu": return torch.nn.ReLU() elif act_fn == "tanh": return torch.nn.Tanh() elif act_fn == "leaky_relu": return torch.nn.LeakyReLU() elif act_fn == "sigmoid": return torch.nn.Sigmoid() else: raise ValueError("Activation function not implemented.") def forward(self, x): """Forward pass of the neural network. Args: x (torch.Tensor): Input tensor. Returns: torch.Tensor: Output tensor. """ for i, layer in enumerate(self.layers): x = layer(x) return x @torch.no_grad() def count_params(self): """Count the number of parameters in the neural network. Returns: int: Number of parameters. """ n_params = sum( param.numel() for param in self.parameters() if param.requires_grad ) return n_params
# Approximate MPC
[docs] class ApproxMPC(torch.nn.Module): """Neural Network Approximation of the Model Predictive Controller. .. versionadded:: >v4.5.1 Use this class to configure and run the ApproxMPC controller based on a previously configured :py:class:`do_mpc.controller.MPC` instance. **Configuration and setup:** Configuring and setting up the ApproxMPC controller involves the following steps: 1. Configure the ApproxMPC controller with :py:class:`ApproximateMPCSettings`. The ApproxMPC instance has the attribute ``settings`` which is an instance of :py:class:`ApproximateMPCSettings`. 2. To finalize the class configuration :py:meth:`setup` may be called. This method sets up the neural network and the MPC controller. **Usage:** The ApproxMPC controller can be used in a closed loop setting by calling the :py:meth:`make_step` method. This method takes the current state of the system and returns the control action. **Example:** :: # Create an MPC instance mpc = do_mpc.controller.MPC(...) mpc.setup() # Create an ApproxMPC instance ampc = do_mpc.controller.ApproxMPC(mpc) ampc.setup() # Closed loop simulation for k in range(N): x = get_current_state() u = ampc.make_step(x) Args: mpc (do_mpc.controller.MPC): MPC instance to be approximated by the neural network. """ def __init__(self, mpc): # initiates torch.nn.Module super().__init__() # storage self._settings = ApproximateMPCSettings() self.mpc = mpc self._settings.lbx = torch.tensor(ca.DM(self.mpc._x_lb).full()) self._settings.ubx = torch.tensor(ca.DM(self.mpc._x_ub).full()) self._settings.lbu = torch.tensor(ca.DM(self.mpc._u_lb).full()) self._settings.ubu = torch.tensor(ca.DM(self.mpc._u_ub).full()) # flags self.flags = { "setup": False, } @property def settings(self): """ All necessary parameters of the mpc formulation. This is a core attribute of the ApproxMPC class. It is used to set and change parameters when setting up the controller by accessing an instance of :py:class:`ApproximateMPCSettings`. Example to change settings: :: ApproxMPC.settings.n_hidden_layers = 3 Note: Settings cannot be updated after calling :py:meth:`do_mpc.controller.ApproxMPC.setup`. For a detailed list of all available parameters see :py:class:`ApproximateMPCSettings`. """ return self._settings @settings.setter def settings(self, val): warnings.warn("Cannot change the settings attribute") def setup(self): """Setup the ApproxMPC controller. Internally, this method sets the device, initializes the neural network, and sets the box constraints. Returns: None: None """ assert self.flags["setup"] is False, "Setup can only be once." self.flags.update( { "setup": True, } ) # sets the device self._set_device() # ampc initilisation if self.mpc.flags["set_rterm"]: self.net = FeedforwardNN( n_in=self.mpc.model.n_x + self.mpc.model.n_u, n_out=self.mpc.model.n_u, n_hidden_layers=self.settings.n_hidden_layers, n_neurons=self.settings.n_neurons, act_fn=self.settings.act_fn, output_act_fn=self.settings.output_act_fn, ) else: self.net = FeedforwardNN( n_in=self.mpc.model.n_x, n_out=self.mpc.model.n_u, n_hidden_layers=self.settings.n_hidden_layers, n_neurons=self.settings.n_neurons, act_fn=self.settings.act_fn, output_act_fn=self.settings.output_act_fn) # Default settings self.torch_data_type = torch.float32 self.step_return_type = "numpy" # "torch" or "numpy" # storing initial guess self.x0 = self.mpc.x0 self.u0 = self.mpc.u0 print("----------------------------------") print(self) print("----------------------------------") assert any(torch.isinf(self.settings.lbx))==False, "There are missing lower bounds for state variables that are required for clipping and scaling." assert any(torch.isinf( self.settings.ubx))==False, "There are missing upper bounds for state variables that are required for clipping and scaling." assert any(torch.isinf( self.settings.lbu))==False, "There are missing lower bounds for input variables that are required for clipping and scaling." assert any(torch.isinf( self.settings.ubu))==False, "There are missing upper bounds for input variables that are required for clipping and scaling." # setup box constraints self.set_shift_values() # setup ends return None def _set_device(self): """Sets the device for the neural network. Available options for the are 'auto', 'cuda', and 'cpu'. Returns: None """ # auto choose gpu if gpu is available if self.settings.device == "auto": self.settings.device = "cuda" if torch.cuda.is_available() else "cpu" # torch default device is set self.torch_device = torch.device(self.settings.device) torch.set_default_device(self.torch_device) return None def set_shift_values(self): """Shifts and scales the input and output data based on the box constraints. Returns: None """ if self.mpc.flags["set_rterm"]: lb = torch.concatenate((self.settings.lbx.T, self.settings.lbu.T), axis=1) ub = torch.concatenate((self.settings.ubx.T, self.settings.ubu.T), axis=1) else: lb = self.settings.lbx.T ub = self.settings.ubx.T self.x_shift = torch.tensor(lb) self.x_range = torch.tensor(ub - lb) self.y_shift = torch.tensor(self.settings.lbu.T) self.y_range = torch.tensor(self.settings.ubu.T - self.settings.lbu.T) return None def forward(self, x): """Forward pass of the neural network. Args: x (torch.Tensor): Input tensor. Returns: torch.Tensor: Output tensor. """ # x_scaled = self.scale_inputs(x) y = self.net(x) # y = self.rescale_outputs(y_scaled) return y def scale_inputs(self, x): """Scales inputs Args: x (torch.Tensor): Inputs. Returns: x_scaled (torch.Tensor): Scaled inputs. """ x_scaled = (x - self.x_shift) / self.x_range x_scaled = x_scaled.type(self.torch_data_type) return x_scaled def rescale_outputs(self, y_scaled): """Rescale outputs. Args: y_scaled (torch.Tensor): Scaled outputs. Returns: y (torch.Tensor): Rescaled outputs. """ y = y_scaled * self.y_range + self.y_shift return y def clip_control_actions(self, y): """Clip (rescaled) outputs of net to satisfy input (control) constraints. Args: y (torch.Tensor): Outputs. Returns: y (torch.Tensor): Clipped outputs. """ if self.settings.lbu is not None: y = torch.max(y, self.settings.lbu.T) if self.settings.ubu is not None: y = torch.min(y, self.settings.ubu.T) if self.settings.lbu is None and self.settings.ubu is None: raise ValueError("No output constraints defined. Clipping not possible.") return y # Predict (Batch) @torch.no_grad() def predict(self, x_batch): """Predicts the output of the neural network for a batch of inputs. Args: x_batch (torch.Tensor): Batch of inputs. Returns: y_batch (torch.Tensor): Batch of outputs. """ y_batch = self.net(x_batch) return y_batch # approximate MPC step method for use in closed loop @torch.no_grad() def make_step(self, x0, u_prev=None, clip_to_bounds=True): """Make one step with the approximate MPC. Args: x (torch.tensor): Input tensor of shape (n_in,). clip_to_bounds (bool, optional): Whether to clip the control actions. Defaults to True. Returns: np.array: Array of shape (n_out,). """ # Check setup. assert self.flags['setup'] == True, 'MPC was not setup yet. Please call ApproxMPC.setup().' assert isinstance(x0,np.ndarray), "x0 must be a numpy array" assert isinstance(u_prev, (np.ndarray, type(None))), "u_prev must be a numpy array or None" # taking optional input if provided if u_prev is not None: self.u0 = u_prev if self.mpc.flags["set_rterm"]: x = np.concatenate((x0, ca.DM(self.u0).full()), axis=0).squeeze() else: x = x0 # Check if inputs are tensors if self.mpc.flags["set_rterm"]: x = torch.tensor(x, dtype=self.torch_data_type).reshape( (-1, self.mpc.model.n_x + self.mpc.model.n_u) ) else: x = torch.tensor(x, dtype=self.torch_data_type).reshape( (-1, self.mpc.model.n_x) ) # forward pass if self.settings.scaling: x_scaled = self.scale_inputs(x) y_scaled = self.net(x_scaled) y = self.rescale_outputs(y_scaled) else: y = self.net(x) # Clip outputs to satisfy input constraints of MPC if clip_to_bounds: y = self.clip_control_actions(y) if self.step_return_type == "numpy": y = y.cpu().numpy().reshape((-1, 1)) elif self.step_return_type == "torch": y = y else: raise ValueError("step_return_type must be either 'numpy' or 'torch'.") # storing self.u0 = y return y def save_to_state_dict(self, directory="approx_mpc.pth"): """Save the neural network to a state dictionary. Args: directory (str, optional): Directory to save the model. Defaults to "approx_mpc.pth". """ torch.save(self.net.state_dict(), directory) def load_from_state_dict(self, directory="approx_mpc.pth"): """Load the neural network from a state dictionary. Args: directory (str, optional): Directory to load the model. Defaults to "approx_mpc.pth". """ self.net.load_state_dict(torch.load(directory))