Using ODEint in C++: A Practical Guide

Debugging Runtime Errors in Time March Simulation Using Boost ODEINT and Eigen

Recently, while working on an aircraft dynamics simulation project using the Boost ODEINT library integrated with the Eigen library for vector and matrix operations, I faced a vexing runtime error that had me scratching my head. In this post, I’d like to share my journey of how I tackled this problem, diving deep into the complexities of the simulation code, identifying possible issues, and implementing solutions.

The Initial Setup and Issue

The goal of my project was to simulate the aircraft’s state over time in a numerical approach called time marching, leveraging Boost’s ODEINT library for numerically solving ordinary differential equations (ODEs). Here’s the gist of what my simulation sought to do:

  1. Represent the aircraft’s state at any given time with a vector of doubles using Eigen.
  1. Use the Runge-Kutta Dopri5 method from Boost ODEINT to evolve these states over time.

However, when running my simulation, I encountered runtime errors which led to abrupt crashes. The errors were not explicitly clear, leaving me uncertain about where the actual problem lay.

Diving Into the Code

The function signature I employed to define aircraft dynamics is as below:

void Flight::aircraftDynamics(const state_type& x, state_type& dx, double t, const Aircraft& aircraft)

Here, ‘x’ represents the current state of the aircraft, ‘dx’ the change in state, ‘t’ the current time, and ‘aircraft’ the specific aircraft model in use.

In the function TimeMarcher, I initialized state x with 18 elements, assuming all initial conditions were known. Understanding that these need precise setting based on your simulation needs might adjust initial conditions:

state_type x(18);

A potential red flag could be if any elements in ‘x’ require non-default values for meaningful simulation results.

The following portion iteratively steps the simulation time using the do_step method from the Runge-Kutta Dopri5 solver:

stepper.do_step([this, &aircraft](const state_type& x, state_type& dx, double t) {
    aircraftDynamics(x, dx, t, aircraft);
    }, x, t, dt);

Lambda functions capture ‘this’ pointer to access member functions and ‘aircraft’ by reference because it likely encapsulates considerable data not requiring duplication.

Suspecting and Solving Issues

  1. Size Mistakes in Eigen Operations: One critical segment that stands out is converting an STL vector to an Eigen vector without ensuring the sizes exactly match:

VectorXd X = Eigen::Map<VectorXd>(const_cast<double*>(x.data()), x.size());

Here, ensure that the source vector ‘x’ is correctly sized prior to mapping. Incorrect sizing could lead to Eigen asserting on debug builds or undefined behavior in release builds.

  1. Mismanagement of Dynamical Computations: Calculations involving aircraft dynamics involve several parameters and states:

auto Udot = -Q * W + V * R - grav * sin(theta) * condition.Nz + Fx / m;

Ensuring condition.Nz, Fx, and m are properly computed and valid at the point of usage is crucial. Uninitialized or incorrectly computed values here could easily cause runtime malfunctions.

  1. Boundary and Validity Checks: Adding checks before performing divisions or inverse trigonometric calculations helps prevent runtime errors due to undefined mathematical operations.

auto betadot = atan(Vdot / (U + 1e-5)); // Adding a small number to avoid division by zero

After implementing these checks and ensuring all variables were initialized correctly before their usage, my simulation began working reliably without the previous runtime errors.

Through this debugging process, I gained a deeper understanding of integrating different libraries and the importance of rigorous validation of data before its use in computation-heavy simulations. Remember, every undefined behavior is a silent beast in numerical computations!


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *