[sfwa]

Flight dynamics model

A key component of the UKF implementation, as well as the yet-to-be-developed NMPC system, is the dynamics model. Currently we’re using a simple centripetal model in the UKF:

AccelerationVector CentripetalModel::evaluate(
const State &in, const ControlVector &control) const {
    AccelerationVector output;

    /* First convert velocity to body frame. */
    Eigen::Matrix velocity_body;
    velocity_body = Quaternionr(in.attitude()) * in.velocity();

    /* Calculate centripetal acceleration. */
    output.segment<3>(0) = in.angular_velocity().cross(velocity_body);

    /* Clear angular acceleration. */
    output.segment<3>(3) << 0, 0, 0;

    return output;
}

In our UKF implementation, the dynamics model is called with the current UKF state, as well as the system control vector. The control vector is model-defined, but in our case will be the control deflection in radians for elevons, and the current motor RPM for the throttle. The DynamicsModel::evaluate(const State&, const ControlVector&) method returns an AccelerationVector, which is a 6-element vector containing linear and angular acceleration X, Y and Z components.

Based on the attitude error statistics returned by the horizon detection code the simple CentripetalModel above yields a reduction in error of about 2°, despite the significant lag in GPS velocity relative to the accelerometer readings; it therefore seems likely that a dynamics model that takes thrust, control surface position and aerodynamic forces into consideration could result in further significant accuracy improvements.

The first step in developing the model was reviewing existing implementations, most notably JSBSim as it’s commonly used as a reference FDM in papers on the topic. The JSBSim model is quite clear, but highly structured and considerably more complex than required for our application, so I also reviewed simpler from-scratch implementations (e.g. the FlightSim example from “Physics for Game Developers”) and of course the underlying physics.

The second, considerably more challenging step was determining how detailed the model should be—obviously a finite-element approach was out of the question, but how many simplifying assumptions should be made? For example, do we assume evenly-distributed mass? What about the effect of α and β on control surface forces? Do we consider lift, drag or moment due to β at all?

After some research it appeared that most aerodynamic coefficients depend non-linearly on α, and then approximately linearly on the relevant other parameter (β, yaw rate, pitch, rate, roll rate, and control surface deflection—assuming those angles stay “small”). Thus, we opted to use a quartic model to relate the coefficient values with α, and then multiply by the appropriate other parameter.

The coefficient model we decided to use was:

The resulting code pretty much wrote itself, and at an estimated 1500 DSP cycles per update, requires considerably less power than our budget of 2500 cycles (the UKF evaluates the model in double precision about 72,000 times per second, while the NMPC system will evaluate it in single precision something like 250,000 times per second).

AccelerationVector FixedWingFlightDynamicsModel::evaluate(
const State &in, const ControlVector &control) const {
    /* Cache state data for convenience */
    Quaternionr attitude = Quaternionr(in.attitude());
    real_t yaw_rate = in.angular_velocity()[2],
           pitch_rate = in.angular_velocity()[1],
           roll_ratre = in.angular_velocity()[0];

    /* External axes */
    Vector3r airflow, lift_axis, drag_axis, side_axis;
    real_t v;

    airflow = attitude * (in.wind_velocity() - in.velocity());
    v = airflow.norm();

    if (v < DYNAMICS_MIN_V || v > DYNAMICS_MAX_V) {
        /* Airflow too slow or too fast for any of this to work */
        return AccelerationVector();
    }

    /*
    Lift is always perpendicular to airflow, drag is always parallel, and
    side is always towards the starboard wing of the aircraft.
    */
    drag_axis = airflow / v;
    side_axis = Vector3r(0, 1, 0);
    lift_axis = drag_axis.cross(side_axis);

    /* Determine alpha and beta: alpha = atan(wz/wx), beta = atan(wy/|wxz|) */
    real_t alpha, beta,
           v2_axial = airflow.x() * airflow.x() + airflow.z() * airflow.z(),
           v2_side = airflow.x() * airflow.x() + airflow.y() * airflow.y(),
           qbar_alpha = RHO * v2_axial * 0.5, qbar_beta = RHO * v2_side * 0.5;
    alpha = atan2(-airflow.z(), -airflow.x()) / M_PI * 180.0;
    beta = atan2(-airflow.y(), sqrt(v2_axial)) / M_PI * 180.0;

    if (alpha < -20.0 || alpha > 20.0 || beta < -20.0 || beta > 20.0) {
        /* Alpha or beta completely out of range */
        return AccelerationVector();
    }

    /*
    Set up a^4, a^3, a^2, a, 1 so that we can use dot product for the
    polynomial evaluation.
    */
    Eigen::Matrix<real_t, 5, 1> alpha_pow;
    alpha_pow << pow(alpha, 4), pow(alpha, 3), pow(alpha, 2), alpha, 1.0;

    /*
    Determine control surface base force and moment contributions -- sum all
    the alpha polynomial coefficients to minimise the number of dot products.
    */
    Eigen::Matrix<real_t, 5, 1> cv_lift, cv_drag, cv_side, cv_yaw_moment,
                                cv_pitch_moment, cv_roll_moment;
    cv_lift.setZero();
    cv_drag.setZero();
    cv_side.setZero();
    cv_yaw_moment.setZero();
    cv_pitch_moment.setZero();
    cv_roll_moment.setZero();
    for (int8_t i = control.rows() - 1; i >= 0; i--) {
        if (i == motor_idx) {
            continue;
        }

        real_t cv = control[i];
        cv_lift += c_lift_alpha_cv.col(i) * cv;
        cv_drag += c_drag_alpha_cv.col(i) * cv;
        cv_side += c_side_alpha_cv.col(i) * cv;
        cv_yaw_moment += c_yaw_moment_alpha_cv.col(i) * cv;
        cv_pitch_moment += c_pitch_moment_alpha_cv.col(i) * cv;
        cv_roll_moment += c_roll_moment_alpha_cv.col(i) * cv;
    }

    /* Find total lift, drag, side force and moment coefficient values */
    real_t c_lift, c_drag, c_side, c_yaw_moment, c_pitch_moment,
           c_roll_moment;

    c_lift = alpha_pow.dot(c_lift_alpha + cv_lift) +
             c_lift_pitch_rate * pitch_rate;
    c_drag = alpha_pow.dot(c_drag_alpha + cv_drag);
    c_side = alpha_pow.dot(
        c_side_force_alpha_beta * beta +
        c_side_force_alpha_roll_rate * roll_rate +
        cv_side);
    c_yaw_moment = alpha_pow.dot(
        c_yaw_moment_alpha_beta * beta +
        c_yaw_moment_alpha_roll_rate * roll_rate +
        c_yaw_moment_alpha_yaw_rate * yaw_rate +
        cv_yaw_moment);
    c_roll_moment = alpha_pow.dot(
        c_roll_moment_alpha_beta * beta +
        c_roll_moment_alpha_roll_rate * roll_rate +
        c_roll_moment_alpha_yaw_rate * yaw_rate +
        cv_roll_moment);
    c_pitch_moment = alpha_pow.dot(c_pitch_moment_alpha + cv_pitch_moment) +
                     c_pitch_moment_pitch_rate * pitch_rate;

    /*
    Sum and apply forces and moments based on aerodynamic reference point.
    */
    Vector3r sum_force, sum_torque;
    Vector3r lift, drag, side, yaw_moment, pitch_moment, roll_moment;

    lift = c_lift * qbar_alpha * lift_axis;
    drag = c_drag * qbar_alpha * drag_axis;
    side = c_side * qbar_alpha * side_axis;

    yaw_moment = c_yaw_moment * qbar_alpha * Vector3r(0, 0, 1);
    pitch_moment = c_pitch_moment * qbar_alpha * Vector3r(0, 1, 0);
    roll_moment = c_roll_moment * qbar_alpha * Vector3r(1, 0, 0);

    sum_force = lift + drag + side;
    sum_torque = lift.cross(aero_ref_pos) + drag.cross(aero_ref_pos) +
                 yaw_moment + pitch_moment + roll_moment;

    /*
    Determine motor thrust and torque:
    https://www.grc.nasa.gov/WWW/Wright/airplane/propth.html

    Propeller thrust =
    F = 0.5 * rho * A * (Ve^2 - V0^2)
    where A = propeller disc area, Ve = exit velocity, V0 = air velocity

    In this formulation,
    F = 0.5 * rho * A * ((k * rpm)^2 - V0^2)

    Presumably the relationship between thrust and torque on airframe is
    constant? If so, they're related by prop_ct.
    */
    real_t thrust;
    if (motor_idx < control.rows()) {
        thrust = 0.5 * RHO * prop_area *
            (pow(prop_cve * control[motor_idx], 2) - pow(airflow.x(), 2));
        if (thrust < 0.0) {
            /* Folding prop, so no drag */
            thrust = 0.0;
        }

        /* Add motor thrust and torque to the net force */
        sum_force += thrust * motor_thrust;
        sum_torque += thrust * prop_ct * motor_moment;
    }

    /* Calculate linear acceleration (F / m) */
    AccelerationVector output;
    output.segment<3>(0) = sum_force * mass_inv +
                           attitude * Vector3r(0, 0, G_ACCEL);

    /* Calculate angular acceleration (tau / inertia tensor) */
    output.segment<3>(3) = inertia_tensor_inv * sum_torque;

    return output;
}

The next step was obtaining values for those coefficients. Ideally, this would be done in a wind tunnel, but we don’t have access to one; thus, we had to make do with simulation and confirmation based on flight data logs.

Initially we used a model built in XFLR5 to generate polar plots at varying values of α and β. Unfortunately, I wasn’t able to obtain a particularly wide range of data due to some simulation issues with the winglets and airfoil, so the model wasn't as comprehensive as I would have liked, and in particular the stability derivatives (yaw, pitch and roll moments due to yaw, pitch and roll rate) were almost completely absent. In addition, XFLR5 doesn’t appear to support elevon simulation, so I wasn’t able to determine the control coefficients.

Screenshot of XFLR5 simulation of X8, at 20m/s with β = 5.0°

Plugging the very approximate coefficients obtained from XFLR5 into the flight dynamics model and running through the artificial horizon test case resulted in something of an improvement in attitude accuracy:

Model ° Error at Percentile
Pitch Roll
50th 75th 95th RMS 50th 75th 95th RMS
None 4.0 6.6 13.4 6.5 4.5 7.8 14.6 7.3
Centripetal 2.6 4.4 9.3 5.4 3.0 5.4 12.8 7.7
Fixed-wing 2.2 3.7 7.2 4.3 2.7 4.7 8.3 6.1

While we’re still not at our minimum accuracy target of ±2° 75th percentile and ±5° 95th percentile error, we’re close enough that sensor alignment, calibration and vibration reduction should get us there.

github.com/sfwa twitter.com/sfwa_uav youtube.com/user/sfwavideo