Sylvestre SENGNA MVONDO
Published

Experimental Determination of a Fixed Wing UAV's drag polar

The goal of this project is to determine experimentally the drag polar of a fixed wing UAV with Commercial Of the Shelf (COTS) components.

AdvancedWork in progress163
Experimental Determination of a Fixed Wing UAV's drag polar

Things used in this project

Hardware components

Fixed Wing UAV Airframe (Easy Glider)
×1
Flight Controller (SpeedyBee F405 Wing)
×1
GPS receiver (generic)
×1
Airspeed sensor
×1
radio transmitter
×1

Software apps and online services

inav

Story

Read more

Schematics

Airframe Instrumentation

Equipping our Easy Glider with our SpeedyBee F405 Wing, GPS sensor and Airspeed sensor.

3d path of a glide

3d path of a glide reconstructed from the blackbox Data

Flight Analysis curves

Relevant data extracted from a glide

Analysis of a glide

Values computed by the homemade Python code for a give glide

Code

Origninal Flight Analysis Python code

Python
To use this code, the user must know the exact instants of the glides, the operating temperature, and the operating pressures. He must provide the CSV file from the black box and then enter manually, the initial times and end times of each glide. The code the produces for each glide, the figures with the 3d path and the flight analysis curves, all that with the average values and the computed Cd and CL gathered in a .txt file.
# -*- coding: utf-8 -*-
"""
Created on Thu Nov 13 09:25:21 2025

@author: Sylvestre
"""

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import csv

""" This code helps analysing the flight datas of a black box after a flight
flown with an INAV flashed Flight Controller.

In order to be used, the Blackbox decode tool must be used to generate
a .CSV file from the .txt log file available in the black box.

In order to use this Python program, place this Python file and the .CSV file
in the same directory. 
Then, replace "18_02_2026_flight.01.csv" by the name of your CSV file as the first 
argument of the function pd.read_csv() which is called a bit lower in the script.

You can then launch the program and call the function "flight_analysis" in the console.
Feel free to modify the program at your convenience, depending on the 
analysis you want to perform.

! If the file is not red properly, check if the delimiter argument of the function
pd.read_csv() actually matches the the delimiter of your CSV file. !

The blackbox decode tool can be downloaded here:
    
    https://github.com/iNavFlight/blackbox-tools/releases/tag/v9.0.0-rc1
    
a tutorial concerning the use of the tool can be found here:
    https://www.youtube.com/watch?v=9f0ApQtwop8
    
The docmentation concerning the Blackbox data fields can be found here:
    https://github.com/iNavFlight/inav/wiki/INAV-blackbox-variables
    
Enjoy your flight analyses !! """
    
""" Aircraft caracteristics """

AC_mass = 1.101 # aircraft mass in kg
g = 9.81 # acceleration of gravity in m.s^-2
AC_weight = AC_mass*g # aircraft weight in N
Wing_area = 0.333 # wing area in m^2
R_air = 287 # gas constant of air in J.K^-1.kg^-1


""" reading the log CSV file"""


fl_data = pd.read_csv("18_02_2026_flight.01.csv", delimiter = ",")


test = fl_data.loc[7]

time_samp = fl_data["time (us)"]


nb_values = len(time_samp)

"""This part is dedicated to extracting the relevant data in SI Units"""


""" extracting the time"""

t_init = time_samp[0]
T = np.ones(nb_values)
T = time_samp*T

time = (T-t_init)/1000000 #the time from the blackbox is given in micro seconds and does not start at 0

Current = fl_data["amperage (A)"] # Current taken from the battery in Amps
V_bat = fl_data["vbat (V)"] # battery instant voltage

Altitude_baro = fl_data["BaroAlt (cm)"]
Altitude_baro = Altitude_baro/100 # Barometer altitude in m

Mesured_airspeed = fl_data["AirSpeed"]
Mesured_airspeed = Mesured_airspeed/100 # Anemometer airspeed in m/s

Pitch_deg = fl_data["attitude[1]"]
Pitch_deg = Pitch_deg/(-10) # Pitch angle in degrees
Pitch_rad = Pitch_deg*np.pi/180

Bank_angle_deg = fl_data["attitude[0]"]
Bank_angle_deg = Bank_angle_deg/(10) # Bank angle in degrees
Bank_angle_rad = Bank_angle_deg*np.pi/180

Compas_heading_deg = fl_data["attitude[2]"]
Compas_heading_deg = Compas_heading_deg/(10) # Compas angle in degrees
Compas_heading_rad = Compas_heading_deg*np.pi/180


""" normalised vector representing the longitudinal axis of the AC"""

axis_AC = np.ones((nb_values,3))

axis_AC[:,0] = np.cos(Pitch_rad)*np.cos(Compas_heading_rad)
axis_AC[:,1] = np.cos(Pitch_rad)*np.sin(Compas_heading_rad)
axis_AC[:,2] = np.sin(Pitch_rad)

axis_AC_E = axis_AC[:,1] # The coordinates of the unit vector orienting the longitudinal axis of the plane
axis_AC_N = axis_AC[:,0] # in the ENU coordinate system
axis_AC_U = axis_AC[:,2]

""" Wind caracteristics """

V_wind = np.ones((nb_values,3))

V_wind[:,0] = fl_data["wind[0]" ]/100 #unlike what is said in the doc, the wind velocity of the blackbox is in cm/s
V_wind[:,1] = fl_data["wind[1]" ]/100
V_wind[:,2] = fl_data["wind[2]" ]/100

V_wind_extract = V_wind[::10,:]

Wind_vel = fl_data["wind[1]" ]



""" pitot speed"""

pitot_velocity = fl_data["AirSpeed"]/100

""" nav position in the coordinate system (Nord, East, Up). The coordinates are given in meters and taken in comparison to a reference point """

Nav_pos = np.ones((nb_values,3))

Nav_pos[:,0] = fl_data["navPos[0]" ]/100
Nav_pos[:,1] = fl_data["navPos[1]" ]/100
Nav_pos[:,2] = fl_data["navPos[2]" ]/100

x = Nav_pos[:,1] # The Nav position and velocity coordinates and the wind
y = Nav_pos[:,0] # velocity are given in the NEU coordinate system in the
z = Nav_pos[:,2] # ground referential. To respect the right hand rule, I rather
                # plot it in a ENU coordinate system


""" nav velocity in the coordinate system (Nord, East, Up). The coordinates are given in meters and taken in comparison to a reference point """
Nav_vel = np.ones((nb_values,3))

Nav_vel[:,0] = fl_data["navVel[0]" ]/100
Nav_vel[:,1] = fl_data["navVel[1]" ]/100
Nav_vel[:,2] = fl_data["navVel[2]" ]/100

u = Nav_vel[:,1]
v = Nav_vel[:,0] 
w = Nav_vel[:,2]
nav_vel_norm = np.sqrt(u**2 + v**2 + w**2)

normed_u = u/nav_vel_norm
normed_v = v/nav_vel_norm
normed_w = w/nav_vel_norm

""" targeted position in the coordinate system (Nord, East, Up). The coordinates are given in meters and taken in comparison to a reference point """

Consigne_pos = np.ones((nb_values,3))

Consigne_pos[:,0] = fl_data["navTgtPos[0]" ]/100
Consigne_pos[:,1] = fl_data["navTgtPos[1]" ]/100
Consigne_pos[:,2] = fl_data["navTgtPos[2]" ]/100

x_cons = Consigne_pos[:,1] # The NavTgt position and velocity coordinates and the wind
y_cons = Consigne_pos[:,0] # velocity are given in the NEU coordinate system in the
z_cons = Consigne_pos[:,2] # ground referential. To respect the right hand rule, I rather
                # plot it in a ENU coordinate system


""" desired velocity in the coordinate system (Nord, East, Up). The coordinates are given in meters and taken in comparison to a reference point """
Consigne_vel = np.ones((nb_values,3))

Consigne_vel[:,0] = fl_data["navTgtVel[0]" ]/100
Consigne_vel[:,1] = fl_data["navTgtVel[1]" ]/100
Consigne_vel[:,2] = fl_data["navTgtVel[2]" ]/100

u_cons = Consigne_vel[:,1]
v_cons = Consigne_vel[:,0] 
w_cons = Consigne_vel[:,2]

""" Pitch angle commands"""

Stick_Pitch_command = fl_data["rcData[1]" ]
FC_Pitch_command = fl_data["rcCommand[1]" ]

""" wind velocity in the coordinate system (Nord, East, Up). The coordinates are given in meters and taken in comparison to a reference point """

u_wind = V_wind[:,1]
v_wind = V_wind[:,0] 
w_wind = V_wind[:,2]

""" horizontal and vertical standard deviations """

hor_error = fl_data["navEPH" ]
ver_error = fl_data["navEPV" ]

""" Computed wind velocity relative to the AC the ENU coordinate system"""

V_wind_rel_AC = V_wind - Nav_vel

V_wind_rel_AC_E = V_wind_rel_AC[:,1]
V_wind_rel_AC_N = V_wind_rel_AC[:,0]
V_wind_rel_AC_U = V_wind_rel_AC[:,2]

Computed_Airspeed = np.sqrt(V_wind_rel_AC_E**2 + V_wind_rel_AC_N**2 + V_wind_rel_AC_U**2)

n_V_wind_rel_AC_E = V_wind_rel_AC_E/Computed_Airspeed# coordinates of the normalised version
n_V_wind_rel_AC_N = V_wind_rel_AC_N/Computed_Airspeed
n_V_wind_rel_AC_U = V_wind_rel_AC_U/Computed_Airspeed

""" Computation of the angle of attack"""

Climb_angle_rad = np.arctan(w/pitot_velocity )
Climb_angle_deg = Climb_angle_rad*180/np.pi

AOA_rad = Pitch_rad - Climb_angle_rad
AOA_deg = AOA_rad*180/np.pi

hybrid_airspeed= pitot_velocity/np.cos(AOA_rad)
#AOA_2_rad = np.arccos(pitot_velocity/Computed_Airspeed)
#AOA_2_deg = AOA_2_rad*180/np.pi



""" Flight modes and states """

FM_Flags = fl_data["flightModeFlags (flags)" ] # Active Flight Modes
State_Flags = fl_data["stateFlags (flags)" ] # Active Control states

def flight_analysis ():
    
    
    """ selecting the flight portion to study"""
    
    again = 1 
    
    print("enter the desired name for CSV file of the session ")
    name_csv_2 = input()
    name_csv = name_csv_2 + ".csv"
    
    print("enter the operating temperature in °C ")
    Temp = float(input())+273 # Operating temperature in K
    
    print("enter the operating Pressure in hPa ")
    Press = float(input())*100 # Operating pressure in Pa
    
    with open(name_csv, "a") as file_to_write:
        file_writer = csv.writer(file_to_write, delimiter=";")
        file_writer.writerow(["Date", "glide_id", "Avg_AOA °", "dev_AOA °", "Cd", "err_Cd", "CL", "CL/Cd", "Comment", "CL^2", "Temperature"+str(Temp)+" °C", "Pressure"+str(Press/100)+" hPa" ])
    
    while again > 0:
        
    
        print ("enter the initial time in second: ")
        tdeb_seq = input()
        tdeb_seq = float(tdeb_seq)
        dt = 0.1
        deb_seq = np.where((time >= tdeb_seq) & (time < tdeb_seq+dt))
        #print (deb_seq)
        index_start= deb_seq[0][0]
        
        print ("enter the final time in second: ")
        tfin_seq = float(input())
        fin_seq = np.where((time > tfin_seq-dt) & (time <= tfin_seq))
        #print (fin_seq)
        index_end= fin_seq[0][-1]
        
    
        
        print("enter the desired name for the report file ")
        Report_file_name = input() # This name will be used for the files
        name_3d_plot = Report_file_name + "_3d_path.png"
        name_2d_plots = Report_file_name + "gliding_analysis.png"
        report_name = Report_file_name + ".txt"
        
        print ("any comment ?")
        Comment = input()
        
        rho = Press/(R_air*Temp)
        
        
        index_3djump_step = 15 # to simplyfing the plotting
        index_jump_step = 3 # to simplyfing the plotting
        
        Nav_pos_seq = Nav_pos[index_start:index_end,:]
        Nav_vel_seq = Nav_pos[index_start:index_end,:]
        
        Nav_pos_extract = Nav_pos[index_start:index_end:index_jump_step,:]
        Nav_vel_extract = Nav_pos[index_start:index_end:index_jump_step,:]
        Nav_pos_3dextract = Nav_pos[index_start:index_end:index_3djump_step,:]
        Nav_vel_3dextract = Nav_pos[index_start:index_end:index_3djump_step,:]
        
        time_seq = time[index_start:index_end]
        time_extract = time[index_start:index_end:index_jump_step]
        time_3dextract = time[index_start:index_end:index_3djump_step]
        nb_sample = len(time_extract)
        
        pitot_vel_seq = pitot_velocity[index_start:index_end]
        pitot_vel_extract = pitot_velocity[index_start:index_end:index_jump_step]
        pitot_vel_3dextract = pitot_velocity[index_start:index_end:index_3djump_step]
        
        Pitch_deg_seq = Pitch_deg[index_start:index_end]
        Bank_angle_deg_seq = Bank_angle_deg[index_start:index_end]
        Bank_angle_rad_seq = Bank_angle_rad[index_start:index_end]
        
        
        Pitch_deg_extract = Pitch_deg[index_start:index_end:index_jump_step]
        Bank_angle_deg_extract = Bank_angle_deg[index_start:index_end:index_jump_step]
        Bank_angle_rad_extract = Bank_angle_rad[index_start:index_end:index_jump_step]
        Pitch_deg_3dextract = Pitch_deg[index_start:index_end:index_3djump_step]
        Bank_angle_deg_3dextract = Bank_angle_deg[index_start:index_end:index_3djump_step]
        Bank_angle_rad_3dextract = Bank_angle_rad[index_start:index_end:index_3djump_step]
        
        
        x_seq = x[index_start:index_end]
        y_seq = y[index_start:index_end]
        z_seq = z[index_start:index_end]
        height_asfc_seq = z_seq - z[0] 
        
        x_extract = x[index_start:index_end:index_jump_step]
        y_extract = y[index_start:index_end:index_jump_step]
        z_extract = z[index_start:index_end:index_jump_step]
        height_asfc_extract = z_extract - z[0] 
        x_3dextract = x[index_start:index_end:index_3djump_step]
        y_3dextract = y[index_start:index_end:index_3djump_step]
        z_3dextract = z[index_start:index_end:index_3djump_step]
        height_asfc_3dextract = z_3dextract - z[0] 
        
        
        axis_AC_E_seq = axis_AC_E[index_start:index_end]
        axis_AC_N_seq = axis_AC_N[index_start:index_end]
        axis_AC_U_seq = axis_AC_U[index_start:index_end]
        
        axis_AC_E_extract = axis_AC_E[index_start:index_end:index_jump_step]
        axis_AC_N_extract = axis_AC_N[index_start:index_end:index_jump_step]
        axis_AC_U_extract = axis_AC_U[index_start:index_end:index_jump_step]
        axis_AC_E_3dextract = axis_AC_E[index_start:index_end:index_3djump_step]
        axis_AC_N_3dextract = axis_AC_N[index_start:index_end:index_3djump_step]
        axis_AC_U_3dextract = axis_AC_U[index_start:index_end:index_3djump_step]
        
        
        u_seq = u[index_start:index_end]
        v_seq = v[index_start:index_end]
        w_seq = w[index_start:index_end]
        nav_vel_norm_seq = nav_vel_norm[index_start:index_end]
        
        u_extract = u[index_start:index_end:index_jump_step]
        v_extract = v[index_start:index_end:index_jump_step]
        w_extract = w[index_start:index_end:index_jump_step]
        nav_vel_norm_extract = nav_vel_norm[index_start:index_end:index_jump_step]
        u_3dextract = u[index_start:index_end:index_3djump_step]
        v_3dextract = v[index_start:index_end:index_3djump_step]
        w_3dextract = w[index_start:index_end:index_3djump_step]
        nav_vel_norm_3dextract = nav_vel_norm[index_start:index_end:index_3djump_step]
        
        
        V_wind_rel_AC_seq = V_wind_rel_AC[index_start:index_end,:]
        V_wind_rel_AC_E_seq = V_wind_rel_AC_E[index_start:index_end]
        V_wind_rel_AC_N_seq = V_wind_rel_AC_N[index_start:index_end]
        V_wind_rel_AC_U_seq = V_wind_rel_AC_U[index_start:index_end]
        
        V_wind_rel_AC_extract = V_wind_rel_AC[index_start:index_end:index_jump_step,:]
        V_wind_rel_AC_E_extract = V_wind_rel_AC_E[index_start:index_end:index_jump_step]
        V_wind_rel_AC_N_extract = V_wind_rel_AC_N[index_start:index_end:index_jump_step]
        V_wind_rel_AC_U_extract = V_wind_rel_AC_U[index_start:index_end:index_jump_step]
        V_wind_rel_AC_3dextract = V_wind_rel_AC[index_start:index_end:index_3djump_step,:]
        V_wind_rel_AC_E_3dextract = V_wind_rel_AC_E[index_start:index_end:index_3djump_step]
        V_wind_rel_AC_N_3dextract = V_wind_rel_AC_N[index_start:index_end:index_3djump_step]
        V_wind_rel_AC_U_3dextract = V_wind_rel_AC_U[index_start:index_end:index_3djump_step]
        
        
        
        x_cons_seq = x_cons[index_start:index_end]
        y_cons_seq = y_cons[index_start:index_end]
        z_cons_seq = z_cons[index_start:index_end]
        
        x_cons_extract = x_cons[index_start:index_end:index_jump_step]
        y_cons_extract = y_cons[index_start:index_end:index_jump_step]
        z_cons_extract = z_cons[index_start:index_end:index_jump_step]
        
        
        
        u_cons_seq = u_cons[index_start:index_end]
        v_cons_seq = v_cons[index_start:index_end]
        w_cons_seq = w_cons[index_start:index_end]
        
        u_cons_extract = u_cons[index_start:index_end:index_jump_step]
        v_cons_extract = v_cons[index_start:index_end:index_jump_step]
        w_cons_extract = w_cons[index_start:index_end:index_jump_step]
        u_cons_3dextract = u_cons[index_start:index_end:index_3djump_step]
        v_cons_3dextract = v_cons[index_start:index_end:index_3djump_step]
        w_cons_3dextract = w_cons[index_start:index_end:index_3djump_step]
        
        Stick_Pitch_command_seq = Stick_Pitch_command[index_start:index_end]
        FC_Pitch_command_seq = FC_Pitch_command[index_start:index_end]
        Stick_Pitch_command_extract = Stick_Pitch_command[index_start:index_end:index_jump_step]
        FC_Pitch_command_extract = FC_Pitch_command[index_start:index_end:index_jump_step]
        Stick_Pitch_command_3dextract = Stick_Pitch_command[index_start:index_end:index_3djump_step]
        FC_Pitch_command_3dextract = FC_Pitch_command[index_start:index_end:index_3djump_step]
        
        
        u_wind_seq = u_wind[index_start:index_end]
        v_wind_seq = v_wind[index_start:index_end]
        w_wind_seq = w_wind[index_start:index_end]
        
        u_wind_extract = u_wind[index_start:index_end:index_jump_step]
        v_wind_extract = v_wind[index_start:index_end:index_jump_step]
        w_wind_extract = w_wind[index_start:index_end:index_jump_step]
        u_wind_3dextract = u_wind[index_start:index_end:index_3djump_step]
        v_wind_3dextract = v_wind[index_start:index_end:index_3djump_step]
        w_wind_3dextract = w_wind[index_start:index_end:index_3djump_step]
        
        Computed_Airspeed_seq = Computed_Airspeed[index_start:index_end]
        Computed_Airspeed_extract = Computed_Airspeed[index_start:index_end:index_jump_step]
        Computed_Airspeed_3dextract = Computed_Airspeed[index_start:index_end:index_3djump_step]
        
        hybrid_airspeed_seq = hybrid_airspeed[index_start:index_end]
        hybrid_airspeed_extract = hybrid_airspeed[index_start:index_end:index_jump_step]
        hybrid_airspeed_3dextract = hybrid_airspeed[index_start:index_end:index_3djump_step]
        
        AC_coord_syst_extract = np.ones((nb_sample,6))
        AC_ey_extract = np.ones((nb_sample,3))
        AC_ez_extract = np.ones((nb_sample,3))
        AC_coord_syst_3dextract = np.ones((nb_sample,6))
        AC_ey_3dextract = np.ones((nb_sample,3))
        AC_ez_3dextract = np.ones((nb_sample,3))
        
        AC_wing_axis_extract = np.ones((nb_sample,3))
        AC_seat_axis_extract = np.ones((nb_sample,3))
        AC_wing_axis_3dextract = np.ones((nb_sample,3))
        AC_seat_axis_3dextract = np.ones((nb_sample,3))
        
        
    
        
        Climb_angle_deg_seq= Climb_angle_deg[index_start:index_end]
        Climb_angle_deg_extract= Climb_angle_deg[index_start:index_end:index_jump_step]
        Climb_angle_deg_3dextract= Climb_angle_deg[index_start:index_end:index_3djump_step]
        
        AOA_deg_seq= AOA_deg[index_start:index_end]
        AOA_deg_extract= AOA_deg[index_start:index_end:index_jump_step]
        AOA_deg_3dextract= AOA_deg[index_start:index_end:index_3djump_step]
        #AOA_2_deg_extract= AOA_2_deg[index_start:index_end:index_jump_step]
        
    
        
        avg_speed = np.mean(pitot_vel_seq)
        speed_ecart = np.std(pitot_vel_seq)
        
        avg_vert_speed = np.mean(w_seq)
        vert_speed_ecart = np.std(w_seq)
        
        avg_pitch = np.mean(Pitch_deg_seq)
        pitch_ecart = np.std(Pitch_deg_seq)
        
        avg_bank = np.mean(Bank_angle_deg_seq)
        bank_ecart = np.std(Bank_angle_deg_seq)
        
        avg_aoa = np.mean(AOA_deg_seq)
        aoa_ecart = np.std(AOA_deg_seq)
        
        Cd= 2* AC_weight*(-1)*avg_vert_speed/(rho*Wing_area*avg_speed**(3))
        Cl= 2* AC_weight/(rho*Wing_area*avg_speed**(2))
     
        avg_speed = round(avg_speed, 2)
        speed_ecart = round(speed_ecart,2)
        
        avg_vert_speed = round(avg_vert_speed, 2)
        vert_speed_ecart = round(vert_speed_ecart, 2)
        
        avg_pitch = round(avg_pitch, 2)
        pitch_ecart = round(pitch_ecart, 2)
        
        avg_bank = round(avg_bank, 2)
        bank_ecart = round(bank_ecart, 2)
        
        avg_climb_anglee = round(avg_aoa, 2)
        climb_angle_ecart = round(aoa_ecart, 2)
        
        avg_aoa = round(avg_aoa, 2)
        aoa_ecart = round(aoa_ecart, 2)
        
        Cd= round(Cd, 6)
        Cl = round(Cl, 6)
        finesse= Cl/Cd
        
        
        print(f"Average Airspeed: {avg_speed} +- {speed_ecart} m/s \n")
        print(f"Average Vertical Speed: {avg_vert_speed} m/s +- {vert_speed_ecart} m/s \n")
        print(f"Average Pitch Angle: {avg_pitch}° +- {pitch_ecart}° \n")
        print(f"Average Bank Angle: {avg_bank}° +- {bank_ecart}° \n")
        print(f"Average Angle of Attack: {avg_aoa}° +- {aoa_ecart}° \n")
        print(f"Cd = {Cd} \n")
        print(f"Cl = {Cl} \n")
        print(f"Cl/Cd = {Cl/Cd} \n")
        
        
        
        """ 3d plotting"""
        
        plt.figure(1)
        ax = plt.figure(figsize=(8, 8)).add_subplot(projection='3d')
        ax.plot(x_3dextract, y_3dextract, z_3dextract, label='nav trajectory')
        #ax.plot(x_cons_extract, y_cons_extract, z_cons_extract, color ="black", label='Targeted trajectory')
        
        ax.quiver(x_3dextract, y_3dextract, z_3dextract, axis_AC_E_3dextract, axis_AC_N_3dextract, axis_AC_U_3dextract, length=4, color="purple", label='Aicraft fuselage')
        
        ax.quiver(x_3dextract, y_3dextract, z_3dextract, u_wind_3dextract, v_wind_3dextract, w_wind_3dextract, length=0.5, color="green", label='Wind velocity')
        
        #ax.quiver(x_extract, y_extract, z_extract, AC_wing_axis_E_extract, AC_wing_axis_N_extract, AC_wing_axis_U_extract, length=5, color="black", label='Aircraft wings')
        
        ax.set_xlabel("Est direction (m)")
        ax.set_ylabel("Nord direction (m)")
        ax.set_zlabel("Height (m)")
        ax.set_title("AC position tracking")
        ax.legend(loc="upper left")
        plt.tight_layout(pad=1, w_pad=1, h_pad=1)
        plt.savefig(name_3d_plot)
        plt.show()
        
        
        
        
        """ 2d plotting"""
        
        
        plt.figure(2)
        fig,(attitudes, H, vz, vx, aoa, pitch_command) = plt.subplots(6, figsize=(15, 25))
    
        #pitch_command.plot(time_extract, Stick_Pitch_command_extract, label='Stick pitch command')
        pitch_command.plot(time_extract, FC_Pitch_command_extract, label='FC pitch command')
        pitch_command.set_xlabel("time (s)", fontsize = 14)
        pitch_command.set_ylabel("Pitch command", fontsize = 14)
        pitch_command.set_title("Horizontal stab input signal", fontsize = 14)
        pitch_command.legend()
    
    
    
        attitudes.plot(time_extract, Pitch_deg_extract, label='Pitch Angle')
        attitudes.plot(time_extract, Bank_angle_deg_extract, label='Bank Angle')
        attitudes.set_xlabel("time (s)", fontsize = 14)
        attitudes.set_ylabel("Angles in °", fontsize = 14)
        attitudes.set_title("Attitude", fontsize = 14)
        attitudes.legend()
        
        H.plot(time_extract, z_extract, label='Altitude (ASFC)')
        H.set_xlabel("time (s)", fontsize = 14)
        H.set_ylabel("Height (m)", fontsize = 14)
        H.set_title("Attitude (ASFC)", fontsize = 14)
        H.legend()
        
        
        vz.plot(time_extract, w_extract, label='Sink rate')
        vz.set_xlabel("time (s)", fontsize = 14)
        vz.set_ylabel("vz (m/s)", fontsize = 14)
        vz.set_title("Vertical velocity", fontsize = 14)
        vz.legend()
        
        vx.plot(time_extract, pitot_vel_extract, label='Pitot velocity')
        vx.set_xlabel("time (s)", fontsize = 14)
        vx.set_ylabel("Pitot speed (m/s)", fontsize = 14)
        vx.set_title("Air Speed", fontsize = 14)
        vx.legend()
        
        aoa.plot(time_extract, AOA_deg_extract, label='AOA')
        aoa.plot(time_extract, Climb_angle_deg_extract, label='gamma')
        aoa.set_xlabel("time (s)", fontsize = 14)
        aoa.set_ylabel("Angle of Attack in °", fontsize = 14)
        aoa.set_title("Angle of Attack (when angle of incidence = 0)", fontsize = 14)
        aoa.legend()
            
        fig.tight_layout()
        plt.savefig(name_2d_plots)
        plt.show()
        
        with open(report_name, "w") as report:
          
          report.write(f"The sequece begins at {tdeb_seq}s in the blackbox log \n")
          report.write(f"Duration: {round(tfin_seq - tdeb_seq, 2)}s \n\n")
          report.write(f"Operating Temperature: {round(Temp -273, 2)}°C \n")
          report.write(f"Operating Pressure: {round(Press/100,2)}hPa \n")
          report.write(f"Aircraft wing area: {round(Wing_area,2)} m^2 \n\n")
          
          report.write(f"Average Airspeed: {avg_speed} +-{speed_ecart} m/s \n")
          report.write(f"Average Vertical Speed: {avg_vert_speed} m/s +-{vert_speed_ecart} m/s \n")
          report.write(f"Average Pitch Angle: {avg_pitch}° +-{pitch_ecart}° \n")
          report.write(f"Average Bank Angle: {avg_bank}° +-{bank_ecart}° \n")
          report.write(f"Average Angle of Attack: {avg_aoa}° +-{aoa_ecart}° \n")
          report.write(f"Cd = {Cd} \n")
          report.write(f"Cl = {Cl} \n")
          report.write(f"Cl/Cd = {round(Cl/Cd,3)} \n") 
        
        with open(name_csv, "a") as file_to_write:
            file_writer = csv.writer(file_to_write, delimiter=";")
            file_writer.writerow([name_csv_2, Report_file_name, str(avg_aoa), str(aoa_ecart), str(Cd), "_", str(Cl), str(Cl/Cd), Comment, str(Cl**2)])  
        
        print("Do you want to analyse another sequence? anser YES or NO")
        
        answer = input()
        
        if answer == "YES":
            again +=1 
            
        else:
            again = 0
      
    
   

Optimised code (optimised with I.A.)

Python
This code has the same function as the previous one. But here, the user only feeds the operating temperature, operating pressure and the desired names for the output files. This codes automatically identifies the glides in the CSV and produces the interesting documents.
# -*- coding: utf-8 -*-
"""
Created on Sun Feb 22 15:27:55 2026

@author: Sylvestre
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.constants import g

class AeroFlightAnalyzer:
    def __init__(self, mass=1.101, wing_area=0.33):
        self.mass = mass
        self.S = wing_area
        self.weight = mass * g

    def process_flight_log(self, csv_input, temp_c, press_hpa, output_base):
        # 1. Chargement des données
        df = pd.read_csv(csv_input, sep=None, engine='python')
        
        # Conversions et Préparation
        df['t'] = (df['time (us)'] - df['time (us)'].iloc[0]) / 1e6
        df['v_tas'] = df['AirSpeed'] / 100.0
        df['v_z'] = df['navVel[2]'] / 100.0 
        df['pitch'] = df['attitude[1]'] / -10.0 
        df['roll'] = df['attitude[0]'] / 10.0
        df['yaw'] = df['attitude[2]'] / 10.0
        
        # Coordonnées ENU
        df['E'] = (df['navPos[1]'] - df['navPos[1]'].iloc[0]) / 100.0
        df['N'] = (df['navPos[0]'] - df['navPos[0]'].iloc[0]) / 100.0
        df['U'] = df['BaroAlt (cm)'] / 100.0
        
        # Vent
        df['wind_N'] = df['wind[0]'] / 100.0
        df['wind_E'] = df['wind[1]'] / 100.0
        df['wind_U'] = df['wind[2]'] / 100.0

        rho = (press_hpa * 100) / (287.05 * (temp_c + 273.15))
        summary_list = []

        # 2. Détection des glides (Filtre Flight Mode + Moteur)
        mask = (df['motor[0]'] < 1050) & (df['v_tas'] > 5) & (df['roll'].abs() < 10)
        if 'flightModeFlags' in df.columns:
            f_modes = df['flightModeFlags'].astype(str).str.upper()
            mask &= (~f_modes.str.contains("MANUAL"))
            mask &= (~f_modes.str.contains("NAV WP"))
            mask &= (~f_modes.str.contains("WAYPOINT"))
        
        df['block'] = (mask != mask.shift()).cumsum()
        
        # On ne garde que les blocs de plus de 25s
        raw_glides = [group for _, group in df[mask].groupby('block') if (group['t'].max() - group['t'].min()) >= 25]

        for i, full_seq in enumerate(raw_glides, 1):
            # --- APPLICATION DU PADDING (On retire les 2 premières secondes) ---
            start_time = full_seq['t'].min() + 2.0
            seq = full_seq[full_seq['t'] >= start_time].copy()
            
            if seq.empty: continue

            # Calcul des angles de flux
            climb_angle_deg = np.degrees(np.arcsin(seq['v_z'] / seq['v_tas']))
            aoa_seq = seq['pitch'] - climb_angle_deg
            
            # --- Graphique 3D avec légende "Path" ---
            self._plot_3d(seq, i, output_base)

            # --- Graphiques 2D ---
            self._plot_history(seq, aoa_seq, climb_angle_deg, i, output_base)
            
            # --- Calculs Aérodynamiques ---
            avg_vtas = seq['v_tas'].mean()
            avg_vz_abs = abs(seq['v_z'].mean())
            gamma_rad = np.arcsin(seq['v_z'].mean() / avg_vtas)
            q = 0.5 * rho * avg_vtas**2
            
            cd_pwr = (self.weight * avg_vz_abs) / (q * self.S * avg_vtas)
            cd_frc = (self.weight * np.sin(abs(gamma_rad))) / (q * self.S)
            cl = (self.weight * np.cos(gamma_rad)) / (q * self.S)

            # --- Résumé pour CSV ---
            summary_list.append({
                'Date': output_base,
                'glide_id': f"{output_base}_glide_{i}",
                'Avg_AOA °': aoa_seq.mean(),
                'dev_AOA °': aoa_seq.std(),
                'Cd_pwr': cd_pwr,
                'Cd_frc': cd_frc,
                'CL': cl,
                'CL/Cd': cl/cd_frc,
                'CL2': cl**2
            })

            # --- Rapport TXT exhaustif ---
            self._write_txt_report(seq, aoa_seq, i, output_base, temp_c, press_hpa, cd_pwr, cd_frc, cl)

        self._export_summary_csv(summary_list, output_base)
        print(f"Traitement terminé : {len(summary_list)} glides analysés (avec retrait des 2s de transition).")

    def _plot_3d(self, seq, i, name):
        fig = plt.figure(figsize=(12, 10))
        ax = fig.add_subplot(111, projection='3d')
        
        # Ajout du "Path" dans la légende
        ax.plot(seq['E'], seq['N'], seq['U'], color='black', lw=1, alpha=0.4, label='Path')
        
        step = max(1, len(seq) // 38)
        sv = seq.iloc[::step]
        L = 18.0
        ax.quiver(sv['E'], sv['N'], sv['U'], 
                  L*np.cos(np.radians(sv['pitch']))*np.sin(np.radians(sv['yaw'])),
                  L*np.cos(np.radians(sv['pitch']))*np.cos(np.radians(sv['yaw'])),
                  L*np.sin(np.radians(sv['pitch'])), color='purple', lw=1.6, label='Aircraft Orientation')
        
        ax.quiver(sv['E'], sv['N'], sv['U'], sv['wind_E'], sv['wind_N'], sv['wind_U'], 
                  color='green', alpha=0.6, label='Wind Direction')
        
        ax.set_title(f"3D Vectorial Trajectory - Glide {i}")
        ax.set_xlabel("East (m)"); ax.set_ylabel("North (m)"); ax.set_zlabel("Altitude (m)")
        ax.legend()
        plt.savefig(f"{name}_glide_{i}_3D.png"); plt.close()

    def _plot_history(self, seq, aoa, gamma, i, name):
        fig = plt.figure(figsize=(15, 12))
        fig.suptitle(f"Time History - Glide {i} (Stable Phase)", fontsize=16)
        ax1 = plt.subplot(321); ax1.plot(seq['t'], seq['v_tas'], 'b'); ax1.set_title("Airspeed (m/s)")
        ax2 = plt.subplot(322); ax2.plot(seq['t'], seq['U'], 'g'); ax2.set_title("Altitude (m)")
        ax3 = plt.subplot(323); ax3.plot(seq['t'], seq['pitch'], label='Pitch Angle'); ax3.plot(seq['t'], seq['roll'], label='Bank Angle')
        ax3.set_title("Attitudes (deg)"); ax3.legend()
        ax4 = plt.subplot(324); ax4.plot(seq['t'], seq['v_z'], 'r'); ax4.set_title("Vertical Speed (m/s)")
        ax5 = plt.subplot(313); ax5.plot(seq['t'], aoa, label='Angle of Attack'); ax5.plot(seq['t'], gamma, label='Climb angle')
        ax5.set_title("Flow Angles (deg)"); ax5.legend()
        for ax in [ax1, ax2, ax3, ax4, ax5]: ax.set_xlabel("Time (s)"); ax.grid(True)
        plt.tight_layout(rect=[0, 0.03, 1, 0.95]); plt.savefig(f"{name}_glide_{i}_history.png"); plt.close()

    def _write_txt_report(self, seq, aoa, i, name, temp, press, cd_pwr, cd_frc, cl):
        with open(f"{name}_glide_{i}.txt", 'w', encoding='utf-8') as f:
            f.write(f"The sequence begins at {round(seq['t'].min(),1)}s in the blackbox log (after 2s stabilization) \n")
            f.write(f"Duration: {round(seq['t'].max()-seq['t'].min(),1)}s \n\n")
            f.write(f"Operating Temperature: {temp}°C \n")
            f.write(f"Operating Pressure: {press}hPa \n")
            f.write(f"Aircraft wing area: {self.S} m^2 \n\n")
            f.write(f"Average Airspeed: {round(seq['v_tas'].mean(),2)} +- {round(seq['v_tas'].std(),2)} m/s \n")
            f.write(f"Average Vertical Speed: {round(seq['v_z'].mean(),2)} m/s +- {round(seq['v_z'].std(),2)} m/s \n")
            f.write(f"Average Pitch Angle: {round(seq['pitch'].mean(),2)}° +- {round(seq['pitch'].std(),2)}° \n")
            f.write(f"Average Bank Angle: {round(seq['roll'].mean(),2)}° +- {round(seq['roll'].std(),2)}° \n")
            f.write(f"Average Angle of Attack: {round(aoa.mean(),2)}° +- {round(aoa.std(),2)}° \n\n")
            f.write(f"Cd (Power Balance) = {round(cd_pwr,6)} \n")
            f.write(f"Cd (Force Balance) = {round(cd_frc,6)} \n")
            f.write(f"Cl = {round(cl,6)} \n")
            f.write(f"Cl/Cd (Force) = {round(cl/cd_frc,3)} \n")

    def _export_summary_csv(self, data, name):
        df_res = pd.DataFrame(data)
        cols = ['Avg_AOA °', 'dev_AOA °', 'Cd_pwr', 'Cd_frc', 'CL', 'CL/Cd', 'CL2']
        for col in cols:
            df_res[col] = df_res[col].apply(lambda x: f"{x:.6f}".replace('.', ','))
        df_res.to_csv(f"{name}_summary.csv", index=False, sep=';')

if __name__ == "__main__":
    analyzer = AeroFlightAnalyzer()
    analyzer.process_flight_log("18_02_2026_flight.01.csv", 2.0, 1014.0, "IA_18_02_2026")

Credits

Sylvestre SENGNA MVONDO
1 project • 0 followers

Comments