# generate_V_n_diagram.py
#
# Created: Nov 2018, S. Karpuk
# Modified:
# ----------------------------------------------------------------------------------------------------------------------
# Imports
# ----------------------------------------------------------------------------------------------------------------------
# RCAIDE Imports
import RCAIDE
from RCAIDE.Framework.Core import Data
from RCAIDE.Framework.Core import Units
from RCAIDE.Framework.Mission.Common import Results
# package imports
import numpy as np
import matplotlib.pyplot as plt
# ----------------------------------------------------------------------------------------------------------------------
# Compute a V-n diagram
# ----------------------------------------------------------------------------------------------------------------------
[docs]
def generate_V_n_diagram(vehicle,analyses,altitude,delta_ISA):
"""
Computes a V-n (velocity-load factor) diagram for an aircraft according to FAR requirements.
Parameters
----------
vehicle : Vehicle
The vehicle instance containing:
- reference_area : float
Wing reference area [m²]
- maximum_lift_coefficient : float
Maximum lift coefficient
- minimum_lift_coefficient : float
Minimum lift coefficient
- chords.mean_aerodynamic : float
Mean aerodynamic chord [m]
- flight_envelope : Data
Container with:
- FAR_part_number : str
'23' or '25' for certification category
- category : str
For Part 23: 'normal', 'utility', 'acrobatic', or 'commuter'
- design_mach_number : float
Cruise Mach number
- positive_limit_load : float
Positive load factor limit
- negative_limit_load : float
Negative load factor limit
analyses : Analyses
Container with atmosphere and aerodynamic analyses
altitude : float
Analysis altitude [m]
delta_ISA : float
Temperature offset from ISA conditions [K]
Returns
-------
V_n_data : Data
Container of V-n diagram data including:
- limit_loads : Data
Positive and negative load factor limits
- airspeeds : Data
Critical airspeeds (Vs, Va, Vb, Vc, Vd)
- gust_load_factors : Data
Load factors from gust conditions
- load_factors : Data
Complete set of load factors vs velocity
Notes
-----
Computes the following critical speeds and conditions:
* Vs1: Stall speed
* Va: Maneuvering speed
* Vb: Design speed for maximum gust intensity
* Vc: Design cruise speed
* Vd: Design diving speed
**Major Assumptions**
* Quasi-steady aerodynamics
* Linear lift curve slope
* Rigid aircraft structure
* Standard atmosphere modified by altitude and delta_ISA
**Theory**
Load factor limits are determined by:
.. math::
n = \\frac{L}{W} = \\frac{\\rho V^2 S C_L}{2W}
Gust loads are computed using:
.. math::
\\Delta n = \\frac{K_g U_de V a C_{L_\\alpha}}{498 W/S}
References
----------
[1] FAR Part 23: https://www.ecfr.gov/current/title-14/part-23
[2] FAR Part 25: https://www.ecfr.gov/current/title-14/part-25
[3] Gudmundsson, S. (2022). General Aviation Aircraft Design: Applied Methods and procedures. Elsevier.
"""
weight = vehicle.mass_properties.max_takeoff
# ----------------------------------------------
# Unpack
# ----------------------------------------------
FAR_part_number = vehicle.flight_envelope.FAR_part_number
atmo = analyses.atmosphere
Mc = vehicle.flight_envelope.design_mach_number
for wing in vehicle.wings:
reference_area = vehicle.reference_area
Cmac = wing.chords.mean_aerodynamic
pos_limit_load = vehicle.flight_envelope.positive_limit_load
neg_limit_load = vehicle.flight_envelope.positive_limit_load
category_tag = vehicle.flight_envelope.category
# ----------------------------------------------
# Computing atmospheric conditions
# ----------------------------------------------
atmo_values = atmo.compute_values(altitude,delta_ISA)
SL_atmo_values = atmo.compute_values(0,delta_ISA)
conditions = Results()
rho = atmo_values.density
sea_level_rho = SL_atmo_values.density
sea_level_gravity = atmo.planet.sea_level_gravity
Vc = Mc * atmo_values.speed_of_sound
# ------------------------------
# Computing lift-curve slope
# ------------------------------
results = evalaute_aircraft(vehicle,altitude,Vc)
CLa = results.segments.cruise.conditions.static_stability.derivatives.Clift_alpha[0, 0]
# -----------------------------------------------------------
# Determining vehicle minimum and maximum lift coefficients
# -----------------------------------------------------------
if vehicle.flight_envelope.maximum_lift_coefficient != None:
maximum_lift_coefficient = vehicle.flight_envelope.maximum_lift_coefficient
else:
raise ValueError("Maximum lift coefficient not specified.")
if vehicle.flight_envelope.minimum_lift_coefficient != None:
minimum_lift_coefficient = vehicle.flight_envelope.minimum_lift_coefficient
else:
raise ValueError("Minimum lift coefficient not specified.")
# -----------------------------------------------------------------------------
# Convert all terms to English (Used for FAR) and remove elements from arrays
# -----------------------------------------------------------------------------
altitude = altitude / Units.ft
rho = rho[0,0] / Units['slug/ft**3']
sea_level_rho = sea_level_rho[0,0] / Units['slug/ft**3']
density_ratio = (rho/sea_level_rho)**0.5
sea_level_gravity = sea_level_gravity / Units['ft/s**2']
weight = weight / Units['slug'] * sea_level_gravity
reference_area = reference_area / Units['ft**2']
Cmac = Cmac / Units.ft
wing_loading = weight / reference_area
Vc = Vc / Units['ft/s']
load_factors_pos = np.zeros(shape=(5));
load_factors_neg = np.zeros(shape=(5));
load_factors_pos[1] = 1;
load_factors_neg[1] = -1;
airspeeds_pos = np.zeros(shape=(5));
airspeeds_neg = np.zeros(shape=(5))
# --------------------------------------------------
# Establish limit maneuver load factors n+ and n-
# --------------------------------------------------
# CFR Part 25
# Positive and negative limits
if FAR_part_number == '25':
# Positive limit
load_factors_pos[2] = 2.1 + 24000 / (weight + 10000)
if load_factors_pos[2] < 2.5:
load_factors_pos[2] = 2.5
elif load_factors_pos[2] < pos_limit_load:
load_factors_pos[2] = pos_limit_load
elif load_factors_pos[2] > 3.8:
load_factors_pos[2] = 3.8
# Negative limit
load_factors_neg[2] = neg_limit_load
if load_factors_neg[2] > -1:
load_factors_neg[2] = -1
elif FAR_part_number == '23':
if category_tag == 'normal' or category_tag == 'commuter':
# Positive limit
load_factors_pos[2] = 2.1 + 24000 / (weight + 10000)
if load_factors_pos[2] < 2.5:
load_factors_pos[2] = 2.5
elif load_factors_pos[2] < pos_limit_load:
load_factors_pos[2] = pos_limit_load
elif load_factors_pos[2] > 3.8:
load_factors_pos[2] = 3.8
# Negative limit
load_factors_neg[2] = -0.4 * load_factors_pos[2]
elif category_tag == 'utility':
# Positive limit
load_factors_pos[2] = pos_limit_load
if load_factors_pos[2] < 4.4:
load_factors_pos[2] = 4.4
# Negative limit
load_factors_neg[2] = -0.4 * load_factors_pos[2]
elif category_tag == 'acrobatic':
# Positive limit
load_factors_pos[2] = pos_limit_load
if load_factors_pos[2] < 6.0:
load_factors_pos[2] = 6.0
# Negative limit
load_factors_neg[2] = -0.5 * load_factors_pos[2]
else:
raise ValueError("Check the category_tag input. The parameter was not found")
else:
raise ValueError("Check the FARflag input. The parameter was not found")
# Check input of the limit load
if abs(neg_limit_load) > abs(load_factors_neg[2]):
load_factors_neg[2] = -neg_limit_load
#----------------------------------------
# Generate a V-n diagram data structure
#----------------------------------------
V_n_data = Data()
V_n_data.limit_loads = Data()
V_n_data.limit_loads.dive = Data()
V_n_data.load_factors = Data()
V_n_data.gust_load_factors = Data()
V_n_data.Vb_load_factor = Data()
V_n_data.airspeeds = Data()
V_n_data.Vs1 = Data()
V_n_data.Va = Data()
V_n_data.Vb = Data()
V_n_data.load_factors.positive = load_factors_pos
V_n_data.load_factors.negative = load_factors_neg
V_n_data.airspeeds.positive = airspeeds_pos
V_n_data.airspeeds.negative = airspeeds_neg
V_n_data.Vc = Vc
V_n_data.weight = weight
V_n_data.wing_loading = wing_loading
V_n_data.altitude = altitude
V_n_data.density = rho
V_n_data.density_ratio = density_ratio
V_n_data.reference_area = reference_area
V_n_data.maximum_lift_coefficient = maximum_lift_coefficient
V_n_data.minimum_lift_coefficient = minimum_lift_coefficient
V_n_data.positive_limit_load = load_factors_pos[2]
V_n_data.negative_limit_load = load_factors_neg[2]
# --------------------------------------------------
# Computing critical speeds (Va, Vc, Vb, Vd, Vs1)
# --------------------------------------------------
# Calculate Stall and Maneuver speeds
stall_maneuver_speeds(V_n_data)
# convert speeds to KEAS for future calculations
convert_keas(V_n_data)
# unpack modified airspeeds
airspeeds_pos = V_n_data.airspeeds.positive
airspeeds_neg = V_n_data.airspeeds.negative
Vc = V_n_data.Vc
Va_pos = V_n_data.Va.positive
Va_neg = V_n_data.Va.negative
Va_pos = V_n_data.Va.positive
Va_neg = V_n_data.Va.negative
if Va_neg > Vc and Va_neg > Va_pos:
Vc = 1.15 * Va_neg
elif Va_pos > Vc and Va_neg < Va_pos:
Vc = 1.15 * Va_pos
# Gust speeds between Vb and Vc (EAS) and minimum Vc
miu = 2 * wing_loading / (rho * Cmac * CLa * sea_level_gravity)
Kg = 0.88 * miu / (5.3 + miu)
if FAR_part_number == '25':
if altitude < 15000:
Uref_cruise = (-0.0008 * altitude + 56)
Uref_rough = Uref_cruise
Uref_dive = 0.5 * Uref_cruise
else:
Uref_cruise = (-0.0005142 * altitude + 51.7133)
Uref_rough = Uref_cruise
Uref_dive = 0.5 * Uref_cruise
# Minimum Cruise speed Vc_min
coefs = [1, -Uref_cruise * (2.64 + (Kg * CLa * airspeeds_pos[1]**2)/(498 * wing_loading)), 1.72424 * Uref_cruise**2 - airspeeds_pos[1]**2]
Vc1 = max(np.roots(coefs))
elif FAR_part_number == '23':
if altitude < 20000:
Uref_cruise = 50;
Uref_dive = 25;
else:
Uref_cruise = (-0.0008333 * altitude + 66.67);
Uref_dive = (-0.0004167 * altitude + 33.334);
if category_tag == 'commuter':
if altitude < 20000:
Uref_rough = 66
else:
Uref_rough = -0.000933 * altitude + 84.667
else:
Uref_rough = Uref_cruise
# Minimum Cruise speed Vc_min
if category_tag == 'acrobatic':
Vc1 = 36 * wing_loading**0.5
if wing_loading >= 20:
Vc1 = (-0.0925 * wing_loading + 37.85) * wing_loading **0.5
else:
Vc1 = 33 * wing_loading**0.5
if wing_loading >= 20:
Vc1 = (-0.055 * wing_loading + 34.1) * wing_loading **0.5
# checking input Cruise speed
if Vc1 > Vc:
Vc = Vc1
# Dive speed
if FAR_part_number == '25':
airspeeds_pos[4] = 1.25 * Vc
elif FAR_part_number == '23':
if category_tag == 'acrobatic':
airspeeds_pos[4] = 1.55 * Vc1
if wing_loading > 20:
airspeeds_pos[4] = (-0.0025 * wing_loading + 1.6) * Vc1
elif category_tag == 'utility':
airspeeds_pos[4] = 1.5 * Vc1
if wing_loading > 20:
airspeeds_pos[4] = (-0.001875 * wing_loading + 1.5375) * Vc1
else:
airspeeds_pos[4] = 1.4 * Vc1
if wing_loading > 20:
airspeeds_pos[4] = (-0.000625 * wing_loading + 1.4125) * Vc1
if airspeeds_pos[4] < 1.15 * Vc:
airspeeds_pos[4] = 1.15 * Vc
Vd = airspeeds_pos[4]
airspeeds_pos[3] = airspeeds_pos[4]
airspeeds_neg[3] = Vc
airspeeds_neg[4] = airspeeds_pos[4]
# complete initial load factors
load_factors_pos[4] = 0
load_factors_neg[4] = 0
if category_tag == 'acrobatic' or category_tag == 'utility':
load_factors_neg[4] = -1
load_factors_pos[3] = load_factors_pos[2]
load_factors_neg[3] = load_factors_neg[2]
# add parameters to the data structure
V_n_data.load_factors.positive = load_factors_pos
V_n_data.load_factors.negative = load_factors_neg
V_n_data.airspeeds.positive = airspeeds_pos
V_n_data.airspeeds.negative = airspeeds_neg
V_n_data.Vd = Vd
V_n_data.Vc = Vc
#------------------------
# Create Stall lines
#------------------------
Num_of_points = 20 # number of points for the stall line
upper_bound = 2;
lower_bound = 1;
stall_line(V_n_data, upper_bound, lower_bound, Num_of_points, 1)
stall_line(V_n_data, upper_bound, lower_bound, Num_of_points, 2)
# ----------------------------------------------
# Determine Gust loads
# ----------------------------------------------
V_n_data.gust_data = Data()
V_n_data.gust_data.airspeeds = Data()
V_n_data.gust_data.airspeeds.rough_gust = Uref_rough
V_n_data.gust_data.airspeeds.cruise_gust = Uref_cruise
V_n_data.gust_data.airspeeds.dive_gust = Uref_dive
gust_loads(category_tag, V_n_data, Kg, CLa, Num_of_points, FAR_part_number, 1)
gust_loads(category_tag, V_n_data, Kg, CLa, Num_of_points, FAR_part_number, 2)
#----------------------------------------------------------------
# Finalize the load factors for acrobatic and utility aircraft
#----------------------------------------------------------------
if category_tag == 'acrobatic' or category_tag == 'utility':
V_n_data.airspeeds.negative = np.append(V_n_data.airspeeds.negative, Vd)
V_n_data.load_factors.negative = np.append(V_n_data.load_factors.negative, 0)
# ----------------------------------------------
# Post-processing the V-n diagram
# ----------------------------------------------
V_n_data.positive_limit_load = max(V_n_data.load_factors.positive)
V_n_data.negative_limit_load = min(V_n_data.load_factors.negative)
post_processing(category_tag, Uref_rough, Uref_cruise, Uref_dive, V_n_data, vehicle)
return V_n_data
[docs]
def evalaute_aircraft(vehicle,altitude,Vc):
# Set up vehicle configs
configs = configs_setup(vehicle)
# create analyses
analyses = analyses_setup(configs)
# mission analyses
mission = base_mission_setup(analyses,altitude,Vc)
# create mission instances (for multiple types of missions)
missions = missions_setup(mission)
# mission analysis
results = missions.base_mission.evaluate()
return results
[docs]
def analyses_setup(configs):
analyses = RCAIDE.Framework.Analyses.Analysis.Container()
# build a base analysis for each config
for tag,config in configs.items():
analysis = base_analysis(config)
analyses[tag] = analysis
return analyses
[docs]
def base_analysis(vehicle):
# ------------------------------------------------------------------
# Initialize the Analyses
# ------------------------------------------------------------------
analyses = RCAIDE.Framework.Analyses.Vehicle()
# ------------------------------------------------------------------
# Weights
# ------------------------------------------------------------------
weights = RCAIDE.Framework.Analyses.Weights.Conventional()
weights.aircraft_type = 'General_Aviation'
weights.vehicle = vehicle
analyses.append(weights)
# ------------------------------------------------------------------
# Aerodynamics Analysis
# ------------------------------------------------------------------
aerodynamics = RCAIDE.Framework.Analyses.Aerodynamics.Vortex_Lattice_Method()
aerodynamics.vehicle = vehicle
aerodynamics.settings.use_surrogate = False
aerodynamics.settings.trim_aircraft = False
analyses.append(aerodynamics)
# ------------------------------------------------------------------
# Energy
# ------------------------------------------------------------------
energy = RCAIDE.Framework.Analyses.Energy.Energy()
energy.vehicle = vehicle
analyses.append(energy)
# ------------------------------------------------------------------
# Planet Analysis
# ------------------------------------------------------------------
planet = RCAIDE.Framework.Analyses.Planets.Earth()
analyses.append(planet)
# ------------------------------------------------------------------
# Atmosphere Analysis
# ------------------------------------------------------------------
atmosphere = RCAIDE.Framework.Analyses.Atmospheric.US_Standard_1976()
atmosphere.features.planet = planet.features
analyses.append(atmosphere)
# done!
return analyses
[docs]
def configs_setup(vehicle):
# ------------------------------------------------------------------
# Initialize Configurations
# ------------------------------------------------------------------
configs = RCAIDE.Library.Components.Configs.Config.Container()
base_config = RCAIDE.Library.Components.Configs.Config(vehicle)
base_config.tag = 'base'
configs.append(base_config)
return configs
[docs]
def base_mission_setup(analyses,altitude,Vc):
'''
This sets up the nominal cruise of the aircraft
'''
mission = RCAIDE.Framework.Mission.Sequential_Segments()
mission.tag = 'base_mission'
# unpack Segments module
Segments = RCAIDE.Framework.Mission.Segments
# Cruise Segment: constant Speed, constant altitude
segment = Segments.Untrimmed.Untrimmed()
segment.analyses.extend( analyses.base )
segment.tag = "cruise"
segment.altitude = altitude
segment.air_speed = Vc
segment.flight_dynamics.force_x = True
segment.flight_dynamics.force_z = True
segment.flight_dynamics.force_y = True
segment.flight_dynamics.moment_y = True
segment.flight_dynamics.moment_x = True
segment.flight_dynamics.moment_z = True
mission.append_segment(segment)
return mission
[docs]
def missions_setup(mission):
missions = RCAIDE.Framework.Mission.Missions()
# base mission
mission.tag = 'base_mission'
missions.append(mission)
return missions
#------------------------------------------------------------------------------------------------------
# USEFUL FUNCTIONS
#------------------------------------------------------------------------------------------------------
[docs]
def stall_maneuver_speeds(V_n_data):
""" Computes stall and maneuver speeds for positive and negative halves of the the V-n diagram
Source:
S. Gudmundsson "General Aviation Aircraft Design: Applied Methods and Procedures", Butterworth-Heinemann; 1 edition
Inputs:
V_n_data.
airspeeds.positive [kts]
negative [kts]
weight [lb]
density [slug/ft**3]
density_ratio [Unitless]
reference_area [ft**2]
maximum_lift_coefficient [Unitless]
minimum_lift_coefficient [Unitless]
load_factors.positive [Unitless]
load_factors.negative [Unitless]
Outputs:
V_n_data.
airspeeds.positive [kts]
negative [kts]
Vs1.positive [kts]
negative [kts]
Va.positive [kts]
negative [kts]
load_factors.positive [Unitless]
negative [Unitless]
Properties Used:
N/A
Description:
"""
# Unpack
airspeeds_pos = V_n_data.airspeeds.positive
airspeeds_neg = V_n_data.airspeeds.negative
weight = V_n_data.weight
rho = V_n_data.density
reference_area = V_n_data.reference_area
max_lift_coef = V_n_data.maximum_lift_coefficient
min_lift_coef = V_n_data.minimum_lift_coefficient
load_factors_pos = V_n_data.load_factors.positive
load_factors_neg = V_n_data.load_factors.negative
# Stall speeds
airspeeds_pos[1] = (2 * weight / (rho * reference_area * max_lift_coef)) ** 0.5
airspeeds_neg[1] = (2 * weight / (rho * reference_area * abs(min_lift_coef))) ** 0.5
airspeeds_pos[0] = airspeeds_pos[1]
airspeeds_neg[0] = airspeeds_neg[1]
# Maneuver speeds
airspeeds_pos[2] = airspeeds_pos[1] * load_factors_pos[2] ** 0.5
airspeeds_neg[2] = (2 * weight * abs(load_factors_neg[2]) / (rho * reference_area * \
abs(min_lift_coef))) ** 0.5
# Pack
V_n_data.airspeeds.positive = airspeeds_pos
V_n_data.airspeeds.negative = airspeeds_neg
V_n_data.load_factors.positive = load_factors_pos
V_n_data.load_factors.negative = load_factors_neg
V_n_data.Vs1.positive = airspeeds_pos[1]
V_n_data.Vs1.negative = airspeeds_neg[1]
V_n_data.Va.positive = airspeeds_pos[2]
V_n_data.Va.negative = airspeeds_neg[2]
#------------------------------------------------------------------------------------------------------------
[docs]
def stall_line(V_n_data, upper_bound, lower_bound, Num_of_points, sign_flag):
""" Calculates Stall lines of positive and negative halves of the V-n diagram
Source:
S. Gudmundsson "General Aviation Aircraft Design: Applied Methods and Procedures", Butterworth-Heinemann; 1 edition
Inputs:
V_n_data.
airspeeds.positive [kts]
negative [kts]
load_factors.positive [Unitless]
negative [Unitless]
weight [lb]
density [slug/ft**3]
density_ratio [Unitless]
lift_coefficient [Unitless]
reference_area [ft**2]
lower_bound [Unitless]
Num_of_points [Unitless]
sign_flag [Unitless]
Outputs:
V_n_data.
load_factors.positive [Unitless]
negative [Unitless]
airspeeds.positive [kts]
negative [kts]
Properties Used:
N/A
Description:
"""
# Unpack
weight = V_n_data.weight
reference_area = V_n_data.reference_area
density_ratio = V_n_data.density_ratio
rho = V_n_data.density
if sign_flag == 1:
load_factors = V_n_data.load_factors.positive
airspeeds = V_n_data.airspeeds.positive
lift_coefficient = V_n_data.maximum_lift_coefficient
elif sign_flag == 2:
load_factors = V_n_data.load_factors.negative
airspeeds = V_n_data.airspeeds.negative
lift_coefficient = V_n_data.minimum_lift_coefficient
delta = (airspeeds[upper_bound] - airspeeds[lower_bound]) / (Num_of_points + 1) # Step size
for i in range(Num_of_points):
coef = lower_bound + i + 1
airspeeds = np.concatenate((airspeeds[:coef], [airspeeds[lower_bound] + (i + 1) * delta], airspeeds[coef:]))
Vtas = airspeeds[coef] / density_ratio * Units.knots / Units['ft/s']
if load_factors[1] > 0:
nl = 0.5 * rho * Vtas**2 * reference_area * lift_coefficient / weight
else:
nl = -0.5 * rho * Vtas**2 * reference_area * abs(lift_coefficient) / weight
load_factors = np.concatenate((load_factors[:coef], [nl], load_factors[coef:]))
# Pack
if sign_flag == 1:
V_n_data.load_factors.positive = load_factors
V_n_data.airspeeds.positive = airspeeds
elif sign_flag == 2:
V_n_data.load_factors.negative = load_factors
V_n_data.airspeeds.negative = airspeeds
return
#--------------------------------------------------------------------------------------------------------------
[docs]
def gust_loads(category_tag, V_n_data, Kg, CLa, Num_of_points, FAR_part_number, sign_flag):
""" Calculates airspeeds and load factors for gust loads and modifies the V-n diagram
Source:
S. Gudmundsson "General Aviation Aircraft Design: Applied Methods and Procedures", Butterworth-Heinemann; 1 edition
Inputs:
V_n_data.
airspeeds.positive [kts]
negative [kts]
load_factors.positive [Unitless]
negative [Unitless]
positive_limit_load [Unitless]
negative [Unitless]
weight [lb]
wing_loading [lb/ft**2]
reference_area [ft**2]
density [slug/ft**3]
density_ratio [Unitless]
lift_coefficient [Unitless]
Vc [kts]
Vd [kts]
Uref_rough [ft/s]
Uref_cruise [ft/s]
Uref_dive [ft/s]
Num_of_points [Unitless]
sign_flag [Unitless]
Outputs:
V_n_data.
airspeeds.positive [kts]
negative [kts]
load_factors.positive [Unitless]
.negative [Unitless]
gust_load_factors.positive [Unitless]
negative [Unitless]
Properties Used:
N/A
Description:
The function calculates the gust-related load factors at critical speeds (Va, Vc, Vd). Then, if the load factors exceed
the standart diagram limits, the diagram is modified to include the new limit loads.
For more details, refer to S. Gudmundsson "General Aviation Aircraft Design: Applied Methods and Procedures"
"""
# Unpack
weight = V_n_data.weight
wing_loading = V_n_data.wing_loading
reference_area = V_n_data.reference_area
density = V_n_data.density
density_ratio = V_n_data.density_ratio
Vc = V_n_data.Vc
Vd = V_n_data.Vd
if sign_flag == 1:
airspeeds = V_n_data.airspeeds.positive
load_factors = V_n_data.load_factors.positive
lift_coefficient = V_n_data.maximum_lift_coefficient
limit_load = V_n_data.positive_limit_load
Uref_rough = V_n_data.gust_data.airspeeds.rough_gust
Uref_cruise = V_n_data.gust_data.airspeeds.cruise_gust
Uref_dive = V_n_data.gust_data.airspeeds.dive_gust
elif sign_flag == 2:
airspeeds = V_n_data.airspeeds.negative
load_factors = V_n_data.load_factors.negative
lift_coefficient = V_n_data.minimum_lift_coefficient
limit_load = V_n_data.negative_limit_load
Uref_rough = -V_n_data.gust_data.airspeeds.rough_gust
Uref_cruise = -V_n_data.gust_data.airspeeds.cruise_gust
Uref_dive = -V_n_data.gust_data.airspeeds.dive_gust
gust_load_factors = np.zeros(shape=(4));
gust_load_factors[0] = 1;
# Cruise speed Gust loads at Va and Vc
gust_load_factors[1] = 1 + Kg * CLa * airspeeds[Num_of_points+2] * Uref_rough / (498 * wing_loading)
gust_load_factors[2] = 1 + Kg * CLa * Vc * Uref_cruise/(498 * wing_loading)
# Intersection between cruise gust load and Va
if abs(gust_load_factors[1]) > abs(limit_load):
sea_level_rho = density / density_ratio**2
coefs = [709.486 * sea_level_rho * lift_coefficient, -Kg * Uref_rough * CLa, -498 * wing_loading]
V_inters = max(np.roots(coefs))
load_factor_inters = 1 + Kg * CLa * V_inters * Uref_rough / (498 * wing_loading)
airspeeds = np.concatenate((airspeeds[:(Num_of_points + 3)], [V_inters], airspeeds[(Num_of_points + 3):]))
load_factors = np.concatenate((load_factors[:(Num_of_points + 3)], [load_factor_inters], load_factors[(Num_of_points + 3):]))
# Pack
if sign_flag == 1:
V_n_data.airspeeds.positive = airspeeds
V_n_data.load_factors.positive = load_factors
V_n_data.gust_load_factors.positive = gust_load_factors
V_n_data.Vb_load_factor.positive = load_factor_inters
V_n_data.Vb.positive = V_inters
if sign_flag == 2:
V_n_data.airspeeds.negative = airspeeds
V_n_data.load_factors.negative = load_factors
V_n_data.gust_load_factors.negative = gust_load_factors
V_n_data.Vb_load_factor.negative = load_factor_inters
V_n_data.Vb.negative = V_inters
# continue stall lines
Num_of_points_ext = 5;
upper_bound = Num_of_points+3;
lower_bound = Num_of_points+2;
stall_line(V_n_data, upper_bound, lower_bound, Num_of_points_ext, sign_flag)
Num_of_points = Num_of_points + Num_of_points_ext + 1
# Unpack
if sign_flag == 1:
airspeeds = V_n_data.airspeeds.positive
load_factors = V_n_data.load_factors.positive
lift_coefficient = V_n_data.maximum_lift_coefficient
elif sign_flag == 2:
airspeeds = V_n_data.airspeeds.negative
load_factors = V_n_data.load_factors.negative
lift_coefficient = V_n_data.minimum_lift_coefficient
# insert the cruise speed Vc in the positive load factor line
if load_factors[1] > 0:
airspeeds = np.concatenate((airspeeds[:(Num_of_points + 3)], [Vc], airspeeds[(Num_of_points + 3):]))
load_factors = np.concatenate((load_factors[:(Num_of_points + 3)], [gust_load_factors[2]], \
load_factors[(Num_of_points + 3):]))
# Intersection between cruise gust load and maximum load at Vc
elif abs(gust_load_factors[2]) > abs(limit_load):
V_inters = 498 * (load_factors[Num_of_points + 2] - 1) * wing_loading / (Kg * Uref_cruise * CLa)
airspeeds = np.concatenate((airspeeds[:(Num_of_points + 3)], [V_inters], airspeeds[(Num_of_points + 3):]))
load_factors = np.concatenate((load_factors[:(Num_of_points + 3)], [limit_load], \
load_factors[(Num_of_points + 3):]))
Num_of_points = Num_of_points + 1
if load_factors[1] > 0:
airspeeds = np.concatenate((airspeeds[:(Num_of_points + 3)], [Vc], airspeeds[(Num_of_points + 3):]))
load_factors = np.concatenate((load_factors[:(Num_of_points + 3)], [gust_load_factors[2]], \
load_factors[(Num_of_points + 3):]))
else:
load_factors[len(airspeeds)-2] = gust_load_factors[2]
# Dive speed Gust loads Vd
gust_load_factors[3] = 1 + Kg * CLa * Vd * Uref_dive / (498 * wing_loading)
# Resolve the upper half of the dive section
if load_factors[1] > 0:
if abs(gust_load_factors[2]) > abs(limit_load):
if abs(gust_load_factors[3]) > abs(limit_load):
load_factors[len(load_factors) - 2] = gust_load_factors[3]
else:
airspeeds, load_factors = gust_dive_speed_intersection(category_tag, load_factors, gust_load_factors, airspeeds, \
len(load_factors)-2, Vc, Vd)
# Resolve the lower half of the dive section
else:
if gust_load_factors[3] < load_factors[len(load_factors) - 1]:
airspeeds = np.concatenate((airspeeds[:(len(load_factors) - 1)], [airspeeds[(len(load_factors) - 1)]], \
airspeeds[(len(load_factors) - 1):]))
load_factors = np.concatenate((load_factors[:(len(load_factors) - 1)], [gust_load_factors[3]], \
load_factors[(len(load_factors) - 1):]))
if abs(gust_load_factors[2]) > abs(limit_load):
load_factors[len(load_factors) - 2] = gust_load_factors[3]
else:
airspeeds, load_factors = gust_dive_speed_intersection(category_tag, load_factors, gust_load_factors, airspeeds, \
len(load_factors)-2, Vc, Vd)
V_n_data.limit_loads.dive.negative = load_factors[len(load_factors) - 2]
else:
V_n_data.limit_loads.dive.negative = load_factors[len(load_factors) - 1]
# gusts load extension for gust lines at Vd
gust_load_factors = np.append(gust_load_factors, 1 + Kg * CLa * (1.05 * Vd) * Uref_cruise/(498 * wing_loading))
gust_load_factors = np.append(gust_load_factors, 1 + Kg * CLa * (1.05 * Vd) * Uref_dive/(498 * wing_loading))
# guts load extension for gust lines at Vb and Vc for a rough gust
if category_tag == 'commuter':
gust_load_factors = np.append(gust_load_factors, 1 + Kg * CLa * (1.05 * Vd) * Uref_rough/(498 * wing_loading))
else:
gust_load_factors = np.append(gust_load_factors, 0)
# Pack
if sign_flag == 1:
V_n_data.airspeeds.positive = airspeeds
V_n_data.load_factors.positive = load_factors
V_n_data.gust_load_factors.positive = gust_load_factors
V_n_data.limit_loads.dive.positive = load_factors[len(load_factors) - 2]
if sign_flag == 2:
V_n_data.airspeeds.negative = airspeeds
V_n_data.load_factors.negative = load_factors
V_n_data.gust_load_factors.negative = gust_load_factors
return
#------------------------------------------------------------------------------------------------------------------------
[docs]
def gust_dive_speed_intersection(category_tag, load_factors, gust_load_factors, airspeeds, element_num, Vc, Vd):
""" Calculates intersection between the general V-n diagram and the gust load for Vd
Source:
S. Gudmundsson "General Aviation Aircraft Design: Applied Methods and Procedures", Butterworth-Heinemann; 1 edition
Inputs:
load_factors [Unitless]
gust_load_factors [Unitless]
airspeeds [kts]
Vc [kts]
Vd [kts]
element_num [Unitless]
Outputs:
airspeeds [kts]
load_factors [Unitless]
Properties Used:
N/A
Description:
A specific function for CFR FAR Part 25 regulations. For negative loads, the general curve must go linear from a specific
load factor at Vc to zero at Vd. Gust loads may put a non-zero load factor at Vd, so an intersection between the old and
the new curves is required
"""
if load_factors[1] > 0:
V_inters = (load_factors[element_num] - gust_load_factors[2]) * (Vd - Vc) \
/ (gust_load_factors[3] - gust_load_factors[2]) + Vc
load = load_factors[element_num]
else:
if category_tag == 'acrobatic':
V_inters = ((gust_load_factors[3] + 1) * Vc + (min(load_factors) - gust_load_factors[2]) * Vd) / \
(gust_load_factors[3] - gust_load_factors[2] + min(load_factors) + 1)
load = (min(load_factors) + 1) / (Vc - Vd) * V_inters - ((min(load_factors) + 1) / (Vc - Vd) * Vd + 1)
else:
V_inters = (gust_load_factors[3] * Vc + (min(load_factors) - gust_load_factors[2]) * Vd) / \
(gust_load_factors[3] - gust_load_factors[2] + min(load_factors))
load = min(load_factors) * (V_inters - Vd) / (Vc - Vd)
airspeeds = np.concatenate((airspeeds[:(element_num)], [V_inters], airspeeds[(element_num):]))
load_factors = np.concatenate((load_factors[:(element_num)], [load], \
load_factors[(element_num):]))
return airspeeds, load_factors
#--------------------------------------------------------------------------------------------------------------------------
[docs]
def post_processing(category_tag, Uref_rough, Uref_cruise, Uref_dive, V_n_data, vehicle):
""" Plot graph, save the final figure, and create results output file
Source:
Inputs:
V_n_data.
airspeeds.positive [kts]
airspeeds.negative [kts]
Vc [kts]
Vd [kts]
Vs1.positive [kts]
negative [kts]
Va.positive [kts]
negative [kts]
load_factors.positive [Unitless]
negative [Unitless]
gust_load_factors.positive [Unitless]
negative [Unitless]
weight [lb]
altitude [ft]
vehicle._base.tag [Unitless]
Uref_rough [ft/s]
Uref_cruise [ft/s]
Uref_dive [ft/s]
Outputs:
Properties Used:
N/A
Description:
"""
# Unpack
load_factors_pos = V_n_data.load_factors.positive
load_factors_neg = V_n_data.load_factors.negative
airspeeds_pos = V_n_data.airspeeds.positive
airspeeds_neg = V_n_data.airspeeds.negative
Vc = V_n_data.Vc
Vd = V_n_data.Vd
Vs1_pos = V_n_data.Vs1.positive
Vs1_neg = V_n_data.Vs1.negative
Va_pos = V_n_data.Va.positive
Va_neg = V_n_data.Va.negative
gust_load_factors_pos = V_n_data.gust_load_factors.positive
gust_load_factors_neg = V_n_data.gust_load_factors.negative
weight = V_n_data.weight
altitude = V_n_data.altitude
#-----------------------------
# Plotting the V-n diagram
#-----------------------------
fig, ax = plt.subplots()
ax.fill(airspeeds_pos, load_factors_pos, c='b', alpha=0.3)
ax.fill(airspeeds_neg, load_factors_neg, c='b', alpha=0.3)
ax.plot(airspeeds_pos, load_factors_pos, c='b')
ax.plot(airspeeds_neg, load_factors_neg, c='b')
# Plotting gust lines
ax.plot([0, Vc,1.05*Vd],[1,gust_load_factors_pos[2],gust_load_factors_pos[len(gust_load_factors_pos)-3]],'--', c='r', label = ('Gust ' + str(round(Uref_cruise)) + 'fps'))
ax.plot([0, Vd,1.05*Vd],[1,gust_load_factors_pos[3],gust_load_factors_pos[len(gust_load_factors_pos)-2]],'--', c='g', label = ('Gust ' + str(round(Uref_dive)) + 'fps'))
ax.plot([0, Vc,1.05*Vd],[1,gust_load_factors_neg[2],gust_load_factors_neg[len(gust_load_factors_neg)-3]],'--', c='r')
ax.plot([0, Vd,1.05*Vd],[1,gust_load_factors_neg[3],gust_load_factors_neg[len(gust_load_factors_neg)-2]],'--', c='g')
if category_tag == 'commuter':
ax.plot([0, 1.05*Vd],[1,gust_load_factors_pos[len(gust_load_factors_pos)-1]],'--', c='m', label = ('Gust ' + str(round(Uref_rough)) + 'fps'))
ax.plot([0, 1.05*Vd],[1,gust_load_factors_neg[len(gust_load_factors_neg)-1]],'--', c='m')
# Formating the plot
ax.set_xlabel('Airspeed, KEAS')
ax.set_ylabel('Load Factor')
ax.set_title(vehicle.tag + ' Weight=' + str(round(weight)) + 'lb ' + ' Altitude=' + str(round(altitude)) + 'ft ')
ax.legend()
ax.grid()
#---------------------------------
# Creating results output file
#---------------------------------
fres = open("V_n_diagram_results_" + vehicle.tag +".dat","w")
fres.write('V-n diagram summary\n')
fres.write('-------------------\n')
fres.write('Aircraft: ' + vehicle.tag + '\n')
fres.write('category: ' + vehicle.flight_envelope.category + '\n')
fres.write('FAR certification: Part ' + vehicle.flight_envelope.FAR_part_number + '\n')
fres.write('Weight = ' + str(round(weight)) + ' lb\n')
fres.write('Altitude = ' + str(round(altitude)) + ' ft\n')
fres.write('---------------------------------------------------------------\n\n')
fres.write('Airspeeds: \n')
fres.write(' Positive stall speed (Vs1) = ' + str(round(Vs1_pos,1)) + ' KEAS\n')
fres.write(' Negative stall speed (Vs1) = ' + str(round(Vs1_neg,1)) + ' KEAS\n')
fres.write(' Positive maneuver speed (Va) = ' + str(round(Va_pos,1)) + ' KEAS\n')
fres.write(' Negative maneuver speed (Va) = ' + str(round(Va_neg,1)) + ' KEAS\n')
fres.write(' Cruise speed (Vc) = ' + str(round(Vc,1)) + ' KEAS\n')
fres.write(' Dive speed (Vd) = ' + str(round(Vd,1)) + ' KEAS\n')
fres.write('Load factors: \n')
fres.write(' Positive limit load factor (n+) = ' + str(round(max(load_factors_pos),2)) + '\n')
fres.write(' Negative limit load factor (n-) = ' + str(round(min(load_factors_neg),2)) + '\n')
fres.write(' Positive load factor at Vd = ' + str(round(V_n_data.limit_loads.dive.positive,2)) + '\n')
fres.write(' Negative load factor at Vd = ' + str(round(V_n_data.limit_loads.dive.negative,2)) + '\n')
return
#------------------------------------------------------------------------------------------------------------------------
[docs]
def convert_keas(V_n_data):
""" Convert speed to KEAS
Source:
Inputs:
V_n_data.
airspeeds.positive [ft/s]
negative [ft/s]
Vc [ft/s]
Va.positive [ft/s]
Va.negative [ft/s]
Vs1.negative [ft/s]
Vs1.positive [ft/s]
density_ratio [Unitless]
Outputs:
V_n_data.
airspeeds.positive [kts]
negative [kts]
Vc [kts]
Va.positive [kts]
Va.negative [kts]
Vs1.negative [kts]
Vs1.positive [kts]
Properties Used:
N/A
Description:
"""
# Unpack
airspeeds_pos = V_n_data.airspeeds.positive
airspeeds_neg = V_n_data.airspeeds.negative
density_ratio = V_n_data.density_ratio
Vc = V_n_data.Vc
Va_pos = V_n_data.Va.positive
Va_neg = V_n_data.Va.negative
Vs1_neg = V_n_data.Vs1.negative
Vs1_pos = V_n_data.Vs1.positive
airspeeds_pos = airspeeds_pos * Units['ft/s'] / Units.knots * density_ratio
airspeeds_neg = airspeeds_neg * Units['ft/s'] / Units.knots * density_ratio
Vs1_pos = Vs1_pos * Units['ft/s'] / Units.knots * density_ratio
Vs1_neg = Vs1_neg * Units['ft/s'] / Units.knots * density_ratio
Va_pos = Va_pos * Units['ft/s'] / Units.knots * density_ratio
Va_neg = Va_neg * Units['ft/s'] / Units.knots * density_ratio
Vc = Vc[0] * Units['ft/s'] / Units.knots * density_ratio
Vc = Vc[0]
# Pack
V_n_data.airspeeds.positive = airspeeds_pos
V_n_data.airspeeds.negative = airspeeds_neg
V_n_data.Vc = Vc
V_n_data.Va.positive = Va_pos
V_n_data.Va.negative = Va_neg
V_n_data.Vs1.negative = Vs1_neg
V_n_data.Vs1.positive = Vs1_pos
return