Corner-cutting and trajectory tracking issues in NMPC for a racing car (acados)

Hello,
I am working on a nonlinear model predictive control (NMPC) problem for a racing car using the Matlab interface of Acados. The goal is to control the racing car to follow a given trajectory while minimizing control effort. The trajectory optimization also using Acados solver and successfull. I’m using the below model:
The state variables of prediction model

I am encountering three main issues during the simulation:

  1. Trajectory tracking: The vehicle does not follow the reference trajectory properly – it cuts corners significantly, especially in tight turns (see attached figure: gray = reference, blue = actual, color = speed).

  2. Cost function: Please reference to the below code

%% COST FUNCTION

eV_max = 1;

ebeta_max = 0.05;

n_max = 0.1;

chi_max = 0.05;

beta_ref = atan(delta*veh.lr/veh.l);

symx = vertcat((beta-beta_ref)/ebeta_max, (V-V_ref)/eV_max, n/n_max, chi/chi_max);

symu = vertcat(dot_Tfl/dot_Tfl_s, dot_Tfr/dot_Tfr_s,dot_Trl/dot_Trl_s, dot_Trr/dot_Trr_s, dot_delta/dot_delta_s);

Q = diag([1, 1, 100, 100]);

R = diag([1, 1, 1, 1, 1]);

delta_ff = atan(veh.l * kappa);

% (1) Track curvature feedforward

J_delta_ff = 2 * (delta - delta_ff)^2;

% (2) Frenet feedback (force convergence to centerline)

J_delta_fb = 5 * (delta - delta_ff - 0.8*n - 0.5*xi)^2;

% -------- Torque vectoring penalties (LIGHT) --------

LR = (Tfl - Tfr)^2 + (Trl - Trr)^2;

FB = ((Tfl + Tfr) - (Trl + Trr))^2;

Mz_drive = veh.wt/(2*veh.rw) * ( (Tfr - Tfl) + (Trr - Trl) );

Yaw_term = Mz_drive^2;

% -------- Stage cost --------

model.expr_ext_cost = ...

0.5 * (symx.' * Q * symx) ...

+ 0.5 * (symu.' * R * symu) ...

+ J_delta_ff ...

+ J_delta_fb ...

+ 0.0005 * LR ...

+ 0.001 * FB ...

+ 0.001 * Yaw_term;

model.expr_ext_cost_e = 0.5*symx'*Q*symx;


3. NMPC generate: Please reference to the below code

```matlab
% 4WD NMPC – ACADOS Codegen

% Matching new ocp_model_racecar_4wd (13 states, 5 inputs)

clear all; clc;

if ~isdeployed

cd(fileparts(which('nmpc_gen')));

end

if isfile('c_generated_code\CMakeCache.txt')

delete('c_generated_code\CMakeCache.txt');

end

acados_env_variables_windows

%% Vehicle parameters, initial conditions

veh = veh_params();

opt_e = 1e-3;

V0 = 5; % initial speed

%% Load model (13-state 4WD)

model = ocp_model_racecar();

nx = model.nx;

nu = model.nu;

np = model.np;

x_s = model.x_s;

u_s = model.u_s;

N = model.N;

T = model.T;

Ts = model.Ts;

%% Create OCP model

ocp_model = acados_ocp_model();

ocp_model.set('name', model.name);

ocp_model.set('T', model.T);

ocp_model.set('sym_x', model.x);

ocp_model.set('sym_xdot', model.xdot);

ocp_model.set('sym_u', model.u);

ocp_model.set('sym_p', model.p);

%% Dynamics: IRK

sim_method = 'irk';

%Dynamics

if (strcmp(sim_method,'erk'))

ocp_model.set('dyn_type', 'explicit');

ocp_model.set('dyn_expr_f', model.expr_f_expl);

elseif (strcmp(sim_method, 'irk'))

ocp_model.set('dyn_type', 'implicit');

ocp_model.set('dyn_expr_f', model.expr_f_impl);

end

%% Cost function

ocp_model.set('cost_type', 'ext_cost');

ocp_model.set('cost_type_e', 'ext_cost');

ocp_model.set('cost_expr_ext_cost', model.expr_ext_cost);

ocp_model.set('cost_expr_ext_cost_e', model.expr_ext_cost_e);

ny_x = 4;

ny_e = ny_x;

ny_u = 5;

ny = ny_x + ny_u;

nbx = nx;

Jbx = eye(nx);

ocp_model.set('constr_Jbx', Jbx);

ocp_model.set('constr_lbx', model.x_min);

ocp_model.set('constr_ubx', model.x_max);

%% Input bounds (5)

nbu = 5;

Jbu = eye(nbu,nu);

ocp_model.set('constr_Jbu', Jbu);

ocp_model.set('constr_lbu', model.u_min);

ocp_model.set('constr_ubu', model.u_max);

% Nonlinear constraints: 5 inequalities (μ ellipse + orthogonality)

nh = 5;

ocp_model.set('constr_expr_h', model.constraint.expr);

ocp_model.set('constr_lh', [0, 0, 0, 0, 0]);

ocp_model.set('constr_uh', [0, 1, 1, 1, 1]);

Jsh = eye(nh);

ocp_model.set('constr_Jsh', Jsh);

ocp_model.set('cost_zl', zeros(nh,1));

ocp_model.set('cost_zu', zeros(nh,1));

ocp_model.set('cost_Zl', diag([10 10 10 10 1]));

ocp_model.set('cost_Zu', diag([10 10 10 10 1]));


% Initial state for solver (13 states)

x0 = zeros(nx,1);

x0(9) = 5; % Tfl

x0(10) = 5; % Tfr

x0(11) = 5; % Trl

x0(12) = 5; % Trr

%x0(13) = 0; % delta

x0 = x0 ./ model.x_s; % normalize

ocp_model.set('constr_x0', x0);

%% Solver parameters

compile_interface = 'auto';

codgen_model = 'true';

nlp_solver = 'sqp_rti'; % sqp, sqp_rti

% nlp_solver = 'sqp_rti'; % sqp, sqp_rti

qp_solver = 'full_condensing_hpipm'; % full_condensing_hpipm, partial_condensing_hpipm, full_condensing_qpoases

nlp_solver_exact_hessian = 'true'; % false=gauss_newton, true=exact


%% acados ocp set opts

ocp_opts = acados_ocp_opts();

ocp_opts.set('compile_interface', compile_interface);

ocp_opts.set('codgen_model', codgen_model);

ocp_opts.set('param_scheme_N', model.N);

ocp_opts.set('nlp_solver', nlp_solver);

ocp_opts.set('nlp_solver_exact_hessian', nlp_solver_exact_hessian);

ocp_opts.set('sim_method', sim_method);

ocp_opts.set('sim_method_num_stages', 2);

ocp_opts.set('sim_method_num_steps', 1)

ocp_opts.set('qp_solver', qp_solver);

% nlp solver

eps_nlp = 1e-4;

ocp_opts.set('nlp_solver_max_iter', 30);

ocp_opts.set('nlp_solver_tol_stat', eps_nlp);

ocp_opts.set('nlp_solver_tol_eq', eps_nlp);

ocp_opts.set('nlp_solver_tol_ineq', eps_nlp);

ocp_opts.set('nlp_solver_tol_comp', eps_nlp);

% ... see ocp_opts.opts_struct to see what other fields can be set

p0 = [5, V0];

ocp_opts.set('parameter_values', p0);

%% get available simulink_opts with default options

simulink_opts = get_acados_simulink_opts;

% manipulate simulink_opts

% inputs

simulink_opts.inputs.lbx = 1;

simulink_opts.inputs.ubx = 1;

simulink_opts.inputs.lh = 0;

simulink_opts.inputs.uh = 0;

simulink_opts.inputs.lbu = 0;

simulink_opts.inputs.ubu = 0;

% outputs

simulink_opts.outputs.utraj = 1;

simulink_opts.outputs.xtraj = 1;

simulink_opts.outputs.cost_value = 1;

simulink_opts.outputs.KKT_residual = 0;

simulink_opts.outputs.KKT_residuals = 1;

%% Generate acados OCP

ocp = acados_ocp(ocp_model, ocp_opts, simulink_opts);

%% set initial trajectory

x_init = zeros(N+1,nx);

x_init(:,1) = V0*ones(N+1,1);

x_init(:,9) = opt_e*ones(N+1,1);

x_init(:,10) = opt_e*ones(N+1,1);

x_init(:,11) = opt_e*ones(N+1,1);

x_init(:,12) = opt_e*ones(N+1,1);

x_init = x_init./model.x_s';

u_init = zeros(N,nu);

ocp.set('init_x', x_init');

ocp.set('init_u', u_init');

ocp.set('init_pi', zeros(nx, N));

%% Compile Sfunctions

cd c_generated_code

make_sfun; % ocp solver

% make_sfun_sim; % integrator

copyfile('acados_solver_sfunction_nmpc.mexw64','..\sim')

cd ..\sim

What I have checked/tried:

  • Reference trajectory is correctly interpolated and updated at every MPC step.
  • Cost function weights have been tuned (tracking weights increased significantly).
  • Control inputs and states are updated correctly in the loop.
  • No obvious mismatches in dimensions or scaling.
    Question:
  • Any common pitfalls in Acados setup that lead to corner-cutting?
  • Are there any configuration issues in my OCP setup that could cause these problems?
  • What steps should I take to improve the trajectory tracking and ensure the cost function reflects the error properly?

Hi :waving_hand:

did you check that the solver actually converges, i.e. you get a solver status of zero? Could you provide the output of solver.print_statistics()

Best, Katrin

Hi,
Thank you for your support!
I have tested with a model using 3 control inputs (Trl, Trr, delta) and it worked successfully with the solve output as shown below.

When I changed the model to use 5 control inputs (Tfl, Tfr, Trl, Trr, delta), the NMPC is no longer able to track the reference path. I will provide the solve output and the vehicle model for this case below. I would greatly appreciate any feedback or suggestions from you.

  • Vehicle model (You can find the complete ocp_model_racecar() function in this OneDrive link):

Please check that you model does not contain any operations that might lead to NaNs, i.e. check for things like division by zero, logarithms of negative numbers etc. and add additional constraints or regularization to avoid undefined beaviour, e.g. a term like 1/x^2 can be approximated by 1/(x^2 + \epsilon) avoiding a division by zero.