# -*- coding: utf-8 -*-
"""
pyBADA
Generic BADA3 aircraft performance module
Developped @EUROCONTROL (EIH)
2024
"""
from math import sqrt, isnan, asin, atan
import numpy as np
import os
from datetime import date
import xml.etree.ElementTree as ET
import pandas as pd
from pyBADA import constants as const
from pyBADA import conversions as conv
from pyBADA import atmosphere as atm
from pyBADA import configuration as configuration
from pyBADA.aircraft import Airplane, BadaFamily, Bada
[docs]
def proper_round(num, dec=0):
num = str(num)[: str(num).index(".") + dec + 2]
if num[-1] >= "5":
return float(num[: -2 - (not dec)] + str(int(num[-2 - (not dec)]) + 1))
return float(num[:-1])
[docs]
def checkArgument(argument, **kwargs):
if kwargs.get(argument) is not None:
return kwargs.get(argument)
else:
raise TypeError("Missing " + argument + " argument")
[docs]
class Parser(object):
"""This class implements the BADA3 parsing mechanism to parse APF, OPF and GPF BADA3 files."""
def __init__(self):
pass
[docs]
@staticmethod
def parseXML(filePath, acName):
"""
Parses a BADA3 XML formatted file for aircraft performance data.
:param filePath: Path to the XML file containing BADA data.
:param acName: Name of the aircraft for which data is being parsed from the XML file.
:type filePath: str
:type acName: str
:raises IOError: If the file cannot be found or read.
:raises ValueError: If the BADA version is unsupported or if parsing fails.
:returns: A pandas DataFrame containing the parsed aircraft performance data.
:rtype: pd.DataFrame
"""
filename = os.path.join(filePath, acName, acName) + ".xml"
try:
tree = ET.parse(filename)
root = tree.getroot()
except:
raise IOError(filename + " not found or in correct format")
modificationDateOPF = "UNKNOWN"
modificationDateAPF = "UNKNOWN"
# Parse general aircraft data
model = root.find("model").text # aircraft model
engineType = root.find("type").text # engine type
engines = root.find("engine").text # engine name
ICAO = root.find("ICAO").find("designator").text
WTC = root.find("ICAO").find("WTC").text
# Parse engine data
AFCM = root.find("AFCM") # get AFCM
PFM = root.find("PFM") # get PFM
ALM = root.find("ALM") # get ALM
Ground = root.find("Ground") # get Ground
ARPM = root.find("ARPM") # get ARPM
# AFCM
S = float(AFCM.find("S").text)
MREF = float(AFCM.find("mref").text)
mass = {}
mass["reference"] = float(AFCM.find("mref").text)
name = {}
HLids = []
d = {}
CD0 = {}
CD2 = {}
Vstall = {}
for conf in AFCM.findall("Configuration"):
HLid = int(conf.get("HLid"))
HLids.append(str(HLid))
name[HLid] = conf.find("name").text
d[HLid] = {}
CD0[HLid] = {}
CD2[HLid] = {}
Vstall[HLid] = {}
LGUP = conf.find("LGUP")
LGDN = conf.find("LGDN")
if LGUP is not None:
DPM = LGUP.find("DPM")
d[HLid]["LGUP"] = []
for i in DPM.find("CD").findall("d"):
d[HLid]["LGUP"].append(float(i.text))
CD0[HLid]["LGUP"] = d[HLid]["LGUP"][0]
CD2[HLid]["LGUP"] = d[HLid]["LGUP"][1]
BLM = LGUP.find("BLM")
Vstall[HLid]["LGUP"] = []
if BLM is not None: # BLM is not clean
Vstall[HLid]["LGUP"] = float(BLM.find("VS").text)
else: # BLM is clean
BLM = LGUP.find("BLM_clean")
Vstall[HLid]["LGUP"] = float(BLM.find("VS").text)
CL_clean = BLM.find("CL_clean")
Clbo = float(CL_clean.find("Clbo").text)
k = float(CL_clean.find("k").text)
if (
LGDN is not None
): # Landing gear NOT allowed in clean configuration
d[HLid]["LGDN"] = []
for i in LGDN.find("DPM").find("CD").findall("d"):
d[HLid]["LGDN"].append(float(i.text))
CD0[HLid]["LGDN"] = d[HLid]["LGDN"][0]
CD2[HLid]["LGDN"] = d[HLid]["LGDN"][1]
if LGDN.find("DPM").find("DeltaCD") is None:
DeltaCD = 0.0
else:
DeltaCD = float(LGDN.find("DPM").find("DeltaCD").text)
Vstall[HLid]["LGDN"] = float(LGDN.find("BLM").find("VS").text)
elif LGDN is None:
CD0[HLid]["LGDN"] = 0.0
CD2[HLid]["LGDN"] = 0.0
DeltaCD = 0.0
drone = False
if Vstall[0]["LGUP"] == 0.0:
drone = True
# PFM
numberOfEngines = float(PFM.find("n_eng").text)
CT = PFM.find("CT")
CTc1 = float(CT.find("CTc1").text)
CTc2 = float(CT.find("CTc2").text)
CTc3 = float(CT.find("CTc3").text)
CTc4 = float(CT.find("CTc4").text)
CTc5 = float(CT.find("CTc5").text)
Ct = [CTc1, CTc2, CTc3, CTc4, CTc5]
CTdeslow = float(CT.find("CTdeslow").text)
CTdeshigh = float(CT.find("CTdeshigh").text)
CTdesapp = float(CT.find("CTdesapp").text)
CTdesld = float(CT.find("CTdesld").text)
HpDes = float(CT.find("Hpdes").text)
CF = PFM.find("CF")
Cf1 = float(CF.find("Cf1").text)
Cf2 = float(CF.find("Cf2").text)
Cf3 = float(CF.find("Cf3").text)
Cf4 = float(CF.find("Cf4").text)
Cfcr = float(CF.find("Cfcr").text)
CfDes = [Cf3, Cf4]
CfCrz = float(CF.find("Cfcr").text)
Cf = [Cf1, Cf2]
# ALM
GLM = ALM.find("GLM")
hmo = float(GLM.find("hmo").text)
Hmax = float(GLM.find("hmax").text)
tempGrad = float(GLM.find("temp_grad").text)
massGrad = float(GLM.find("mass_grad").text)
mass["mass grad"] = float(GLM.find("mass_grad").text)
KLM = ALM.find("KLM")
MMO = float(KLM.find("mmo").text)
VMO = float(KLM.find("vmo").text)
DLM = ALM.find("DLM")
MTOW = float(DLM.find("MTOW").text)
OEW = float(DLM.find("OEW").text)
MPL = float(DLM.find("MPL").text)
mass["minimum"] = float(DLM.find("OEW").text)
mass["maximum"] = float(DLM.find("MTOW").text)
mass["max payload"] = float(DLM.find("MPL").text)
# Ground
dimensions = Ground.find("Dimensions")
Runway = Ground.find("Runway")
TOL = float(Runway.find("TOL").text)
LDL = float(Runway.find("LDL").text)
span = float(dimensions.find("span").text)
length = float(dimensions.find("length").text)
# ARPM
aeroConfSchedule = ARPM.find("AeroConfSchedule")
# all aerodynamic configurations
aeroConfig = {}
for conf in aeroConfSchedule.findall("AeroPhase"):
name = conf.find("name").text
HLid = int(conf.find("HLid").text)
LG = "LG" + conf.find("LG").text
aeroConfig[name] = {"name": name, "HLid": HLid, "LG": LG}
speedScheduleList = ARPM.find("SpeedScheduleList")
SpeedSchedule = speedScheduleList.find("SpeedSchedule")
# all phases of flight
speedSchedule = {}
for phaseOfFlight in SpeedSchedule.findall("SpeedPhase"):
name = phaseOfFlight.find("name").text
CAS1 = conv.kt2ms(float(phaseOfFlight.find("CAS1").text))
CAS2 = conv.kt2ms(float(phaseOfFlight.find("CAS2").text))
M = float(phaseOfFlight.find("M").text)
speedSchedule[name] = {"CAS1": CAS1, "CAS2": CAS2, "M": M}
V1 = {}
V1["cl"] = speedSchedule["Climb"]["CAS1"]
V1["cr"] = speedSchedule["Cruise"]["CAS1"]
V1["des"] = speedSchedule["Descent"]["CAS1"]
V2 = {}
V2["cl"] = speedSchedule["Climb"]["CAS2"]
V2["cr"] = speedSchedule["Cruise"]["CAS2"]
V2["des"] = speedSchedule["Descent"]["CAS2"]
M = {}
M["cl"] = speedSchedule["Climb"]["M"]
M["cr"] = speedSchedule["Cruise"]["M"]
M["des"] = speedSchedule["Descent"]["M"]
xmlFiles = True
# Single row dataframe
data = {
"acName": [acName],
"model": [model],
"engineType": [engineType],
"engines": [engines],
"ICAO": [ICAO],
"WTC": [WTC],
"modificationDateOPF": [modificationDateOPF],
"modificationDateAPF": [modificationDateAPF],
"S": [S],
"MREF": [MREF],
"mass": [mass],
"name": [name],
"HLids": [HLids],
"d": [d],
"CD0": [CD0],
"CD2": [CD2],
"Vstall": [Vstall],
"Clbo": [Clbo],
"k": [k],
"DeltaCD": [DeltaCD],
"drone": [drone],
"numberOfEngines": [numberOfEngines],
"CTc1": [CTc1],
"CTc2": [CTc2],
"CTc3": [CTc3],
"CTc4": [CTc4],
"CTc5": [CTc5],
"Ct": [Ct],
"CTdeslow": [CTdeslow],
"CTdeshigh": [CTdeshigh],
"CTdeshigh": [CTdeshigh],
"CTdesapp": [CTdesapp],
"CTdesld": [CTdesld],
"HpDes": [HpDes],
"Cf1": [Cf1],
"Cf2": [Cf2],
"Cf3": [Cf3],
"Cf4": [Cf4],
"Cfcr": [Cfcr],
"CfDes": [CfDes],
"CfCrz": [CfCrz],
"Cf": [Cf],
"hmo": [hmo],
"Hmax": [Hmax],
"tempGrad": [tempGrad],
"massGrad": [massGrad],
"mass": [mass],
"MMO": [MMO],
"VMO": [VMO],
"MTOW": [MTOW],
"OEW": [OEW],
"MPL": [MPL],
"TOL": [TOL],
"LDL": [LDL],
"span": [span],
"length": [length],
"span": [span],
"length": [length],
"aeroConfig": [aeroConfig],
"speedSchedule": [speedSchedule],
"V1": [V1],
"V2": [V2],
"M": [M],
"speedSchedule": [speedSchedule],
"xmlFiles": [xmlFiles],
}
df_single = pd.DataFrame(data)
return df_single
[docs]
@staticmethod
def findData(f):
"""
Searches for specific data lines in an open file stream.
:param f: An open file object from which lines are read.
:type f: file object
:returns: A tuple containing the file object and a parsed line split into a list, or None if no relevant line is found.
:rtype: tuple(file object, list of str or None)
This function reads the file line by line until it finds a line that starts with "CD".
Once found, the line is stripped of extra spaces, split into a list, and returned.
If no such line is found, it returns None for the line.
"""
line = f.readline()
while line is not None and not line.startswith("CD"):
line = f.readline()
if line is None:
return f, None
line = " ".join(line.split())
line = line.strip().split(" ")
return f, line
[docs]
@staticmethod
def parseOPF(filePath, acName):
"""
Parses a BADA3 OPF (Operational Performance File) ASCII formatted file for aircraft performance data.
:param filePath: Path to the BADA3 OPF ASCII formatted file.
:param acName: ICAO aircraft designation (e.g., 'A320').
:type filePath: str
:type acName: str
:raises IOError: If the file cannot be opened or read.
:returns: A pandas DataFrame containing the parsed aircraft performance data.
:rtype: pd.DataFrame
"""
filename = (
os.path.join(
filePath,
acName,
)
+ ".OPF"
)
idx = 0
with open(filename, "r", encoding="latin-1") as f:
while True:
line = f.readline()
if idx == 13:
if "with" in line:
engines = (
line.split("with")[1].split("engines")[0].strip()
)
else:
engines = "unknown"
idx += 1
if not line:
break
elif "Modification_date" in line:
data = line.split(":")[1].strip().split(" ")
modificationDateOPF = " ".join([data[0], data[1], data[2]])
elif "CC====== Actype" in line:
f, line = Parser.findData(f=f)
if line is None:
break
ICAO = line[1].replace("_", "")
numberOfEngines = int(line[2])
engineType = line[4].upper()
WTC = line[5]
elif "CC====== Mass (t)" in line:
f, line = Parser.findData(f=f)
if line is None:
break
mass = {}
MREF = float(line[1]) * 1000.0
mass["reference"] = float(line[1]) * 1000.0
mass["minimum"] = float(line[2]) * 1000.0
mass["maximum"] = float(line[3]) * 1000.0
mass["max payload"] = float(line[4]) * 1000.0
mass["mass grad"] = float(line[5])
MTOW = mass["maximum"]
OEW = mass["minimum"]
MPL = mass["max payload"]
massGrad = mass["mass grad"]
elif "CC====== Flight envelope" in line:
f, line = Parser.findData(f=f)
if line is None:
break
VMO = float(line[1])
MMO = float(line[2])
hmo = float(line[3])
Hmax = float(line[4])
tempGrad = float(line[5])
elif "CC====== Aerodynamics" in line:
f, line = Parser.findData(f=f)
if line is None:
break
ndrst = int(line[1])
S = float(line[2])
Clbo = float(line[3])
k = float(line[4])
n = 1
Vstall = {}
CD0 = {}
CD2 = {}
HLids = []
while n <= ndrst:
f, line = Parser.findData(f=f)
if line is None:
break
HLid = line[2]
Vstall[HLid] = float(line[-5])
CD0[HLid] = float(line[-4])
CD2[HLid] = float(line[-3])
HLids.append(str(HLid))
n += 1
drone = False
if Vstall["CR"] == 0.0:
drone = True
iterator = 1
while iterator <= 2:
f, line = Parser.findData(f=f)
if "EXT" in line[2]:
CD2["SPOILER_EXT"] = float(line[3])
iterator += 1
iterator = 1
while iterator <= 2:
f, line = Parser.findData(f=f)
if "DOWN" in line[2]:
CD0["GEAR_DOWN"] = float(line[3])
CD2["GEAR_DOWN"] = float(line[4])
iterator += 1
iterator = 1
while iterator <= 2:
f, line = Parser.findData(f=f)
if "ON" in line[2]:
CD2["BRAKES_ON"] = float(line[3])
iterator += 1
elif "CC====== Engine Thrust" in line:
f, line = Parser.findData(f=f)
if line is None:
break
Ct = [float(i) for i in line[1:-1]]
f, line = Parser.findData(f=f)
if line is None:
break
CTdeslow = float(line[1])
CTdeshigh = float(line[2])
CTdesapp = float(line[4])
CTdesld = float(line[5])
HpDes = float(line[3])
# self.CtDes = {}
# self.CtDes["low"] = float(line[1])
# self.CtDes["high"] = float(line[2])
# self.HpDes = float(line[3])
# self.CtDes["app"] = float(line[4])
# self.CtDes["lnd"] = float(line[5])
f, line = Parser.findData(f=f)
if line is None:
break
elif "CC====== Fuel Consumption" in line:
f, line = Parser.findData(f=f)
if line is None:
break
Cf = [float(i) for i in line[1:-1]]
f, line = Parser.findData(f=f)
if line is None:
break
CfDes = [float(i) for i in line[1:-1]]
f, line = Parser.findData(f=f)
if line is None:
break
CfCrz = float(line[1])
elif "CC====== Ground" in line:
f, line = Parser.findData(f=f)
if line is None:
break
TOL = float(line[1])
LDL = float(line[2])
span = float(line[3])
length = float(line[4])
# Single row dataframe
data = {
"acName": [acName],
"engineType": [engineType],
"engines": [engines],
"ICAO": [ICAO],
"WTC": [WTC],
"modificationDateOPF": [modificationDateOPF],
"modificationDateOPF": [modificationDateOPF],
"S": [S],
"MREF": [MREF],
"mass": [mass],
"HLids": [HLids],
"CD0": [CD0],
"CD2": [CD2],
"Vstall": [Vstall],
"Clbo": [Clbo],
"k": [k],
"drone": [drone],
"numberOfEngines": [numberOfEngines],
"Ct": [Ct],
"CTdeslow": [CTdeslow],
"CTdeshigh": [CTdeshigh],
"CTdeshigh": [CTdeshigh],
"CTdesapp": [CTdesapp],
"CTdesld": [CTdesld],
"HpDes": [HpDes],
"CfDes": [CfDes],
"CfCrz": [CfCrz],
"Cf": [Cf],
"hmo": [hmo],
"Hmax": [Hmax],
"tempGrad": [tempGrad],
"mass": [mass],
"MMO": [MMO],
"VMO": [VMO],
"massGrad": [massGrad],
"MTOW": [MTOW],
"OEW": [OEW],
"MPL": [MPL],
"TOL": [TOL],
"LDL": [LDL],
"span": [span],
"length": [length],
"span": [span],
"length": [length],
}
df_single = pd.DataFrame(data)
return df_single
[docs]
@staticmethod
def parseAPF(filePath, acName):
"""
Parses a BADA3 APF ASCII formatted file for aircraft performance data.
:param filePath: Path to the BADA3 APF ASCII formatted file.
:param acName: ICAO aircraft designation (e.g., 'A320').
:type filePath: str
:type acName: str
:raises IOError: If the file cannot be opened or read.
:returns: A pandas DataFrame containing the parsed aircraft performance data.
:rtype: pd.DataFrame
"""
filename = os.path.join(filePath, acName) + ".APF"
dataLines = list()
with open(filename, "r", encoding="latin-1") as f:
while True:
line = f.readline()
if line.startswith("CC"):
if "Modification_date" in line:
data = line.split(":")[1].strip().split(" ")
modificationDateAPF = " ".join(
[data[0], data[1], data[2]]
)
if line.startswith("CD"):
line = " ".join(line.split())
line = line.strip().split(" ")
if "LO" in line:
line = line[line.index("LO") + 1 :]
elif "AV" in line:
line = line[line.index("AV") + 1 :]
elif "HI" in line:
line = line[line.index("HI") + 1 :]
dataLines.append(line)
elif "THE END" in line:
break
dataLines.pop(
0
) # remove first line that does not contain usefull data
# AV - average - line with average data
AVLine = dataLines[1]
# reading of V1 parameter from APF file
V1 = {}
V1["cl"] = conv.kt2ms(AVLine[0])
V1["cr"] = conv.kt2ms(AVLine[3])
V1["des"] = conv.kt2ms(AVLine[8])
V2 = {}
V2["cl"] = conv.kt2ms(AVLine[1])
V2["cr"] = conv.kt2ms(AVLine[4])
V2["des"] = conv.kt2ms(AVLine[7])
M = {}
M["cl"] = float(AVLine[2]) / 100
M["cr"] = float(AVLine[5]) / 100
M["des"] = float(AVLine[6]) / 100
# Single row dataframe
data = {
"modificationDateAPF": [modificationDateAPF],
"V1": [V1],
"V2": [V2],
"M": [M],
}
df_single = pd.DataFrame(data)
return df_single
[docs]
@staticmethod
def combineOPF_APF(OPFDataFrame, APFDataFrame):
"""
Combines data from OPF and APF DataFrames.
:param OPFDataFrame: DataFrame containing parsed data from the OPF file.
:param APFDataFrame: DataFrame containing parsed data from the APF file.
:type OPFDataFrame: pd.DataFrame
:type APFDataFrame: pd.DataFrame
:returns: A single DataFrame combining both OPF and APF data.
:rtype: pd.DataFrame
"""
# Combine data with GPF data (temporary solution)
combined_df = pd.concat(
[
OPFDataFrame.reset_index(drop=True),
APFDataFrame.reset_index(drop=True),
],
axis=1,
)
return combined_df
[docs]
@staticmethod
def readSynonym(filePath):
"""
Reads a BADA3 SYNONYM.NEW ASCII file and returns a dictionary of model-synonym pairs.
:param filePath: Path to the directory containing BADA3 files.
:type filePath: str
:returns: A dictionary where the keys are aircraft models and the values are the corresponding file names.
:rtype: dict
"""
filename = os.path.join(filePath, "SYNONYM.NEW")
# synonym - file name pair dictionary
synonym_fileName = {}
if os.path.isfile(filename):
with open(filename, "r", encoding="latin-1") as f:
while True:
line = f.readline()
if not line:
break
if line.startswith("CD"):
line = " ".join(line.split())
line = line.strip().split(" ")
model = str(line[2])
file = str(line[-3])
synonym_fileName[model] = file
return synonym_fileName
[docs]
@staticmethod
def readSynonymXML(filePath):
"""
Reads a BADA3 SYNONYM.xml file and returns a dictionary of model-synonym pairs.
:param filePath: Path to the directory containing BADA3 files.
:type filePath: str
:returns: A dictionary where the keys are aircraft models (codes) and the values are the corresponding file names.
:rtype: dict
:raises IOError: If the XML file is not found or cannot be read.
This function parses the 'SYNONYM.xml' file to extract aircraft model codes and their associated file names.
If the XML file is not found or is improperly formatted, an IOError is raised.
"""
filename = os.path.join(filePath, "SYNONYM.xml")
# synonym - file name pair dictionary
synonym_fileName = {}
if os.path.isfile(filename):
try:
tree = ET.parse(filename)
root = tree.getroot()
except:
raise IOError(filename + " not found or in correct format")
for child in root.iter("SYN"):
code = child.find("code").text
manufacturer = child.find("manu").text
file = child.find("file").text
ICAO = child.find("ICAO").text
synonym_fileName[code] = file
return synonym_fileName
[docs]
@staticmethod
def parseSynonym(filePath, acName):
"""
Parses either the ASCII or XML synonym file and returns the file name corresponding to the aircraft.
:param filePath: Path to the directory containing BADA3 files.
:param acName: ICAO aircraft designation for which the file name is needed.
:type filePath: str
:type acName: str
:returns: The file name corresponding to the aircraft, or None if not found.
:rtype: str or None
This function first attempts to read the aircraft synonym from the ASCII file ('SYNONYM.NEW').
If the synonym is not found, it then tries to read the XML version ('SYNONYM.xml').
It returns the associated file name or None if the aircraft synonym is not found.
"""
synonym_fileName = Parser.readSynonym(filePath)
# if ASCI synonym does not exist, try XML synonym file
if not synonym_fileName:
synonym_fileName = Parser.readSynonymXML(filePath)
if synonym_fileName and acName in synonym_fileName:
fileName = synonym_fileName[acName]
return fileName
else:
return None
[docs]
@staticmethod
def readGPF(filePath):
"""
Parses a BADA3 GPF ASCII formatted file.
:param filePath: Path to the directory containing BADA3 files.
:type filePath: str
:raises IOError: If the GPF file cannot be opened or read.
:returns: A list of dictionaries, each containing GPF parameters like engine type, flight phase, and parameter values.
:rtype: list of dict
"""
filename = os.path.join(filePath, "BADA.GPF")
GPFparamList = list()
if os.path.isfile(filename):
with open(filename, "r", encoding="latin-1") as f:
while True:
line = f.readline()
if not line:
break
if line.startswith("CD"):
line = " ".join(line.split())
line = line.strip().split(" ")
param = {
"name": str(line[1]),
"value": float(line[5]),
"engine": str(line[3]).split(","),
"phase": str(line[4]).split(","),
"flight": str(line[2]),
}
GPFparamList.append(param)
return GPFparamList
[docs]
@staticmethod
def readGPFXML(filePath):
"""
Parses a BADA3 GPF XML formatted file.
:param filePath: Path to the directory containing BADA3 files.
:type filePath: str
:raises IOError: If the XML file is not found or cannot be read.
:returns: A list of dictionaries, each containing GPF parameters such as engine type, flight phase, and performance values.
:rtype: list of dict
This function reads the 'GPF.xml' file and extracts general performance parameters for the aircraft,
including maximum acceleration, bank angles, thrust coefficients, speed limits, and more.
It parses the XML structure and returns a list of dictionaries representing these parameters.
"""
filename = os.path.join(filePath, "GPF.xml")
GPFparamList = list()
if os.path.isfile(filename):
try:
tree = ET.parse(filename)
root = tree.getroot()
except:
raise IOError(filename + " not found or in correct format")
allEngines = ["JET", "TURBOPROP", "PISTON", "ELECTRIC"]
allPhases = ["to", "ic", "cl", "cr", "des", "hold", "app", "lnd"]
allFlights = ["civ", "mil"]
# Parse general aircraft data
AccMax = root.find("AccMax")
GPFparamList.append(
{
"name": "acc_long_max",
"value": float(AccMax.find("long").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
GPFparamList.append(
{
"name": "acc_norm_max",
"value": float(AccMax.find("norm").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
AngBank = root.find("AngBank")
GPFparamList.append(
{
"name": "ang_bank_nom",
"value": float(
AngBank.find("Nom").find("Civ").find("ToLd").text
),
"engine": allEngines,
"phase": ["to", "ld"],
"flight": ["civ"],
}
)
GPFparamList.append(
{
"name": "ang_bank_nom",
"value": float(
AngBank.find("Nom").find("Civ").find("Others").text
),
"engine": allEngines,
"phase": ["ic", "cl", "cr", "des", "hold", "app"],
"flight": ["civ"],
}
)
GPFparamList.append(
{
"name": "ang_bank_nom",
"value": float(AngBank.find("Nom").find("Mil").text),
"engine": allEngines,
"phase": allPhases,
"flight": ["mil"],
}
)
GPFparamList.append(
{
"name": "ang_bank_max",
"value": float(
AngBank.find("Max").find("Civ").find("ToLd").text
),
"engine": allEngines,
"phase": ["to", "ld"],
"flight": ["civ"],
}
)
GPFparamList.append(
{
"name": "ang_bank_max",
"value": float(
AngBank.find("Max").find("Civ").find("Hold").text
),
"engine": allEngines,
"phase": ["hold"],
"flight": ["civ"],
}
)
GPFparamList.append(
{
"name": "ang_bank_max",
"value": float(
AngBank.find("Max").find("Civ").find("Others").text
),
"engine": allEngines,
"phase": ["ic", "cl", "cr", "des", "app"],
"flight": ["civ"],
}
)
GPFparamList.append(
{
"name": "ang_bank_max",
"value": float(AngBank.find("Max").find("Mil").text),
"engine": allEngines,
"phase": allPhases,
"flight": ["mil"],
}
)
GPFparamList.append(
{
"name": "C_des_exp",
"value": float(root.find("CDesExp").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
GPFparamList.append(
{
"name": "C_th_to",
"value": float(root.find("CThTO").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
GPFparamList.append(
{
"name": "C_th_cr",
"value": float(root.find("CTcr").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
GPFparamList.append(
{
"name": "C_v_min_to",
"value": float(root.find("CVminTO").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
GPFparamList.append(
{
"name": "C_v_min",
"value": float(root.find("CVmin").text),
"engine": allEngines,
"phase": allPhases,
"flight": allFlights,
}
)
HmaxList = {}
for phase in root.find("HmaxList").findall("HmaxPhase"):
HmaxList[phase.find("Phase").text] = float(
phase.find("Hmax").text
)
if phase.find("Phase").text == "TO":
GPFparamList.append(
{
"name": "H_max_to",
"value": float(phase.find("Hmax").text),
"engine": allEngines,
"phase": ["to"],
"flight": allFlights,
}
)
elif phase.find("Phase").text == "IC":
GPFparamList.append(
{
"name": "H_max_ic",
"value": float(phase.find("Hmax").text),
"engine": allEngines,
"phase": ["ic"],
"flight": allFlights,
}
)
elif phase.find("Phase").text == "AP":
GPFparamList.append(
{
"name": "H_max_app",
"value": float(phase.find("Hmax").text),
"engine": allEngines,
"phase": ["app"],
"flight": allFlights,
}
)
elif phase.find("Phase").text == "LD":
GPFparamList.append(
{
"name": "H_max_ld",
"value": float(phase.find("Hmax").text),
"engine": allEngines,
"phase": ["lnd"],
"flight": allFlights,
}
)
VdList = {}
for vdphase in root.find("VdList").findall("VdPhase"):
Phase = vdphase.find("Phase")
name = Phase.find("name").text
index = int(Phase.find("index").text)
Vd = float(vdphase.find("Vd").text)
if name not in VdList:
VdList[name] = {}
VdList[name][index] = Vd
if name == "CL":
if index == 1:
V_cl_1 = Vd
GPFparamList.append(
{
"name": "V_cl_1",
"value": V_cl_1,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 2:
V_cl_2 = Vd
GPFparamList.append(
{
"name": "V_cl_2",
"value": V_cl_2,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 3:
V_cl_3 = Vd
GPFparamList.append(
{
"name": "V_cl_3",
"value": V_cl_3,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 4:
V_cl_4 = Vd
GPFparamList.append(
{
"name": "V_cl_4",
"value": V_cl_4,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 5:
V_cl_5 = Vd
GPFparamList.append(
{
"name": "V_cl_5",
"value": V_cl_5,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 6:
V_cl_6 = Vd
GPFparamList.append(
{
"name": "V_cl_6",
"value": V_cl_6,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 7:
V_cl_7 = Vd
GPFparamList.append(
{
"name": "V_cl_7",
"value": V_cl_7,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
elif index == 8:
V_cl_8 = Vd
GPFparamList.append(
{
"name": "V_cl_8",
"value": V_cl_8,
"engine": allEngines,
"phase": ["cl"],
"flight": allFlights,
}
)
if name == "DES":
if index == 1:
V_des_1 = Vd
GPFparamList.append(
{
"name": "V_des_1",
"value": V_des_1,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
elif index == 2:
V_des_2 = Vd
GPFparamList.append(
{
"name": "V_des_2",
"value": V_des_2,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
elif index == 3:
V_des_3 = Vd
GPFparamList.append(
{
"name": "V_des_3",
"value": V_des_3,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
elif index == 4:
V_des_4 = Vd
GPFparamList.append(
{
"name": "V_des_4",
"value": V_des_4,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
elif index == 5:
V_des_5 = Vd
GPFparamList.append(
{
"name": "V_des_5",
"value": V_des_5,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
elif index == 6:
V_des_6 = Vd
GPFparamList.append(
{
"name": "V_des_6",
"value": V_des_6,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
elif index == 7:
V_des_7 = Vd
GPFparamList.append(
{
"name": "V_des_7",
"value": V_des_7,
"engine": allEngines,
"phase": ["des"],
"flight": allFlights,
}
)
VList = {}
for vphase in root.find("VList").findall("VPhase"):
Phase = vphase.find("Phase")
name = Phase.find("name").text
index = int(Phase.find("index").text)
V = float(vphase.find("V").text)
if name not in VList:
VList[name] = {}
VList[name][index] = V
if name == "HOLD":
if index == 1:
V_hold_1 = V
GPFparamList.append(
{
"name": "V_hold_1",
"value": V_hold_1,
"engine": allEngines,
"phase": ["hold"],
"flight": allFlights,
}
)
elif index == 2:
V_hold_2 = V
GPFparamList.append(
{
"name": "V_hold_2",
"value": V_hold_2,
"engine": allEngines,
"phase": ["hold"],
"flight": allFlights,
}
)
elif index == 3:
V_hold_3 = V
GPFparamList.append(
{
"name": "V_hold_3",
"value": V_hold_3,
"engine": allEngines,
"phase": ["hold"],
"flight": allFlights,
}
)
elif index == 4:
V_hold_4 = V
GPFparamList.append(
{
"name": "V_hold_4",
"value": V_hold_4,
"engine": allEngines,
"phase": ["hold"],
"flight": allFlights,
}
)
V_backtrack = float(root.find("Vground").find("backtrack").text)
GPFparamList.append(
{
"name": "V_backtrack",
"value": V_backtrack,
"engine": allEngines,
"phase": ["gnd"],
"flight": allFlights,
}
)
V_taxi = float(root.find("Vground").find("taxi").text)
GPFparamList.append(
{
"name": "V_taxi",
"value": V_taxi,
"engine": allEngines,
"phase": ["gnd"],
"flight": allFlights,
}
)
V_apron = float(root.find("Vground").find("apron").text)
GPFparamList.append(
{
"name": "V_apron",
"value": V_apron,
"engine": allEngines,
"phase": ["gnd"],
"flight": allFlights,
}
)
V_gate = float(root.find("Vground").find("gate").text)
GPFparamList.append(
{
"name": "V_gate",
"value": V_gate,
"engine": allEngines,
"phase": ["gnd"],
"flight": allFlights,
}
)
CredList = {}
for CredEng in root.find("CredList").findall("CredEng"):
EngineType = CredEng.find("EngineType").text
Cred = float(CredEng.find("Cred").text)
CredList[EngineType] = Cred
if EngineType == "JET":
GPFparamList.append(
{
"name": "C_red_jet",
"value": Cred,
"engine": ["jet"],
"phase": ["ic", "cl"],
"flight": allFlights,
}
)
elif EngineType == "TURBOPROP":
GPFparamList.append(
{
"name": "C_red_turbo",
"value": Cred,
"engine": ["tbp"],
"phase": ["ic", "cl"],
"flight": allFlights,
}
)
elif EngineType == "PISTON":
GPFparamList.append(
{
"name": "C_red_piston",
"value": Cred,
"engine": ["pst"],
"phase": ["ic", "cl"],
"flight": allFlights,
}
)
elif EngineType == "ELECTRIC":
GPFparamList.append(
{
"name": "C_red_elec",
"value": Cred,
"engine": ["elc"],
"phase": ["ic", "cl"],
"flight": allFlights,
}
)
return GPFparamList
[docs]
@staticmethod
def parseGPF(filePath):
"""
Parses a BADA3 (GPF) from either ASCII or XML format.
:param filePath: Path to the directory containing BADA3 files.
:type filePath: str
:returns: A pandas DataFrame containing GPF data.
:rtype: pd.DataFrame
"""
GPFdata = Parser.readGPF(filePath)
# if ASCI GPF does not exist, try XML GPF file
if not GPFdata:
GPFdata = Parser.readGPFXML(filePath)
# Single row dataframe
data = {"GPFdata": [GPFdata]}
df_single = pd.DataFrame(data)
return df_single
[docs]
@staticmethod
def getGPFValue(GPFdata, name, engine="JET", phase="cr", flight="civ"):
"""
Retrieves the value of a specified GPF parameter based on engine type, flight phase, and flight type.
:param GPFdata: List of dictionaries containing GPF parameters.
:param name: Name of the GPF parameter to retrieve.
:param engine: Engine type to filter by (e.g., 'JET', 'TURBOPROP', 'PISTON', 'ELECTRIC'). Default is 'JET'.
:param phase: Flight phase to filter by (e.g., 'cr', 'cl', 'des'). Default is 'cr'.
:param flight: Flight type to filter by ('civ' or 'mil'). Default is 'civ'.
:type GPFdata: list
:type name: str
:type engine: str
:type phase: str
:type flight: str
:returns: The value of the specified GPF parameter or None if not found.
:rtype: float or None
"""
# implementation required because 3.16 GPF contains different engine names than 3.15 GPF file
if engine == "JET":
engineList = [engine, "jet"]
if engine == "TURBOPROP":
engineList = [engine, "turbo", "tbp"]
if engine == "PISTON":
engineList = [engine, "piston", "pst"]
if engine == "ELECTRIC":
engineList = [engine, "electric", "elc"]
for param in GPFdata:
if (
(param["name"] == name)
& (any(i in engineList for i in param["engine"]))
& (phase in param["phase"])
& (flight in param["flight"])
):
return float(param["value"])
return None
[docs]
@staticmethod
def combineACDATA_GPF(ACDataFrame, GPFDataframe):
"""
Combines two DataFrames: one containing aircraft-specific data (ACData) and another containing (GPF) data.
:param ACDataFrame: DataFrame containing parsed aircraft data.
:param GPFDataframe: DataFrame containing parsed GPF data.
:type ACDataFrame: pd.DataFrame
:type GPFDataframe: pd.DataFrame
:returns: A combined DataFrame containing both ACData and GPF data.
:rtype: pd.DataFrame
"""
# Combine data with GPF data (temporary solution)
combined_df = pd.concat(
[
ACDataFrame.reset_index(drop=True),
GPFDataframe.reset_index(drop=True),
],
axis=1,
)
return combined_df
[docs]
@staticmethod
def parseAll(badaVersion, filePath=None):
"""
Parses all BADA3 formatted files and combines them into a final DataFrame.
:param badaVersion: BADA version being used.
:param filePath: Path to the BADA3 formatted files. If not provided, the default path is used.
:type badaVersion: str
:type filePath: str, optional
:returns: A pandas DataFrame containing all parsed BADA3 data.
:rtype: pd.DataFrame
:raises IOError: If any of the required files cannot be opened or read.
"""
if filePath == None:
filePath = configuration.getBadaVersionPath(
badaFamily="BADA3", badaVersion=badaVersion
)
else:
filePath = filePath
# parsing GPF file
GPFparsedDataframe = Parser.parseGPF(filePath)
# try to get subfolders, if they exist
# get names of all the folders in the main BADA model folder to search for XML files
subfolders = configuration.list_subfolders(filePath)
if not subfolders:
# use APF and OPF files
merged_df = pd.DataFrame()
# get synonym-filename pairs
synonym_fileName = Parser.readSynonym(filePath)
for synonym in synonym_fileName:
file = synonym_fileName[synonym]
# parse the original data of a model
OPFDataFrame = Parser.parseOPF(filePath, file)
APFDataFrame = Parser.parseAPF(filePath, file)
df = Parser.combineOPF_APF(OPFDataFrame, APFDataFrame)
# rename acName in the dateaframe to match the synonym model name
df.at[0, "acName"] = synonym
# Combine data with GPF data (temporary solution)
combined_df = Parser.combineACDATA_GPF(df, GPFparsedDataframe)
# Merge DataFrames
merged_df = pd.concat(
[merged_df, combined_df], ignore_index=True
)
return merged_df
else:
# use xml files inside those subfolders
merged_df = pd.DataFrame()
# get synonym-filename pairs
synonym_fileName = Parser.readSynonymXML(filePath)
for synonym in synonym_fileName:
file = synonym_fileName[synonym]
if file in subfolders:
# parse the original XML of a model
df = Parser.parseXML(filePath, file)
# rename acName in the dateaframe to match the synonym model name
df.at[0, "acName"] = synonym
# Combine data with GPF data (temporary solution)
combined_df = Parser.combineACDATA_GPF(
df, GPFparsedDataframe
)
# Merge DataFrames
merged_df = pd.concat(
[merged_df, combined_df], ignore_index=True
)
return merged_df
[docs]
class BADA3(Airplane, Bada):
"""This class implements the part of BADA3 performance model that will be used in other classes following the BADA3 manual.
:param AC: Aircraft object {BADA3}.
:type AC: bada3Aircraft.
"""
def __init__(self, AC):
super().__init__()
self.AC = AC
[docs]
def CL(self, sigma, mass, tas, nz=1.0):
"""
Computes the lift coefficient for the aircraft.
:param sigma: Normalized air density [-].
:param mass: Aircraft mass in kilograms [kg].
:param tas: True airspeed in meters per second [m/s].
:param nz: Load factor [-], default is 1.0 (straight and level flight).
:type sigma: float
:type mass: float
:type tas: float
:type nz: float
:returns: Lift coefficient [-].
:rtype: float
"""
return (
2
* mass
* const.g
* nz
/ (sigma * const.rho_0 * tas * tas * self.AC.S)
)
[docs]
def CD(
self,
CL,
config,
expedite=False,
speedBrakes={"deployed": False, "value": 0.03},
):
"""
Computes the drag coefficient based on the lift coefficient and aircraft configuration.
:param CL: Lift coefficient [-].
:param config: Aircraft aerodynamic configuration (e.g., 'CR', 'IC', 'TO', 'AP', 'LD').
:param expedite: Flag indicating if expedite descent is used (default is False).
:param speedBrakes: Dictionary indicating if speed brakes are deployed and their effect.
:type CL: float
:type config: str
:type expedite: bool
:type speedBrakes: dict
:returns: Drag coefficient [-].
:rtype: float
:raises: ValueError if an invalid configuration is provided.
"""
if self.AC.xmlFiles:
HLid_CR = self.AC.aeroConfig["CR"]["HLid"]
LG_CR = self.AC.aeroConfig["CR"]["LG"]
HLid_AP = self.AC.aeroConfig["AP"]["HLid"]
LG_AP = self.AC.aeroConfig["AP"]["LG"]
HLid_LD = self.AC.aeroConfig["LD"]["HLid"]
LG_LD = self.AC.aeroConfig["LD"]["LG"]
if (
self.AC.CD0[HLid_AP][LG_AP] == 0.0
and self.AC.CD0[HLid_LD][LG_LD] == 0.0
and self.AC.CD2[HLid_AP][LG_AP] == 0.0
and self.AC.CD2[HLid_LD][LG_LD] == 0.0
and self.AC.DeltaCD == 0.0
):
CD = self.AC.CD0[HLid_CR][LG_CR] + self.AC.CD2[HLid_CR][
LG_CR
] * (CL * CL)
else:
if config == "CR" or config == "IC" or config == "TO":
CD = (
self.AC.CD0[HLid_CR][LG_CR]
+ self.AC.CD2[HLid_CR][LG_CR] * CL**2
)
elif config == "AP":
CD = (
self.AC.CD0[HLid_AP][LG_AP]
+ self.AC.CD2[HLid_AP][LG_AP] * CL**2
)
elif config == "LD":
CD = (
self.AC.CD0[HLid_LD][LG_LD]
+ self.AC.DeltaCD
+ self.AC.CD2[HLid_LD][LG_LD] * CL**2
)
else:
return float("Nan")
else:
if (
self.AC.CD0["AP"] == 0.0
and self.AC.CD0["LD"] == 0.0
and self.AC.CD2["AP"] == 0.0
and self.AC.CD2["LD"] == 0.0
and self.AC.CD0["GEAR_DOWN"] == 0.0
):
CD = self.AC.CD0["CR"] + self.AC.CD2["CR"] * (CL * CL)
else:
if config == "CR" or config == "IC" or config == "TO":
CD = self.AC.CD0["CR"] + self.AC.CD2["CR"] * CL**2
elif config == "AP":
CD = self.AC.CD0[config] + self.AC.CD2[config] * CL**2
elif config == "LD":
CD = (
self.AC.CD0[config]
+ self.AC.CD0["GEAR_DOWN"]
+ self.AC.CD2[config] * CL**2
)
else:
return float("Nan")
# implementation of a simple speed brakes model
if speedBrakes["deployed"]:
if speedBrakes["value"] is not None:
CD = CD + speedBrakes["value"]
else:
CD = CD + 0.03
return CD
# expedite descent
C_des_exp = 1.0
if expedite:
C_des_exp = Parser.getGPFValue(
self.AC.GPFdata, "C_des_exp", phase="des"
)
CD = CD * C_des_exp
return CD
[docs]
def D(self, sigma, tas, CD):
"""
Computes the aerodynamic drag force.
:param sigma: Normalized air density [-].
:param tas: True airspeed in meters per second [m/s].
:param CD: Drag coefficient [-].
:type sigma: float
:type tas: float
:type CD: float
:returns: Aerodynamic drag in Newtons [N].
:rtype: float
"""
return 0.5 * sigma * const.rho_0 * tas * tas * self.AC.S * CD
[docs]
def L(self, sigma, tas, CL):
"""
Computes the aerodynamic lift force.
:param sigma: Normalized air density [-].
:param tas: True airspeed in meters per second [m/s].
:param CL: Lift coefficient [-].
:type sigma: float
:type tas: float
:type CL: float
:returns: Aerodynamic lift in Newtons [N].
:rtype: float
"""
return 0.5 * sigma * const.rho_0 * tas * tas * self.AC.S * CL
[docs]
def Thrust(self, h, DeltaTemp, rating, v, config, **kwargs):
"""
Computes the aircraft thrust based on engine rating and flight conditions.
:param rating: Engine rating ('MCMB', 'MCRZ', 'MTKF', 'LIDL', 'ADAPTED').
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param v: True airspeed (TAS) in meters per second [m/s].
:param config: Aircraft aerodynamic configuration (e.g., 'CR', 'IC', 'TO', 'AP', 'LD').
:type rating: str
:type h: float
:type DeltaTemp: float
:type v: float
:type config: str
:returns: Thrust in Newtons [N].
:rtype: float
"""
if rating == "MCMB":
# MCMB
T = self.TMax(h=h, DeltaTemp=DeltaTemp, rating=rating, v=v)
elif rating == "MTKF":
# MTKF
T = self.TMax(h=h, DeltaTemp=DeltaTemp, rating=rating, v=v)
elif rating == "MCRZ":
# MCRZ
T = self.TMax(h=h, DeltaTemp=DeltaTemp, rating=rating, v=v)
elif rating == "LIDL":
# LIDL
T = self.TDes(h=h, DeltaTemp=DeltaTemp, v=v, config=config)
elif rating == "ADAPTED":
# ADAPTED
ROCD = checkArgument("ROCD", **kwargs)
mass = checkArgument("mass", **kwargs)
v = checkArgument("v", **kwargs)
acc = checkArgument("acc", **kwargs)
Drag = checkArgument("Drag", **kwargs)
T = self.TAdapted(
h=h,
DeltaTemp=DeltaTemp,
ROCD=ROCD,
mass=mass,
v=v,
acc=acc,
Drag=Drag,
)
else:
T = float("Nan")
return T
[docs]
def TAdapted(self, h, DeltaTemp, ROCD, mass, v, acc, Drag):
"""
Computes adapted thrust for non-standard flight conditions (e.g., climb, acceleration).
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param ROCD: Rate of climb or descent in meters per second [m/s].
:param mass: Aircraft mass in kilograms [kg].
:param v: True airspeed (TAS) in meters per second [m/s].
:param acc: Acceleration in meters per second squared [m/s²].
:param Drag: Aerodynamic drag in Newtons [N].
:type h: float
:type DeltaTemp: float
:type ROCD: float
:type mass: float
:type v: float
:type acc: float
:type Drag: float
:returns: Adapted thrust in Newtons [N].
:rtype: float
"""
theta = atm.theta(h=h, DeltaTemp=DeltaTemp)
tau_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp)
Tadapted = ROCD * mass * const.g * tau_const / v + mass * acc + Drag
return Tadapted
[docs]
def TMax(self, h, DeltaTemp, rating, v):
"""
Computes the maximum thrust based on engine type, altitude, and temperature deviation.
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param rating: Engine rating ('MCMB', 'MCRZ', 'MTKF').
:param v: True airspeed (TAS) in meters per second [m/s].
:type h: float
:type DeltaTemp: float
:type rating: str
:type v: float
:returns: Maximum thrust in Newtons [N].
:rtype: float
"""
acModel = self.AC.engineType
if acModel == "JET":
TMaxISA = self.AC.Ct[0] * (
1
- (conv.m2ft(h)) / self.AC.Ct[1]
+ self.AC.Ct[2] * (conv.m2ft(h)) * (conv.m2ft(h))
)
elif acModel == "TURBOPROP":
TMaxISA = (self.AC.Ct[0] / conv.ms2kt(v)) * (
1 - conv.m2ft(h) / self.AC.Ct[1]
) + self.AC.Ct[2]
elif acModel == "PISTON" or acModel == "ELECTRIC":
TMaxISA = self.AC.Ct[0] * (1 - conv.m2ft(h) / self.AC.Ct[1]) + (
self.AC.Ct[2] / conv.ms2kt(v)
)
else:
return float("Nan")
DeltaTempEff = DeltaTemp - self.AC.Ct[3]
if self.AC.Ct[4] < 0:
Ctc5 = 0
else:
Ctc5 = self.AC.Ct[4]
DeltaTemp_ = Ctc5 * DeltaTempEff
if DeltaTemp_ <= 0:
DeltaTemp_ = 0
elif DeltaTemp_ > 0.4:
DeltaTemp_ = 0.4
TMax = TMaxISA * (1 - DeltaTemp_)
if rating == "MCMB" or rating == "MTKF":
return TMax
elif rating == "MCRZ":
return TMax * Parser.getGPFValue(
self.AC.GPFdata, "C_th_cr", phase="cr"
)
[docs]
def TDes(self, h, DeltaTemp, v, config):
"""
Computes descent thrust based on altitude, temperature deviation, and configuration.
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param v: True airspeed (TAS) in meters per second [m/s].
:param config: Aircraft aerodynamic configuration (e.g., 'CR', 'IC', 'TO', 'AP', 'LD').
:type h: float
:type DeltaTemp: float
:type v: float
:type config: str
:returns: Descent thrust in Newtons [N].
:rtype: float
"""
H_max_app = Parser.getGPFValue(
self.AC.GPFdata, "H_max_app", phase="app"
)
if (
self.AC.engineType == "PISTON"
or self.AC.engineType == "ELECTRIC"
or self.AC.engineType == "TURBOPROP"
):
TMaxClimb = self.TMax(rating="MCMB", h=h, DeltaTemp=DeltaTemp, v=v)
elif self.AC.engineType == "JET":
TMaxClimb = self.TMax(rating="MCMB", h=h, DeltaTemp=DeltaTemp, v=v)
# non-clean data available -> Hp,des cannot be below Hmax,AP
HpDes_ = self.AC.HpDes
if self.AC.xmlFiles:
[HLid_CR, LG_CR] = self.flightEnvelope.getAeroConfig(config="CR")
[HLid_AP, LG_AP] = self.flightEnvelope.getAeroConfig(config="AP")
[HLid_LD, LG_LD] = self.flightEnvelope.getAeroConfig(config="LD")
if (
self.AC.CD0[HLid_AP][LG_AP] != 0.0
and self.AC.CD0[HLid_LD][LG_LD] != 0.0
and self.AC.CD2[HLid_AP][LG_AP] != 0.0
and self.AC.CD2[HLid_LD][LG_LD] != 0.0
and self.AC.DeltaCD != 0.0
):
if HpDes_ < H_max_app:
HpDes_ = H_max_app
else:
if (
self.AC.CD0["AP"] != 0.0
and self.AC.CD0["LD"] != 0.0
and self.AC.CD2["AP"] != 0.0
and self.AC.CD2["LD"] != 0.0
and self.AC.CD0["GEAR_DOWN"] != 0.0
):
if HpDes_ < H_max_app:
HpDes_ = H_max_app
if h > conv.ft2m(HpDes_):
Tdes = self.AC.CTdeshigh * TMaxClimb
elif h <= conv.ft2m(HpDes_):
if config == "CR":
Tdes = self.AC.CTdeslow * TMaxClimb
elif config == "AP":
Tdes = self.AC.CTdesapp * TMaxClimb
elif config == "LD":
Tdes = self.AC.CTdesld * TMaxClimb
else:
Tdes = float("Nan")
return Tdes
[docs]
def ffnom(self, v, T):
"""
Computes the nominal fuel flow based on airspeed and thrust.
:param v: True airspeed (TAS) in meters per second [m/s].
:param T: Thrust in Newtons [N].
:type v: float
:type T: float
:returns: Nominal fuel flow in kilograms per second [kg/s].
:rtype: float
"""
if self.AC.engineType == "JET":
eta = (
self.AC.Cf[0]
* (1 + conv.ms2kt(v) / self.AC.Cf[1])
/ (1000 * 60)
)
ffnom = eta * T
elif self.AC.engineType == "TURBOPROP":
eta = (
self.AC.Cf[0]
* (1 - conv.ms2kt(v) / self.AC.Cf[1])
* (conv.ms2kt(v) / 1000)
/ (1000 * 60)
)
ffnom = eta * T
elif (
self.AC.engineType == "PISTON" or self.AC.engineType == "ELECTRIC"
):
ffnom = self.AC.Cf[0] / 60
return ffnom
[docs]
def ffMin(self, h):
"""
Computes the minimum fuel flow based on altitude.
:param h: Altitude in meters [m].
:type h: float
:returns: Minimum fuel flow in kilograms per second [kg/s].
:rtype: float
"""
if self.AC.engineType == "JET" or self.AC.engineType == "TURBOPROP":
ffmin = (
self.AC.CfDes[0] * (1 - (conv.m2ft(h)) / self.AC.CfDes[1]) / 60
)
elif (
self.AC.engineType == "PISTON" or self.AC.engineType == "ELECTRIC"
):
ffmin = self.AC.CfDes[0] / 60 # Cf3 param
return ffmin
[docs]
def ff(self, h, v, T, config=None, flightPhase=None, adapted=False):
"""
Computes the fuel flow based on flight phase and current flight conditions.
:param h: Altitude in meters [m].
:param v: True airspeed (TAS) in meters per second [m/s].
:param T: Thrust in Newtons [N].
:param config: Aircraft aerodynamic configuration (e.g., 'CR', 'AP', 'LD'). Optional.
:param flightPhase: Flight phase (e.g., 'Climb', 'Cruise', 'Descent'). Optional.
:param adapted: If True, computes fuel flow for adapted thrust. Default is False.
:type h: float
:type v: float
:type T: float
:type config: str, optional
:type flightPhase: str, optional
:type adapted: bool, optional
:returns: Fuel flow in kilograms per second [kg/s].
:rtype: float
"""
if adapted:
# adapted thrust
ffnom = self.ffnom(v=v, T=T)
ff = max(ffnom, self.ffMin(h=h))
else:
if flightPhase == "Climb":
# climb thrust
ffnom = self.ffnom(v=v, T=T)
ff = max(ffnom, self.ffMin(h=h))
elif flightPhase == "Cruise":
# cruise thrust
ffnom = self.ffnom(v=v, T=T)
ff = ffnom * self.AC.CfCrz
elif flightPhase == "Descent":
# descent in IDLE
if config == "CR":
ff = self.ffMin(h=h)
elif config == "AP" or config == "LD":
ffnom = self.ffnom(v=v, T=T)
ff = max(ffnom, self.ffMin(h=h))
else:
ff = float("Nan")
return ff
[docs]
def reducedPower(self, h, mass, DeltaTemp):
"""
Computes the reduced climb power coefficient based on altitude, mass, and temperature deviation.
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param mass: Aircraft mass in kilograms [kg].
:type h: float
:type DeltaTemp: float
:type mass: float
:returns: Reduced climb power coefficient [-].
:rtype: float
"""
hMax = self.flightEnvelope.maxAltitude(mass=mass, DeltaTemp=DeltaTemp)
mMax = self.AC.mass["maximum"]
mMin = self.AC.mass["minimum"]
ep = 1e-6 # floating point precision
if (h + ep) < 0.8 * hMax:
if self.AC.engineType == "JET":
CRed = Parser.getGPFValue(
self.AC.GPFdata,
"C_red_jet",
engine=self.AC.engineType,
phase="cl",
)
elif self.AC.engineType == "TURBOPROP":
CRed = Parser.getGPFValue(
self.AC.GPFdata,
"C_red_turbo",
engine="TURBOPROP",
phase="cl",
)
elif self.AC.engineType == "PISTON":
CRed = Parser.getGPFValue(
self.AC.GPFdata,
"C_red_piston",
engine="PISTON",
phase="cl",
)
elif self.AC.engineType == "ELECTRIC":
CRed = Parser.getGPFValue(
self.AC.GPFdata,
"C_red_elec",
engine="ELECTRIC",
phase="cl",
)
else:
CRed = 0
CPowRed = 1 - CRed * (mMax - mass) / (mMax - mMin)
return CPowRed
[docs]
def ROCD(self, T, D, v, mass, ESF, h, DeltaTemp, reducedPower=False):
"""
Computes the rate of climb or descent (ROCD) based on thrust, drag, airspeed, and other flight parameters.
:param T: Aircraft thrust in Newtons [N].
:param D: Aircraft drag in Newtons [N].
:param v: True airspeed (TAS) in meters per second [m/s].
:param mass: Aircraft mass in kilograms [kg].
:param ESF: Energy share factor [-].
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param reducedPower: Whether to account for reduced power in the calculation. Default is False.
:type T: float
:type D: float
:type v: float
:type mass: float
:type ESF: float
:type h: float
:type DeltaTemp: float
:type reducedPower: bool, optional
:returns: Rate of climb or descent in meters per second [m/s].
:rtype: float
"""
theta = atm.theta(h=h, DeltaTemp=DeltaTemp)
temp = theta * const.temp_0
CPowRed = 1.0
if reducedPower:
CPowRed = self.reducedPower(h=h, mass=mass, DeltaTemp=DeltaTemp)
ROCD = (
((temp - DeltaTemp) / temp)
* (T - D)
* v
* ESF
* CPowRed
/ (mass * const.g)
)
return ROCD
[docs]
class FlightEnvelope(BADA3):
"""This class is a BADA3 aircraft subclass and implements the flight envelope caclulations
following the BADA3 manual.
:param AC: Aircraft object {BADA3}.
:type AC: bada3Aircraft.
"""
def __init__(self, AC):
super().__init__(AC)
[docs]
def maxAltitude(self, mass, DeltaTemp):
"""
Computes the maximum altitude for a given aircraft mass and deviation from ISA.
:param mass: Actual aircraft mass in kilograms [kg].
:param DeltaTemp: Deviation from International Standard Atmosphere (ISA) temperature in Kelvin [K].
:type mass: float
:type DeltaTemp: float
:returns: Maximum altitude in meters [m].
:rtype: float
This function calculates the maximum possible altitude based on the aircraft's mass,
ISA temperature deviation, and engine-specific parameters such as temperature and mass gradients.
It considers the maximum operational altitude and adjusts for the given conditions.
"""
Gt = self.AC.tempGrad
Gw = self.AC.mass["mass grad"]
Ctc4 = self.AC.Ct[3]
mMax = self.AC.mass["maximum"]
if Gw < 0:
Gw = 0
if Gt > 0:
Gt = 0
var = DeltaTemp - Ctc4
if var < 0:
var = 0
if self.AC.Hmax == 0:
hMax = self.AC.hmo
else:
hMax = min(
self.AC.hmo, self.AC.Hmax + Gt * var + Gw * (mMax - mass)
)
return conv.ft2m(hMax)
[docs]
def VStall(self, mass, config):
"""
Computes the stall speed based on the aircraft configuration and mass.
:param config: Aircraft configuration (e.g., 'CR', 'TO', 'AP', 'LD').
:param mass: Aircraft mass in kilograms [kg].
:type config: str
:type mass: float
:returns: Stall speed in meters per second [m/s].
:rtype: float
"""
if self.AC.xmlFiles:
[HLid, LG] = self.getAeroConfig(config=config)
vStall = conv.kt2ms(self.AC.Vstall[HLid][LG]) * sqrt(
mass / self.AC.mass["reference"]
)
else:
vStall = conv.kt2ms(self.AC.Vstall[config]) * sqrt(
mass / self.AC.mass["reference"]
)
return vStall
[docs]
def VMin(
self, h, mass, config, DeltaTemp, nz=1.2, envelopeType="OPERATIONAL"
):
"""
Computes the minimum speed for a given configuration and conditions.
:param h: Altitude in meters [m].
:param mass: Aircraft mass in kilograms [kg].
:param config: Aircraft configuration (e.g., 'CR', 'TO', 'LD').
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param nz: Load factor, default is 1.2.
:param envelopeType: Type of flight envelope ('OPERATIONAL' or 'CERTIFIED').
:type h: float
:type mass: float
:type config: str
:type DeltaTemp: float
:type nz: float, optional
:type envelopeType: str, optional
:returns: Minimum speed in meters per second [m/s].
:rtype: float
"""
if envelopeType == "OPERATIONAL":
if config == "TO":
VminStall = Parser.getGPFValue(
self.AC.GPFdata, "C_v_min_to", phase="to"
) * self.VStall(mass=mass, config=config)
else:
VminStall = Parser.getGPFValue(
self.AC.GPFdata, "C_v_min"
) * self.VStall(mass=mass, config=config)
elif envelopeType == "CERTIFIED":
VminStall = self.VStall(mass=mass, config=config)
if self.AC.Clbo == 0.0 and self.AC.k == 0.0:
Vmin = VminStall
else:
if h < conv.ft2m(15000):
Vmin = VminStall
elif h >= conv.ft2m(15000):
# low speed buffeting limit applies only for JET and TURBOPROP
if (
self.AC.engineType == "JET"
or self.AC.engineType == "TURBOPROP"
):
[theta, delta, sigma] = atm.atmosphereProperties(
h=h, DeltaTemp=DeltaTemp
)
buffetLimit = self.lowSpeedBuffetLimit(
h=h, mass=mass, DeltaTemp=DeltaTemp, nz=nz
)
if buffetLimit == float("Nan"):
Vmin = VminStall
else:
Vmin = max(
VminStall,
atm.mach2Cas(
buffetLimit,
theta=theta,
delta=delta,
sigma=sigma,
),
)
elif (
self.AC.engineType == "PISTON"
or self.AC.engineType == "ELECTRIC"
):
Vmin = VminStall
return Vmin
[docs]
def Vmax_thrustLimited(self, h, mass, DeltaTemp, rating, config):
"""
Computes the maximum CAS speed considering thrust limitations within the certified flight envelope.
:param h: Altitude in meters [m].
:param mass: Aircraft mass in kilograms [kg].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param rating: Aircraft engine rating (e.g., 'MTKF', 'MCMB', 'MCRZ').
:param config: Aircraft configuration (e.g., 'TO', 'CR').
:type h: float
:type mass: float
:type DeltaTemp: float
:type rating: str
:type config: str
:returns: Maximum thrust-limited speed in meters per second [m/s].
:rtype: float
"""
[theta, delta, sigma] = atm.atmosphereProperties(
h=h, DeltaTemp=DeltaTemp
)
VmaxCertified = self.VMax(h=h, DeltaTemp=DeltaTemp)
VminCertified = self.VMin(
h=h,
mass=mass,
config=config,
DeltaTemp=DeltaTemp,
nz=1.0,
envelopeType="CERTIFIED",
)
maxCASList = []
DragValue = None
ThrustValue = None
CDvalue = None
CASValue = None
MValue = None
for CAS in np.linspace(
VminCertified, VmaxCertified, num=200, endpoint=True
):
TAS = atm.cas2Tas(cas=CAS, delta=delta, sigma=sigma)
M = atm.cas2Mach(cas=CAS, theta=theta, delta=delta, sigma=sigma)
maxThrust = self.Thrust(
h=h, DeltaTemp=DeltaTemp, rating=rating, v=TAS, config=config
)
CL = self.CL(sigma=sigma, mass=mass, tas=TAS, nz=1.0)
CD = self.CD(CL=CL, config=config)
Drag = self.D(sigma=sigma, tas=TAS, CD=CD)
if maxThrust >= Drag:
maxCASList.append(CAS)
DragValue = Drag
ThrustValue = maxThrust
CDvalue = CD
CASValue = conv.ms2kt(CAS)
MValue = M
if not maxCASList:
return None
else:
return max(maxCASList)
[docs]
def Vx(self, h, mass, DeltaTemp, rating, config):
"""
Computes the best angle of climb (Vx) speed.
:param h: Altitude in meters [m].
:param mass: Aircraft mass in kilograms [kg].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param rating: Aircraft engine rating (e.g., 'MTKF', 'MCMB', 'MCRZ').
:param config: Aircraft configuration (e.g., 'TO', 'CR').
:type h: float
:type mass: float
:type DeltaTemp: float
:type rating: str
:type config: str
:returns: Best angle of climb speed (Vx) in meters per second [m/s].
:rtype: float
"""
[theta, delta, sigma] = atm.atmosphereProperties(
h=h, DeltaTemp=DeltaTemp
)
VmaxCertified = self.VMax(h=h, DeltaTemp=DeltaTemp)
VminCertified = self.VMin(
h=h,
mass=mass,
config=config,
DeltaTemp=DeltaTemp,
nz=1.0,
envelopeType="CERTIFIED",
)
excessThrustList = []
VxList = []
for CAS in np.linspace(
VminCertified, VmaxCertified, num=200, endpoint=True
):
TAS = atm.cas2Tas(cas=CAS, delta=delta, sigma=sigma)
maxThrust = self.Thrust(
h=h, DeltaTemp=DeltaTemp, rating=rating, v=TAS, config=config
)
CL = self.CL(sigma=sigma, mass=mass, tas=TAS, nz=1.0)
CD = self.CD(CL=CL, config=config)
Drag = self.D(sigma=sigma, tas=TAS, CD=CD)
excessThrustList.append(maxThrust - Drag)
VxList.append(CAS)
idx = excessThrustList.index(max(excessThrustList))
return VxList[idx]
[docs]
def VMax(self, h, DeltaTemp):
"""
Computes the maximum speed based on altitude and temperature deviation.
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:type h: float
:type DeltaTemp: float
:returns: Maximum speed in meters per second [m/s].
:rtype: float
"""
crossoverAlt = atm.crossOver(
cas=conv.kt2ms(self.AC.VMO), Mach=self.AC.MMO
)
if h >= crossoverAlt:
[theta, delta, sigma] = atm.atmosphereProperties(
h=h, DeltaTemp=DeltaTemp
)
VMax = atm.mach2Cas(
Mach=self.AC.MMO, theta=theta, delta=delta, sigma=sigma
)
else:
VMax = conv.kt2ms(self.AC.VMO)
return VMax
[docs]
def lowSpeedBuffetLimit(self, h, mass, DeltaTemp, nz=1.2):
"""
Computes the low-speed buffet limit using numerical methods.
:param h: Altitude in meters [m].
:param mass: Aircraft mass in kilograms [kg].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param nz: Load factor, default is 1.2.
:type h: float
:type mass: float
:type DeltaTemp: float
:type nz: float, optional
:returns: Low-speed buffet limit as Mach number [-].
:rtype: float
"""
p = atm.delta(h, DeltaTemp) * const.p_0
a1 = self.AC.k
a2 = -(self.AC.Clbo)
a3 = (mass * const.g) / (self.AC.S * p * (0.7 / nz))
coef = [a1, a2, 0, a3]
roots = np.roots(coef)
Mb = list()
for root in roots:
if root > 0 and not isinstance(root, complex):
Mb.append(root)
if not Mb:
return float("Nan")
return min(Mb)
[docs]
def getConfig(self, phase, h, mass, v, DeltaTemp, hRWY=0.0, nz=1.2):
"""
Returns the aerodynamic configuration based on altitude, speed, and phase of flight.
:param phase: Phase of flight (e.g., 'Climb', 'Cruise', 'Descent').
:param h: Altitude in meters [m].
:param v: Calibrated airspeed in meters per second [m/s].
:param mass: Aircraft mass in kilograms [kg].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param hRWY: Runway elevation above mean sea level in meters [m], default is 0.
:param nz: Load factor, default is 1.2.
:type phase: str
:type h: float
:type v: float
:type mass: float
:type DeltaTemp: float
:type hRWY: float, optional
:type nz: float, optional
:returns: Aerodynamic configuration (e.g., 'TO', 'IC', 'CR', 'AP', 'LD').
:rtype: str
"""
config = None
# aircraft AGL altitude assuming being close to the RWY [m]
h_AGL = h - hRWY
HmaxTO_AGL = (
conv.ft2m(
Parser.getGPFValue(self.AC.GPFdata, "H_max_to", phase="to")
)
- hRWY
)
HmaxIC_AGL = (
conv.ft2m(
Parser.getGPFValue(self.AC.GPFdata, "H_max_ic", phase="ic")
)
- hRWY
)
HmaxAPP_AGL = (
conv.ft2m(
Parser.getGPFValue(self.AC.GPFdata, "H_max_app", phase="app")
)
- hRWY
)
HmaxLD_AGL = (
conv.ft2m(
Parser.getGPFValue(self.AC.GPFdata, "H_max_ld", phase="lnd")
)
- hRWY
)
if phase == "Climb" and h_AGL <= HmaxTO_AGL:
config = "TO"
elif phase == "Climb" and (h_AGL > HmaxTO_AGL and h_AGL <= HmaxIC_AGL):
config = "IC"
else:
vMinCR = self.VMin(
h=h, mass=mass, config="CR", DeltaTemp=DeltaTemp, nz=nz
)
vMinAPP = self.VMin(
h=h, mass=mass, config="AP", DeltaTemp=DeltaTemp, nz=nz
)
ep = 1e-6
if (
phase == "Descent"
and (h_AGL + ep) < HmaxLD_AGL
and (v + ep) < (vMinAPP + conv.kt2ms(10))
):
config = "LD"
elif (
phase == "Descent"
and h_AGL >= HmaxLD_AGL
and (h_AGL + ep) < HmaxAPP_AGL
and (v - ep) < (vMinCR + conv.kt2ms(10))
) or (
phase == "Descent"
and (h_AGL + ep) < HmaxLD_AGL
and (
(v - ep) < (vMinCR + conv.kt2ms(10))
and v >= (vMinAPP + conv.kt2ms(10))
)
):
config = "AP"
elif (
(phase == "Climb" and h_AGL > HmaxIC_AGL)
or phase == "Cruise"
or (phase == "Descent" and h_AGL >= HmaxAPP_AGL)
or (
phase == "Descent"
and (h_AGL + ep) < HmaxAPP_AGL
and v >= (vMinCR + conv.kt2ms(10))
)
):
config = "CR"
if config is None:
raise TypeError("Unable to determine aircraft configuration")
return config
[docs]
def getAeroConfig(self, config):
"""
Returns the aerodynamic configuration ID for a given configuration.
:param config: Aircraft configuration (e.g., 'CR', 'IC', 'TO', 'AP', 'LD').
:type config: str
:returns: A list containing the HLid and LG for the given configuration.
:rtype: [int, str]
"""
HLid = self.AC.aeroConfig[config]["HLid"]
LG = self.AC.aeroConfig[config]["LG"]
return [HLid, LG]
[docs]
def getHoldSpeed(self, h, theta, delta, sigma, DeltaTemp):
"""
Computes the aircraft's holding speed (CAS) based on the current altitude.
:param h: Altitude in meters [m].
:param theta: Normalized temperature [-].
:param delta: Normalized pressure [-].
:param sigma: Normalized air density [-].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:type h: float
:type theta: float
:type delta: float
:type sigma: float
:type DeltaTemp: float
:returns: Holding calibrated airspeed (CAS) in meters per second [m/s].
:rtype: float
"""
if h <= conv.ft2m(14000):
vHold = Parser.getGPFValue(
self.AC.GPFdata, "V_hold_1", phase="hold"
)
elif h > conv.ft2m(14000) and h <= conv.ft2m(20000):
vHold = Parser.getGPFValue(
self.AC.GPFdata, "V_hold_2", phase="hold"
)
elif h > conv.ft2m(20000) and h <= conv.ft2m(34000):
vHold = Parser.getGPFValue(
self.AC.GPFdata, "V_hold_3", phase="hold"
)
elif h > conv.ft2m(34000):
MHold = Parser.getGPFValue(
self.AC.GPFdata, "V_hold_4", phase="hold"
)
vHold = atm.mach2Cas(Mach=M, theta=theta, delta=delta, sigma=sigma)
return conv.kt2ms(vHold)
[docs]
def getGroundMovementSpeed(self, pos):
"""
Returns the ground movement speed based on the aircraft's position on the ground.
:param pos: Aircraft position on the airport ground (e.g., 'backtrack', 'taxi', 'apron', 'gate').
:type pos: str
:returns: Ground movement speed in meters per second [m/s].
:rtype: float
"""
if pos == "backtrack":
vGround = Parser.getGPFValue(
self.AC.GPFdata, "V_backtrack", phase="gnd"
)
elif pos == "taxi":
vGround = Parser.getGPFValue(
self.AC.GPFdata, "V_taxi", phase="gnd"
)
elif pos == "apron":
vGround = Parser.getGPFValue(
self.AC.GPFdata, "V_apron", phase="gnd"
)
elif pos == "gate":
vGround = Parser.getGPFValue(
self.AC.GPFdata, "V_gate", phase="gnd"
)
return conv.kt2ms(vGround)
[docs]
def getBankAngle(self, phase, flightUnit, value):
"""
Returns the nominal or maximum bank angle for the given flight phase and unit type.
:param phase: Phase of flight (e.g., 'to', 'ic', 'cl', 'cr', etc.).
:param flightUnit: Flight unit (e.g., 'civ' for civilian, 'mil' for military).
:param value: Desired value, either 'nom' for nominal or 'max' for maximum bank angle.
:type phase: str
:type flightUnit: str
:type value: str
:returns: Bank angle in degrees [deg].
:rtype: float
"""
nomBankAngle = Parser.getGPFValue(
self.AC.GPFdata, "ang_bank_nom", flightUnit=flightUnit, phase=phase
)
maxBankAngle = Parser.getGPFValue(
self.AC.GPFdata, "ang_bank_max", flightUnit=flightUnit, phase=phase
)
if value == "nom":
return nomBankAngle
elif value == "max":
return maxBankAngle
[docs]
def isAccOK(self, v1, v2, type="long", flightUnit="civ", deltaTime=1.0):
"""
Checks whether the acceleration between two time steps is within allowable limits.
:param v1: Airspeed (or vertical speed for 'norm') at the previous time step [m/s].
:param v2: Airspeed (or vertical speed for 'norm') at the current time step [m/s].
:param type: Type of acceleration to check ('long' for longitudinal, 'norm' for normal).
:param flightUnit: Flight unit type ('civ' for civilian, 'mil' for military).
:param deltaTime: Time difference between the two time steps in seconds [s].
:type v1: float
:type v2: float
:type type: str
:type flightUnit: str
:type deltaTime: float
:returns: True if the acceleration is within limits, False otherwise.
:rtype: bool
"""
OK = False
if flightUnit == "civ":
if type == "long":
if (
abs(v2 - v1)
<= conv.ft2m(
Parser.getGPFValue(self.AC.GPFdata, "acc_long_max")
)
* deltaTime
):
OK = True
elif type == "norm":
if (
abs(v2 - v1)
<= conv.ft2m(
Parser.getGPFValue(self.AC.GPFdata, "acc_norm_max")
)
* deltaTime
):
OK = True
# currently undefined for BADA3
elif flightUnit == "mil":
OK = True
return OK
[docs]
def getSpeedSchedule(self, phase):
"""
Returns the speed schedule for a given phase of flight.
:param phase: Flight phase ('Climb', 'Cruise', 'Descent').
:type phase: str
:returns: A list containing CAS1, CAS2, and Mach number for the specified phase [m/s, m/s, -].
:rtype: list[float, float, float]
"""
if phase == "Climb":
phase = "cl"
if phase == "Cruise":
phase = "cr"
if phase == "Descent":
phase = "des"
CAS1 = self.AC.V1[phase]
CAS2 = self.AC.V2[phase]
M = self.AC.M[phase]
return [CAS1, CAS2, M]
[docs]
def checkConfigurationContinuity(
self, phase, previousConfig, currentConfig
):
"""
Ensures the continuity of aerodynamic configuration changes based on the phase of flight.
:param phase: Current flight phase ('Climb', 'Cruise', 'Descent').
:param previousConfig: The previous aerodynamic configuration.
:param currentConfig: The current aerodynamic configuration.
:type phase: str
:type previousConfig: str
:type currentConfig: str
:returns: Updated aerodynamic configuration.
:rtype: str
This function ensures that the aerodynamic configuration transitions logically
based on the phase of flight. For example, during descent, the configuration
should not revert to a clean configuration after deploying flaps for approach or landing.
"""
newConfig = ""
# previous configuration is NOT empty/unknown
if previousConfig is not None:
if phase == "Descent":
if currentConfig == "CR" and (
previousConfig == "AP" or previousConfig == "LD"
):
newConfig = previousConfig
elif currentConfig == "AP" and previousConfig == "LD":
newConfig = previousConfig
else:
newConfig = currentConfig
elif phase == "Climb":
if currentConfig == "TO" and (
previousConfig == "IC" or previousConfig == "CR"
):
newConfig = previousConfig
elif currentConfig == "IC" and previousConfig == "CR":
newConfig = previousConfig
else:
newConfig = currentConfig
elif phase == "Cruise":
newConfig = currentConfig
# previous configuration is empty/unknown
else:
newConfig = currentConfig
return newConfig
[docs]
class ARPM:
"""This class is a BADA3 aircraft subclass and implements the Airline Procedure Model (ARPM)
following the BADA3 user manual.
:param AC: Aircraft object {BADA3}.
:type AC: bada3Aircraft.
"""
def __init__(self, AC):
self.AC = AC
self.flightEnvelope = FlightEnvelope(AC)
[docs]
def climbSpeed(
self,
theta,
delta,
mass,
h,
DeltaTemp,
speedSchedule_default=None,
applyLimits=True,
config=None,
procedure="BADA",
NADP1_ALT=3000,
NADP2_ALT=[1000, 3000],
):
"""
Computes the climb speed schedule (CAS) for the given altitude based on various procedures and aircraft parameters.
:param theta: Normalized air temperature [-].
:param delta: Normalized air pressure [-].
:param mass: Aircraft mass in kilograms [kg].
:param h: Altitude in meters [m].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param speedSchedule_default: Optional, a default speed schedule that overrides the BADA schedule. It should be in the form [Vcl1, Vcl2, Mcl].
:param applyLimits: Boolean flag indicating whether to apply the minimum and maximum speed limits based on the flight envelope.
:param config: Optional, current aircraft aerodynamic configuration (TO/IC/CR/AP/LD).
:param procedure: Climb procedure to be followed, e.g., 'BADA', 'NADP1', 'NADP2'. Default is 'BADA'.
:param NADP1_ALT: Altitude in feet for NADP1 procedure. Default is 3000 feet.
:param NADP2_ALT: Altitude range in feet for NADP2 procedure. Default is [1000, 3000].
:type theta: float
:type delta: float
:type mass: float
:type h: float
:type DeltaTemp: float
:type speedSchedule_default: list[float, float, float], optional
:type applyLimits: bool
:type config: str, optional
:type procedure: str
:type NADP1_ALT: float
:type NADP2_ALT: list[float, float]
:returns: A tuple containing the climb calibrated airspeed (CAS) in meters per second [m/s] and a status flag indicating whether the calculated CAS is constrained ('C'), unconstrained ('V' or 'v'), or not altered ('').
:rtype: tuple[float, str]
This function computes the climb speed schedule for different phases of flight and aircraft types.
It supports BADA, NADP1, and NADP2 procedures for both jet and turboprop/piston/electric aircraft.
The climb schedule uses specific speed profiles depending on altitude and aircraft model. For jet engines, the speed is constrained
below 250 knots below 10,000 feet, and then it follows a defined speed schedule, either from BADA or NADP procedures.
Additionally, the function applies speed limits based on the aircraft's flight envelope, adjusting the calculated climb speed if necessary.
- For `procedure='BADA'`, it uses the BADA climb speed schedule.
- For `procedure='NADP1'`, it implements the Noise Abatement Departure Procedure 1.
- For `procedure='NADP2'`, it implements the Noise Abatement Departure Procedure 2.
The function also ensures that the calculated CAS remains within the bounds of the aircraft's minimum and maximum speeds.
"""
phase = "cl"
acModel = self.AC.engineType
Cvmin = Parser.getGPFValue(self.AC.GPFdata, "C_v_min", phase=phase)
CvminTO = Parser.getGPFValue(self.AC.GPFdata, "C_v_min_to", phase="to")
VstallTO = self.flightEnvelope.VStall(config="TO", mass=mass)
VstallCR = self.flightEnvelope.VStall(config="CR", mass=mass)
[Vcl1, Vcl2, Mcl] = self.flightEnvelope.getSpeedSchedule(
phase=phase
) # BADA Climb speed schedule
if speedSchedule_default is not None:
Vcl1 = speedSchedule_default[0]
Vcl2 = speedSchedule_default[1]
Mcl = speedSchedule_default[2]
crossOverAlt = atm.crossOver(cas=Vcl2, Mach=Mcl)
sigma = atm.sigma(theta=theta, delta=delta)
if procedure == "BADA":
if acModel == "JET":
speed = list()
speed.append(min(Vcl1, conv.kt2ms(250)))
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_5", phase=phase
)
)
)
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_4", phase=phase
)
)
)
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_3", phase=phase
)
)
)
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_2", phase=phase
)
)
)
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_1", phase=phase
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(1500):
cas = speed[5]
elif h >= conv.ft2m(1500) and h < conv.ft2m(3000):
cas = speed[4]
elif h >= conv.ft2m(3000) and h < conv.ft2m(4000):
cas = speed[3]
elif h >= conv.ft2m(4000) and h < conv.ft2m(5000):
cas = speed[2]
elif h >= conv.ft2m(5000) and h < conv.ft2m(6000):
cas = speed[1]
elif h >= conv.ft2m(6000) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcl2
elif h >= crossOverAlt:
cas = atm.mach2Cas(
Mach=Mcl, theta=theta, delta=delta, sigma=sigma
)
elif (
acModel == "TURBOPROP"
or acModel == "PISTON"
or acModel == "ELECTRIC"
):
speed = list()
speed.append(min(Vcl1, conv.kt2ms(250)))
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata,
"V_cl_8",
engine="TURBOPROP",
phase=phase,
)
)
)
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata,
"V_cl_7",
engine="TURBOPROP",
phase=phase,
)
)
)
speed.append(
Cvmin * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata,
"V_cl_6",
engine="TURBOPROP",
phase=phase,
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(500):
cas = speed[3]
elif h >= conv.ft2m(500) and h < conv.ft2m(1000):
cas = speed[2]
elif h >= conv.ft2m(1000) and h < conv.ft2m(1500):
cas = speed[1]
elif h >= conv.ft2m(1500) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcl2
elif h >= crossOverAlt:
cas = atm.mach2Cas(
Mach=Mcl, theta=theta, delta=delta, sigma=sigma
)
elif procedure == "NADP1":
if acModel == "JET":
speed = list()
speed.append(min(Vcl1, conv.kt2ms(250)))
speed.append(
CvminTO * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_2", phase=phase
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(NADP1_ALT):
cas = speed[1]
elif h >= conv.ft2m(NADP1_ALT) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcl2
elif h >= crossOverAlt:
sigma = atm.sigma(theta=theta, delta=delta)
cas = atm.mach2Cas(
Mach=Mcl, theta=theta, delta=delta, sigma=sigma
)
elif acModel == "TURBOPROP" or acModel == "PISTON":
speed = list()
speed.append(min(Vcl1, conv.kt2ms(250)))
speed.append(
CvminTO * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_1", phase=phase
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(NADP1_ALT):
cas = speed[1]
elif h >= conv.ft2m(NADP1_ALT) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcl2
elif h >= crossOverAlt:
sigma = atm.sigma(theta=theta, delta=delta)
cas = atm.mach2Cas(
Mach=Mcl, theta=theta, delta=delta, sigma=sigma
)
elif procedure == "NADP2":
if acModel == "JET":
speed = list()
speed.append(min(Vcl1, conv.kt2ms(250)))
speed.append(
Cvmin * VstallCR
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_2", phase=phase
)
)
)
speed.append(
CvminTO * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_2", phase=phase
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(NADP2_ALT[0]):
cas = speed[2]
elif h >= conv.ft2m(NADP2_ALT[0]) and h < conv.ft2m(
NADP2_ALT[1]
):
cas = speed[1]
elif h >= conv.ft2m(NADP2_ALT[1]) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcl2
elif h >= crossOverAlt:
sigma = atm.sigma(theta=theta, delta=delta)
cas = atm.mach2Cas(
Mach=Mcl, theta=theta, delta=delta, sigma=sigma
)
elif acModel == "TURBOPROP" or acModel == "PISTON":
speed = list()
speed.append(min(Vcl1, conv.kt2ms(250)))
speed.append(
Cvmin * VstallCR
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_2", phase=phase
)
)
)
speed.append(
CvminTO * VstallTO
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata, "V_cl_1", phase=phase
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(NADP2_ALT[0]):
cas = speed[2]
elif h >= conv.ft2m(NADP2_ALT[0]) and h < conv.ft2m(
NADP2_ALT[1]
):
cas = speed[1]
elif h >= conv.ft2m(NADP2_ALT[1]) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcl2
elif h >= crossOverAlt:
sigma = atm.sigma(theta=theta, delta=delta)
cas = atm.mach2Cas(
Mach=Mcl, theta=theta, delta=delta, sigma=sigma
)
if applyLimits:
# check if the speed is within the limits of minimum and maximum speed from the flight envelope, if not, overwrite calculated speed with flight envelope min/max speed
if config is None:
config = self.flightEnvelope.getConfig(
h=h, phase="Climb", v=cas, mass=mass, DeltaTemp=DeltaTemp
)
minSpeed = self.flightEnvelope.VMin(
h=h, mass=mass, config=config, DeltaTemp=DeltaTemp
)
maxSpeed = self.flightEnvelope.VMax(h=h, DeltaTemp=DeltaTemp)
eps = 1e-6 # float calculation precision
# empty envelope - keep the original calculated CAS speed
if maxSpeed < minSpeed:
if (cas - eps) > maxSpeed and (cas - eps) > minSpeed:
return [cas, "V"]
elif (cas + eps) < minSpeed and (cas + eps) < maxSpeed:
return [cas, "v"]
else:
return [cas, "vV"]
if minSpeed > (cas + eps):
return [minSpeed, "C"]
if maxSpeed < (cas - eps):
return [maxSpeed, "C"]
return [cas, ""]
[docs]
def cruiseSpeed(
self,
theta,
delta,
mass,
h,
DeltaTemp,
speedSchedule_default=None,
applyLimits=True,
config=None,
):
"""
Computes the cruise speed schedule (CAS) for a given altitude based on aircraft parameters and procedures.
:param h: Altitude in meters [m].
:param mass: Aircraft mass in kilograms [kg].
:param theta: Normalized air temperature [-].
:param delta: Normalized air pressure [-].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param speedSchedule_default: Optional, a default speed schedule that overrides the BADA schedule. It should be in the form [Vcr1, Vcr2, Mcr].
:param applyLimits: Boolean flag indicating whether to apply the minimum and maximum speed limits based on the flight envelope.
:param config: Optional, current aircraft aerodynamic configuration (TO/IC/CR/AP/LD).
:type h: float
:type mass: float
:type theta: float
:type delta: float
:type DeltaTemp: float
:type speedSchedule_default: list[float, float, float], optional
:type applyLimits: bool
:type config: str, optional
:returns: A tuple containing the cruise calibrated airspeed (CAS) in meters per second [m/s] and a status flag indicating whether the calculated CAS is constrained ('C'), unconstrained ('V' or 'v'), or not altered ('').
:rtype: tuple[float, str]
This function computes the cruise speed schedule for various phases of flight and aircraft models.
It supports both jet and turboprop/piston/electric aircraft models by using the BADA (Base of Aircraft Data) speed schedules.
- If a `speedSchedule_default` is provided, it overwrites the BADA speed schedule.
- For jet engines, the speed is constrained based on altitude, starting with 170 knots below 3000 feet, 220 knots below 6000 feet, and then follows the standard speed schedule.
- For other aircraft types (TURBOPROP, PISTON, ELECTRIC), the speed limits are lower, starting with 150 knots below 3000 feet.
The function also applies limits based on the aircraft's flight envelope, ensuring the calculated speed does not exceed the minimum or maximum allowable speeds.
"""
phase = "cr"
acModel = self.AC.engineType
[Vcr1, Vcr2, Mcr] = self.flightEnvelope.getSpeedSchedule(
phase=phase
) # BADA Cruise speed schedule
if speedSchedule_default is not None:
Vcr1 = speedSchedule_default[0]
Vcr2 = speedSchedule_default[1]
Mcr = speedSchedule_default[2]
crossOverAlt = atm.crossOver(cas=Vcr2, Mach=Mcr)
sigma = atm.sigma(theta=theta, delta=delta)
if acModel == "JET":
if h < conv.ft2m(3000):
cas = min(Vcr1, conv.kt2ms(170))
elif h >= conv.ft2m(3000) and h < conv.ft2m(6000):
cas = min(Vcr1, conv.kt2ms(220))
elif h >= conv.ft2m(6000) and h < conv.ft2m(14000):
cas = min(Vcr1, conv.kt2ms(250))
elif h >= conv.ft2m(14000) and h < crossOverAlt:
cas = Vcr2
elif h >= crossOverAlt:
cas = atm.mach2Cas(
Mach=Mcr, theta=theta, delta=delta, sigma=sigma
)
elif (
acModel == "TURBOPROP"
or acModel == "PISTON"
or acModel == "ELECTRIC"
):
if h < conv.ft2m(3000):
cas = min(Vcr1, conv.kt2ms(150))
elif h >= conv.ft2m(3000) and h < conv.ft2m(6000):
cas = min(Vcr1, conv.kt2ms(180))
elif h >= conv.ft2m(6000) and h < conv.ft2m(10000):
cas = min(Vcr1, conv.kt2ms(250))
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vcr2
elif h >= crossOverAlt:
cas = atm.mach2Cas(
Mach=Mcr, theta=theta, delta=delta, sigma=sigma
)
if applyLimits:
# check if the speed is within the limits of minimum and maximum speed from the flight envelope, if not, overwrite calculated speed with flight envelope min/max speed
if config is None:
config = self.flightEnvelope.getConfig(
h=h, phase="Cruise", v=cas, mass=mass, DeltaTemp=DeltaTemp
)
minSpeed = self.flightEnvelope.VMin(
h=h, mass=mass, config=config, DeltaTemp=DeltaTemp
)
maxSpeed = self.flightEnvelope.VMax(h=h, DeltaTemp=DeltaTemp)
eps = 1e-6 # float calculation precision
# empty envelope - keep the original calculated CAS speed
if maxSpeed < minSpeed:
if (cas - eps) > maxSpeed and (cas - eps) > minSpeed:
return [cas, "V"]
elif (cas + eps) < minSpeed and (cas + eps) < maxSpeed:
return [cas, "v"]
else:
return [cas, "vV"]
if minSpeed > (cas + eps):
return [minSpeed, "C"]
if maxSpeed < (cas - eps):
return [maxSpeed, "C"]
return [cas, ""]
[docs]
def descentSpeed(
self,
theta,
delta,
mass,
h,
DeltaTemp,
speedSchedule_default=None,
applyLimits=True,
config=None,
):
"""
Computes the descent speed schedule (CAS) for a given altitude based on aircraft parameters and procedures.
:param h: Altitude in meters [m].
:param mass: Aircraft mass in kilograms [kg].
:param theta: Normalized air temperature [-].
:param delta: Normalized air pressure [-].
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param speedSchedule_default: Optional, a default speed schedule that overrides the BADA schedule. It should be in the form [Vdes1, Vdes2, Mdes].
:param applyLimits: Boolean flag indicating whether to apply the minimum and maximum speed limits based on the flight envelope.
:param config: Optional, current aircraft aerodynamic configuration (TO/IC/CR/AP/LD).
:type h: float
:type mass: float
:type theta: float
:type delta: float
:type DeltaTemp: float
:type speedSchedule_default: list[float, float, float], optional
:type applyLimits: bool
:type config: str, optional
:returns: A tuple containing the descent calibrated airspeed (CAS) in meters per second [m/s] and a status flag indicating whether the calculated CAS is constrained ('C'), unconstrained ('V' or 'v'), or not altered ('').
:rtype: tuple[float, str]
This function computes the descent speed schedule for various phases of flight and aircraft models.
It supports both jet and turboprop/piston/electric aircraft models using the BADA (Base of Aircraft Data) speed schedules.
- If a `speedSchedule_default` is provided, it overwrites the BADA speed schedule.
- For jet and turboprop engines, the speed schedule is constrained based on altitude, starting from 220 knots below 3000 feet and then following the standard speed schedule.
- For piston and electric engines, lower speed limits are applied based on stall speeds.
The function also applies limits based on the aircraft's flight envelope, ensuring that the calculated speed does not exceed the minimum or maximum allowable speeds.
"""
phase = "des"
acModel = self.AC.engineType
Cvmin = Parser.getGPFValue(self.AC.GPFdata, "C_v_min", phase=phase)
VstallDES = self.flightEnvelope.VStall(config="LD", mass=mass)
[Vdes1, Vdes2, Mdes] = self.flightEnvelope.getSpeedSchedule(
phase=phase
) # BADA Descent speed schedule
if speedSchedule_default is not None:
Vdes1 = speedSchedule_default[0]
Vdes2 = speedSchedule_default[1]
Mdes = speedSchedule_default[2]
crossOverAlt = atm.crossOver(cas=Vdes2, Mach=Mdes)
sigma = atm.sigma(theta=theta, delta=delta)
if acModel == "JET" or acModel == "TURBOPROP":
speed = list()
speed.append(min(Vdes1, conv.kt2ms(220)))
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(self.AC.GPFdata, "V_des_4", phase=phase)
)
)
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(self.AC.GPFdata, "V_des_3", phase=phase)
)
)
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(self.AC.GPFdata, "V_des_2", phase=phase)
)
)
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(self.AC.GPFdata, "V_des_1", phase=phase)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(1000):
cas = speed[4]
elif h >= conv.ft2m(1000) and h < conv.ft2m(1500):
cas = speed[3]
elif h >= conv.ft2m(1500) and h < conv.ft2m(2000):
cas = speed[2]
elif h >= conv.ft2m(2000) and h < conv.ft2m(3000):
cas = speed[1]
elif h >= conv.ft2m(3000) and h < conv.ft2m(6000):
cas = speed[0]
elif h >= conv.ft2m(6000) and h < conv.ft2m(10000):
cas = min(Vdes1, conv.kt2ms(250))
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vdes2
elif h >= crossOverAlt:
cas = atm.mach2Cas(
Mach=Mdes, theta=theta, delta=delta, sigma=sigma
)
elif acModel == "PISTON" or acModel == "ELECTRIC":
speed = list()
speed.append(Vdes1)
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata,
"V_des_7",
engine="PISTON",
phase=phase,
)
)
)
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata,
"V_des_6",
engine="PISTON",
phase=phase,
)
)
)
speed.append(
Cvmin * VstallDES
+ conv.kt2ms(
Parser.getGPFValue(
self.AC.GPFdata,
"V_des_5",
engine="PISTON",
phase=phase,
)
)
)
n = 1
while n < len(speed):
if speed[n] > speed[n - 1]:
speed[n] = speed[n - 1]
n = n + 1
if h < conv.ft2m(500):
cas = speed[3]
elif h >= conv.ft2m(500) and h < conv.ft2m(1000):
cas = speed[2]
elif h >= conv.ft2m(1000) and h < conv.ft2m(1500):
cas = speed[1]
elif h >= conv.ft2m(1500) and h < conv.ft2m(10000):
cas = speed[0]
elif h >= conv.ft2m(10000) and h < crossOverAlt:
cas = Vdes2
elif h >= crossOverAlt:
cas = atm.mach2Cas(
Mach=Mdes, theta=theta, delta=delta, sigma=sigma
)
if applyLimits:
# check if the speed is within the limits of minimum and maximum speed from the flight envelope, if not, overwrite calculated speed with flight envelope min/max speed
if config is None:
config = self.flightEnvelope.getConfig(
h=h, phase="Descent", v=cas, mass=mass, DeltaTemp=DeltaTemp
)
minSpeed = self.flightEnvelope.VMin(
h=h, mass=mass, config=config, DeltaTemp=DeltaTemp
)
maxSpeed = self.flightEnvelope.VMax(h=h, DeltaTemp=DeltaTemp)
eps = 1e-6 # float calculation precision
# empty envelope - keep the original calculated CAS speed
if maxSpeed < minSpeed:
if (cas - eps) > maxSpeed and (cas - eps) > minSpeed:
return [cas, "V"]
elif (cas + eps) < minSpeed and (cas + eps) < maxSpeed:
return [cas, "v"]
else:
return [cas, "vV"]
if minSpeed > (cas + eps):
return [minSpeed, "C"]
if maxSpeed < (cas - eps):
return [maxSpeed, "C"]
return [cas, ""]
[docs]
class PTD(BADA3):
"""This class implements the PTD file creator for BADA3 aircraft following BADA3 manual.
:param AC: Aircraft object {BADA3}.
:type AC: bada3Aircraft.
"""
def __init__(self, AC):
super().__init__(AC)
self.flightEnvelope = FlightEnvelope(AC)
self.ARPM = ARPM(AC)
[docs]
def create(self, DeltaTemp, saveToPath):
"""
Creates a BADA3 PTD file based on specified temperature deviation from ISA
and saves it to the provided directory path. It generates performance data for different aircraft mass levels (low,
medium, high) in both climb and descent phases.
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param saveToPath: Path to the directory where the PTD file will be stored.
:type DeltaTemp: float.
:type saveToPath: str.
:returns: None
:rtype: None
"""
# 3 different mass levels [kg]
if 1.2 * self.AC.mass["minimum"] > self.AC.mass["reference"]:
massLow = self.AC.mass["minimum"]
else:
massLow = 1.2 * self.AC.mass["minimum"]
massList = [
massLow,
self.AC.mass["reference"],
self.AC.mass["maximum"],
]
max_alt_ft = self.AC.hmo
# original PTD altitude list
altitudeList = list(range(0, 2000, 500))
altitudeList.extend(range(2000, 4000, 1000))
if int(max_alt_ft) < 30000:
altitudeList.extend(range(4000, int(max_alt_ft), 2000))
altitudeList.append(int(max_alt_ft))
else:
altitudeList.extend(range(4000, 30000, 2000))
altitudeList.extend(range(29000, int(max_alt_ft), 2000))
altitudeList.append(int(max_alt_ft))
CLList = []
for mass in massList:
CLList.append(
self.PTD_climb(
mass=mass, altitudeList=altitudeList, DeltaTemp=DeltaTemp
)
)
DESList_med = self.PTD_descent(
mass=self.AC.mass["reference"],
altitudeList=altitudeList,
DeltaTemp=DeltaTemp,
)
self.save2PTD(
saveToPath=saveToPath,
CLList_low=CLList[0],
CLList_med=CLList[1],
CLList_high=CLList[2],
DESList_med=DESList_med,
DeltaTemp=DeltaTemp,
)
[docs]
def save2PTD(
self,
saveToPath,
CLList_low,
CLList_med,
CLList_high,
DESList_med,
DeltaTemp,
):
"""
Saves BADA3 (PTD) to a file. It stores performance data for low, medium,
and high aircraft masses during the climb phase, and medium aircraft mass during the descent phase. The file
is saved in a predefined format.
:param saveToPath: Path to the directory where the PTD file should be saved.
:param CLList_low: List containing PTD data for CLIMB at low aircraft mass.
:param CLList_med: List containing PTD data for CLIMB at medium aircraft mass.
:param CLList_high: List containing PTD data for CLIMB at high aircraft mass.
:param DESList_med: List containing PTD data for DESCENT at medium aircraft mass.
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:type saveToPath: str.
:type CLList_low: list.
:type CLList_med: list.
:type CLList_high: list.
:type DESList_med: list.
:type DeltaTemp: float.
:returns: None
"""
def Nan2Zero(list):
# replace NAN values by 0 for printing purposes
for k in range(len(list)):
for m in range(len(list[k])):
if isinstance(list[k][m], float):
if isnan(list[k][m]):
list[k][m] = 0
return list
newpath = saveToPath
if not os.path.exists(newpath):
os.makedirs(newpath)
if DeltaTemp == 0.0:
ISA = ""
elif DeltaTemp > 0.0:
ISA = "+" + str(int(DeltaTemp))
elif DeltaTemp < 0.0:
ISA = str(int(DeltaTemp))
acName = self.AC.acName
while len(acName) < 6:
acName = acName + "_"
filename = saveToPath + acName + "_ISA" + ISA + ".PTD"
file = open(filename, "w")
file.write("BADA PERFORMANCE FILE RESULTS\n")
file = open(filename, "a")
file.write(
"=============================\n=============================\n\n"
)
file.write("Low mass CLIMBS\n")
file.write("===============\n\n")
file.write(
" FL[-] T[K] p[Pa] rho[kg/m3] a[m/s] TAS[kt] CAS[kt] M[-] mass[kg] Thrust[N] Drag[N] Fuel[kgm] ESF[-] ROC[fpm] TDC[N] PWC[-]\n"
)
# replace NAN values by 0 for printing purposes
CLList_low = Nan2Zero(CLList_low)
CLList_med = Nan2Zero(CLList_med)
CLList_high = Nan2Zero(CLList_high)
DESList_med = Nan2Zero(DESList_med)
for k in range(0, len(CLList_low[0])):
file.write(
"%6d %3.0f %6.0f %7.3f %7.0f %8.2f %8.2f %7.2f %6.0f %9.0f %9.0f %7.1f %7.2f %7.0f %8.0f %7.2f \n"
% (
CLList_low[0][k],
CLList_low[1][k],
CLList_low[2][k],
CLList_low[3][k],
CLList_low[4][k],
CLList_low[5][k],
CLList_low[6][k],
CLList_low[7][k],
CLList_low[8][k],
CLList_low[9][k],
CLList_low[10][k],
CLList_low[11][k],
CLList_low[12][k],
CLList_low[13][k],
CLList_low[14][k],
CLList_low[15][k],
)
)
file.write("\n\nMedium mass CLIMBS\n")
file.write("==================\n\n")
file.write(
" FL[-] T[K] p[Pa] rho[kg/m3] a[m/s] TAS[kt] CAS[kt] M[-] mass[kg] Thrust[N] Drag[N] Fuel[kgm] ESF[-] ROC[fpm] TDC[N] PWC[-]\n"
)
for k in range(0, len(CLList_med[0])):
file.write(
"%6d %3.0f %6.0f %7.3f %7.0f %8.2f %8.2f %7.2f %6.0f %9.0f %9.0f %7.1f %7.2f %7.0f %8.0f %7.2f \n"
% (
CLList_med[0][k],
CLList_med[1][k],
CLList_med[2][k],
CLList_med[3][k],
CLList_med[4][k],
CLList_med[5][k],
CLList_med[6][k],
CLList_med[7][k],
CLList_med[8][k],
CLList_med[9][k],
CLList_med[10][k],
CLList_med[11][k],
CLList_med[12][k],
CLList_med[13][k],
CLList_med[14][k],
CLList_med[15][k],
)
)
file.write("\n\nHigh mass CLIMBS\n")
file.write("================\n\n")
file.write(
" FL[-] T[K] p[Pa] rho[kg/m3] a[m/s] TAS[kt] CAS[kt] M[-] mass[kg] Thrust[N] Drag[N] Fuel[kgm] ESF[-] ROC[fpm] TDC[N] PWC[-]\n"
)
for k in range(0, len(CLList_high[0])):
file.write(
"%6d %3.0f %6.0f %7.3f %7.0f %8.2f %8.2f %7.2f %6.0f %9.0f %9.0f %7.1f %7.2f %7.0f %8.0f %7.2f \n"
% (
CLList_high[0][k],
CLList_high[1][k],
CLList_high[2][k],
CLList_high[3][k],
CLList_high[4][k],
CLList_high[5][k],
CLList_high[6][k],
CLList_high[7][k],
CLList_high[8][k],
CLList_high[9][k],
CLList_high[10][k],
CLList_high[11][k],
CLList_high[12][k],
CLList_high[13][k],
CLList_high[14][k],
CLList_high[15][k],
)
)
file.write("\nMedium mass DESCENTS\n")
file.write("====================\n\n")
file.write(
" FL[-] T[K] p[Pa] rho[kg/m3] a[m/s] TAS[kt] CAS[kt] M[-] mass[kg] Thrust[N] Drag[N] Fuel[kgm] ESF[-] ROD[fpm] TDC[N] gammaTAS[deg]\n"
)
for k in range(0, len(DESList_med[0])):
file.write(
"%6d %3.0f %6.0f %7.3f %7.0f %8.2f %8.2f %7.2f %6.0f %9.0f %9.0f %7.1f %7.2f %7.0f %8.0f %8.2f \n"
% (
DESList_med[0][k],
DESList_med[1][k],
DESList_med[2][k],
DESList_med[3][k],
DESList_med[4][k],
DESList_med[5][k],
DESList_med[6][k],
DESList_med[7][k],
DESList_med[8][k],
DESList_med[9][k],
DESList_med[10][k],
DESList_med[11][k],
DESList_med[12][k],
DESList_med[13][k],
DESList_med[14][k],
DESList_med[15][k],
)
)
file.write("\nTDC stands for (Thrust - Drag) * Cred\n")
[docs]
def PTD_climb(self, mass, altitudeList, DeltaTemp):
"""
Calculates the BADA3 PTD data in climb phase.
:param mass: Aircraft mass [kg]
:param altitudeList: List of altitude levels for calculation (in feet)
:param DeltaTemp: Deviation from International Standard Atmosphere (ISA) temperature [K]
:type mass: float
:type altitudeList: list of int
:type DeltaTemp: float
:returns: A list of calculated PTD data for the climb phase
:rtype: list
"""
FL_complet = []
T_complet = []
p_complet = []
rho_complet = []
a_complet = []
TAS_complet = []
CAS_complet = []
M_complet = []
mass_complet = []
Thrust_complet = []
Drag_complet = []
ff_comlet = []
ESF_complet = []
ROCD_complet = []
TDC_complet = []
PWC_complet = []
phase = "cl"
Vcl1 = self.AC.V1[phase]
Vcl2 = self.AC.V2[phase]
Mcl = self.AC.M[phase]
Vcl1 = min(Vcl1, conv.kt2ms(250))
crossAlt = atm.crossOver(cas=Vcl2, Mach=Mcl)
for h in altitudeList:
H_m = conv.ft2m(h) # altitude [m]
[theta, delta, sigma] = atm.atmosphereProperties(
h=H_m, DeltaTemp=DeltaTemp
)
[cas, speedUpdated] = self.ARPM.climbSpeed(
theta=theta,
delta=delta,
h=H_m,
mass=mass,
DeltaTemp=DeltaTemp,
speedSchedule_default=[Vcl1, Vcl2, Mcl],
applyLimits=False,
)
tas = atm.cas2Tas(cas=cas, delta=delta, sigma=sigma)
M = atm.tas2Mach(v=tas, theta=theta)
a = atm.aSound(theta=theta)
FL = h / 100
config = self.flightEnvelope.getConfig(
h=H_m, phase="Climb", v=cas, mass=mass, DeltaTemp=DeltaTemp
)
Thrust = self.Thrust(
rating="MCMB", v=tas, h=H_m, config=config, DeltaTemp=DeltaTemp
)
ff = self.ff(flightPhase="Climb", v=tas, h=H_m, T=Thrust) * 60
CL = self.CL(tas=tas, sigma=sigma, mass=mass)
CD = self.CD(CL=CL, config=config)
Drag = self.D(tas=tas, sigma=sigma, CD=CD)
CPowRed = self.reducedPower(h=H_m, mass=mass, DeltaTemp=DeltaTemp)
TDC = (Thrust - Drag) * CPowRed
if H_m < crossAlt:
ESF = self.esf(
h=H_m, flightEvolution="constCAS", M=M, DeltaTemp=DeltaTemp
)
else:
ESF = self.esf(
h=H_m, flightEvolution="constM", M=M, DeltaTemp=DeltaTemp
)
ROCD = (
conv.m2ft(
self.ROCD(
h=H_m,
T=Thrust,
D=Drag,
v=tas,
mass=mass,
ESF=ESF,
DeltaTemp=DeltaTemp,
reducedPower=True,
)
)
* 60
)
FL_complet.append(proper_round(FL))
T_complet.append(theta * const.temp_0)
p_complet.append(delta * const.p_0)
rho_complet.append(sigma * const.rho_0)
a_complet.append(a)
TAS_complet.append(conv.ms2kt(tas))
CAS_complet.append(conv.ms2kt(cas))
M_complet.append(M)
mass_complet.append(mass)
Thrust_complet.append(Thrust)
Drag_complet.append(Drag)
ff_comlet.append(ff)
ESF_complet.append(ESF)
ROCD_complet.append(ROCD)
TDC_complet.append(TDC)
PWC_complet.append(CPowRed)
CLList = [
FL_complet,
T_complet,
p_complet,
rho_complet,
a_complet,
TAS_complet,
CAS_complet,
M_complet,
mass_complet,
Thrust_complet,
Drag_complet,
ff_comlet,
ESF_complet,
ROCD_complet,
TDC_complet,
PWC_complet,
]
return CLList
[docs]
def PTD_descent(self, mass, altitudeList, DeltaTemp):
"""
Calculates the BADA3 PTD data in descent phase.
This function generates a detailed list of descent performance metrics for different altitudes and
mass configurations based on BADA3 performance models.
:param mass: Aircraft mass [kg].
:param altitudeList: List of aircraft altitudes in feet [ft].
:param DeltaTemp: Deviation from ISA temperature [K].
:type mass: float.
:type altitudeList: list of int.
:type DeltaTemp: float.
:returns: List of descent performance data.
:rtype: list.
"""
FL_complet = []
T_complet = []
p_complet = []
rho_complet = []
a_complet = []
TAS_complet = []
CAS_complet = []
M_complet = []
mass_complet = []
Thrust_complet = []
Drag_complet = []
ff_comlet = []
ESF_complet = []
ROCD_complet = []
TDC_complet = []
gamma_complet = []
phase = "des"
Vdes1 = self.AC.V1[phase]
Vdes2 = self.AC.V2[phase]
Mdes = self.AC.M[phase]
Vdes1 = min(Vdes1, conv.kt2ms(250))
crossAlt = atm.crossOver(cas=Vdes2, Mach=Mdes)
for h in altitudeList:
H_m = conv.ft2m(h) # altitude [m]
[theta, delta, sigma] = atm.atmosphereProperties(
h=H_m, DeltaTemp=DeltaTemp
)
[cas, speedUpdated] = self.ARPM.descentSpeed(
theta=theta,
delta=delta,
h=H_m,
mass=mass,
DeltaTemp=DeltaTemp,
speedSchedule_default=[Vdes1, Vdes2, Mdes],
applyLimits=False,
)
tas = atm.cas2Tas(cas=cas, delta=delta, sigma=sigma)
M = atm.tas2Mach(v=tas, theta=theta)
a = atm.aSound(theta=theta)
FL = h / 100
CL = self.CL(tas=tas, sigma=sigma, mass=mass)
config = self.flightEnvelope.getConfig(
h=H_m, phase="Descent", v=cas, mass=mass, DeltaTemp=DeltaTemp
)
CD = self.CD(CL=CL, config=config)
Drag = self.D(tas=tas, sigma=sigma, CD=CD)
if (
self.AC.engineType == "PISTON"
or self.AC.engineType == "ELECTRIC"
):
# PISTON and ELECTRIC uses LIDL throughout the whole descent phase
Thrust = self.Thrust(
rating="LIDL",
v=tas,
h=H_m,
config="CR",
DeltaTemp=DeltaTemp,
)
ff = (
self.ff(
flightPhase="Descent",
v=tas,
h=H_m,
T=Thrust,
config="CR",
adapted=False,
)
* 60
)
else:
Thrust = self.Thrust(
rating="LIDL",
v=tas,
h=H_m,
config=config,
DeltaTemp=DeltaTemp,
)
ff = (
self.ff(
flightPhase="Descent",
v=tas,
h=H_m,
T=Thrust,
config=config,
adapted=False,
)
* 60
)
CPowRed = 1.0
TDC = (Thrust - Drag) * CPowRed
if H_m < crossAlt:
ESF = self.esf(
h=H_m, flightEvolution="constCAS", M=M, DeltaTemp=DeltaTemp
)
else:
ESF = self.esf(
h=H_m, flightEvolution="constM", M=M, DeltaTemp=DeltaTemp
)
ROCD = (
conv.m2ft(
self.ROCD(
h=H_m,
T=Thrust,
D=Drag,
v=tas,
mass=mass,
ESF=ESF,
DeltaTemp=DeltaTemp,
)
)
* 60
)
tau_const = (theta * const.temp_0) / (
theta * const.temp_0 - DeltaTemp
)
dhdt = (conv.ft2m(ROCD / 60)) * tau_const
if self.AC.drone:
gamma = conv.rad2deg(atan(dhdt / tas))
else:
gamma = conv.rad2deg(asin(dhdt / tas))
FL_complet.append(proper_round(FL))
T_complet.append(theta * const.temp_0)
p_complet.append(delta * const.p_0)
rho_complet.append(sigma * const.rho_0)
a_complet.append(a)
TAS_complet.append(conv.ms2kt(tas))
CAS_complet.append(conv.ms2kt(cas))
M_complet.append(M)
mass_complet.append(mass)
Thrust_complet.append(Thrust)
Drag_complet.append(Drag)
ff_comlet.append(ff)
ESF_complet.append(ESF)
ROCD_complet.append(-1 * ROCD)
TDC_complet.append(TDC)
gamma_complet.append(gamma)
DESList = [
FL_complet,
T_complet,
p_complet,
rho_complet,
a_complet,
TAS_complet,
CAS_complet,
M_complet,
mass_complet,
Thrust_complet,
Drag_complet,
ff_comlet,
ESF_complet,
ROCD_complet,
TDC_complet,
gamma_complet,
]
return DESList
[docs]
class PTF(BADA3):
"""This class implements the PTF file creator for BADA3 aircraft following BADA3 manual.
:param AC: Aircraft object {BADA3}.
:type AC: bada3Aircraft.
"""
def __init__(self, AC):
super().__init__(AC)
self.flightEnvelope = FlightEnvelope(AC)
self.ARPM = ARPM(AC)
[docs]
def create(self, DeltaTemp, saveToPath):
"""
Creates a BADA3 PTF file based on specified temperature deviation from ISA
and saves it to the provided directory path. It generates performance data for different aircraft mass levels (low,
medium, high) in both climb and descent phases.
:param DeltaTemp: Deviation from ISA temperature in Kelvin [K].
:param saveToPath: Path to the directory where the PTF file will be stored.
:type DeltaTemp: float.
:type saveToPath: str.
:returns: None
:rtype: None
"""
# 3 different mass levels [kg]
if 1.2 * self.AC.mass["minimum"] > self.AC.mass["reference"]:
massLow = self.AC.mass["minimum"]
else:
massLow = 1.2 * self.AC.mass["minimum"]
massList = [
massLow,
self.AC.mass["reference"],
self.AC.mass["maximum"],
]
max_alt_ft = self.AC.hmo
# original PTF altitude list
altitudeList = list(range(0, 2000, 500))
altitudeList.extend(range(2000, 4000, 1000))
if int(max_alt_ft) < 30000:
altitudeList.extend(range(4000, int(max_alt_ft), 2000))
altitudeList.append(max_alt_ft)
else:
altitudeList.extend(range(4000, 30000, 2000))
altitudeList.extend(range(29000, int(max_alt_ft), 2000))
altitudeList.append(max_alt_ft)
CRList = self.PTF_cruise(
massList=massList, altitudeList=altitudeList, DeltaTemp=DeltaTemp
)
CLList = self.PTF_climb(
massList=massList, altitudeList=altitudeList, DeltaTemp=DeltaTemp
)
DESList = self.PTF_descent(
massList=massList, altitudeList=altitudeList, DeltaTemp=DeltaTemp
)
self.save2PTF(
saveToPath=saveToPath,
altitudeList=altitudeList,
massList=massList,
CRList=CRList,
CLList=CLList,
DESList=DESList,
DeltaTemp=DeltaTemp,
)
[docs]
def save2PTF(
self,
saveToPath,
CRList,
CLList,
DESList,
altitudeList,
massList,
DeltaTemp,
):
"""
Saves performance data to a PTF file.
:param saveToPath: Directory path where the PTF file will be stored.
:param CRList: List of cruise phase data.
:param CLList: List of climb phase data.
:param DESList: List of descent phase data.
:param altitudeList: List of aircraft altitudes [ft].
:param massList: List of aircraft masses [kg].
:param DeltaTemp: Deviation from ISA temperature [K].
:type saveToPath: string.
:type CRList: list.
:type CLList: list.
:type DESList: list.
:type altitudeList: list of int.
:type massList: list of int.
:type DeltaTemp: float.
:returns: None
"""
def Nan2Zero(list):
# replace NAN values by 0 for printing purposes
for k in range(len(list)):
for m in range(len(list[k])):
if isinstance(list[k][m], float):
if isnan(list[k][m]):
list[k][m] = 0
return list
newpath = saveToPath
if not os.path.exists(newpath):
os.makedirs(newpath)
if DeltaTemp == 0.0:
ISA = ""
elif DeltaTemp > 0.0:
ISA = "+" + str(int(DeltaTemp))
elif DeltaTemp < 0.0:
ISA = str(int(DeltaTemp))
acName = self.AC.acName
while len(acName) < 6:
acName = acName + "_"
filename = saveToPath + acName + "_ISA" + ISA + ".PTF"
V1cl = min(250, conv.ms2kt(self.AC.V1["cl"]))
V2cl = conv.ms2kt(self.AC.V2["cl"])
Mcl = self.AC.M["cl"]
V1des = min(250, conv.ms2kt(self.AC.V1["des"]))
V2des = conv.ms2kt(self.AC.V2["des"])
Mdes = self.AC.M["des"]
V1cr = min(250, conv.ms2kt(self.AC.V1["cr"]))
V2cr = conv.ms2kt(self.AC.V2["cr"])
Mcr = self.AC.M["cr"]
today = date.today()
d3 = today.strftime("%b %d %Y")
OPFModDate = self.AC.modificationDateOPF
APFModDate = self.AC.modificationDateAPF
file = open(filename, "w")
file.write(
"BADA PERFORMANCE FILE %s\n\n"
% (d3)
)
file = open(filename, "a")
file.write("AC/Type: %s\n" % (acName))
file.write(
" Source OPF File: %s\n"
% (OPFModDate)
)
file.write(
" Source APF file: %s\n\n"
% (APFModDate)
)
file.write(
" Speeds: CAS(LO/HI) Mach Mass Levels [kg] Temperature: ISA%s\n"
% (ISA)
)
file.write(
" climb - %3d/%3d %4.2f low - %.0f\n"
% (V1cl, V2cl, Mcl, massList[0])
)
file.write(
" cruise - %3d/%3d %4.2f nominal - %-6.0f Max Alt. [ft]:%7d\n"
% (V1cr, V2cr, Mcr, massList[1], altitudeList[-1])
)
file.write(
" descent - %3d/%3d %4.2f high - %0.f\n"
% (V1des, V2des, Mdes, massList[2])
)
file.write(
"==========================================================================================\n"
)
file.write(
" FL | CRUISE | CLIMB | DESCENT \n"
)
file.write(
" | TAS fuel | TAS ROCD fuel | TAS ROCD fuel \n"
)
file.write(
" | [kts] [kg/min] | [kts] [fpm] [kg/min] | [kts] [fpm] [kg/min]\n"
)
file.write(
" | lo nom hi | lo nom hi nom | nom nom \n"
)
file.write(
"==========================================================================================\n"
)
# replace NAN values by 0 for printing purposes
CLList = Nan2Zero(CLList)
DESList = Nan2Zero(DESList)
for k in range(0, len(altitudeList)):
FL = proper_round(altitudeList[k] / 100)
if FL < 30:
file.write(
"%3.0f | | %3.0f %5.0f %5.0f %5.0f %5.1f | %3.0f %5.0f %5.1f \n"
% (
FL,
CLList[0][k],
CLList[1][k],
CLList[2][k],
CLList[3][k],
CLList[4][k],
DESList[0][k],
DESList[1][k],
DESList[2][k],
)
)
else:
file.write(
"%3.0f | %3.0f %5.1f %5.1f %5.1f | %3.0f %5.0f %5.0f %5.0f %5.1f | %3.0f %5.0f %5.1f \n"
% (
FL,
CRList[0][k],
CRList[1][k],
CRList[2][k],
CRList[3][k],
CLList[0][k],
CLList[1][k],
CLList[2][k],
CLList[3][k],
CLList[4][k],
DESList[0][k],
DESList[1][k],
DESList[2][k],
)
)
file.write(
" | | | \n"
)
file.write(
"==========================================================================================\n"
)
[docs]
def PTF_cruise(self, massList, altitudeList, DeltaTemp):
"""Calculates BADA3 PTF data for the cruise phase.
:param massList: List of aircraft masses [kg] (low, nominal, and high).
:param altitudeList: List of aircraft altitudes [ft].
:param DeltaTemp: Deviation from the International Standard Atmosphere (ISA) temperature [K].
:type massList: list of float.
:type altitudeList: list of int.
:type DeltaTemp: float.
:returns: List containing cruise phase TAS and fuel flow data.
:rtype: list.
"""
TAS_CR_complet = []
FF_CR_LO_complet = []
FF_CR_NOM_complet = []
FF_CR_HI_complet = []
phase = "cr"
massNominal = massList[1]
Vcr1 = self.AC.V1[phase]
Vcr2 = self.AC.V2[phase]
Mcr = self.AC.M[phase]
Vcr1 = min(Vcr1, conv.kt2ms(250))
for h in altitudeList:
H_m = conv.ft2m(h) # altitude [m]
[theta, delta, sigma] = atm.atmosphereProperties(
h=H_m, DeltaTemp=DeltaTemp
)
[cas, speedUpdated] = self.ARPM.cruiseSpeed(
theta=theta,
delta=delta,
h=H_m,
mass=massNominal,
DeltaTemp=DeltaTemp,
speedSchedule_default=[Vcr1, Vcr2, Mcr],
applyLimits=False,
)
tas_nominal = atm.cas2Tas(cas=cas, delta=delta, sigma=sigma)
FL = h / 100
ff = []
for mass in massList:
[cas, speedUpdated] = self.ARPM.cruiseSpeed(
theta=theta,
delta=delta,
h=H_m,
mass=mass,
DeltaTemp=DeltaTemp,
speedSchedule_default=[Vcr1, Vcr2, Mcr],
applyLimits=False,
)
tas = atm.cas2Tas(cas=cas, delta=delta, sigma=sigma)
CL = self.CL(tas=tas, sigma=sigma, mass=mass)
CD = self.CD(CL=CL, config="CR")
Drag = self.D(tas=tas, sigma=sigma, CD=CD)
Thrust = Drag
ff.append(
self.ff(flightPhase="Cruise", v=tas, h=H_m, T=Thrust) * 60
)
TAS_CR_complet.append(conv.ms2kt(tas_nominal))
FF_CR_LO_complet.append(ff[0])
FF_CR_NOM_complet.append(ff[1])
FF_CR_HI_complet.append(ff[2])
CRList = [
TAS_CR_complet,
FF_CR_LO_complet,
FF_CR_NOM_complet,
FF_CR_HI_complet,
]
return CRList
[docs]
def PTF_climb(self, massList, altitudeList, DeltaTemp):
"""Calculates BADA3 PTF data for the climb phase.
:param massList: List of aircraft masses [kg] (low, nominal, high).
:param altitudeList: List of aircraft altitudes [ft].
:param DeltaTemp: Deviation from the International Standard Atmosphere (ISA) temperature [K].
:type massList: list of float.
:type altitudeList: list of int.
:type DeltaTemp: float.
:returns: List containing climb phase TAS, ROCD, and fuel flow data.
:rtype: list.
"""
TAS_CL_complet = []
ROCD_CL_LO_complet = []
ROCD_CL_NOM_complet = []
ROCD_CL_HI_complet = []
FF_CL_NOM_complet = []
phase = "cl"
massNominal = massList[1]
Vcl1 = self.AC.V1[phase]
Vcl2 = self.AC.V2[phase]
Mcl = self.AC.M[phase]
Vcl1 = min(Vcl1, conv.kt2ms(250))
crossAlt = atm.crossOver(cas=Vcl2, Mach=Mcl)
for h in altitudeList:
H_m = conv.ft2m(h) # altitude [m]
[theta, delta, sigma] = atm.atmosphereProperties(
h=H_m, DeltaTemp=DeltaTemp
)
FL = h / 100
ROC = []
tas_list = []
ff_list = []
for mass in massList:
[cas, speedUpdated] = self.ARPM.climbSpeed(
theta=theta,
delta=delta,
h=H_m,
mass=mass,
DeltaTemp=DeltaTemp,
speedSchedule_default=[Vcl1, Vcl2, Mcl],
applyLimits=False,
)
tas = atm.cas2Tas(cas=cas, delta=delta, sigma=sigma)
M = atm.tas2Mach(v=tas, theta=theta)
CL = self.CL(tas=tas, sigma=sigma, mass=mass)
config = self.flightEnvelope.getConfig(
h=H_m,
phase="Climb",
v=cas,
mass=massNominal,
DeltaTemp=DeltaTemp,
)
CD = self.CD(CL=CL, config=config)
Drag = self.D(tas=tas, sigma=sigma, CD=CD)
Thrust = self.Thrust(
rating="MCMB",
v=tas,
h=H_m,
config=config,
DeltaTemp=DeltaTemp,
)
ff = self.ff(flightPhase="Climb", v=tas, h=H_m, T=Thrust) * 60
if H_m < crossAlt:
ESF = self.esf(
h=H_m,
flightEvolution="constCAS",
M=M,
DeltaTemp=DeltaTemp,
)
else:
ESF = self.esf(
h=H_m,
flightEvolution="constM",
M=M,
DeltaTemp=DeltaTemp,
)
# I think this should use all config, not just for nominal weight
ROC_val = (
conv.m2ft(
self.ROCD(
h=H_m,
T=Thrust,
D=Drag,
v=tas,
mass=mass,
ESF=ESF,
DeltaTemp=DeltaTemp,
reducedPower=True,
)
)
* 60
)
if ROC_val < 0:
ROC_val = float("Nan")
ROC.append(ROC_val)
tas_list.append(tas)
ff_list.append(ff)
TAS_CL_complet.append(conv.ms2kt(tas_list[1]))
ROCD_CL_LO_complet.append(ROC[0])
ROCD_CL_NOM_complet.append(ROC[1])
ROCD_CL_HI_complet.append(ROC[2])
FF_CL_NOM_complet.append(ff_list[1])
CLList = [
TAS_CL_complet,
ROCD_CL_LO_complet,
ROCD_CL_NOM_complet,
ROCD_CL_HI_complet,
FF_CL_NOM_complet,
]
return CLList
[docs]
def PTF_descent(self, massList, altitudeList, DeltaTemp):
"""Calculates BADA3 PTF data for the descent phase.
:param massList: List of aircraft masses [kg] (low, nominal, high).
:param altitudeList: List of aircraft altitudes [ft].
:param DeltaTemp: Deviation from the International Standard Atmosphere (ISA) temperature [K].
:type massList: list of float.
:type altitudeList: list of int.
:type DeltaTemp: float.
:returns: List containing descent phase TAS, ROCD, and fuel flow data.
:rtype: list.
"""
TAS_DES_complet = []
ROCD_DES_NOM_complet = []
FF_DES_NOM_complet = []
phase = "des"
massNominal = massList[1]
Vdes1 = self.AC.V1[phase]
Vdes2 = self.AC.V2[phase]
Mdes = self.AC.M[phase]
Vdes1 = min(Vdes1, conv.kt2ms(250))
crossAlt = atm.crossOver(cas=Vdes2, Mach=Mdes)
for h in altitudeList:
H_m = conv.ft2m(h) # altitude [m]
[theta, delta, sigma] = atm.atmosphereProperties(
h=H_m, DeltaTemp=DeltaTemp
)
[cas, speedUpdated] = self.ARPM.descentSpeed(
theta=theta,
delta=delta,
h=H_m,
mass=massNominal,
DeltaTemp=DeltaTemp,
speedSchedule_default=[Vdes1, Vdes2, Mdes],
applyLimits=False,
)
tas_nominal = atm.cas2Tas(cas=cas, delta=delta, sigma=sigma)
M = atm.tas2Mach(v=tas_nominal, theta=theta)
FL = h / 100
config = self.flightEnvelope.getConfig(
h=H_m,
phase="Descent",
v=cas,
mass=massNominal,
DeltaTemp=DeltaTemp,
)
CL = self.CL(tas=tas_nominal, sigma=sigma, mass=massNominal)
CD = self.CD(CL=CL, config=config)
Drag = self.D(tas=tas_nominal, sigma=sigma, CD=CD)
if (
self.AC.engineType == "PISTON"
or self.AC.engineType == "ELECTRIC"
):
# PISTON and ELECTRIC uses LIDL throughout the whole descent phase
Thrust_nominal = self.Thrust(
rating="LIDL",
v=tas_nominal,
h=H_m,
config="CR",
DeltaTemp=DeltaTemp,
)
ff_nominal = (
self.ff(
flightPhase="Descent",
v=tas_nominal,
h=H_m,
T=Thrust_nominal,
config="CR",
adapted=False,
)
* 60
)
else:
Thrust_nominal = self.Thrust(
rating="LIDL",
v=tas_nominal,
h=H_m,
config=config,
DeltaTemp=DeltaTemp,
)
ff_nominal = (
self.ff(
flightPhase="Descent",
v=tas_nominal,
h=H_m,
T=Thrust_nominal,
config=config,
adapted=False,
)
* 60
)
if H_m < crossAlt:
ESF = self.esf(
h=H_m, flightEvolution="constCAS", M=M, DeltaTemp=DeltaTemp
)
else:
ESF = self.esf(
h=H_m, flightEvolution="constM", M=M, DeltaTemp=DeltaTemp
)
ROCD = -1 * (
conv.m2ft(
self.ROCD(
h=H_m,
T=Thrust_nominal,
D=Drag,
v=tas_nominal,
mass=massNominal,
ESF=ESF,
DeltaTemp=DeltaTemp,
)
)
* 60
)
TAS_DES_complet.append(conv.ms2kt(tas_nominal))
ROCD_DES_NOM_complet.append(ROCD)
FF_DES_NOM_complet.append(ff_nominal)
DESList = [TAS_DES_complet, ROCD_DES_NOM_complet, FF_DES_NOM_complet]
return DESList
[docs]
class Bada3Aircraft(BADA3):
"""
Implements the BADA3 performance model for an aircraft following the BADA3 manual.
This class handles the loading of aircraft-specific data from either a predefined
dataset or a set of BADA3 performance model files (e.g., OPF and APF files).
It initializes various parameters such as mass, speed schedules, and engine type
necessary for simulating the aircraft's performance.
:param badaVersion: The BADA version being used.
:param acName: The ICAO aircraft designation (e.g., "A320").
:param filePath: Optional path to the BADA3 formatted file. If not provided,
the default aircraft directory is used.
:param allData: Optional DataFrame containing all aircraft data. If provided,
the class will try to load the aircraft data from this DataFrame.
:type badaVersion: str.
:type acName: str.
:type filePath: str, optional.
:type allData: pd.DataFrame, optional.
"""
def __init__(self, badaVersion, acName, filePath=None, allData=None):
super().__init__(self)
self.APFavailable = False
self.OPFavailable = False
self.ACModelAvailable = False
self.ACinSynonymFile = False
self.BADAFamilyName = "BADA3"
self.BADAFamily = BadaFamily(BADA3=True)
self.BADAVersion = badaVersion
if filePath == None:
self.filePath = configuration.getBadaVersionPath(
badaFamily="BADA3", badaVersion=badaVersion
)
else:
self.filePath = filePath
# check if the aircraft is in the allData dataframe data
if allData is not None and acName in allData["acName"].values:
filtered_df = allData[allData["acName"] == acName]
self.acName = configuration.safe_get(filtered_df, "acName", None)
self.xmlFiles = configuration.safe_get(
filtered_df, "xmlFiles", None
)
self.modificationDateOPF = configuration.safe_get(
filtered_df, "modificationDateOPF", None
)
self.modificationDateAPF = configuration.safe_get(
filtered_df, "modificationDateAPF", None
)
self.ICAO = configuration.safe_get(filtered_df, "ICAO", None)
self.numberOfEngines = configuration.safe_get(
filtered_df, "numberOfEngines", None
)
self.engineType = configuration.safe_get(
filtered_df, "engineType", None
)
self.engines = configuration.safe_get(filtered_df, "engines", None)
self.WTC = configuration.safe_get(filtered_df, "WTC", None)
self.mass = configuration.safe_get(filtered_df, "mass", None)
self.MTOW = configuration.safe_get(filtered_df, "MTOW", None)
self.OEW = configuration.safe_get(filtered_df, "OEW", None)
self.MPL = configuration.safe_get(filtered_df, "MPL", None)
self.MREF = configuration.safe_get(filtered_df, "MREF", None)
self.VMO = configuration.safe_get(filtered_df, "VMO", None)
self.MMO = configuration.safe_get(filtered_df, "MMO", None)
self.hmo = configuration.safe_get(filtered_df, "hmo", None)
self.Hmax = configuration.safe_get(filtered_df, "Hmax", None)
self.tempGrad = configuration.safe_get(
filtered_df, "tempGrad", None
)
self.S = configuration.safe_get(filtered_df, "S", None)
self.Clbo = configuration.safe_get(filtered_df, "Clbo", None)
self.k = configuration.safe_get(filtered_df, "k", None)
self.Vstall = configuration.safe_get(filtered_df, "Vstall", None)
self.CD0 = configuration.safe_get(filtered_df, "CD0", None)
self.CD2 = configuration.safe_get(filtered_df, "CD2", None)
self.HLids = configuration.safe_get(filtered_df, "HLids", None)
self.Ct = configuration.safe_get(filtered_df, "Ct", None)
self.CTdeslow = configuration.safe_get(
filtered_df, "CTdeslow", None
)
self.CTdeshigh = configuration.safe_get(
filtered_df, "CTdeshigh", None
)
self.CTdesapp = configuration.safe_get(
filtered_df, "CTdesapp", None
)
self.CTdesld = configuration.safe_get(filtered_df, "CTdesld", None)
self.HpDes = configuration.safe_get(filtered_df, "HpDes", None)
self.Cf = configuration.safe_get(filtered_df, "Cf", None)
self.CfDes = configuration.safe_get(filtered_df, "CfDes", None)
self.CfCrz = configuration.safe_get(filtered_df, "CfCrz", None)
self.TOL = configuration.safe_get(filtered_df, "TOL", None)
self.LDL = configuration.safe_get(filtered_df, "LDL", None)
self.span = configuration.safe_get(filtered_df, "span", None)
self.length = configuration.safe_get(filtered_df, "length", None)
self.V1 = configuration.safe_get(filtered_df, "V1", None)
self.V2 = configuration.safe_get(filtered_df, "V2", None)
self.M = configuration.safe_get(filtered_df, "M", None)
self.GPFdata = configuration.safe_get(filtered_df, "GPFdata", None)
self.drone = configuration.safe_get(filtered_df, "drone", None)
self.DeltaCD = configuration.safe_get(filtered_df, "DeltaCD", None)
self.speedSchedule = configuration.safe_get(
filtered_df, "speedSchedule", None
)
self.aeroConfig = configuration.safe_get(
filtered_df, "aeroConfig", None
)
self.flightEnvelope = FlightEnvelope(self)
self.ARPM = ARPM(self)
self.PTD = PTD(self)
self.PTF = PTF(self)
else:
# read BADA3 GPF file
GPFDataFrame = Parser.parseGPF(self.filePath)
# check if SYNONYM file exist
synonymFile = os.path.join(self.filePath, "SYNONYM.NEW")
synonymFileXML = os.path.join(self.filePath, "SYNONYM.xml")
if os.path.isfile(synonymFile) or os.path.isfile(synonymFileXML):
self.synonymFileAvailable = True
self.SearchedACName = Parser.parseSynonym(
self.filePath, acName
)
if self.SearchedACName == None:
# look for file name directly, which consists of added "_" at the end of file
fileName = acName
while len(fileName) < 6:
fileName += "_"
self.SearchedACName = fileName
else:
self.ACinSynonymFile = True
else:
# if doesn't exist - look for full name based on acName (may not be ICAO designator)
self.SearchedACName = acName
# look for either found synonym or original full BADA3 model name designator
if self.SearchedACName is not None:
# check for existence of OPF and APF files
OPFfile = (
os.path.join(
self.filePath,
self.SearchedACName,
)
+ ".OPF"
)
APFfile = (
os.path.join(
self.filePath,
self.SearchedACName,
)
+ ".APF"
)
if os.path.isfile(OPFfile):
self.OPFavailable = True
if os.path.isfile(APFfile):
self.APFavailable = True
if self.OPFavailable and self.APFavailable:
self.ACModelAvailable = True
OPFDataFrame = Parser.parseOPF(
self.filePath, self.SearchedACName
)
APFDataFrame = Parser.parseAPF(
self.filePath, self.SearchedACName
)
OPF_APF_combined_df = Parser.combineOPF_APF(
OPFDataFrame, APFDataFrame
)
combined_df = Parser.combineACDATA_GPF(
OPF_APF_combined_df, GPFDataFrame
)
self.acName = configuration.safe_get(
combined_df, "acName", None
)
self.xmlFiles = configuration.safe_get(
combined_df, "xmlFiles", None
)
self.modificationDateOPF = configuration.safe_get(
combined_df, "modificationDateOPF", None
)
self.modificationDateAPF = configuration.safe_get(
combined_df, "modificationDateAPF", None
)
self.ICAO = configuration.safe_get(
combined_df, "ICAO", None
)
self.numberOfEngines = configuration.safe_get(
combined_df, "numberOfEngines", None
)
self.engineType = configuration.safe_get(
combined_df, "engineType", None
)
self.engines = configuration.safe_get(
combined_df, "engines", None
)
self.WTC = configuration.safe_get(combined_df, "WTC", None)
self.mass = configuration.safe_get(
combined_df, "mass", None
)
self.MTOW = configuration.safe_get(
combined_df, "MTOW", None
)
self.OEW = configuration.safe_get(combined_df, "OEW", None)
self.MPL = configuration.safe_get(combined_df, "MPL", None)
self.MREF = configuration.safe_get(
combined_df, "MREF", None
)
self.VMO = configuration.safe_get(combined_df, "VMO", None)
self.MMO = configuration.safe_get(combined_df, "MMO", None)
self.hmo = configuration.safe_get(combined_df, "hmo", None)
self.Hmax = configuration.safe_get(
combined_df, "Hmax", None
)
self.tempGrad = configuration.safe_get(
combined_df, "tempGrad", None
)
self.S = configuration.safe_get(combined_df, "S", None)
self.Clbo = configuration.safe_get(
combined_df, "Clbo", None
)
self.k = configuration.safe_get(combined_df, "k", None)
self.Vstall = configuration.safe_get(
combined_df, "Vstall", None
)
self.CD0 = configuration.safe_get(combined_df, "CD0", None)
self.CD2 = configuration.safe_get(combined_df, "CD2", None)
self.HLids = configuration.safe_get(
combined_df, "HLids", None
)
self.Ct = configuration.safe_get(combined_df, "Ct", None)
self.CTdeslow = configuration.safe_get(
combined_df, "CTdeslow", None
)
self.CTdeshigh = configuration.safe_get(
combined_df, "CTdeshigh", None
)
self.CTdesapp = configuration.safe_get(
combined_df, "CTdesapp", None
)
self.CTdesld = configuration.safe_get(
combined_df, "CTdesld", None
)
self.HpDes = configuration.safe_get(
combined_df, "HpDes", None
)
self.Cf = configuration.safe_get(combined_df, "Cf", None)
self.CfDes = configuration.safe_get(
combined_df, "CfDes", None
)
self.CfCrz = configuration.safe_get(
combined_df, "CfCrz", None
)
self.TOL = configuration.safe_get(combined_df, "TOL", None)
self.LDL = configuration.safe_get(combined_df, "LDL", None)
self.span = configuration.safe_get(
combined_df, "span", None
)
self.length = configuration.safe_get(
combined_df, "length", None
)
self.V1 = configuration.safe_get(combined_df, "V1", None)
self.V2 = configuration.safe_get(combined_df, "V2", None)
self.M = configuration.safe_get(combined_df, "M", None)
self.GPFdata = configuration.safe_get(
combined_df, "GPFdata", None
)
self.drone = configuration.safe_get(
combined_df, "drone", None
)
self.DeltaCD = configuration.safe_get(
combined_df, "DeltaCD", None
)
self.speedSchedule = configuration.safe_get(
combined_df, "speedSchedule", None
)
self.aeroConfig = configuration.safe_get(
combined_df, "aeroConfig", None
)
self.flightEnvelope = FlightEnvelope(self)
self.ARPM = ARPM(self)
self.PTD = PTD(self)
self.PTF = PTF(self)
elif not self.OPFavailable and not self.APFavailable:
# search for xml files
XMLDataFrame = Parser.parseXML(
self.filePath, self.SearchedACName
)
combined_df = Parser.combineACDATA_GPF(
XMLDataFrame, GPFDataFrame
)
self.acName = configuration.safe_get(
combined_df, "acName", None
)
self.xmlFiles = configuration.safe_get(
combined_df, "xmlFiles", None
)
self.modificationDateOPF = configuration.safe_get(
combined_df, "modificationDateOPF", None
)
self.modificationDateAPF = configuration.safe_get(
combined_df, "modificationDateAPF", None
)
self.ICAO = configuration.safe_get(
combined_df, "ICAO", None
)
self.numberOfEngines = configuration.safe_get(
combined_df, "numberOfEngines", None
)
self.engineType = configuration.safe_get(
combined_df, "engineType", None
)
self.engines = configuration.safe_get(
combined_df, "engines", None
)
self.WTC = configuration.safe_get(combined_df, "WTC", None)
self.mass = configuration.safe_get(
combined_df, "mass", None
)
self.MTOW = configuration.safe_get(
combined_df, "MTOW", None
)
self.OEW = configuration.safe_get(combined_df, "OEW", None)
self.MPL = configuration.safe_get(combined_df, "MPL", None)
self.MREF = configuration.safe_get(
combined_df, "MREF", None
)
self.VMO = configuration.safe_get(combined_df, "VMO", None)
self.MMO = configuration.safe_get(combined_df, "MMO", None)
self.hmo = configuration.safe_get(combined_df, "hmo", None)
self.Hmax = configuration.safe_get(
combined_df, "Hmax", None
)
self.tempGrad = configuration.safe_get(
combined_df, "tempGrad", None
)
self.S = configuration.safe_get(combined_df, "S", None)
self.Clbo = configuration.safe_get(
combined_df, "Clbo", None
)
self.k = configuration.safe_get(combined_df, "k", None)
self.Vstall = configuration.safe_get(
combined_df, "Vstall", None
)
self.CD0 = configuration.safe_get(combined_df, "CD0", None)
self.CD2 = configuration.safe_get(combined_df, "CD2", None)
self.HLids = configuration.safe_get(
combined_df, "HLids", None
)
self.Ct = configuration.safe_get(combined_df, "Ct", None)
self.CTdeslow = configuration.safe_get(
combined_df, "CTdeslow", None
)
self.CTdeshigh = configuration.safe_get(
combined_df, "CTdeshigh", None
)
self.CTdesapp = configuration.safe_get(
combined_df, "CTdesapp", None
)
self.CTdesld = configuration.safe_get(
combined_df, "CTdesld", None
)
self.HpDes = configuration.safe_get(
combined_df, "HpDes", None
)
self.Cf = configuration.safe_get(combined_df, "Cf", None)
self.CfDes = configuration.safe_get(
combined_df, "CfDes", None
)
self.CfCrz = configuration.safe_get(
combined_df, "CfCrz", None
)
self.TOL = configuration.safe_get(combined_df, "TOL", None)
self.LDL = configuration.safe_get(combined_df, "LDL", None)
self.span = configuration.safe_get(
combined_df, "span", None
)
self.length = configuration.safe_get(
combined_df, "length", None
)
self.V1 = configuration.safe_get(combined_df, "V1", None)
self.V2 = configuration.safe_get(combined_df, "V2", None)
self.M = configuration.safe_get(combined_df, "M", None)
self.GPFdata = configuration.safe_get(
combined_df, "GPFdata", None
)
self.drone = configuration.safe_get(
combined_df, "drone", None
)
self.DeltaCD = configuration.safe_get(
combined_df, "DeltaCD", None
)
self.speedSchedule = configuration.safe_get(
combined_df, "speedSchedule", None
)
self.aeroConfig = configuration.safe_get(
combined_df, "aeroConfig", None
)
self.flightEnvelope = FlightEnvelope(self)
self.ARPM = ARPM(self)
self.PTD = PTD(self)
self.PTF = PTF(self)
else:
# AC name cannot be found
raise ValueError(acName + " Cannot be found")
def __str__(self):
return f"(BADA3, AC_name: {self.acName}, searched_AC_name: {self.SearchedACName}, model_ICAO: {self.ICAO}, ID: {id(self.AC)})"