NMPC with custom cost function and input rate constraints

Hi,

I am currently working on implementing the following cost function for a nonlinear Model Predictive Control (NMPC) system. This function aims to address two simultaneous objectives:

  1. Minimizing tracking error while enforcing input rate constraints within the cost function.
  2. Maximizing the “F” information metric to enhance the vehicle’s state estimation in an unknown environment.

Cost function - Casadi implementation:

        xr = x_ref[0:12, :]
        x0 = x[0:12, :]
        cost = 0
        for k in range(self.horizon):
            cost += (X[0:12, k] - xr).T @ self.P @ (X[0:12, k] - xr)
            cost += (U[:, k + 1] - U[:, k]).T @ self.R @ (U[:, k + 1] - U[:, k])
            cost += -alpha * F

            opt.subject_to(
                X[:, k + 1]
                == self.forward_dynamics(
                    X[:, k], U[:, k], flow_bf, flow_acc_bf, self.model_type
                )
            )
            opt.subject_to(opt.bounded(self.xmin, X[0:12, k], self.xmax))
            opt.subject_to(opt.bounded(self.umin, U[:, k], self.umax))
            
        cost += (X[0:12, -1] - xr).T @ self.P @ (X[0:12, -1] - xr)
        
        opt.subject_to(opt.bounded(self.xmin, X[0:12, -1], self.xmax))
        opt.subject_to(X[0:12, 0] == X0)

        opt.set_value(X0, x0)

I have successfully implemented the mentioned formulation using CasADi, and it performs well. However, the computation time is relatively high. Consequently, I’ve decided to switch to using acados with a Python interface to potentially improve efficiency.

As an initial step, I’ve broken down the problem and opted to implement only a portion of it, focusing solely on minimizing tracking error:

Reduced cost function - Casadi implementation:

        xr = x_ref[0:12, :]
        x0 = x[0:12, :]
        cost = 0
        for k in range(self.horizon):
            cost += (X[0:12, k] - xr).T @ self.P @ (X[0:12, k] - xr)
            cost += (U[:, k + 1] - U[:, k]).T @ self.R @ (U[:, k + 1] - U[:, k])

            opt.subject_to(
                X[:, k + 1]
                == self.forward_dynamics(
                    X[:, k], U[:, k], flow_bf, flow_acc_bf, self.model_type
                )
            )
            opt.subject_to(opt.bounded(self.xmin, X[0:12, k], self.xmax))
            opt.subject_to(opt.bounded(self.umin, U[:, k], self.umax))
            
        cost += (X[0:12, -1] - xr).T @ self.P @ (X[0:12, -1] - xr)
        
        opt.subject_to(opt.bounded(self.xmin, X[0:12, -1], self.xmax))
        opt.subject_to(X[0:12, 0] == X0)

        opt.set_value(X0, x0)

However, I’ve encountered difficulties defining (u_{k+1} - u{k}).T * R * (u_{k+1} - u{k}) when using “NONLINEAR_LS”. This has resulted in discontinuous output from the optimizer, as illustrated in the figure below:

I would greatly appreciate guidance on how to implement this using the “EXTERNAL” cost type, as I haven’t found any examples addressing a similar challenge.

I’ve attached the acados implementation that produces the discontinuous output as shown above:

model:

    def auv_model(self):
        x = SX.sym("x", (12, 1))
        x_dot = SX.sym("x_dot", (12, 1))
        u = SX.sym("u", (self.thrusters, 1))

        f_expl_expr = self.nonlinear_dynamics(x, u, self.model_type)
        f_impl_expr = x_dot - f_expl_expr

        p = []

        self.acados_model.name = "bluerov2"
        self.acados_model.f_expl_expr = f_expl_expr
        self.acados_model.f_impl_expr = f_impl_expr
        self.acados_model.x = x
        self.acados_model.xdot = x_dot
        self.acados_model.u = u
        self.acados_model.p = p

solver description:

    def create_ocp_solver_description(self):
        self.acados_ocp.model = self.acados_model
        x = self.acados_model.x
        nx = self.acados_model.x.shape[0]
        nu = self.acados_model.u.shape[0]
        self.acados_ocp.dims.N = self.horizon

        self.acados_ocp.cost.cost_type = "NONLINEAR_LS"
        self.acados_ocp.cost.cost_type_e = "NONLINEAR_LS"
        self.acados_ocp.cost.W_e = self.P
        self.acados_ocp.cost.W = self.P
        self.acados_ocp.model.cost_y_expr = x
        self.acados_ocp.model.cost_y_expr_e = x
        self.acados_ocp.cost.yref = np.zeros((nx,))
        self.acados_ocp.cost.yref_e = np.zeros((nx,))

        self.acados_ocp.constraints.lbu = self.umin
        self.acados_ocp.constraints.ubu = self.umax
        self.acados_ocp.constraints.lbx = self.xmin
        self.acados_ocp.constraints.ubx = self.xmax
        self.acados_ocp.constraints.lbx_0 = self.xmin
        self.acados_ocp.constraints.ubx_0 = self.xmax
        self.acados_ocp.constraints.idxbu = np.arange(nu)
        self.acados_ocp.constraints.idxbx = np.arange(nx)
        self.acados_ocp.constraints.idxbx_0 = np.arange(nx)

        # set options

        # self.acados_ocp.solver_options.qp_solver = "PARTIAL_CONDENSING_HPIPM"
        self.acados_ocp.solver_options.qp_solver = "FULL_CONDENSING_QPOASES"
        # self.acados_ocp.solver_options.qp_solver = "PARTIAL_CONDENSING_OSQP"
        self.acados_ocp.solver_options.qp_solver_cond_N = self.horizon
        self.acados_ocp.solver_options.hessian_approx = (
            "GAUSS_NEWTON"  # 'GAUSS_NEWTON', 'EXACT'
        )
        self.acados_ocp.solver_options.integrator_type = "IRK"
        self.acados_ocp.solver_options.sim_method_num_stages = 4
        self.acados_ocp.solver_options.sim_method_num_steps = 1
        self.acados_ocp.solver_options.nlp_solver_type = "SQP"  # SQP_RTI, SQP
        self.acados_ocp.solver_options.nlp_solver_max_iter = 500
        self.acados_ocp.solver_options.levenberg_marquardt = 1e-5
        self.acados_ocp.solver_options.tf = self.T_horizon

optimize:

def optimize(self, x, x_ref):
        xr = x_ref[0:12, :]
        x0 = x[0:12, :]
        N = self.horizon

        nx = self.acados_ocp.model.x.shape[0]
        nu = self.acados_ocp.model.u.shape[0]

        # initialize solver
        for stage in range(N + 1):
            self.acados_ocp_solver.set(stage, "x", np.zeros((nx,)))
        for stage in range(N):
            self.acados_ocp_solver.set(stage, "u", np.zeros((nu,)))

        # set initial state constraint
        self.acados_ocp_solver.set(0, "lbx", x0)
        self.acados_ocp_solver.set(0, "ubx", x0)

        for k in range(N):
            self.acados_ocp_solver.set(k, "yref", xr)
        self.acados_ocp_solver.set(N, "yref", xr)

        status = self.acados_ocp_solver.solve()
        if status != 0:
            print("acados returned status {}".format(status))

        force_axes = self.acados_ocp_solver.get(0, "u")
        # self.previous_control = np.array([force_axes]).T

        u_next = evalf(mtimes(pinv(self.auv.tam), force_axes)).full()

        return force_axes, u_next

Any assistance would be greatly appreciated! Thank you!

Hi and welcome to the acados forum! :wave:

the acados OCP formulation allows you to define costs as a function of the state and controls associated with a single shooting node only, i.e. the cost may only depend on x_k and u_k. To include a cost on the rate of change you will therefore need to augment your system dynamics. Please check out this post.

The external cost module is straight forward to use, you just provide a casadi expression defining the cost term, you may want to check this example.

Best, Katrin