From fb5ddd0164c36ca6769bb4c81ff7044e42d3b4fe Mon Sep 17 00:00:00 2001 From: GLASER-OPITZ HENRICH Date: Mon, 7 Oct 2024 20:57:45 +0200 Subject: [PATCH] src: header comments improvement src: atmosphere function header comments improvement src: bada3 function header comments improvement src: configuration function header comments improvement src: geodesic function header comments improvement src: magnetic function header comments improvement src: trajectoryPrediction function header comments improvement src: flightTrajectory function header comments improvement src: TCL function header comments improvement --- src/pyBADA/TCL.py | 2361 ++++++++++++++++++---------- src/pyBADA/aircraft.py | 315 ++-- src/pyBADA/atmosphere.py | 194 ++- src/pyBADA/bada3.py | 1825 ++++++++++++++------- src/pyBADA/configuration.py | 60 + src/pyBADA/flightTrajectory.py | 269 ++-- src/pyBADA/geodesic.py | 438 ++++-- src/pyBADA/magnetic.py | 59 +- src/pyBADA/trajectoryPrediction.py | 27 +- 9 files changed, 3633 insertions(+), 1915 deletions(-) diff --git a/src/pyBADA/TCL.py b/src/pyBADA/TCL.py index 13af37c..6e2c80d 100644 --- a/src/pyBADA/TCL.py +++ b/src/pyBADA/TCL.py @@ -60,64 +60,97 @@ def constantSpeedLevel( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to fly given distance at constant speed in level flight - - :param AC: aircraft {BADA3/4/H/E} - :param lengthType: what kind of length applies {distance, time}. - :param length: length of a segment to fly - [NM] distance to fly or [s] time to fly - :param step_length: length of a step of a segment - [NM] distance to fly or [s] time to fly - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param v: what speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param m_init: initial aircraft mass [kg]. - :param stepClimb: kind of vertical evolution during cruise {constAlt=False, stepClimb=True}. - :param HpStep: altitude step for the stepClimb. - :param DeltaTemp: deviation with respect to ISA [K]. - :param maxRFL: maximum cruise altitude [ft]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param flightPhase: aircraft phase of flight {Climb,Cruise,Descent} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type lengthType: string. - :type length: float. - :type step_length: float. - :type speedType: string. - :type v: float. - :type Hp_init: float. - :type m_init: float. - :type stepClimb: boolean. - :type HpStep: float. - :type DeltaTemp: float. - :type maxRFL: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type flightPhase: string. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Calculates the time, fuel consumption, and other parameters for a level flight + segment at a constant speed for a given aircraft in the BADA model. It supports step-climb, + constant heading (true or magnetic), and turns. + + The function handles different BADA families (BADA3, BADA4, BADAH, BADAE), and computes various + parameters such as altitude, speed, fuel consumption, power, heading, and mass based on aircraft + performance data. + + :param AC: Aircraft object {BADA3/4/H/E} + :param lengthType: Specifies if the length of the flight segment is based on 'distance' [NM] or 'time' [s]. + :param length: The length of the flight segment. Distance [NM] or Time [s] depending on lengthType. + :param speedType: Specifies the type of speed to follow {M, CAS, TAS}. + :param v: The target speed in [kt] for CAS/TAS or [-] for MACH. + :param Hp_init: Initial pressure altitude at the start of the flight segment [ft]. + :param m_init: Initial mass of the aircraft [kg]. + :param DeltaTemp: Deviation from the standard ISA temperature [K]. + :param maxRFL: Maximum cruise altitude limit [ft]. Default is infinity. + :param wS: Wind speed component along the longitudinal axis (positive for headwind, negative for tailwind) [kt]. Default is 0.0. + :param turnMetrics: Dictionary containing turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is no turn (straight flight). + :param stepClimb: Boolean to enable or disable step climb during the cruise segment. Default is False. + :param Lat: Geographical latitude of the starting point [deg]. + :param Lon: Geographical longitude of the starting point [deg]. + :param initialHeading: Dictionary defining the initial heading and its type: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to fly along a constant heading (loxodrome). Default is None. + :param flightPhase: Defines the phase of flight, e.g., "Cruise", "Climb", "Descent". Default is "Cruise". + :param magneticDeclinationGrid: Optional magnetic declination grid to correct headings. Default is None. + :param kwargs: Additional optional parameters: + - mass_const: Boolean. If True, keeps the aircraft mass constant during the segment. Default is False. + - SOC_init: Initial battery state of charge for electric aircraft [%]. Default is 100 for BADAE. + - speedBrakes: Dictionary defining whether speed brakes are deployed and their drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum rate of climb/descent threshold to identify service ceiling [ft/min]. Default varies by aircraft type. + - config: Default aerodynamic configuration. Values: TO, IC, CR, AP, LD. + - HpStep: Altitude step for step climb [ft]. Default is 2000 ft. + - m_iter: Number of iterations for mass integration. Default is 1 for BADAE, 2 for others. + - step_length: The step length for each iteration in [NM] or [s], depending on the lengthType. Default is 100 NM for BADA3/4 and 10 NM for BADAH/BADAE. + + :returns: A pandas DataFrame containing the flight trajectory with columns such as: + - Hp: Altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of Climb/Descent [ft/min] + - ESF: Energy Share Factor[-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Trajectory slope [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle [deg] + - ROT: Rate of turn [deg/s] + - For BADAH: + - Preq: Required power [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Power supplied by the battery [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + - Comment: Comments for each segment + :rtype: pandas.DataFrame + + This function works by iteratively calculating the flight trajectory for a given segment of the flight, + taking into account the specified flight conditions, and updating the aircraft’s state (altitude, speed, fuel, etc.) + at each step of the iteration. The trajectory is returned as a DataFrame containing all relevant flight parameters. + + Key considerations: + - Magnetic declination can be corrected based on the provided grid. + - Supports handling turns and constant heading navigation. + - Step climbs are optionally allowed if enabled. + - Takes into account wind speed and flight phase-specific configurations. + - Power and fuel consumption are calculated differently for each BADA family (BADAH and BADAE handle power instead of fuel in different ways). """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -168,7 +201,9 @@ def constantSpeedLevel( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # optional parameter - iteration step length based on the type of aircraft if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: @@ -182,9 +217,13 @@ def constantSpeedLevel( # weight iteration constant if AC.BADAFamily.BADAE: - m_iter = kwargs.get("m_iter", 1) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 1 + ) # number of iterations for integration loop[-] else: - m_iter = kwargs.get("m_iter", 2) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 2 + ) # number of iterations for integration loop[-] # comment line describing type of trajectory calculation if flightPhase != "Cruise": @@ -308,7 +347,9 @@ def constantSpeedLevel( # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( v=v, speedType=speedType, theta=theta, delta=delta, sigma=sigma @@ -318,10 +359,14 @@ def constantSpeedLevel( if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined - bankAngle = AC.bankAngle(rateOfTurn=rateOfTurn, v=TAS_i) # [degrees] + bankAngle = AC.bankAngle( + rateOfTurn=rateOfTurn, v=TAS_i + ) # [degrees] if lengthType == "distance": # step time is: distance differantial divided by ground speed @@ -350,7 +395,9 @@ def constantSpeedLevel( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required for level flight - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) Peng_i = Preq_i if AC.BADAFamily.BADAH: Pav_i = AC.Pav( @@ -363,7 +410,8 @@ def constantSpeedLevel( if Pav_i < Preq_i: warnings.warn( - "Power Available is lower than Power Required", UserWarning + "Power Available is lower than Power Required", + UserWarning, ) # BADAH @@ -381,7 +429,9 @@ def constantSpeedLevel( Pelc_i = Preq_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC_i) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC_i) - Vgbat_i = AC.Vocbat(SOC=SOC_i) - AC.R0bat(SOC=SOC_i) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC_i) - AC.R0bat(SOC=SOC_i) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: @@ -405,12 +455,16 @@ def constantSpeedLevel( currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) # compute thrust force and fuel flow @@ -693,7 +747,9 @@ def constantSpeedLevel( # determine atmosphere properties at upper cruise altitude nextHp = min(Hp_i + HpStep, maxRFL) H_m = conv.ft2m(nextHp) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) # aircraft speed at upper cruise altitude [M_up, CAS_up, TAS_up] = atm.convertSpeed( @@ -762,19 +818,31 @@ def constantSpeedLevel( currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_up, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_up, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_up, + CL=CL, + HLid=HLid_i, + LG=LG_i, + speedBrakes=speedBrakes, + ) # compute drag force Drag = AC.D(M=M_up, delta=delta, CD=CD) # compute thrust force and fuel flow THR_up = Drag CT = AC.CT(Thrust=THR_up, delta=delta) FUEL_up = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_up, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_up, + DeltaTemp=DeltaTemp, ) # [kg/s] # Compare specific range at current and upper cruise altitudes @@ -799,7 +867,10 @@ def constantSpeedLevel( flightEvolution = "const" + speedType ESF_i = AC.esf( - h=H_m, flightEvolution=flightEvolution, M=M_up, DeltaTemp=DeltaTemp + h=H_m, + flightEvolution=flightEvolution, + M=M_up, + DeltaTemp=DeltaTemp, ) temp_const = (theta * const.temp_0) / ( theta * const.temp_0 - DeltaTemp @@ -885,7 +956,9 @@ def constantSpeedLevel( ROT.extend(flightTrajectory_CL["ROT"]) comment_CL = flightTrajectory_CL["comment"] - Comment.extend([com + "_stepClimb" for com in comment_CL]) + Comment.extend( + [com + "_stepClimb" for com in comment_CL] + ) # BADA4 if AC.BADAFamily.BADA4: @@ -907,7 +980,9 @@ def constantSpeedLevel( if Lat and Lon and magneticHeading: LAT.extend(flightTrajectory_CL["LAT"]) LON.extend(flightTrajectory_CL["LON"]) - HDGMagnetic.extend(flightTrajectory_CL["HDGMagnetic"]) + HDGMagnetic.extend( + flightTrajectory_CL["HDGMagnetic"] + ) HDGTrue.extend(flightTrajectory_CL["HDGTrue"]) # Compute cruise fuel at upper altitude @@ -927,18 +1002,20 @@ def constantSpeedLevel( # ensure continuity of configuration change within the segment if config: - config_i = ( - AC.flightEnvelope.checkConfigurationContinuity( - phase=flightPhase, - previousConfig=config[-1], - currentConfig=config_i, - ) + config_i = AC.flightEnvelope.checkConfigurationContinuity( + phase=flightPhase, + previousConfig=config[-1], + currentConfig=config_i, ) # compute lift coefficient - CL = AC.CL(tas=TAS_up, sigma=sigma, mass=mass[-1], nz=nz) + CL = AC.CL( + tas=TAS_up, sigma=sigma, mass=mass[-1], nz=nz + ) # compute drag coefficient - CD = AC.CD(CL=CL, config=config_i, speedBrakes=speedBrakes) + CD = AC.CD( + CL=CL, config=config_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(tas=TAS_up, sigma=sigma, CD=CD) # compute thrust force and fuel flow @@ -968,12 +1045,10 @@ def constantSpeedLevel( # ensure continuity of configuration change within the segment if config: - config_i = ( - AC.flightEnvelope.checkConfigurationContinuity( - phase=flightPhase, - previousConfig=config[-1], - currentConfig=config_i, - ) + config_i = AC.flightEnvelope.checkConfigurationContinuity( + phase=flightPhase, + previousConfig=config[-1], + currentConfig=config_i, ) [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( @@ -981,7 +1056,9 @@ def constantSpeedLevel( ) # compute lift coefficient - CL = AC.CL(M=M_up, delta=delta, mass=mass[-1], nz=nz) + CL = AC.CL( + M=M_up, delta=delta, mass=mass[-1], nz=nz + ) # compute drag coefficient CD = AC.CD( M=M_up, @@ -1112,58 +1189,89 @@ def constantSpeedROCD( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform a climb/descent from Hp_init to Hp_final at constant speed and constant rate of climb/descent - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param v: what kind of speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param Hp_final: final pressure altitude [ft]. - :param ROCDtarget: Rate of climb/descent to be followed [ft/min]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param Hp_step: length of an altitude step of a segment [ft]. - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type v: float. - :type Hp_init: float. - :type Hp_final: float. - :type ROCDtarget: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type Hp_step: float. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Computes the time, fuel consumption, and other parameters required for an aircraft to climb or descend + from a given initial altitude (Hp_init) to a final altitude (Hp_final) at a constant speed and rate of climb/descent (ROCD). + + The function handles multiple BADA families (BADA3, BADA4, BADAH, BADAE), computing various parameters + such as altitude, speed, fuel consumption, power, heading, and mass based on the aircraft's performance + characteristics. The function supports turn performance, optional heading (true or magnetic), and + handling mass changes during the flight. + + :param AC: Aircraft object {BADA3/4/H/E} + :param speedType: Type of speed to maintain during the flight {M, CAS, TAS}. + :param v: Speed to maintain during the flight - [kt] CAS/TAS or [-] MACH. + :param Hp_init: Initial pressure altitude at the start of the segment [ft]. + :param Hp_final: Final pressure altitude at the end of the segment [ft]. + :param ROCDtarget: Target rate of climb/descent [ft/min]. + :param m_init: Initial aircraft mass at the start of the segment [kg]. + :param DeltaTemp: Deviation from standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis [kt]. Positive values for headwind, negative for tailwind. Default is 0.0. + :param turnMetrics: Dictionary defining turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Geographical latitude of the starting point [deg]. Default is None. + :param Lon: Geographical longitude of the starting point [deg]. Default is None. + :param initialHeading: Dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to fly along a constant heading (loxodrome). Default is None. + :param reducedPower: Boolean specifying whether to apply reduced power during the climb. Default is None. + :param directionOfTurn: Direction of the turn {LEFT, RIGHT}. Default is None. + :param magneticDeclinationGrid: Optional grid of magnetic declinations used to correct headings. Default is None. + :param kwargs: Additional optional parameters: + - Hp_step: Altitude step size [ft]. Default is 1000 for BADA3/4, 500 for BADAH/BADAE. + - SOC_init: Initial state of charge for electric aircraft [%]. Default is 100 for BADAE. + - speedBrakes: Dictionary specifying whether speed brakes are deployed and their drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum ROCD to identify the service ceiling [ft/min]. Default varies by aircraft type. + - config: Default aerodynamic configuration. Values: TO, IC, CR, AP, LD. Default is None. + - mass_const: Boolean specifying whether to keep the mass constant during the segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 5. + + :returns: A pandas DataFrame containing the flight trajectory, including parameters like: + - Hp: Altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of Climb/Descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Trajectory slope [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle [deg] + - ROT: Rate of turn [deg/s] + - For BADAH: + - Preq: Required power [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Power supplied by the battery [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + - Comment: Comments for each segment + :rtype: pandas.DataFrame + + Notes: + - The function iteratively calculates flight parameters for each altitude step, adjusting fuel, power, and mass. + - Magnetic heading and true heading can be adjusted using the magnetic declination grid if provided. + - The function supports turns, and constant or changing headings based on input parameters. """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -1214,7 +1322,9 @@ def constantSpeedROCD( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # optional parameter - iteration step for altitude loop if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: @@ -1262,7 +1372,9 @@ def constantSpeedROCD( constHeadingStr = "" # comment line describing type of trajectory calculation - comment = phase + turnComment + "_const_ROCD_" + speedType + constHeadingStr + comment = ( + phase + turnComment + "_const_ROCD_" + speedType + constHeadingStr + ) if Lat and Lon and (magneticHeading or trueHeading): comment = comment + "_" + headingToFly + "_Heading" @@ -1283,7 +1395,9 @@ def constantSpeedROCD( ) # weight iteration constant - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # convert ROCD to IS units ROCDisu = conv.ft2m(ROCDtarget) / 60 @@ -1352,8 +1466,12 @@ def constantSpeedROCD( # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( @@ -1362,16 +1480,23 @@ def constantSpeedROCD( # compute Energy Share Factor (ESF) ESF_i = AC.esf( - h=H_m, M=M_i, DeltaTemp=DeltaTemp, flightEvolution=("const" + speedType) + h=H_m, + M=M_i, + DeltaTemp=DeltaTemp, + flightEvolution=("const" + speedType), ) if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined - bankAngle = AC.bankAngle(rateOfTurn=rateOfTurn, v=TAS_i) # [degrees] + bankAngle = AC.bankAngle( + rateOfTurn=rateOfTurn, v=TAS_i + ) # [degrees] # Load factor nz = 1 / cos(radians(bankAngle)) @@ -1385,7 +1510,9 @@ def constantSpeedROCD( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required for level flight - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # Compute power required for target ROCD Preq_target_i = AC.Peng_target( temp=theta * const.temp_0, @@ -1413,20 +1540,27 @@ def constantSpeedROCD( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # BADA3 @@ -1459,7 +1593,8 @@ def constantSpeedROCD( Drag = AC.D(tas=TAS_i, sigma=sigma, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # check that required thrust/power fits in the avialable thrust/power envelope, @@ -1558,21 +1693,39 @@ def constantSpeedROCD( Pelc_i = Preq_target_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: THR_min = AC.Thrust( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Thrust FUEL_max = AC.ff( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Fuel Flow if THR_i < THR_min: THR_i = THR_min @@ -1603,14 +1756,22 @@ def constantSpeedROCD( else: CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # [kg/s] ROCD_i = ROCDtarget # BADA3 elif AC.BADAFamily.BADA3: THR_min = AC.Thrust( - rating="LIDL", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="LIDL", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( flightPhase="Descent", @@ -1621,7 +1782,11 @@ def constantSpeedROCD( adapted=False, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", v=TAS_i, h=H_m, DeltaTemp=DeltaTemp, config="CR" + rating="MCMB", + v=TAS_i, + h=H_m, + DeltaTemp=DeltaTemp, + config="CR", ) # MCMB Thrust FUEL_max = AC.ff( flightPhase="Climb", @@ -1770,10 +1935,14 @@ def constantSpeedROCD( gamma_i = 90 * np.sign(ROCD_i) else: if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) Slope.append(gamma_i) @@ -1806,7 +1975,9 @@ def constantSpeedROCD( if turnFlight: step_distance = conv.m2nm( turn.distance( - rateOfTurn=rateOfTurn, TAS=TAS_i, timeOfTurn=step_time + rateOfTurn=rateOfTurn, + TAS=TAS_i, + timeOfTurn=step_time, ) ) # arcLength during the turn [NM] else: @@ -2026,58 +2197,84 @@ def constantSpeedROCD_time( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform a climb/descent from Hp_init for set amount of time at constant speed and constant rate of climb/descent - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param length: length of a segment to fly [s] - :param step_length: length of a step of a segment - [s] - :param v: what kind of speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param ROCDtarget: Rate of climb/descent to be followed [ft/min]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type length: float. - :type step_length: float. - :type v: float. - :type Hp_init: float. - :type ROCDtarget: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Computes the time, fuel consumption, and performance parameters required for an aircraft to + perform a climb or descent based on a set amount of time, while maintaining a constant speed and constant + rate of climb/descent (ROCD). + + The function supports various BADA families (BADA3, BADA4, BADAH, BADAE), with different handling for mass changes, + aerodynamic configurations, and energy consumption. It calculates parameters such as fuel consumption, power + requirements, speed, heading, and altitude changes over the specified duration. + + :param AC: Aircraft object {BADA3/4/H/E} + :param length: The length of the segment to fly in time [s]. + :param speedType: Type of speed to maintain during the flight {M, CAS, TAS}. + :param v: Speed to follow - [kt] CAS/TAS or [-] MACH. + :param Hp_init: Initial pressure altitude [ft]. + :param ROCDtarget: Rate of climb or descent [ft/min]. + :param m_init: Initial aircraft mass at the start of the segment [kg]. + :param DeltaTemp: Deviation from standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis [kt]. Default is 0.0. + :param turnMetrics: Dictionary defining turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Geographical latitude at the start [deg]. Default is None. + :param Lon: Geographical longitude at the start [deg]. Default is None. + :param initialHeading: Dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to fly along a constant heading (loxodrome). Default is None. + :param reducedPower: Boolean specifying whether to apply reduced power during the climb. Default is None. + :param directionOfTurn: Direction of the turn {LEFT, RIGHT}. Default is None. + :param magneticDeclinationGrid: Optional grid of magnetic declinations used to correct headings. Default is None. + :param kwargs: Additional optional parameters: + - step_length: Step size in seconds for time iteration. Default is 1 second. + - SOC_init: Initial state of charge for electric aircraft [%]. Default is 100 for BADAE. + - speedBrakes: Dictionary specifying whether speed brakes are deployed and their drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum ROCD to identify the service ceiling [ft/min]. Default varies by aircraft type. + - config: Default aerodynamic configuration. Values: TO, IC, CR, AP, LD. Default is None. + - mass_const: Boolean specifying whether to keep the mass constant during the segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 5. + + :returns: A pandas DataFrame containing the flight trajectory, including parameters like: + - Hp: Altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of Climb/Descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Trajectory slope [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle [deg] + - ROT: Rate of turn [deg/s] + - For BADAH: + - Preq: Required power [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Power supplied by the battery [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + - Comment: Comments for each segment. + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -2128,7 +2325,9 @@ def constantSpeedROCD_time( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # step size in [s] step_length = kwargs.get("step_length", 1) @@ -2161,7 +2360,9 @@ def constantSpeedROCD_time( constHeadingStr = "" # comment line describing type of trajectory calculation - comment = phase + turnComment + "_const_ROCD_" + speedType + constHeadingStr + comment = ( + phase + turnComment + "_const_ROCD_" + speedType + constHeadingStr + ) if Lat and Lon and (magneticHeading or trueHeading): comment = comment + "_" + headingToFly + "_Heading" @@ -2182,7 +2383,9 @@ def constantSpeedROCD_time( ) # weight iteration constant - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # convert ROCD to IS units ROCDisu = conv.ft2m(ROCDtarget) / 60 @@ -2260,8 +2463,12 @@ def constantSpeedROCD_time( for _ in itertools.repeat(None, m_iter): # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( @@ -2270,13 +2477,18 @@ def constantSpeedROCD_time( # compute Energy Share Factor (ESF) ESF_i = AC.esf( - h=H_m, M=M_i, DeltaTemp=DeltaTemp, flightEvolution=("const" + speedType) + h=H_m, + M=M_i, + DeltaTemp=DeltaTemp, + flightEvolution=("const" + speedType), ) if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined bankAngle = AC.bankAngle( @@ -2291,7 +2503,9 @@ def constantSpeedROCD_time( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required for level flight - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # Compute power required for target ROCD Preq_target_i = AC.Peng_target( temp=theta * const.temp_0, @@ -2319,20 +2533,27 @@ def constantSpeedROCD_time( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # BADA3 @@ -2365,7 +2586,8 @@ def constantSpeedROCD_time( Drag = AC.D(tas=TAS_i, sigma=sigma, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # check that required thrust/power fits in the avialable thrust/power envelope, @@ -2464,21 +2686,39 @@ def constantSpeedROCD_time( Pelc_i = Preq_target_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: THR_min = AC.Thrust( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Thrust FUEL_max = AC.ff( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Fuel Flow if THR_i < THR_min: THR_i = THR_min @@ -2509,14 +2749,22 @@ def constantSpeedROCD_time( else: CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # [kg/s] ROCD_i = ROCDtarget # BADA3 elif AC.BADAFamily.BADA3: THR_min = AC.Thrust( - rating="LIDL", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="LIDL", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( flightPhase="Descent", @@ -2527,7 +2775,11 @@ def constantSpeedROCD_time( adapted=False, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", v=TAS_i, h=H_m, DeltaTemp=DeltaTemp, config="CR" + rating="MCMB", + v=TAS_i, + h=H_m, + DeltaTemp=DeltaTemp, + config="CR", ) # MCMB Thrust FUEL_max = AC.ff( flightPhase="Climb", @@ -2679,12 +2931,18 @@ def constantSpeedROCD_time( [theta, delta, sigma] = atm.atmosphereProperties( h=conv.ft2m(Hp_i), DeltaTemp=DeltaTemp ) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -2721,7 +2979,9 @@ def constantSpeedROCD_time( if turnFlight: step_distance = conv.m2nm( turn.distance( - rateOfTurn=rateOfTurn, TAS=TAS_i, timeOfTurn=step_time + rateOfTurn=rateOfTurn, + TAS=TAS_i, + timeOfTurn=step_time, ) ) # arcLength during the turn [NM] else: @@ -2923,58 +3183,83 @@ def constantSpeedSlope( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform a climb/descent from Hp_init to Hp_final at constant speed and constant slope - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param v: what kind of speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param Hp_final: final pressure altitude [ft]. - :param slopetarget: slope to be followed [deg]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param Hp_step: length of an altitude step of a segment [ft]. - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type v: float. - :type Hp_init: float. - :type Hp_final: float. - :type slopetarget: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type Hp_step: float. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Calculates time, fuel consumption, and other parameters required for an aircraft to perform a climb or descent + from an initial altitude (Hp_init) to a final altitude (Hp_final) while maintaining a constant speed and a constant slope. + + It uses different models for BADA (Base of Aircraft Data) families (BADA3, BADA4, BADAH, BADAE) to compute key flight parameters + such as energy consumption, rate of climb/descent (ROCD), fuel burn, mass changes, and power requirements. The function also supports + complex flight dynamics including turns, heading changes, and wind influences. + + :param AC: Aircraft object {BADA3/4/H/E}. + :param speedType: Type of speed to maintain during the flight {M, CAS, TAS}. + :param v: Speed to follow - [kt] CAS/TAS or [-] MACH. + :param Hp_init: Initial pressure altitude [ft]. + :param Hp_final: Final pressure altitude [ft]. + :param slopetarget: Target slope (trajectory angle) to be maintained during climb/descent [deg]. + :param m_init: Initial mass of the aircraft at the start of the segment [kg]. + :param DeltaTemp: Deviation from the standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis (affects ground speed) [kt]. Default is 0.0. + :param turnMetrics: A dictionary defining the turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Initial latitude [deg]. Default is None. + :param Lon: Initial longitude [deg]. Default is None. + :param initialHeading: A dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to maintain a constant heading. Default is None. + :param reducedPower: Boolean specifying if reduced power is applied during the climb. Default is None. + :param directionOfTurn: Direction of the turn {LEFT, RIGHT}. Default is None. + :param magneticDeclinationGrid: Optional grid of magnetic declination used to correct magnetic heading. Default is None. + :param kwargs: Additional optional parameters: + - Hp_step: Step size in altitude for the iterative calculation [ft]. Default is 1000 ft for BADA3/BADA4, 500 ft for BADAH/BADAE. + - SOC_init: Initial battery state of charge for electric aircraft (BADAE) [%]. Default is 100. + - speedBrakes: A dictionary specifying whether speed brakes are deployed and the additional drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum rate of climb/descent used to determine service ceiling [ft/min]. Default varies based on aircraft type. + - config: Default aerodynamic configuration (TO, IC, CR, AP, LD). Default is None. + - mass_const: Boolean indicating whether mass remains constant during the flight segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 5. + + :returns: A pandas DataFrame containing flight trajectory data with the following columns: + - Hp: Pressure altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of climb/descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Flight trajectory slope (angle) [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle during the turn [deg] + - ROT: Rate of turn [deg/s] + - Comment: Comments describing the flight segment + - For BADAH: + - Preq: Required power for level flight [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Battery power [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -3025,7 +3310,9 @@ def constantSpeedSlope( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # optional parameter - iteration step for altitude loop if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: @@ -3070,7 +3357,9 @@ def constantSpeedSlope( constHeadingStr = "" # comment line describing type of trajectory calculation - comment = phase + turnComment + "_const_Slope_" + speedType + constHeadingStr + comment = ( + phase + turnComment + "_const_Slope_" + speedType + constHeadingStr + ) if Lat and Lon and (magneticHeading or trueHeading): comment = comment + "_" + headingToFly + "_Heading" @@ -3091,7 +3380,9 @@ def constantSpeedSlope( ) # weight iteration constant - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # initialize output parameters Hp = [] @@ -3157,8 +3448,12 @@ def constantSpeedSlope( # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( @@ -3168,17 +3463,24 @@ def constantSpeedSlope( if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined - bankAngle = AC.bankAngle(rateOfTurn=rateOfTurn, v=TAS_i) # [degrees] + bankAngle = AC.bankAngle( + rateOfTurn=rateOfTurn, v=TAS_i + ) # [degrees] # Load factor nz = 1 / cos(radians(bankAngle)) # compute Energy Share Factor (ESF) ESF_i = AC.esf( - h=H_m, M=M_i, DeltaTemp=DeltaTemp, flightEvolution=("const" + speedType) + h=H_m, + M=M_i, + DeltaTemp=DeltaTemp, + flightEvolution=("const" + speedType), ) if AC.BADAFamily.BADAE: @@ -3196,7 +3498,9 @@ def constantSpeedSlope( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required for level flight - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # Compute power required for target ROCD Preq_target_i = AC.Peng_target( temp=theta * const.temp_0, @@ -3224,20 +3528,27 @@ def constantSpeedSlope( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # BADA3 @@ -3270,7 +3581,8 @@ def constantSpeedSlope( Drag = AC.D(tas=TAS_i, sigma=sigma, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # check that required thrust/power fits in the avialable thrust/power envelope, @@ -3369,21 +3681,39 @@ def constantSpeedSlope( Pelc_i = Preq_target_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: THR_min = AC.Thrust( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Thrust FUEL_max = AC.ff( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Fuel Flow if THR_i < THR_min: @@ -3415,14 +3745,22 @@ def constantSpeedSlope( else: CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # [kg/s] ROCD_i = conv.m2ft(ROCDisu) * 60 # BADA3 elif AC.BADAFamily.BADA3: THR_min = AC.Thrust( - rating="LIDL", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="LIDL", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( flightPhase="Descent", @@ -3433,7 +3771,11 @@ def constantSpeedSlope( adapted=False, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", v=TAS_i, h=H_m, DeltaTemp=DeltaTemp, config="CR" + rating="MCMB", + v=TAS_i, + h=H_m, + DeltaTemp=DeltaTemp, + config="CR", ) # MCMB Thrust FUEL_max = AC.ff( flightPhase="Climb", @@ -3581,10 +3923,14 @@ def constantSpeedSlope( gamma_i = 90 * np.sign(ROCD_i) else: if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -3620,7 +3966,9 @@ def constantSpeedSlope( if turnFlight: step_distance = conv.m2nm( turn.distance( - rateOfTurn=rateOfTurn, TAS=TAS_i, timeOfTurn=step_time + rateOfTurn=rateOfTurn, + TAS=TAS_i, + timeOfTurn=step_time, ) ) # arcLength during the turn [NM] else: @@ -3835,59 +4183,82 @@ def constantSpeedSlope_time( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform a climb/descent from Hp_init for set amount of time at constant speed and constant slope - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param length: length of a segment to fly [s] - :param step_length: length of a step of a segment - [s] - :param v: what kind of speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param slopetarget: slope to be followed [deg]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param Hp_step: length of an altitude step of a segment [ft]. - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type length: float. - :type step_length: float. - :type v: float. - :type Hp_init: float. - :type slopetarget: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type Hp_step: float. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Computes the time, fuel, and trajectory parameters required by an aircraft to climb or descend at constant speed and slope over a given duration. + + This function models a trajectory with constant speed (either CAS, TAS, or Mach) and a specified slope (degrees). The aircraft's dynamics are modeled based on BADA family data (BADA3, BADA4, BADAH, BADAE), supporting various aircraft types including electric models. It also accounts for turns, heading changes, and wind effects. + + :param AC: Aircraft object {BADA3, BADA4, BADAH, BADAE}. + :param length: Total duration of the segment [s]. + :param speedType: Speed type to follow during the trajectory {M, CAS, TAS}. + :param v: Speed to follow (in knots for CAS/TAS or as a Mach number) [kt] or [-] for Mach. + :param Hp_init: Initial pressure altitude [ft]. + :param slopetarget: Desired slope (trajectory angle) to follow [deg]. + :param m_init: Initial aircraft mass [kg]. + :param DeltaTemp: Deviation from standard ISA temperature [K]. + :param wS: Longitudinal wind speed component (affects ground speed) [kt]. Default is 0.0. + :param turnMetrics: A dictionary defining the turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Initial latitude [deg]. Default is None. + :param Lon: Initial longitude [deg]. Default is None. + :param initialHeading: A dictionary specifying the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to maintain a constant heading [True/False]. + :param reducedPower: Boolean specifying if reduced power is applied during climb/descent. Default is None. + :param directionOfTurn: Direction of turn {LEFT, RIGHT}. Default is None. + :param magneticDeclinationGrid: Optional magnetic declination grid for correcting magnetic heading. Default is None. + :param kwargs: Additional optional parameters: + - step_length: Step length for trajectory calculation [s]. Default is 1 second. + - Hp_step: Altitude step size for calculations [ft]. Default is 1000 ft for BADA3/BADA4, 500 ft for BADAH/BADAE. + - SOC_init: Initial battery state of charge (for electric aircraft) [%]. Default is 100. + - config: Default aerodynamic configuration {TO, IC, CR, AP, LD}. If not provided, configuration is calculated automatically. + - speedBrakes: Dictionary specifying if speed brakes are deployed and additional drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum Rate of Climb/Descent to determine service ceiling [ft/min]. Defaults depend on aircraft type and engine. + - mass_const: Boolean indicating whether mass remains constant during the flight. Default is False. + - m_iter: Number of iterations for mass integration. Default is 5. + + :returns: A pandas DataFrame containing the flight trajectory data with the following columns: + - Hp: Pressure altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of Climb/Descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Trajectory slope (angle) [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle during turns [deg] + - ROT: Rate of turn [deg/s] + - Comment: Descriptive comments about the trajectory segment. + - For BADAH: + - Preq: Power required for level flight [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Battery power [W] + - SOCr: Battery state of charge rate of depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -3938,7 +4309,9 @@ def constantSpeedSlope_time( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # step size in [s] step_length = kwargs.get("step_length", 1) @@ -3971,7 +4344,9 @@ def constantSpeedSlope_time( constHeadingStr = "" # comment line describing type of trajectory calculation - comment = phase + turnComment + "_const_Slope_" + speedType + constHeadingStr + comment = ( + phase + turnComment + "_const_Slope_" + speedType + constHeadingStr + ) if Lat and Lon and (magneticHeading or trueHeading): comment = comment + "_" + headingToFly + "_Heading" @@ -3992,7 +4367,9 @@ def constantSpeedSlope_time( ) # weight iteration constant - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # initialize output parameters Hp = [Hp_init] @@ -4066,8 +4443,12 @@ def constantSpeedSlope_time( for _ in itertools.repeat(None, m_iter): # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( @@ -4077,7 +4458,9 @@ def constantSpeedSlope_time( if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined bankAngle = AC.bankAngle( @@ -4089,7 +4472,10 @@ def constantSpeedSlope_time( # compute Energy Share Factor (ESF) ESF_i = AC.esf( - h=H_m, M=M_i, DeltaTemp=DeltaTemp, flightEvolution=("const" + speedType) + h=H_m, + M=M_i, + DeltaTemp=DeltaTemp, + flightEvolution=("const" + speedType), ) step_time = length_loop - time[-1] @@ -4097,14 +4483,20 @@ def constantSpeedSlope_time( # Compute required ROCD if AC.BADAFamily.BADAE: # special case for BADAE, in future it may apply also for BADAH - ROCDisu = tan(conv.deg2rad(slopetarget)) * TAS_i * (1 / temp_const) + ROCDisu = ( + tan(conv.deg2rad(slopetarget)) * TAS_i * (1 / temp_const) + ) else: - ROCDisu = sin(conv.deg2rad(slopetarget)) * TAS_i * (1 / temp_const) + ROCDisu = ( + sin(conv.deg2rad(slopetarget)) * TAS_i * (1 / temp_const) + ) # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required for level flight - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # Compute power required for target ROCD Preq_target_i = AC.Peng_target( temp=theta * const.temp_0, @@ -4132,20 +4524,27 @@ def constantSpeedSlope_time( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # BADA3 @@ -4178,7 +4577,8 @@ def constantSpeedSlope_time( Drag = AC.D(tas=TAS_i, sigma=sigma, CD=CD) # compute thrust force THR_i = ( - ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + Drag + ROCDisu * mass_i * const.g * temp_const / (TAS_i * ESF_i) + + Drag ) # [N] # check that required thrust/power fits in the avialable thrust/power envelope, @@ -4277,21 +4677,39 @@ def constantSpeedSlope_time( Pelc_i = Preq_target_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: THR_min = AC.Thrust( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( - rating="LIDL", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="LIDL", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Thrust FUEL_max = AC.ff( - rating="MCMB", delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating="MCMB", + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # MCMB Fuel Flow if THR_i < THR_min: @@ -4323,14 +4741,22 @@ def constantSpeedSlope_time( else: CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # [kg/s] ROCD_i = conv.m2ft(ROCDisu) * 60 # BADA3 elif AC.BADAFamily.BADA3: THR_min = AC.Thrust( - rating="LIDL", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="LIDL", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( flightPhase="Descent", @@ -4341,7 +4767,11 @@ def constantSpeedSlope_time( adapted=False, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", v=TAS_i, h=H_m, DeltaTemp=DeltaTemp, config="CR" + rating="MCMB", + v=TAS_i, + h=H_m, + DeltaTemp=DeltaTemp, + config="CR", ) # MCMB Thrust FUEL_max = AC.ff( flightPhase="Climb", @@ -4493,12 +4923,18 @@ def constantSpeedSlope_time( [theta, delta, sigma] = atm.atmosphereProperties( h=conv.ft2m(Hp_i), DeltaTemp=DeltaTemp ) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -4535,7 +4971,9 @@ def constantSpeedSlope_time( if turnFlight: step_distance = conv.m2nm( turn.distance( - rateOfTurn=rateOfTurn, TAS=TAS_i, timeOfTurn=step_time + rateOfTurn=rateOfTurn, + TAS=TAS_i, + timeOfTurn=step_time, ) ) # arcLength during the turn [NM] else: @@ -4738,58 +5176,83 @@ def constantSpeedRating( initRating=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform a climb/descent from Hp_init to Hp_final at constant speed and constant engine rating - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param v: what kind of speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param Hp_final: final pressure altitude [ft]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param Hp_step: length of an altitude step of a segment [ft]. - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :param initRating: default rating settings - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type v: float. - :type Hp_init: float. - :type Hp_final: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type Hp_step: float. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :type initRating: string. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Calculates time, fuel consumption, and other parameters required for an aircraft to perform a climb or descent + from an initial altitude (Hp_init) to a final altitude (Hp_final) while maintaining a constant speed and engine rating. + + It uses different models for BADA (Base of Aircraft Data) families (BADA3, BADA4, BADAH, BADAE) to compute key flight parameters + such as fuel burn, rate of climb/descent (ROCD), thrust, drag, and energy requirements. The function also supports + complex flight dynamics including turns, heading changes, and wind influences. + + :param AC: Aircraft object {BADA3/4/H/E}. + :param speedType: Type of speed to maintain during the flight {M (Mach), CAS, TAS}. + :param v: Speed to follow - [kt] CAS/TAS or [-] MACH. + :param Hp_init: Initial pressure altitude [ft]. + :param Hp_final: Final pressure altitude [ft]. + :param m_init: Initial mass of the aircraft at the start of the segment [kg]. + :param DeltaTemp: Deviation from the standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis (affects ground speed) [kt]. Default is 0.0. + :param turnMetrics: A dictionary defining the turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Initial latitude [deg]. Default is None. + :param Lon: Initial longitude [deg]. Default is None. + :param initialHeading: A dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to maintain a constant heading. Default is None. + :param reducedPower: Boolean specifying if reduced power is applied during the climb. Default is None. + :param directionOfTurn: Direction of the turn {LEFT, RIGHT}. Default is None. + :param expedite: Boolean flag to expedite climb/descent. Default is False. + :param magneticDeclinationGrid: Optional grid of magnetic declination used to correct magnetic heading. Default is None. + :param kwargs: Additional optional parameters: + - Hp_step: Step size in altitude for the iterative calculation [ft]. Default is 1000 ft for BADA3/BADA4, 500 ft for BADAH/BADAE. + - SOC_init: Initial battery state of charge for electric aircraft (BADAE) [%]. Default is 100. + - speedBrakes: A dictionary specifying whether speed brakes are deployed and the additional drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum rate of climb/descent used to determine service ceiling [ft/min]. Default varies based on aircraft type. + - config: Default aerodynamic configuration (TO, IC, CR, AP, LD). Default is None. + - mass_const: Boolean indicating whether mass remains constant during the flight segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 5. + + :returns: A pandas DataFrame containing flight trajectory data with the following columns: + - Hp: Pressure altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of climb/descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Flight trajectory slope (angle) [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle during the turn [deg] + - ROT: Rate of turn [deg/s] + - Comment: Comments describing the flight segment + - For BADAH: + - Preq: Required power for level flight [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Battery power [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -4840,7 +5303,9 @@ def constantSpeedRating( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # optional parameter - iteration step for altitude loop if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: @@ -4899,7 +5364,13 @@ def constantSpeedRating( # comment line describing type of trajectory calculation comment = ( - phase + turnComment + "_const_" + speedType + "_" + rating + constHeadingStr + phase + + turnComment + + "_const_" + + speedType + + "_" + + rating + + constHeadingStr ) if Lat and Lon and (magneticHeading or trueHeading): @@ -4924,7 +5395,9 @@ def constantSpeedRating( ) # weight iteration constant - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # The thrust_fuel method for BADA 3 models applies the cruise fuel correction # whenever the thrust is adapted, instead of only in cruise: this correction @@ -4995,8 +5468,12 @@ def constantSpeedRating( ## atmosphere, speeds, thrust. fuel flow, ESF # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( @@ -5006,17 +5483,24 @@ def constantSpeedRating( if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined - bankAngle = AC.bankAngle(rateOfTurn=rateOfTurn, v=TAS_i) # [degrees] + bankAngle = AC.bankAngle( + rateOfTurn=rateOfTurn, v=TAS_i + ) # [degrees] # Load factor nz = 1 / cos(radians(bankAngle)) # compute Energy Share Factor (ESF) ESF_i = AC.esf( - h=H_m, M=M_i, DeltaTemp=DeltaTemp, flightEvolution=("const" + speedType) + h=H_m, + M=M_i, + DeltaTemp=DeltaTemp, + flightEvolution=("const" + speedType), ) mass_i = mass[-1] @@ -5025,7 +5509,9 @@ def constantSpeedRating( if AC.BADAFamily.BADAH: # compute available power if rating == "UNKNOWN": - Preq_target_i = 0.1 * AC.P0 # No minimum power model: assume 10% torque + Preq_target_i = ( + 0.1 * AC.P0 + ) # No minimum power model: assume 10% torque else: Preq_target_i = AC.Pav(rating=rating, theta=theta, delta=delta) @@ -5039,7 +5525,9 @@ def constantSpeedRating( elif AC.BADAFamily.BADAE: # compute available power if rating == "UNKNOWN": - Preq_target_i = 0.1 * AC.P0 # No minimum power model: assume 10% torque + Preq_target_i = ( + 0.1 * AC.P0 + ) # No minimum power model: assume 10% torque else: Preq_target_i = AC.Pav(rating=rating, SOC=SOC[-1]) @@ -5058,10 +5546,16 @@ def constantSpeedRating( elif AC.BADAFamily.BADA4: # compute thrust force and fuel flow THR_i = AC.Thrust( - rating=rating, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating=rating, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # [N] CT = AC.CT(Thrust=THR_i, delta=delta) - FUEL_i = AC.ff(CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp) + FUEL_i = AC.ff( + CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + ) # BADA3 elif AC.BADAFamily.BADA3: @@ -5080,12 +5574,18 @@ def constantSpeedRating( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) # compute thrust force and fuel flow THR_i = AC.Thrust( - rating=rating, v=TAS_i, h=H_m, config=config_i, DeltaTemp=DeltaTemp + rating=rating, + v=TAS_i, + h=H_m, + config=config_i, + DeltaTemp=DeltaTemp, ) FUEL_i = AC.ff( flightPhase=phase, @@ -5113,7 +5613,9 @@ def constantSpeedRating( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # compute ROCD ROCD_i = ( conv.m2ft( @@ -5146,15 +5648,21 @@ def constantSpeedRating( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) # compute ROCD @@ -5195,7 +5703,10 @@ def constantSpeedRating( CL = AC.CL(tas=TAS_i, sigma=sigma, mass=mass_i, nz=nz) # compute drag coefficient CD = AC.CD( - CL=CL, config=config_i, expedite=expedite, speedBrakes=speedBrakes + CL=CL, + config=config_i, + expedite=expedite, + speedBrakes=speedBrakes, ) # compute drag force Drag = AC.D(tas=TAS_i, sigma=sigma, CD=CD) @@ -5318,10 +5829,14 @@ def constantSpeedRating( gamma_i = 90 * np.sign(ROCD_i) else: if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -5357,7 +5872,9 @@ def constantSpeedRating( if turnFlight: step_distance = conv.m2nm( turn.distance( - rateOfTurn=rateOfTurn, TAS=TAS_i, timeOfTurn=step_time + rateOfTurn=rateOfTurn, + TAS=TAS_i, + timeOfTurn=step_time, ) ) # arcLength during the turn [NM] else: @@ -5574,60 +6091,85 @@ def constantSpeedRating_time( initRating=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform a climb/descent from Hp_init for set amount of time at constant speed and constant engine rating - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param length: length of a segment to fly [s] - :param step_length: length of a step of a segment - [s] - :param v: what kind of speed is followed - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param Hp_init: initial pressure altitude [ft]. - :param phase: phase of flight {Climb, Descent} - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param ROCD_min: lower ROCD threshold to identify the climbing capabilities (service ceiling) [ft/min]. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :param initRating: default rating settings - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type length: float. - :type step_length: float. - :type v: float. - :type Hp_init: float. - :type phase: string. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type ROCD_min: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :type initRating: string - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + """ + Calculates the time, fuel consumption, and other flight parameters required for an aircraft to perform + a climb or descent at constant speed and engine rating for a specified duration. + + It uses different models for BADA (Base of Aircraft Data) families (BADA3, BADA4, BADAH, BADAE) to compute key parameters + such as rate of climb/descent (ROCD), thrust, drag, fuel burn, and power requirements. The function also supports complex + flight dynamics, including turns, heading changes, and the effect of wind. + + :param AC: Aircraft object {BADA3/4/H/E}. + :param length: Duration of the flight segment [s]. + :param speedType: Type of speed to maintain during the flight {M, CAS, TAS}. + :param v: Speed to follow - [kt] CAS/TAS or [-] MACH. + :param Hp_init: Initial pressure altitude [ft]. + :param phase: Phase of flight (Climb or Descent). + :param m_init: Initial mass of the aircraft at the start of the segment [kg]. + :param DeltaTemp: Deviation from the standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis (affects ground speed) [kt]. Default is 0.0. + :param turnMetrics: A dictionary defining the turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Initial latitude [deg]. Default is None. + :param Lon: Initial longitude [deg]. Default is None. + :param initialHeading: A dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to maintain a constant heading. Default is None. + :param reducedPower: Boolean specifying if reduced power is applied during the climb. Default is None. + :param directionOfTurn: Direction of the turn {LEFT, RIGHT}. Default is None. + :param expedite: Boolean flag to expedite the climb/descent. Default is False. + :param magneticDeclinationGrid: Optional grid of magnetic declination used to correct magnetic heading. Default is None. + :param initRating: Initial engine rating settings. Default is None. + :param kwargs: Additional optional parameters: + - step_length: Step size in time for the iterative calculation [s]. Default is 1 s. + - SOC_init: Initial battery state of charge for electric aircraft (BADAE) [%]. Default is 100. + - speedBrakes: A dictionary specifying whether speed brakes are deployed and the additional drag coefficient {deployed: False, value: 0.03}. + - ROCD_min: Minimum rate of climb/descent to determine service ceiling [ft/min]. Default varies by aircraft type. + - config: Default aerodynamic configuration (TO, IC, CR, AP, LD). Default is None. + - mass_const: Boolean indicating whether mass remains constant during the flight segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 5. + + :returns: A pandas DataFrame containing flight trajectory data with the following columns: + - Hp: Pressure altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - ROCD: Rate of climb/descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Flight trajectory slope (angle) [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle during the turn [deg] + - ROT: Rate of turn [deg/s] + - Comment: Comments describing the flight segment + - For BADAH: + - Preq: Required power for level flight [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Battery power [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -5678,7 +6220,9 @@ def constantSpeedRating_time( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # step size in [s] step_length = kwargs.get("step_length", 1) @@ -5713,7 +6257,9 @@ def constantSpeedRating_time( else: rating = "LIDL" else: - raise Exception("Phase definition is wrong! It should be Climb or Descent") + raise Exception( + "Phase definition is wrong! It should be Climb or Descent" + ) else: rating = initRating @@ -5729,7 +6275,13 @@ def constantSpeedRating_time( # comment line describing type of trajectory calculation comment = ( - phase + turnComment + "_const_" + speedType + "_" + rating + constHeadingStr + phase + + turnComment + + "_const_" + + speedType + + "_" + + rating + + constHeadingStr ) if Lat and Lon and (magneticHeading or trueHeading): @@ -5754,7 +6306,9 @@ def constantSpeedRating_time( ) # weight iteration constant - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # The thrust_fuel method for BADA 3 models applies the cruise fuel correction # whenever the thrust is adapted, instead of only in cruise: this correction @@ -5834,8 +6388,12 @@ def constantSpeedRating_time( for _ in itertools.repeat(None, m_iter): # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( @@ -5845,7 +6403,9 @@ def constantSpeedRating_time( if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined bankAngle = AC.bankAngle( @@ -5857,7 +6417,10 @@ def constantSpeedRating_time( # compute Energy Share Factor (ESF) ESF_i = AC.esf( - h=H_m, M=M_i, DeltaTemp=DeltaTemp, flightEvolution=("const" + speedType) + h=H_m, + M=M_i, + DeltaTemp=DeltaTemp, + flightEvolution=("const" + speedType), ) step_time = length_loop - time[-1] @@ -5870,7 +6433,9 @@ def constantSpeedRating_time( 0.1 * AC.P0 ) # No minimum power model: assume 10% torque else: - Preq_target_i = AC.Pav(rating=rating, theta=theta, delta=delta) + Preq_target_i = AC.Pav( + rating=rating, theta=theta, delta=delta + ) Pav_i = AC.Pav(rating="MTKF", theta=theta, delta=delta) @@ -5895,13 +6460,19 @@ def constantSpeedRating_time( Pelc_i = Preq_target_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: # compute thrust force and fuel flow THR_i = AC.Thrust( - rating=rating, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + rating=rating, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # [N] CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( @@ -5932,7 +6503,11 @@ def constantSpeedRating_time( # compute thrust force and fuel flow THR_i = AC.Thrust( - rating=rating, v=TAS_i, h=H_m, config=config_i, DeltaTemp=DeltaTemp + rating=rating, + v=TAS_i, + h=H_m, + config=config_i, + DeltaTemp=DeltaTemp, ) FUEL_i = AC.ff( flightPhase=phase, @@ -5946,7 +6521,9 @@ def constantSpeedRating_time( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # compute ROCD ROCD_i = ( conv.m2ft( @@ -5979,15 +6556,21 @@ def constantSpeedRating_time( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) @@ -6028,7 +6611,10 @@ def constantSpeedRating_time( CL = AC.CL(tas=TAS_i, sigma=sigma, mass=mass_i, nz=nz) # compute drag coefficient CD = AC.CD( - CL=CL, config=config_i, expedite=expedite, speedBrakes=speedBrakes + CL=CL, + config=config_i, + expedite=expedite, + speedBrakes=speedBrakes, ) # compute drag force Drag = AC.D(tas=TAS_i, sigma=sigma, CD=CD) @@ -6158,12 +6744,18 @@ def constantSpeedRating_time( [theta, delta, sigma] = atm.atmosphereProperties( h=conv.ft2m(Hp_i), DeltaTemp=DeltaTemp ) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -6200,7 +6792,9 @@ def constantSpeedRating_time( if turnFlight: step_distance = conv.m2nm( turn.distance( - rateOfTurn=rateOfTurn, TAS=TAS_i, timeOfTurn=step_time + rateOfTurn=rateOfTurn, + TAS=TAS_i, + timeOfTurn=step_time, ) ) # arcLength during the turn [NM] else: @@ -6402,77 +6996,94 @@ def accDec( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform an acceleration/deceleration from v_init to v_final in climb cruise or descent + """ + Calculates the time, fuel consumption, and other key flight parameters required for an aircraft + to perform an acceleration or deceleration from an initial speed (v_init) to a final speed (v_final) + during the climb, cruise, or descent phases of flight. + + The flight parameters are calculated using different models for the BADA (Base of Aircraft Data) families (BADA3, BADA4, BADAH, BADAE). + The function can also accommodate different control laws, vertical evolution phases, wind conditions, and complex flight dynamics like turns. .. note:: - The control law used during the segment depends on the targets provided in input parameter 'control': - - ROCD/slope+ESF: law is ROCD/slope+ESF - - ROCD/slope+acc: law is ROCD/slope+acc - - ROCD/slope only: law is rating+ROCD/slope - - ESF only: law is rating+ESF - - acc only: law is rating+acc - - Neither: law is rating+default ESF - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param v_init: initial speed to follow - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param v_final: final speed to follow - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param phase: vertical evolution {Climb, Descent, Cruise} - :param control: structure containing a combination of the following targets: - :param ROCDtarget: Rate of climb/descent to be followed [ft/min]. - :param slopetarget: slope (flight path angle) to be followed [deg]. - :param acctarget: acceleration to be followed [m/s^2]. - :param ESFtarget: Energy Share Factor to be followed [-]. - :param maxRating: rating to be used as a limit on the maximum thrust/power [-]. - :param Hp_init: initial pressure altitude [ft]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param speed_step: step of the speed for the speed iteration loop [-] for M, [kt] for TAS or CAS - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type v_init: float. - :type v_final: float. - :type phase: string. - :type control: structure. - :type ROCDtarget: float. - :type slopetarget: float. - :type acctarget: float. - :type ESFtarget: float. - :type maxRating: float. - :type Hp_init: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type speed_step: float. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + The control law used during the segment depends on the targets provided in the input parameter 'control': + - ROCD/slope+ESF: Law based on ROCD/slope + ESF + - ROCD/slope+acc: Law based on ROCD/slope + acceleration + - ROCD/slope only: Law based on rating + ROCD/slope + - ESF only: Law based on rating + ESF + - acc only: Law based on rating + acceleration + - Neither: Law is rating + default ESF + + :param AC: Aircraft object {BADA3/4/H/E}. + :param speedType: Type of speed being followed {M, CAS, TAS}. + :param v_init: Initial speed [kt] (CAS/TAS) or [-] MACH. + :param v_final: Final speed [kt] (CAS/TAS) or [-] MACH. + :param phase: Vertical evolution phase {Climb, Descent, Cruise}. + :param control: A dictionary containing the following targets: + - ROCDtarget: Rate of climb/descent to be followed [ft/min]. + - slopetarget: Slope (flight path angle) to be followed [deg]. + - acctarget: Acceleration to be followed [m/s^2]. + - ESFtarget: Energy Share Factor to be followed [-]. + :param Hp_init: Initial pressure altitude [ft]. + :param m_init: Initial aircraft mass [kg]. + :param DeltaTemp: Deviation from the standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis (affects ground speed) [kt]. Default is 0.0. + :param turnMetrics: A dictionary defining turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Initial latitude [deg]. Default is None. + :param Lon: Initial longitude [deg]. Default is None. + :param initialHeading: A dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to maintain a constant heading. Default is None. + :param reducedPower: Boolean specifying if reduced power is applied during the climb. Default is None. + :param magneticDeclinationGrid: Optional grid of magnetic declination used to correct magnetic heading. Default is None. + :param kwargs: Additional optional parameters: + - speed_step: Speed step size for the iterative calculation [-] for M, [kt] for TAS/CAS. Default is 0.01 Mach, 5 kt for TAS/CAS. + - SOC_init: Initial battery state of charge for electric aircraft (BADAE) [%]. Default is 100. + - config: Default aerodynamic configuration (TO, IC, CR, AP, LD). Default is None. + - mass_const: Boolean indicating whether mass remains constant during the flight segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 10 for BADA3/4/H, 5 for BADAE. + :returns: A pandas DataFrame containing flight trajectory data with the following columns: + - Hp: Pressure altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - acc: Acceleration rate [m/s^2] + - ROCD: Rate of climb/descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Flight trajectory slope (angle) [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle during the turn [deg] + - ROT: Rate of turn [deg/s] + - Comment: Comments describing the flight segment + - For BADAH: + - Preq: Required power for level flight [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Battery power [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -6523,7 +7134,9 @@ def accDec( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # iteratin step of speed loop if speedType == "M": @@ -6534,7 +7147,9 @@ def accDec( # number of iteration of mass/altitude loop # BADAE if AC.BADAFamily.BADAE: - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # BADA3 or BADA4 or BADAH else: m_iter = kwargs.get( @@ -6691,9 +7306,9 @@ def accDec( maxRating = checkArgument("maxRating", **kwargs) # Determine engine rating - if (control.ROCDtarget is not None or control.slopetarget is not None) and ( - control.ESFtarget is not None or control.acctarget is not None - ): + if ( + control.ROCDtarget is not None or control.slopetarget is not None + ) and (control.ESFtarget is not None or control.acctarget is not None): rating = None else: if phase == "Climb" or (phase == "Cruise" and speedEvol == "acc"): @@ -6797,18 +7412,28 @@ def accDec( for _ in itertools.repeat(None, m_iter): # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) # aircraft speed [M_i, CAS_i, TAS_i] = atm.convertSpeed( - v=v_i, speedType=speedType, theta=theta, delta=delta, sigma=sigma + v=v_i, + speedType=speedType, + theta=theta, + delta=delta, + sigma=sigma, ) if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined bankAngle = AC.bankAngle( @@ -6836,7 +7461,9 @@ def accDec( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # compute engine power if rating is None: @@ -6854,11 +7481,17 @@ def accDec( # Check that required thrust/power fits in the available thrust/power envelope, # recompute ROCD if necessary and compute fuel coefficient accordingly - Pmin = 0.1 * AC.P0 # No minimum power model: assume 10% torque + Pmin = ( + 0.1 * AC.P0 + ) # No minimum power model: assume 10% torque if AC.BADAFamily.BADAH: - Pmax = AC.Pav(rating=maxRating, theta=theta, delta=delta) - Pav_i = AC.Pav(rating=maxRating, theta=theta, delta=delta) + Pmax = AC.Pav( + rating=maxRating, theta=theta, delta=delta + ) + Pav_i = AC.Pav( + rating=maxRating, theta=theta, delta=delta + ) elif AC.BADAFamily.BADAE: Pmax = AC.Pav(rating=maxRating, SOC=SOC[-1]) Pav_i = AC.Pav(rating=maxRating, SOC=SOC[-1]) @@ -6883,7 +7516,11 @@ def accDec( elif control.acctarget is not None: ROCD_i = ( conv.m2ft( - (P_i - mass_i * TAS_i * control.acctarget - Preq_i) + ( + P_i + - mass_i * TAS_i * control.acctarget + - Preq_i + ) / (mass_i * const.g * temp_const) ) * 60 @@ -6909,7 +7546,11 @@ def accDec( elif control.acctarget is not None: ROCD_i = ( conv.m2ft( - (P_i - mass_i * TAS_i * control.acctarget - Preq_i) + ( + P_i + - mass_i * TAS_i * control.acctarget + - Preq_i + ) / (mass_i * const.g * temp_const) ) * 60 @@ -6920,12 +7561,20 @@ def accDec( else: # Compute available power if rating == "UNKNOWN": - P_i = 0.1 * AC.P0 # No minimum power model: assume 10% torque - Pav_i = AC.Pav(rating=maxRating, theta=theta, delta=delta) + P_i = ( + 0.1 * AC.P0 + ) # No minimum power model: assume 10% torque + Pav_i = AC.Pav( + rating=maxRating, theta=theta, delta=delta + ) else: if AC.BADAFamily.BADAH: - P_i = AC.Pav(rating=rating, theta=theta, delta=delta) - Pav_i = AC.Pav(rating=rating, theta=theta, delta=delta) + P_i = AC.Pav( + rating=rating, theta=theta, delta=delta + ) + Pav_i = AC.Pav( + rating=rating, theta=theta, delta=delta + ) elif AC.BADAFamily.BADAE: P_i = AC.Pav(rating=rating, SOC=SOC[-1]) Pav_i = AC.Pav(rating=rating, SOC=SOC[-1]) @@ -6948,7 +7597,9 @@ def accDec( Pelc_i = P_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: @@ -6967,15 +7618,21 @@ def accDec( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) @@ -7035,7 +7692,11 @@ def accDec( else: CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) else: THR_i = AC.Thrust( @@ -7047,7 +7708,11 @@ def accDec( ) # [N] CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) # compute excess power @@ -7102,7 +7767,11 @@ def accDec( # recompute ROCD if necessary and compute fuel flow accordingly THR_min = AC.Thrust( - rating="LIDL", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="LIDL", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( flightPhase="Descent", @@ -7113,7 +7782,11 @@ def accDec( adapted=False, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="MCMB", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # MCMB Thrust FUEL_max = AC.ff( flightPhase="Climb", @@ -7132,7 +7805,11 @@ def accDec( FUEL_i = FUEL_max else: FUEL_i = AC.ff( - v=TAS_i, h=H_m, T=THR_i, config=config_i, adapted=True + v=TAS_i, + h=H_m, + T=THR_i, + config=config_i, + adapted=True, ) else: THR_i = AC.Thrust( @@ -7197,7 +7874,10 @@ def accDec( dhdtisu = PC_i / (mass_i * const.g) # [m/s] ROCDisu = dhdtisu * 1 / temp_const # [m/s] ROCD_i = conv.m2ft(ROCDisu) * 60 # [ft/min] - elif control.slopetarget is not None or control.ROCDtarget is not None: + elif ( + control.slopetarget is not None + or control.ROCDtarget is not None + ): dhdtisu = dh_dt_i # [m/s] ROCDisu = dh_dt_i * 1 / temp_const # [m/s] ROCD_i = conv.m2ft(ROCDisu) * 60 # [ft/min] @@ -7300,7 +7980,10 @@ def accDec( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: check.append( - P_i - Preq_i - mass_i * const.g * dhdtisu - mass_i * TAS_i * dVdtisu_i + P_i + - Preq_i + - mass_i * const.g * dhdtisu + - mass_i * TAS_i * dVdtisu_i ) # BADA3 or BADA4 @@ -7318,12 +8001,18 @@ def accDec( [theta, delta, sigma] = atm.atmosphereProperties( h=conv.ft2m(Hp_i), DeltaTemp=DeltaTemp ) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -7418,14 +8107,16 @@ def accDec( else: # calculate the turn - (Lat_i, Lon_i, HDGTrue_i) = turn.destinationPoint_finalBearing( - LAT_init=LAT[-1], - LON_init=LON[-1], - bearingInit=HDGTrue[-1], - TAS=TAS_i, - rateOfTurn=rateOfTurn, - timeOfTurn=step_time, - directionOfTurn=directionOfTurn, + (Lat_i, Lon_i, HDGTrue_i) = ( + turn.destinationPoint_finalBearing( + LAT_init=LAT[-1], + LON_init=LON[-1], + bearingInit=HDGTrue[-1], + TAS=TAS_i, + rateOfTurn=rateOfTurn, + timeOfTurn=step_time, + directionOfTurn=directionOfTurn, + ) ) if magneticDeclinationGrid is not None: @@ -7463,14 +8154,16 @@ def accDec( else: # calculate the turn - (Lat_i, Lon_i, HDGTrue_i) = turn.destinationPoint_finalBearing( - LAT_init=LAT[-1], - LON_init=LON[-1], - bearingInit=HDGTrue[-1], - TAS=TAS_i, - rateOfTurn=rateOfTurn, - timeOfTurn=step_time, - directionOfTurn=directionOfTurn, + (Lat_i, Lon_i, HDGTrue_i) = ( + turn.destinationPoint_finalBearing( + LAT_init=LAT[-1], + LON_init=LON[-1], + bearingInit=HDGTrue[-1], + TAS=TAS_i, + rateOfTurn=rateOfTurn, + timeOfTurn=step_time, + directionOfTurn=directionOfTurn, + ) ) if magneticDeclinationGrid is not None: @@ -7571,79 +8264,95 @@ def accDec_time( magneticDeclinationGrid=None, **kwargs, ): - """This function computes time and fuel required by an aircraft to perform an acceleration/deceleration from v_init for set amount of time in climb cruise or descent + """ + Calculates the time, fuel consumption, and other key flight parameters required for an aircraft + to perform an acceleration or deceleration from an initial speed (v_init) over a set period of time + during the climb, cruise, or descent phases of flight. + + The flight parameters are calculated using different models for the BADA (Base of Aircraft Data) families (BADA3, BADA4, BADAH, BADAE). + The function can also accommodate different control laws, vertical evolution phases, wind conditions, and complex flight dynamics like turns. .. note:: - The control law used during the segment depends on the targets provided in input parameter 'control': - - ROCD/slope+ESF: law is ROCD/slope+ESF - - ROCD/slope+acc: law is ROCD/slope+acc - - ROCD/slope only: law is rating+ROCD/slope - - ESF only: law is rating+ESF - - acc only: law is rating+acc - - Neither: law is rating+default ESF - - :param AC: aircraft {BADA3/4/H/E} - :param speedType: what kind of speed is followed {M, CAS, TAS}. - :param length: length of a segment to fly [s] - :param step_length: length of a step of a segment - [s] - :param v_init: initial speed to follow - [kt] CAS/TAS speed to follow or [-] MACH speed to follow. - :param speedEvol: speed evolution {acc, dec} - :param phase: vertical evolution {Climb, Descent, Cruise} - :param control: structure containing a combination of the following targets: - :param ROCDtarget: Rate of climb/descent to be followed [ft/min]. - :param slopetarget: slope (flight path angle) to be followed [deg]. - :param acctarget: acceleration to be followed [m/s^2]. - :param ESFtarget: Energy Share Factor to be followed [-]. - :param maxRating: rating to be used as a limit on the maximum thrust/power [-]. - :param Hp_init: initial pressure altitude [ft]. - :param m_init: initial aircraft mass [kg]. - :param DeltaTemp: deviation with respect to ISA [K]. - :param wS: longitudinal wind speed (TAS) [kt]. - :param turnMetrics: Metrics for turn performance {"rateOfTurn":0.0,"bankAngle":0.0,"directionOfTurn":None} {[deg/s],[deg],[LEFT/RIGHT]} - :param SOC_init: initial state of charge [%]. - :param config: aircraft default aerodynamic configuration {TO,IC,CR,AP,LD}. - :param speedBrakes: deployed or not speedbrakes including value to be added to the drag coeffcient {deployed:False,value:0.03} {deployed:[True/False],value:[-]}. - :param Lat: Geographical Latitude [deg] - :param Lon: Geographical Longitude [deg] - :param initialHeading: aircraft magnetic heading, true heading and definition of constant heading(ORTHODROME=False, LOXODROME=True) {[deg],[deg],-} - :param magneticDeclinationGrid: geographical grid of a magnetic declination on Earth [deg] - :param mass_const: kind of mass canculation {mass_integrated=False, mass_constant=True}. - :param m_iter: number of iterations for integration loop [-] - :param reducedPower: reduction of Power during the climb {True/False} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type speedType: string. - :type length: float. - :type step_length: float. - :type v_init: float. - :type speedEvol: string. - :type phase: string. - :type control: structure. - :type ROCDtarget: float. - :type slopetarget: float. - :type acctarget: float. - :type ESFtarget: float. - :type maxRating: float. - :type Hp_init: float. - :type m_init: float. - :type DeltaTemp: float. - :type wS: float. - :type turnMetrics: {float,float,string}. - :type SOC_init: float. - :type config: string. - :type speedBrakes: dict{boolean,float}. - :type Lat: float. - :type Lon: float. - :type initialHeading: {float,float,boolean}. - :type magneticDeclinationGrid: magneticDeclinationGrid. - :type mass_const: boolean. - :type m_iter: integer. - :type reducedPower: boolean. - :returns: - BADA3: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, time, dist, slope, mass, config, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, s, NM, deg, kg, -, -,deg,deg,deg,deg,deg,deg/s] - BADA4: [Hp, TAS, CAS, M, GS, acc, ROCD, ESF, FUEL, FUELCONSUMED, THR, P[Pmec, Pbat, Pelc Ibat, Vbat, Vgbat, SOCr, SOC], time, dist, slope, mass, config, HLid, LG, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, N, [W,W,W,A,V,V,%/h,%], s, NM, deg, kg, -, -, -, -,deg,deg,deg,deg,deg,deg/s] - BADAH: [Hp, TAS, CAS, M, GS, ROCD, ESF, FUEL, FUELCONSUMED, Peng, Preq, Pav, time, dist, slope, mass, comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [ft, kt, kt, -, kt, m/s^2, ft/min, kg/s, kg, W, W, W, s, NM, deg, kg, -,deg,deg,deg,deg,deg,deg/s] - BADAE: [time, dist, Hp, TAS, CAS, M, GS, acc, ROCD, ESF, slope, mass, P[Pmec, Pelc, Pbat, SOCr, SOC, Ibat, Vbat, Vgbat] comment, LAT, LON, HDGMagnetic, HDGTrue, bank angle, ROT] [s, NM, ft, kt, kt, -, kt, m/s^2, ft/min, deg, kg, [W,W,W,%/h,%,A,V,V], -,deg,deg,deg,deg,deg,deg/s] - :rtype: dict[list[float]}. + The control law used during the segment depends on the targets provided in the input parameter 'control': + - ROCD/slope+ESF: Law based on ROCD/slope + ESF + - ROCD/slope+acc: Law based on ROCD/slope + acceleration + - ROCD/slope only: Law based on rating + ROCD/slope + - ESF only: Law based on rating + ESF + - acc only: Law based on rating + acceleration + - Neither: Law is rating + default ESF + + :param AC: Aircraft object {BADA3/4/H/E}. + :param length: Total duration of the flight segment [s]. + :param speedType: Type of speed being followed {M, CAS, TAS}. + :param v_init: Initial speed [kt] (CAS/TAS) or [-] MACH. + :param speedEvol: Evolution of speed {acc, dec} (acceleration or deceleration). + :param phase: Vertical evolution phase {Climb, Descent, Cruise}. + :param control: A dictionary containing the following targets: + - ROCDtarget: Rate of climb/descent to be followed [ft/min]. + - slopetarget: Slope (flight path angle) to be followed [deg]. + - acctarget: Acceleration to be followed [m/s^2]. + - ESFtarget: Energy Share Factor to be followed [-]. + :param Hp_init: Initial pressure altitude [ft]. + :param m_init: Initial aircraft mass [kg]. + :param DeltaTemp: Deviation from the standard ISA temperature [K]. + :param wS: Wind speed component along the longitudinal axis (affects ground speed) [kt]. Default is 0.0. + :param turnMetrics: A dictionary defining turn parameters: + - rateOfTurn [deg/s] + - bankAngle [deg] + - directionOfTurn {LEFT/RIGHT}. Default is straight flight. + :param Lat: Initial latitude [deg]. Default is None. + :param Lon: Initial longitude [deg]. Default is None. + :param initialHeading: A dictionary defining the initial heading (magnetic or true) and whether to fly a constant heading: + - magnetic: Magnetic heading [deg]. + - true: True heading [deg]. + - constantHeading: Whether to maintain a constant heading. Default is None. + :param reducedPower: Boolean specifying if reduced power is applied during the climb. Default is None. + :param magneticDeclinationGrid: Optional grid of magnetic declination used to correct magnetic heading. Default is None. + :param kwargs: Additional optional parameters: + - step_length: Length of each time step in the calculation [s]. Default is 1 second. + - SOC_init: Initial battery state of charge for electric aircraft (BADAE) [%]. Default is 100. + - config: Default aerodynamic configuration (TO, IC, CR, AP, LD). Default is None. + - mass_const: Boolean indicating whether mass remains constant during the flight segment. Default is False. + - m_iter: Number of iterations for the mass integration loop. Default is 10 for BADA3/4/H, 5 for BADAE. + :returns: A pandas DataFrame containing flight trajectory data with the following columns: + - Hp: Pressure altitude [ft] + - TAS: True Air Speed [kt] + - CAS: Calibrated Air Speed [kt] + - GS: Ground Speed [kt] + - M: Mach number [-] + - acc: Acceleration rate [m/s^2] + - ROCD: Rate of climb/descent [ft/min] + - ESF: Energy Share Factor [-] + - FUEL: Fuel flow [kg/s] + - FUELCONSUMED: Total fuel consumed [kg] + - THR: Thrust force [N] + - DRAG: Drag force [N] + - time: Elapsed time [s] + - dist: Distance flown [NM] + - slope: Flight trajectory slope (angle) [deg] + - mass: Aircraft mass [kg] + - config: Aerodynamic configuration + - LAT: Latitude [deg] + - LON: Longitude [deg] + - HDGTrue: True heading [deg] + - HDGMagnetic: Magnetic heading [deg] + - BankAngle: Bank angle during the turn [deg] + - ROT: Rate of turn [deg/s] + - Comment: Comments describing the flight segment + - For BADAH: + - Preq: Required power for level flight [W] + - Peng: Generated power [W] + - Pav: Available power [W] + - For BADAE (electric aircraft): + - Pmec: Mechanical power [W] + - Pelc: Electrical power [W] + - Pbat: Battery power [W] + - SOCr: Rate of battery state of charge depletion [%/h] + - SOC: Battery state of charge [%] + - Ibat: Battery current [A] + - Vbat: Battery voltage [V] + - Vgbat: Ground battery voltage [V] + :rtype: pandas.DataFrame """ rateOfTurn = turnMetrics["rateOfTurn"] @@ -7694,7 +8403,9 @@ def accDec_time( # speed brakes application if AC.BADAFamily.BADA3 or AC.BADAFamily.BADA4: - speedBrakes = kwargs.get("speedBrakes", {"deployed": False, "value": 0.03}) + speedBrakes = kwargs.get( + "speedBrakes", {"deployed": False, "value": 0.03} + ) # step size in [s] step_length = kwargs.get("step_length", 1) @@ -7702,7 +8413,9 @@ def accDec_time( # number of iteration of mass/altitude loop # BADAE if AC.BADAFamily.BADAE: - m_iter = kwargs.get("m_iter", 5) # number of iterations for integration loop[-] + m_iter = kwargs.get( + "m_iter", 5 + ) # number of iterations for integration loop[-] # BADA3 or BADA4 or BADAH else: m_iter = kwargs.get( @@ -7852,9 +8565,9 @@ def accDec_time( maxRating = checkArgument("maxRating", **kwargs) # Determine engine rating - if (control.ROCDtarget is not None or control.slopetarget is not None) and ( - control.ESFtarget is not None or control.acctarget is not None - ): + if ( + control.ROCDtarget is not None or control.slopetarget is not None + ) and (control.ESFtarget is not None or control.acctarget is not None): rating = None else: if phase == "Climb" or (phase == "Cruise" and speedEvol == "acc"): @@ -7972,8 +8685,12 @@ def accDec_time( for _ in itertools.repeat(None, m_iter): # atmosphere properties H_m = conv.ft2m(Hp_i) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) step_time = length_loop - time[-1] @@ -7985,7 +8702,9 @@ def accDec_time( if turnFlight: if turnMetrics["bankAngle"] != 0.0: # bankAngle is defined - rateOfTurn = AC.rateOfTurn_bankAngle(TAS=TAS_i, bankAngle=bankAngle) + rateOfTurn = AC.rateOfTurn_bankAngle( + TAS=TAS_i, bankAngle=bankAngle + ) else: # rateOfTurn is defined bankAngle = AC.bankAngle( @@ -8012,7 +8731,9 @@ def accDec_time( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: # compute Power required - Preq_i = AC.Preq(sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle) + Preq_i = AC.Preq( + sigma=sigma, tas=TAS_i, mass=mass_i, phi=bankAngle + ) # compute engine power if rating is None: @@ -8030,11 +8751,17 @@ def accDec_time( # Check that required thrust/power fits in the available thrust/power envelope, # recompute ROCD if necessary and compute fuel coefficient accordingly - Pmin = 0.1 * AC.P0 # No minimum power model: assume 10% torque + Pmin = ( + 0.1 * AC.P0 + ) # No minimum power model: assume 10% torque if AC.BADAFamily.BADAH: - Pmax = AC.Pav(rating=maxRating, theta=theta, delta=delta) - Pav_i = AC.Pav(rating=maxRating, theta=theta, delta=delta) + Pmax = AC.Pav( + rating=maxRating, theta=theta, delta=delta + ) + Pav_i = AC.Pav( + rating=maxRating, theta=theta, delta=delta + ) elif AC.BADAFamily.BADAE: Pmax = AC.Pav(rating=maxRating, SOC=SOC[-1]) Pav_i = AC.Pav(rating=maxRating, SOC=SOC[-1]) @@ -8059,7 +8786,11 @@ def accDec_time( elif control.acctarget is not None: ROCD_i = ( conv.m2ft( - (P_i - mass_i * TAS_i * control.acctarget - Preq_i) + ( + P_i + - mass_i * TAS_i * control.acctarget + - Preq_i + ) / (mass_i * const.g * temp_const) ) * 60 @@ -8084,7 +8815,11 @@ def accDec_time( elif control.acctarget is not None: ROCD_i = ( conv.m2ft( - (P_i - mass_i * TAS_i * control.acctarget - Preq_i) + ( + P_i + - mass_i * TAS_i * control.acctarget + - Preq_i + ) / (mass_i * const.g * temp_const) ) * 60 @@ -8095,12 +8830,20 @@ def accDec_time( else: # Compute available power if rating == "UNKNOWN": - P_i = 0.1 * AC.P0 # No minimum power model: assume 10% torque - Pav_i = AC.Pav(rating=maxRating, theta=theta, delta=delta) + P_i = ( + 0.1 * AC.P0 + ) # No minimum power model: assume 10% torque + Pav_i = AC.Pav( + rating=maxRating, theta=theta, delta=delta + ) else: if AC.BADAFamily.BADAH: - P_i = AC.Pav(rating=rating, theta=theta, delta=delta) - Pav_i = AC.Pav(rating=rating, theta=theta, delta=delta) + P_i = AC.Pav( + rating=rating, theta=theta, delta=delta + ) + Pav_i = AC.Pav( + rating=rating, theta=theta, delta=delta + ) elif AC.BADAFamily.BADAE: P_i = AC.Pav(rating=rating, SOC=SOC[-1]) Pav_i = AC.Pav(rating=rating, SOC=SOC[-1]) @@ -8123,7 +8866,9 @@ def accDec_time( Pelc_i = P_i / AC.eta Ibat_i = AC.Ibat(P=Pelc_i, SOC=SOC[-1]) Vbat_i = AC.Vbat(I=Ibat_i, SOC=SOC[-1]) - Vgbat_i = AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + Vgbat_i = ( + AC.Vocbat(SOC=SOC[-1]) - AC.R0bat(SOC=SOC[-1]) * Ibat_i + ) # BADA4 elif AC.BADAFamily.BADA4: @@ -8142,15 +8887,21 @@ def accDec_time( # ensure continuity of configuration change within the segment if config: config_i = AC.flightEnvelope.checkConfigurationContinuity( - phase=phase, previousConfig=config[-1], currentConfig=config_i + phase=phase, + previousConfig=config[-1], + currentConfig=config_i, ) - [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig(config=config_i) + [HLid_i, LG_i] = AC.flightEnvelope.getAeroConfig( + config=config_i + ) # compute lift coefficient CL = AC.CL(M=M_i, delta=delta, mass=mass_i, nz=nz) # compute drag coefficient - CD = AC.CD(M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes) + CD = AC.CD( + M=M_i, CL=CL, HLid=HLid_i, LG=LG_i, speedBrakes=speedBrakes + ) # compute drag force Drag = AC.D(M=M_i, delta=delta, CD=CD) @@ -8210,7 +8961,11 @@ def accDec_time( else: CT = AC.CT(Thrust=THR_i, delta=delta) FUEL_i = AC.ff( - CT=CT, delta=delta, theta=theta, M=M_i, DeltaTemp=DeltaTemp + CT=CT, + delta=delta, + theta=theta, + M=M_i, + DeltaTemp=DeltaTemp, ) else: THR_i = AC.Thrust( @@ -8284,7 +9039,11 @@ def accDec_time( # recompute ROCD if necessary and compute fuel flow accordingly THR_min = AC.Thrust( - rating="LIDL", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="LIDL", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # IDLE Thrust FUEL_min = AC.ff( flightPhase="Descent", @@ -8295,7 +9054,11 @@ def accDec_time( adapted=False, ) # IDLE Fuel Flow THR_max = AC.Thrust( - rating="MCMB", v=TAS_i, h=H_m, config="CR", DeltaTemp=DeltaTemp + rating="MCMB", + v=TAS_i, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) # MCMB Thrust FUEL_max = AC.ff( flightPhase="Climb", @@ -8314,7 +9077,11 @@ def accDec_time( FUEL_i = FUEL_max else: FUEL_i = AC.ff( - v=TAS_i, h=H_m, T=THR_i, config=config_i, adapted=True + v=TAS_i, + h=H_m, + T=THR_i, + config=config_i, + adapted=True, ) else: THR_i = AC.Thrust( @@ -8377,7 +9144,10 @@ def accDec_time( dhdtisu = PC_i / (mass_i * const.g) # [m/s] ROCDisu = dhdtisu * 1 / temp_const # [m/s] ROCD_i = conv.m2ft(ROCDisu) * 60 # [ft/min] - elif control.slopetarget is not None or control.ROCDtarget is not None: + elif ( + control.slopetarget is not None + or control.ROCDtarget is not None + ): dhdtisu = dh_dt_i # [m/s] ROCDisu = dh_dt_i * 1 / temp_const # [m/s] ROCD_i = conv.m2ft(ROCDisu) * 60 # [ft/min] @@ -8478,7 +9248,10 @@ def accDec_time( # BADAH or BADAE if AC.BADAFamily.BADAH or AC.BADAFamily.BADAE: check.append( - P_i - Preq_i - mass_i * const.g * dhdtisu - mass_i * TAS_i * dVdtisu_i + P_i + - Preq_i + - mass_i * const.g * dhdtisu + - mass_i * TAS_i * dVdtisu_i ) # BADA3 or BADA4 @@ -8496,12 +9269,18 @@ def accDec_time( [theta, delta, sigma] = atm.atmosphereProperties( h=conv.ft2m(Hp_i), DeltaTemp=DeltaTemp ) - temp_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + temp_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) if AC.BADAFamily.BADAE: - gamma_i = degrees(atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + atan(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) else: # using SIN assumes the TAS to be in the direction of the aircraft axis, not ground plane. Which means, this should be mathematically the correct equation for all the aircraft - gamma_i = degrees(asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i)) + gamma_i = degrees( + asin(conv.ft2m(ROCD_i) * temp_const / 60 / TAS_i) + ) # ground speed can be calcualted as TAS projected on the ground minus wind GS_i = cos(radians(gamma_i)) * TAS_i - wS @@ -8512,7 +9291,9 @@ def accDec_time( ROT.append(rateOfTurn) # integrated data - if length_loop != 0: # exclude first point: initial t/d/m already known + if ( + length_loop != 0 + ): # exclude first point: initial t/d/m already known if AC.BADAFamily.BADAE: SOC.append(SOC_i) @@ -8600,14 +9381,16 @@ def accDec_time( else: # calculate the turn - (Lat_i, Lon_i, HDGTrue_i) = turn.destinationPoint_finalBearing( - LAT_init=LAT[-1], - LON_init=LON[-1], - bearingInit=HDGTrue[-1], - TAS=TAS_i, - rateOfTurn=rateOfTurn, - timeOfTurn=step_time, - directionOfTurn=directionOfTurn, + (Lat_i, Lon_i, HDGTrue_i) = ( + turn.destinationPoint_finalBearing( + LAT_init=LAT[-1], + LON_init=LON[-1], + bearingInit=HDGTrue[-1], + TAS=TAS_i, + rateOfTurn=rateOfTurn, + timeOfTurn=step_time, + directionOfTurn=directionOfTurn, + ) ) if magneticDeclinationGrid is not None: @@ -8645,14 +9428,16 @@ def accDec_time( else: # calculate the turn - (Lat_i, Lon_i, HDGTrue_i) = turn.destinationPoint_finalBearing( - LAT_init=LAT[-1], - LON_init=LON[-1], - bearingInit=HDGTrue[-1], - TAS=TAS_i, - rateOfTurn=rateOfTurn, - timeOfTurn=step_time, - directionOfTurn=directionOfTurn, + (Lat_i, Lon_i, HDGTrue_i) = ( + turn.destinationPoint_finalBearing( + LAT_init=LAT[-1], + LON_init=LON[-1], + bearingInit=HDGTrue[-1], + TAS=TAS_i, + rateOfTurn=rateOfTurn, + timeOfTurn=step_time, + directionOfTurn=directionOfTurn, + ) ) if magneticDeclinationGrid is not None: diff --git a/src/pyBADA/aircraft.py b/src/pyBADA/aircraft.py index 0816325..d58ae87 100644 --- a/src/pyBADA/aircraft.py +++ b/src/pyBADA/aircraft.py @@ -48,28 +48,32 @@ def __init__(self): @staticmethod def loadFactor(fi): - """This function computes the load factor from bank angle + """ + Computes the load factor from a given bank angle. - :param fi: bank angle [deg]. - :type fi: float. - :returns: load factor [-]. - :rtype: float. + The load factor is calculated based on the cosine of the bank angle, + which is expressed in degrees. A small rounding operation is applied + to avoid precision issues with small decimal places. + :param fi: Bank angle in degrees. + :type fi: float + :returns: The load factor (dimensionless). + :rtype: float """ - # rounding implemented to try to minimize errors on small decimal places, like 1.9999999999999 instead of 2.0 + return 1 / round(cos(radians(fi)), 10) @staticmethod def bankAngle(rateOfTurn, v): - """This function computes bank angle based on TAS and rate of turn + """ + Computes the bank angle based on true airspeed (TAS) and rate of turn. - :param v: true airspeed TAS [m s^-1]. - :param rateOfTurn: rateOfTurn [deg/s]. - :type v: float. - :type rateOfTurn: float. - :returns: bank angle [deg]. + :param v: True airspeed (TAS) in meters per second (m/s). + :param rateOfTurn: Rate of turn in degrees per second (deg/s). + :type v: float + :type rateOfTurn: float + :returns: Bank angle in degrees. :rtype: float - """ ROT = conv.deg2rad(rateOfTurn) @@ -79,15 +83,15 @@ def bankAngle(rateOfTurn, v): @staticmethod def rateOfTurn_bankAngle(TAS, bankAngle): - """This function computes the rate of turn - - :param TAS: true airspeed TAS [m s^-1]. - :param bankAngle: bank angle [deg]. - :type TAS: float. - :type bankAngle: float. - :returns: rate of turn [deg s^-1]. - :rtype: float. + """ + Computes the rate of turn based on true airspeed (TAS) and bank angle. + :param TAS: True airspeed (TAS) in meters per second (m/s). + :param bankAngle: Bank angle in degrees. + :type TAS: float + :type bankAngle: float + :returns: Rate of turn in degrees per second (deg/s). + :rtype: float """ ROT = tan(radians(bankAngle)) * const.g / TAS @@ -96,79 +100,83 @@ def rateOfTurn_bankAngle(TAS, bankAngle): @staticmethod def rateOfTurn(v, nz=1.0): - """This function computes the rate of turn - - :param v: true airspeed TAS [m s^-1]. - :param nz: load factor [-]. - :type v: float. - :type nz: float. - :returns: rate of turn [rad s^-1]. - :rtype: float. + """ + Computes the rate of turn based on true airspeed (TAS) and load factor. + :param v: True airspeed (TAS) in meters per second (m/s). + :param nz: Load factor (default is 1.0), dimensionless. + :type v: float + :type nz: float + :returns: Rate of turn in degrees per second (deg/s). + :rtype: float """ + return degrees((const.g / v) * sqrt(nz * nz - 1)) @staticmethod def turnRadius(v, nz=1.0): - """This function computes the turn radius from load factor and speed - - :param v: true airspeed TAS [m s^-1]. - :param nz: load factor [-]. - :type v: float. - :type nz: float. - :returns: turn radius [m]. - :rtype: float. + """ + Computes the turn radius based on true airspeed (TAS) and load factor. + :param v: True airspeed (TAS) in meters per second (m/s). + :param nz: Load factor (default is 1.0), dimensionless. + :type v: float + :type nz: float + :returns: Turn radius in meters. + :rtype: float """ + return (v * v / const.g) * (1 / sqrt(nz * nz - 1)) @staticmethod def turnRadius_bankAngle(v, ba): - """This function computes the turn radius from bank angle and speed - - :param v: true airspeed [m s^-1]. - :param ba: bank angle [deg]. - :type v: float. - :type ba: float. - :returns: turn radius [m]. - :rtype: float. + """ + Computes the turn radius based on true airspeed (TAS) and bank angle. + :param v: True airspeed (TAS) in meters per second (m/s). + :param ba: Bank angle in degrees. + :type v: float + :type ba: float + :returns: Turn radius in meters. + :rtype: float """ return (v * v / const.g) * (1 / tan(conv.deg2rad(ba))) @staticmethod def GS(tas, gamma, Ws): - """This function computes the ground speed - - :param tas: true airspeed [m s^-1]. - :param gamma: flight path angle [deg]. - :param Ws: longitudinal wind speed [m s^-1]. - :type tas: float. - :type gamma: float. - :type Ws: float. - :returns: ground speed [m s^-1]. - :rtype: float. - """ + Computes the ground speed based on true airspeed (TAS), flight path angle, and wind speed. + + :param tas: True airspeed (TAS) in meters per second (m/s). + :param gamma: Flight path angle in degrees. + :param Ws: Longitudinal wind speed in meters per second (m/s). + :type tas: float + :type gamma: float + :type Ws: float + :returns: Ground speed in meters per second (m/s). + :rtype: float + """ + return tas * cos(radians(gamma)) + Ws @staticmethod def esf(**kwargs): - """This function computes the energy share factor - - :param h: altitude [m] - :param DeltaTemp: deviation with respect to ISA [K] - :param flightEvolution: character of the flight evolution [constM/constCAS/acc/dec][-] - :param phase: phase of flight [cl/des][-] - :param v: constant speed [M][-] - :type h: float. - :type DeltaTemp: float. - :type flightEvolution: str. - :type phase: str. - :type v: float. - :returns: energy share factor [-]. - :rtype: float. + """ + Computes the energy share factor based on flight conditions. + + :param h: Altitude in meters. + :param DeltaTemp: Temperature deviation with respect to ISA in Kelvin. + :param flightEvolution: Type of flight evolution [constM/constCAS/acc/dec]. + :param phase: Phase of flight [cl/des]. + :param v: Constant speed (Mach number). + :type h: float + :type DeltaTemp: float + :type flightEvolution: str + :type phase: str + :type v: float + :returns: Energy share factor (dimensionless). + :rtype: float """ flightEvolution = checkArgument("flightEvolution", **kwargs) @@ -202,7 +210,14 @@ def esf(**kwargs): temp = atm.theta(h, DeltaTemp) * const.temp_0 ESF = 1 / ( 1 - + (const.Agamma * const.R * (-const.temp_h) * M * M / (2 * const.g)) + + ( + const.Agamma + * const.R + * (-const.temp_h) + * M + * M + / (2 * const.g) + ) * ((temp - DeltaTemp) / temp) ) @@ -213,9 +228,16 @@ def esf(**kwargs): temp = atm.theta(h, DeltaTemp) * const.temp_0 A = ( - const.Agamma * const.R * (-const.temp_h) * M * M / (2 * const.g) + const.Agamma + * const.R + * (-const.temp_h) + * M + * M + / (2 * const.g) ) * ((temp - DeltaTemp) / temp) - B = pow(1 + (const.Agamma - 1) * M * M / 2, -1 / (const.Agamma - 1)) + B = pow( + 1 + (const.Agamma - 1) * M * M / 2, -1 / (const.Agamma - 1) + ) C = pow(1 + (const.Agamma - 1) * M * M / 2, 1 / const.Amu) - 1 ESF = 1 / (1 + A + B * C) @@ -225,8 +247,16 @@ def esf(**kwargs): ESF = 1 / ( 1 - + (pow(1 + (const.Agamma - 1) * M * M / 2, -1 / (const.Agamma - 1))) - * (pow(1 + (const.Agamma - 1) * M * M / 2, 1 / const.Amu) - 1) + + ( + pow( + 1 + (const.Agamma - 1) * M * M / 2, + -1 / (const.Agamma - 1), + ) + ) + * ( + pow(1 + (const.Agamma - 1) * M * M / 2, 1 / const.Amu) + - 1 + ) ) # contant TAS @@ -255,41 +285,47 @@ def __init__(self): @staticmethod def loadFactor(fi): - """This function computes the load factor from bank angle + """ + Computes the load factor from a given bank angle. - :param fi: bank angle [deg]. - :type fi: float. - :returns: load factor [-]. - :rtype: float. + The load factor is calculated based on the cosine of the bank angle, + which is expressed in degrees. A small rounding operation is applied + to avoid precision issues with small decimal places. + :param fi: Bank angle in degrees. + :type fi: float + :returns: The load factor (dimensionless). + :rtype: float """ + return 1 / round(cos(radians(fi)), 10) @staticmethod def rateOfTurn(v, nz=1.0): - """This function computes the rate of turn - - :param v: true airspeed TAS [m s^-1]. - :param nz: load factor [-]. - :type v: float. - :type nz: float. - :returns: rate of turn [rad s^-1]. - :rtype: float. + """ + Computes the rate of turn based on true airspeed (TAS) and load factor. + :param v: True airspeed (TAS) in meters per second (m/s). + :param nz: Load factor (default is 1.0), dimensionless. + :type v: float + :type nz: float + :returns: Rate of turn in degrees per second (deg/s). + :rtype: float """ + return degrees((const.g / v) * sqrt(nz * nz - 1)) @staticmethod def rateOfTurn_bankAngle(TAS, bankAngle): - """This function computes the rate of turn - - :param TAS: true airspeed TAS [m s^-1]. - :param bankAngle: bank angle [deg]. - :type TAS: float. - :type bankAngle: float. - :returns: rate of turn [deg s^-1]. - :rtype: float. + """ + Computes the rate of turn based on true airspeed (TAS) and bank angle. + :param TAS: True airspeed (TAS) in meters per second (m/s). + :param bankAngle: Bank angle in degrees. + :type TAS: float + :type bankAngle: float + :returns: Rate of turn in degrees per second (deg/s). + :rtype: float """ ROT = tan(radians(bankAngle)) * const.g / TAS @@ -298,49 +334,51 @@ def rateOfTurn_bankAngle(TAS, bankAngle): @staticmethod def turnRadius(v, nz=1.0): - """This function computes the turn radius from load factor and speed - - :param v: true airspeed TAS [m s^-1]. - :param nz: load factor [-]. - :type v: float. - :type nz: float. - :returns: turn radius [m]. - :rtype: float. + """ + Computes the turn radius based on true airspeed (TAS) and load factor. + :param v: True airspeed (TAS) in meters per second (m/s). + :param nz: Load factor (default is 1.0), dimensionless. + :type v: float + :type nz: float + :returns: Turn radius in meters. + :rtype: float """ + return (v * v / const.g) * (1 / sqrt(nz * nz - 1)) @staticmethod def turnRadius_bankAngle(v, ba): - """This function computes the turn radius from bank angle and speed - - :param v: true airspeed [m s^-1]. - :param ba: bank angle [deg]. - :type v: float. - :type ba: float. - :returns: turn radius [m]. - :rtype: float. + """ + Computes the turn radius based on true airspeed (TAS) and bank angle. + :param v: True airspeed (TAS) in meters per second (m/s). + :param ba: Bank angle in degrees. + :type v: float + :type ba: float + :returns: Turn radius in meters. + :rtype: float """ return (v * v / const.g) * (1 / tan(conv.deg2rad(ba))) @staticmethod def esf(**kwargs): - """This function computes the energy share factor - - :param h: altitude [m] - :param DeltaTemp: deviation with respect to ISA [K] - :param flightEvolution: character of the flight evolution {constTAS,constCAS,acc,dec}[-] - :param phase: phase of flight {Climb,Descent}[-] - :param M: constant speed M [-] - :type h: float. - :type DeltaTemp: float. - :type flightEvolution: str. - :type phase: str. - :type M: float. - :returns: energy share factor ESF [-]. - :rtype: float. + """ + Computes the energy share factor based on flight conditions. + + :param h: Altitude in meters. + :param DeltaTemp: Temperature deviation with respect to ISA in Kelvin. + :param flightEvolution: Type of flight evolution [constTAS/constCAS/acc/dec]. + :param phase: Phase of flight [Climb/Descent]. + :param v: Constant speed (Mach number). + :type h: float + :type DeltaTemp: float + :type flightEvolution: str + :type phase: str + :type v: float + :returns: Energy share factor (dimensionless). + :rtype: float """ flightEvolution = checkArgument("flightEvolution", **kwargs) @@ -370,9 +408,16 @@ def esf(**kwargs): temp = theta * const.temp_0 A = ( - const.Agamma * const.R * (-const.temp_h) * M * M / (2 * const.g) + const.Agamma + * const.R + * (-const.temp_h) + * M + * M + / (2 * const.g) ) * ((temp - DeltaTemp) / temp) - B = pow(1 + (const.Agamma - 1) * M * M / 2, -1 / (const.Agamma - 1)) + B = pow( + 1 + (const.Agamma - 1) * M * M / 2, -1 / (const.Agamma - 1) + ) C = pow(1 + (const.Agamma - 1) * M * M / 2, 1 / const.Amu) - 1 ESF = 1 / (1 + A + B * C) @@ -387,15 +432,15 @@ def esf(**kwargs): @staticmethod def bankAngle(rateOfTurn, v): - """This function computes bank angle based on TAS and rate of turn + """ + Computes the bank angle based on true airspeed (TAS) and rate of turn. - :param v: true airspeed TAS [m s^-1]. - :param rateOfTurn: rateOfTurn [deg/s]. - :type v: float. - :type rateOfTurn: float. - :returns: bank angle [deg]. + :param v: True airspeed (TAS) in meters per second (m/s). + :param rateOfTurn: Rate of turn in degrees per second (deg/s). + :type v: float + :type rateOfTurn: float + :returns: Bank angle in degrees. :rtype: float - """ ROT = conv.deg2rad(rateOfTurn) diff --git a/src/pyBADA/atmosphere.py b/src/pyBADA/atmosphere.py index ff213ef..8768fdc 100644 --- a/src/pyBADA/atmosphere.py +++ b/src/pyBADA/atmosphere.py @@ -24,15 +24,20 @@ def proper_round(num, dec=0): def theta(h, DeltaTemp): - """This function returns normalised temperature according to the ISA model + """ + Calculates the normalized temperature according to the International Standard Atmosphere (ISA) model. - :param h: altitude [m] - :param DeltaTemp: deviation with respect to ISA [K] + :param h: Altitude in meters (m). + :param DeltaTemp: Deviation from ISA temperature in Kelvin (K). :type h: float :type DeltaTemp: float - :returns: normalised temperature [-] + :returns: Normalized temperature [-]. + The function accounts for whether the altitude is below or above the tropopause (11,000 m). + Below the tropopause, it applies the temperature lapse rate. + Above the tropopause, a constant temperature is assumed. """ + if h < const.h_11: theta = 1 - const.temp_h * h / const.temp_0 + DeltaTemp / const.temp_0 @@ -43,15 +48,18 @@ def theta(h, DeltaTemp): def delta(h, DeltaTemp): - """This function returns normalised pressure according to the ISA model + """ + Calculates the normalized pressure according to the ISA model. - :param h: altitude [m] - :param DeltaTemp: deviation with respect to ISA [K] + :param h: Altitude in meters (m). + :param DeltaTemp: Deviation from ISA temperature in Kelvin (K). :type h: float :type DeltaTemp: float - :returns: normalised pressure [-] + :returns: Normalized pressure [-]. + The function uses the barometric equation for pressure changes below and above the tropopause. """ + p = pow( (theta(h, DeltaTemp) - DeltaTemp / const.temp_0), const.g / (const.temp_h * const.R), @@ -66,28 +74,33 @@ def delta(h, DeltaTemp): def sigma(theta, delta): - """This function returns normalised air denstity according to the ISA model + """ + Calculates the normalized air density according to the ISA model. - :param theta: normalised temperature according to the ISA model [-] - :param delta: normalised pressure according to the ISA model [-] + :param theta: Normalized temperature [-]. + :param delta: Normalized pressure [-]. :type theta: float :type delta: float - :returns: normalised air density [-] + :returns: Normalized air density [-]. + The function uses the ideal gas law to relate pressure, temperature, and density. """ return proper_round( - ((delta * const.p_0) / (theta * const.temp_0 * const.R)) / const.rho_0, 10 + ((delta * const.p_0) / (theta * const.temp_0 * const.R)) / const.rho_0, + 10, ) def aSound(theta): - """This function calculates the speed of sound + """ + Calculates the speed of sound based on the normalized air temperature. - :param theta: normalised air temperature [-] + :param theta: Normalized temperature [-]. :type theta: float - :returns: speed of sound [m s^-1] + :returns: Speed of sound in meters per second (m/s). + The speed of sound depends on air temperature and is calculated using the specific heat ratio and the gas constant. """ a = sqrt(const.Agamma * const.R * theta * const.temp_0) @@ -95,15 +108,16 @@ def aSound(theta): def mach2Tas(Mach, theta): - """This function converts Mach number to true airspeed + """ + Converts Mach number to true airspeed (TAS). - :param Mach: Mach number [-] - :param theta: normalised air temperature [-] - :type theta: float + :param Mach: Mach number [-]. + :param theta: Normalized air temperature [-]. :type Mach: float - :returns: true airspeed [m s^-1] - + :type theta: float + :returns: True airspeed in meters per second (m/s). """ + if Mach == float("inf"): tas = float("inf") elif Mach == float("-inf"): @@ -115,30 +129,34 @@ def mach2Tas(Mach, theta): def tas2Mach(v, theta): - """This function converts true airspeed to Mach + """ + Converts true airspeed (TAS) to Mach number. - :param v: true airspeed [m s^-1] - :param theta: normalised air temperature [-] + :param v: True airspeed in meters per second (m/s). + :param theta: Normalized air temperature [-]. :type v: float :type theta: float - :returns: Mach number [-] - + :returns: Mach number [-]. """ + return v / aSound(theta) def tas2Cas(tas, delta, sigma): - """This function converts true airspeed to callibrated airspeed + """ + Converts true airspeed (TAS) to calibrated airspeed (CAS). - :param tas: callibrated airspeed [m s^-1] - :param sigma: normalised air density [-] - :param delta: normalised air pressure [-] + :param tas: True airspeed in meters per second (m/s). + :param sigma: Normalized air density [-]. + :param delta: Normalized air pressure [-]. + :type tas: float :type sigma: float :type delta: float - :type tas: float - :returns: callibrated airspeed [m s^-1] + :returns: Calibrated airspeed in meters per second (m/s). + The function uses a complex formula to account for air compressibility effects at high speeds. """ + if tas == float("inf"): cas = float("inf") elif tas == float("-inf"): @@ -155,22 +173,28 @@ def tas2Cas(tas, delta, sigma): def cas2Tas(cas, delta, sigma): - """This function converts callibrated airspeed to true airspeed + """ + Converts calibrated airspeed (CAS) to true airspeed (TAS). - :param cas: callibrated airspeed [m s^-1] - :param sigma: normalised air density [-] - :param delta: normalised air pressure [-] + :param cas: Calibrated airspeed in meters per second (m/s). + :param sigma: Normalized air density [-]. + :param delta: Normalized air pressure [-]. + :type cas: float :type delta: float :type sigma: float - :type cas: float - :returns: true airspeed [m s^-1] + :returns: True airspeed in meters per second (m/s). + This function inverts the compressibility adjustments to compute TAS from CAS. """ + rho = sigma * const.rho_0 p = delta * const.p_0 A = ( - pow(1 + const.Amu * const.rho_0 * cas * cas / (2 * const.p_0), 1 / const.Amu) + pow( + 1 + const.Amu * const.rho_0 * cas * cas / (2 * const.p_0), + 1 / const.Amu, + ) - 1 ) B = pow(1 + (1 / delta) * A, const.Amu) - 1 @@ -180,21 +204,20 @@ def cas2Tas(cas, delta, sigma): def mach2Cas(Mach, theta, delta, sigma): - """This function converts Mach to callibrated airspeed + """ + Converts Mach number to calibrated airspeed (CAS). - :param Mach: Mach number [-] - :param theta: normalised air temperature [-] - :param delta: normalised air pressure [-] - :param sigma: normalised air density [-] + :param Mach: Mach number [-]. + :param theta: Normalized air temperature [-]. + :param delta: Normalized air pressure [-]. + :param sigma: Normalized air density [-]. :type Mach: float :type theta: float :type delta: float :type sigma: float - :type h: float - :type DeltaTemp: float - :returns: true airspeed [m s^-1] - + :returns: Calibrated airspeed in meters per second (m/s). """ + if Mach == float("inf"): cas = float("inf") elif Mach == float("-inf"): @@ -207,19 +230,20 @@ def mach2Cas(Mach, theta, delta, sigma): def cas2Mach(cas, theta, delta, sigma): - """This function converts callibrated airspeed to Mach + """ + Converts calibrated airspeed (CAS) to Mach number. - :param cas: callibrated airspeed [m s^-1] - :param theta: normalised air temperature [-] - :param delta: normalised air pressure [-] - :param sigma: normalised air density [-] + :param cas: Calibrated airspeed in meters per second (m/s). + :param theta: Normalized air temperature [-]. + :param delta: Normalized air pressure [-]. + :param sigma: Normalized air density [-]. :type cas: float :type theta: float :type delta: float :type sigma: float - :returns: true airspeed [m s^-1] - + :returns: Mach number [-]. """ + tas = cas2Tas(cas, delta, sigma) M = tas2Mach(tas, theta) @@ -227,15 +251,20 @@ def cas2Mach(cas, theta, delta, sigma): def hp(delta, QNH=101325.0): - """This function calculates pressure altitude + """ + Calculates pressure altitude based on normalized pressure and reference pressure (QNH). - :param QNH: reference pressure [Pa] - :param delta: normalised air pressure [-] + :param QNH: Reference pressure in Pascals (Pa), default is standard sea level pressure (101325 Pa). + :param delta: Normalized air pressure [-]. :type delta: float :type QNH: float - :returns: pressure altitude [m] + :returns: Pressure altitude in meters (m). + The pressure altitude is calculated by applying the barometric formula. + Below the tropopause, the altitude is computed using the standard temperature lapse rate. + Above the tropopause, it applies an exponential relationship for altitude based on pressure ratio. """ + if delta * const.p_0 > const.p_11: hp = (const.temp_0 / const.temp_h) * ( 1 - pow(delta * const.p_0 / QNH, const.R * const.temp_h / const.g) @@ -249,15 +278,19 @@ def hp(delta, QNH=101325.0): def crossOver(cas, Mach): - """This function calculates cross-over altitude + """ + Calculates the cross-over altitude where calibrated airspeed (CAS) and Mach number intersect. - :param cas: callibrated airspeed [m s^-1] - :param Mach: Mach number [-] + :param cas: Calibrated airspeed in meters per second (m/s). + :param Mach: Mach number [-]. :type cas: float :type Mach: float - :returns: cross-over altitude [m] + :returns: Cross-over altitude in meters (m). + The cross-over altitude is where CAS and Mach produce the same true airspeed. + The function calculates pressure and temperature at this altitude based on the given Mach number and CAS. """ + p_trans = const.p_0 * ( ( pow( @@ -266,7 +299,13 @@ def crossOver(cas, Mach): ) - 1.0 ) - / (pow(1 + ((const.Agamma - 1.0) / 2.0) * (Mach**2), pow(const.Amu, -1)) - 1.0) + / ( + pow( + 1 + ((const.Agamma - 1.0) / 2.0) * (Mach**2), + pow(const.Amu, -1), + ) + - 1.0 + ) ) theta_trans = pow(p_trans / const.p_0, (const.temp_h * const.R) / const.g) @@ -282,14 +321,14 @@ def crossOver(cas, Mach): def atmosphereProperties(h, DeltaTemp): - """This function calculates the atmosphere properties in form of a density, temperature and pressure ratio - based on altitude and deviation from ISA temperature + """ + Calculates atmospheric properties: normalized temperature, pressure, and density ratios based on altitude and temperature deviation from ISA. - :param h: altitude [m] - :param DeltaTemp: deviation with respect to ISA [K] + :param h: Altitude in meters (m). + :param DeltaTemp: Deviation from ISA temperature in Kelvin (K). :type h: float :type DeltaTemp: float - :returns: normalised temperature, pressure and density [-] + :returns: Normalized temperature, pressure, and density ratios as a list [-]. """ theta_norm = theta(h=h, DeltaTemp=DeltaTemp) @@ -300,19 +339,20 @@ def atmosphereProperties(h, DeltaTemp): def convertSpeed(v, speedType, theta, delta, sigma): - """This function calculates the M, TAS and CAS speed based on imput speed and its type + """ + Calculates Mach, true airspeed (TAS), and calibrated airspeed (CAS) based on input speed and its type. - :param v: airspeed {M,CAS,TAS}[-,kt,kt] - :param speedType: type of speed as input {M,CAS,TAS} - :param theta: normalised air temperature [-] - :param delta: normalised air pressure [-] - :param sigma: normalised air density [-] + :param v: Airspeed value, depending on the type provided (M, CAS, TAS) [-, kt, kt]. + :param speedType: Type of input speed, which can be one of "M" (Mach), "CAS", or "TAS". + :param theta: Normalized air temperature [-]. + :param delta: Normalized air pressure [-]. + :param sigma: Normalized air density [-]. :type v: float :type speedType: string :type theta: float :type delta: float :type sigma: float - :returns: [M, CAS, TAS] [-, m/s, m/s] + :returns: A list of [Mach number, CAS in m/s, TAS in m/s]. """ if speedType == "TAS": diff --git a/src/pyBADA/bada3.py b/src/pyBADA/bada3.py index 4ce0e4a..937fd26 100644 --- a/src/pyBADA/bada3.py +++ b/src/pyBADA/bada3.py @@ -43,28 +43,52 @@ def __init__(self): @staticmethod def list_subfolders(folderPath): + """ + Lists all subfolders within a specified directory. + + :param folderPath: Path to the directory where subfolders are to be listed. + :type folderPath: str + :returns: A list of subfolder names within the specified directory. + :rtype: list of str + + This function retrieves all entries in the given directory and filters out + the ones that are not directories. Only the names of the subfolders are returned. + """ + # List all entries in the directory entries = os.listdir(folderPath) # Filter out entries that are directories subfolders = [ - entry for entry in entries if os.path.isdir(os.path.join(folderPath, entry)) + entry + for entry in entries + if os.path.isdir(os.path.join(folderPath, entry)) ] return subfolders @staticmethod def parseXML(filePath, badaVersion, acName): - """This function parses BADA3 xml formatted file + """ + Parses a BADA3 XML formatted file for aircraft performance data. - :param filePath: path to the folder with BADA4 xml formatted file. - :param acName: name of Aircraft for BADA3 xml formatted file. - :type filePath: str. - :type acName: str. - :raises: IOError + :param filePath: Path to the XML file containing BADA data. + :param badaVersion: Version of BADA to be parsed. + :param acName: Name of the aircraft for which data is being parsed from the XML file. + :type filePath: str + :type badaVersion: 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, "BADA3", badaVersion, acName, acName) + ".xml" + filename = ( + os.path.join(filePath, "BADA3", badaVersion, acName, acName) + + ".xml" + ) try: tree = ET.parse(filename) @@ -143,7 +167,9 @@ def parseXML(filePath, badaVersion, acName): 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 + 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"): @@ -340,6 +366,19 @@ def parseXML(filePath, badaVersion, acName): @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() @@ -353,13 +392,18 @@ def findData(f): @staticmethod def parseOPF(filePath, badaVersion, acName): - """This function parses BADA3 ascii formatted file + """ + Parses a BADA3 OPF (Operational Performance File) ASCII formatted file for aircraft performance data. - :param filePath: path to the BADA3 ascii formatted file. - :param acName: ICAO aircraft designation - :type filePath: str. + :param filePath: Path to the BADA3 OPF ASCII formatted file. + :param badaVersion: BADA version being used. + :param acName: ICAO aircraft designation (e.g., 'A320'). + :type filePath: str + :type badaVersion: str :type acName: str - :raises: IOError + :raises IOError: If the file cannot be opened or read. + :returns: A pandas DataFrame containing the parsed aircraft performance data. + :rtype: pd.DataFrame """ filename = ( @@ -379,7 +423,9 @@ def parseOPF(filePath, badaVersion, acName): if idx == 13: if "with" in line: - engines = line.split("with")[1].split("engines")[0].strip() + engines = ( + line.split("with")[1].split("engines")[0].strip() + ) else: engines = "unknown" idx += 1 @@ -579,16 +625,23 @@ def parseOPF(filePath, badaVersion, acName): @staticmethod def parseAPF(filePath, badaVersion, acName): - """This function parses BADA3 APF ascii formatted file + """ + 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 - :type filePath: str. + :param filePath: Path to the BADA3 APF ASCII formatted file. + :param badaVersion: BADA version being used. + :param acName: ICAO aircraft designation (e.g., 'A320'). + :type filePath: str + :type badaVersion: str :type acName: str - :raises: IOError + :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, "BADA3", badaVersion, acName) + ".APF" + filename = ( + os.path.join(filePath, "BADA3", badaVersion, acName) + ".APF" + ) dataLines = list() with open(filename, "r", encoding="latin-1") as f: @@ -598,7 +651,9 @@ def parseAPF(filePath, badaVersion, acName): if line.startswith("CC"): if "Modification_date" in line: data = line.split(":")[1].strip().split(" ") - modificationDateAPF = " ".join([data[0], data[1], data[2]]) + modificationDateAPF = " ".join( + [data[0], data[1], data[2]] + ) if line.startswith("CD"): line = " ".join(line.split()) line = line.strip().split(" ") @@ -613,7 +668,9 @@ def parseAPF(filePath, badaVersion, acName): dataLines.append(line) elif "THE END" in line: break - dataLines.pop(0) # remove first line that does not contain usefull data + dataLines.pop( + 0 + ) # remove first line that does not contain usefull data # AV - average - line with average data AVLine = dataLines[1] @@ -647,10 +704,23 @@ def parseAPF(filePath, badaVersion, acName): @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)], + [ + OPFDataFrame.reset_index(drop=True), + APFDataFrame.reset_index(drop=True), + ], axis=1, ) @@ -658,6 +728,16 @@ def combineOPF_APF(OPFDataFrame, APFDataFrame): @staticmethod def readSynonym(filePath, badaVersion): + """ + Reads a BADA3 SYNONYM.NEW ASCII file and returns a dictionary of model-synonym pairs. + + :param filePath: Path to the directory containing BADA3 files. + :param badaVersion: BADA version being used. + :type filePath: str + :type badaVersion: 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, "BADA3", badaVersion, "SYNONYM.NEW") @@ -685,6 +765,20 @@ def readSynonym(filePath, badaVersion): @staticmethod def readSynonymXML(filePath, badaVersion): + """ + Reads a BADA3 SYNONYM.xml file and returns a dictionary of model-synonym pairs. + + :param filePath: Path to the directory containing BADA3 files. + :param badaVersion: BADA version being used. + :type filePath: str + :type badaVersion: 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, "BADA3", badaVersion, "SYNONYM.xml") @@ -710,6 +804,22 @@ def readSynonymXML(filePath, badaVersion): @staticmethod def parseSynonym(filePath, badaVersion, 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 badaVersion: BADA version being used. + :param acName: ICAO aircraft designation for which the file name is needed. + :type filePath: str + :type badaVersion: 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, badaVersion) @@ -725,11 +835,15 @@ def parseSynonym(filePath, badaVersion, acName): @staticmethod def readGPF(filePath, badaVersion): - """This function parses BADA3 GPF ascii formatted file - - :param filePath: path to the BADA3 GPF ascii formatted file. - :type filePath: str. - :raises: IOError + """ + Parses a BADA3 GPF ASCII formatted file. + + :param filePath: Path to the directory containing BADA3 files. + :param badaVersion: BADA version being used. + :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, "BADA3", badaVersion, "BADA.GPF") @@ -760,11 +874,19 @@ def readGPF(filePath, badaVersion): @staticmethod def readGPFXML(filePath, badaVersion): - """This function parses BADA3 GPF xml formatted file - - :param filePath: path to the GPF xml formatted file. - :type filePath: str. - :raises: IOError + """ + Parses a BADA3 GPF XML formatted file. + + :param filePath: Path to the directory containing BADA3 files. + :param badaVersion: BADA version being used. + :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, "BADA3", badaVersion, "GPF.xml") @@ -807,7 +929,9 @@ def readGPFXML(filePath, badaVersion): GPFparamList.append( { "name": "ang_bank_nom", - "value": float(AngBank.find("Nom").find("Civ").find("ToLd").text), + "value": float( + AngBank.find("Nom").find("Civ").find("ToLd").text + ), "engine": allEngines, "phase": ["to", "ld"], "flight": ["civ"], @@ -816,7 +940,9 @@ def readGPFXML(filePath, badaVersion): GPFparamList.append( { "name": "ang_bank_nom", - "value": float(AngBank.find("Nom").find("Civ").find("Others").text), + "value": float( + AngBank.find("Nom").find("Civ").find("Others").text + ), "engine": allEngines, "phase": ["ic", "cl", "cr", "des", "hold", "app"], "flight": ["civ"], @@ -834,7 +960,9 @@ def readGPFXML(filePath, badaVersion): GPFparamList.append( { "name": "ang_bank_max", - "value": float(AngBank.find("Max").find("Civ").find("ToLd").text), + "value": float( + AngBank.find("Max").find("Civ").find("ToLd").text + ), "engine": allEngines, "phase": ["to", "ld"], "flight": ["civ"], @@ -843,7 +971,9 @@ def readGPFXML(filePath, badaVersion): GPFparamList.append( { "name": "ang_bank_max", - "value": float(AngBank.find("Max").find("Civ").find("Hold").text), + "value": float( + AngBank.find("Max").find("Civ").find("Hold").text + ), "engine": allEngines, "phase": ["hold"], "flight": ["civ"], @@ -852,7 +982,9 @@ def readGPFXML(filePath, badaVersion): GPFparamList.append( { "name": "ang_bank_max", - "value": float(AngBank.find("Max").find("Civ").find("Others").text), + "value": float( + AngBank.find("Max").find("Civ").find("Others").text + ), "engine": allEngines, "phase": ["ic", "cl", "cr", "des", "app"], "flight": ["civ"], @@ -917,7 +1049,9 @@ def readGPFXML(filePath, badaVersion): HmaxList = {} for phase in root.find("HmaxList").findall("HmaxPhase"): - HmaxList[phase.find("Phase").text] = float(phase.find("Hmax").text) + HmaxList[phase.find("Phase").text] = float( + phase.find("Hmax").text + ) if phase.find("Phase").text == "TO": GPFparamList.append( @@ -1297,6 +1431,17 @@ def readGPFXML(filePath, badaVersion): @staticmethod def parseGPF(filePath, badaVersion): + """ + Parses a BADA3 (GPF) from either ASCII or XML format. + + :param filePath: Path to the directory containing BADA3 files. + :param badaVersion: BADA version being used. + :type filePath: str + :type badaVersion: str + :returns: A pandas DataFrame containing GPF data. + :rtype: pd.DataFrame + """ + GPFdata = Parser.readGPF(filePath, badaVersion) # if ASCI GPF does not exist, try XML GPF file @@ -1311,19 +1456,23 @@ def parseGPF(filePath, badaVersion): @staticmethod def getGPFValue(GPFdata, name, engine="JET", phase="cr", flight="civ"): - """This function returns value of the GPF parameter based on the defined features - like flight, Engine and Phase of flight - - :param Name: name of the GPF parameter. - :param Engine: type of the engine where this parameter can be applied. - :param Phase: phase of the flight, where this parameter can be applied. - :param flight: flight where this parameter can be applied (civ or mil). - :type Name: str. - :type Engine: str. - :type Phase: str. - :type flight: str. - """ + 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"] @@ -1346,13 +1495,23 @@ def getGPFValue(GPFdata, name, engine="JET", phase="cr", flight="civ"): @staticmethod def combineACDATA_GPF(ACDataFrame, GPFDataframe): - """This function combines 2 dataframes, the parsed aircraft file - and parsed GPF file + """ + 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)], + [ + ACDataFrame.reset_index(drop=True), + GPFDataframe.reset_index(drop=True), + ], axis=1, ) @@ -1360,12 +1519,16 @@ def combineACDATA_GPF(ACDataFrame, GPFDataframe): @staticmethod def parseAll(badaVersion, filePath=None): - """This function parses all BADA3 formatted file and stores - all data in the final dataframe containing all the BADA data. - - :param filePath: path to the BADA3 formatted file. - :type filePath: str. - :raises: IOError + """ + 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: @@ -1404,7 +1567,9 @@ def parseAll(badaVersion, filePath=None): combined_df = Parser.combineACDATA_GPF(df, GPFparsedDataframe) # Merge DataFrames - merged_df = pd.concat([merged_df, combined_df], ignore_index=True) + merged_df = pd.concat( + [merged_df, combined_df], ignore_index=True + ) return merged_df @@ -1426,25 +1591,31 @@ def parseAll(badaVersion, filePath=None): df.at[0, "acName"] = synonym # Combine data with GPF data (temporary solution) - combined_df = Parser.combineACDATA_GPF(df, GPFparsedDataframe) + combined_df = Parser.combineACDATA_GPF( + df, GPFparsedDataframe + ) # Merge DataFrames - merged_df = pd.concat([merged_df, combined_df], ignore_index=True) + merged_df = pd.concat( + [merged_df, combined_df], ignore_index=True + ) return merged_df @staticmethod def getBADAParameters(df, acName, parameters): - """Retrieves specified parameters for a given aircraft name from the DataFrame. - - :param df: The DataFrame containing aircraft data. - :param acName: The name of the aircraft to search for - :param parameters: A list of column names to retrieve or a single column name - :type df: pandas dataframe. - :type acName: list[string]. - :type parameters: list[string]. - :return: parameter values: dataframe - :rtype: dataframe. + """ + Retrieves specified parameters for a given aircraft name from a DataFrame. + + :param df: DataFrame containing BADA aircraft data. + :param acName: Name of the aircraft or list of aircraft names to search for. + :param parameters: List of column names (or a single column name) to retrieve. + :type df: pd.DataFrame + :type acName: list or str + :type parameters: list or str + :returns: A DataFrame containing the specified parameters for the given aircraft. + :rtype: pd.DataFrame + :raises ValueError: If any of the specified columns or aircraft names are not found. """ # Ensure parameters is a list @@ -1470,12 +1641,25 @@ def getBADAParameters(df, acName, parameters): raise ValueError(f"No entries found for aircraft(s): {acName}.") else: # Select the required columns - result_df = filtered_df[["acName"] + parameters].reset_index(drop=True) + result_df = filtered_df[["acName"] + parameters].reset_index( + drop=True + ) return result_df @staticmethod def safe_get(df, column_name, default_value=None): - """Accessing a potentially dropped column from a dataframe""" + """ + Safely retrieves a column's value from a DataFrame, returning a default value if the column does not exist. + + :param df: DataFrame to retrieve the value from. + :param column_name: Name of the column to retrieve. + :param default_value: Value to return if the column does not exist. Default is None. + :type df: pd.DataFrame + :type column_name: str + :type default_value: any + :returns: The value from the specified column or the default value if the column is missing. + :rtype: any + """ if column_name in df.columns: return df[column_name].iloc[0] @@ -1495,38 +1679,50 @@ def __init__(self, AC): self.AC = AC def CL(self, sigma, mass, tas, nz=1.0): - """This function computes the lift coefficient - - :param tas: true airspeed [m s^-1]. - :param sigma: normalised air density [-]. - :param mass: aircraft mass [kg]. - :param nz: load factor [-]. - :type tas: float. - :type sigma: float. - :type mass: float. - :type nz: float. + """ + 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. + :rtype: float """ - return 2 * mass * const.g * nz / (sigma * const.rho_0 * tas * tas * self.AC.S) + return ( + 2 + * mass + * const.g + * nz + / (sigma * const.rho_0 * tas * tas * self.AC.S) + ) def CD( - self, CL, config, expedite=False, speedBrakes={"deployed": False, "value": 0.03} + self, + CL, + config, + expedite=False, + speedBrakes={"deployed": False, "value": 0.03}, ): - """This function computes the drag coefficient + """ + Computes the drag coefficient based on the lift coefficient and aircraft configuration. :param CL: Lift coefficient [-]. - :param config: aircraft aerodynamic configuration [CR/IC/TO/AP/LD][-]. - :param expedite: expedite descend factor [-]. - :param speedBrakes: speed brakes used or not [-]. - :type CL: float. - :type config: str. - :type expedite: boolean. - :type speedBrakes: boolean. + :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 + :raises: ValueError if an invalid configuration is provided. """ if self.AC.xmlFiles: @@ -1545,9 +1741,9 @@ def CD( and self.AC.DeltaCD == 0.0 ): - CD = self.AC.CD0[HLid_CR][LG_CR] + self.AC.CD2[HLid_CR][LG_CR] * ( - CL * CL - ) + 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 = ( @@ -1602,55 +1798,60 @@ def CD( # expedite descent C_des_exp = 1.0 if expedite: - C_des_exp = Parser.getGPFValue(self.AC.GPFdata, "C_des_exp", phase="des") + C_des_exp = Parser.getGPFValue( + self.AC.GPFdata, "C_des_exp", phase="des" + ) CD = CD * C_des_exp return CD def D(self, sigma, tas, CD): - """This function computes the aerodynamic drag + """ + Computes the aerodynamic drag force. - :param tas: true airspeed [m s^-1]. - :param sigma: normalised air density [-]. + :param sigma: Normalized air density [-]. + :param tas: True airspeed in meters per second [m/s]. :param CD: Drag coefficient [-]. - :type tas: float. - :type sigma: float. - :type CD: float. - :returns: Aerodynamic drag [N]. - :rtype: float. + :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 def L(self, sigma, tas, CL): - """This function computes the aerodynamic lift + """ + Computes the aerodynamic lift force. - :param tas: true airspeed [m s^-1]. - :param sigma: normalised air density [-]. + :param sigma: Normalized air density [-]. + :param tas: True airspeed in meters per second [m/s]. :param CL: Lift coefficient [-]. - :type tas: float. - :type sigma: float. - :type CL: float. - :returns: Aerodynamic lift [N]. - :rtype: float. + :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 def Thrust(self, h, DeltaTemp, rating, v, config, **kwargs): - """This function computes the aircraft thrust - - :param rating: engine rating {MCMB,MCRZ,MTKF,LIDL}. - :param h: altitude [m]. - :param DeltaTemp: deviation with respect to ISA [K] - :param v: true airspeed (TAS) [m s^-1]. - :param config: aircraft aerodynamic configuration [CR/IC/TO/AP/LD][-]. - :type rating: string. + """ + 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 h: float. - :type v: float. - :type config: string. - :returns: Thrust [N]. + :type v: float + :type config: str + :returns: Thrust in Newtons [N]. :rtype: float """ @@ -1678,7 +1879,13 @@ def Thrust(self, h, DeltaTemp, rating, v, config, **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 + h=h, + DeltaTemp=DeltaTemp, + ROCD=ROCD, + mass=mass, + v=v, + acc=acc, + Drag=Drag, ) else: @@ -1687,22 +1894,24 @@ def Thrust(self, h, DeltaTemp, rating, v, config, **kwargs): return T def TAdapted(self, h, DeltaTemp, ROCD, mass, v, acc, Drag): - """This function computes the adapted thrust - - :param h: altitude [m]. - :param DeltaTemp: deviation with respect to ISA [K] - :param ROCD: rate of climb/descend [m s^-1]. - :param mass: actual aircraft weight [kg] - :param v: true airspeed (TAS) [m s^-1]. - :param acc: acceleration [m s^-2]. - :param Drag: aerodynamic drag [N]. - :type h: float. - :type DeltaTemp: float. - :type ROCD: float. - :type mass: float. - :type acc: float. - :type Drag: float. - :returns: maximum thrust [N]. + """ + 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 """ @@ -1713,17 +1922,18 @@ def TAdapted(self, h, DeltaTemp, ROCD, mass, v, acc, Drag): return Tadapted def TMax(self, h, DeltaTemp, rating, v): - """This function computes the maximum thrust - - :param rating: engine rating {MCMB,MCRZ,MTKF}. - :param h: altitude [m]. - :param v: true airspeed (TAS) [m s^-1]. - :param DeltaTemp: deviation with respect to ISA [K] - :type rating: string. - :type h: float. - :type v: float. + """ + 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 - :returns: maximum thrust [N]. + :type rating: str + :type v: float + :returns: Maximum thrust in Newtons [N]. :rtype: float """ @@ -1769,24 +1979,29 @@ def TMax(self, h, DeltaTemp, rating, v): return TMax elif rating == "MCRZ": - return TMax * Parser.getGPFValue(self.AC.GPFdata, "C_th_cr", phase="cr") + return TMax * Parser.getGPFValue( + self.AC.GPFdata, "C_th_cr", phase="cr" + ) def TDes(self, h, DeltaTemp, v, config): - """This function computes the descent thrust + """ + Computes descent thrust based on altitude, temperature deviation, and configuration. - :param h: altitude [m]. - :param DeltaTemp: deviation with respect to ISA [K] - :param config: aircraft aerodynamic configuration [CR/IC/TO/AP/LD][-]. - :param v: true airspeed (TAS) [m s^-1]. - :type h: float. - :type DeltaTemp: float. - :type config: string. - :type v: float. - :returns: minimum thrust [N]. + :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") + H_max_app = Parser.getGPFValue( + self.AC.GPFdata, "H_max_app", phase="app" + ) if ( self.AC.engineType == "PISTON" @@ -1842,18 +2057,23 @@ def TDes(self, h, DeltaTemp, v, config): return Tdes def ffnom(self, v, T): - """This function computes the nominal fuel flow + """ + Computes the nominal fuel flow based on airspeed and thrust. - :param v: true airspeed (TAS) [m s^-1]. - :param T: Thrust [N]. - :type v: float. - :type T: float. - :returns: nominal fuel flow [kg s^-1]. + :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) + eta = ( + self.AC.Cf[0] + * (1 + conv.ms2kt(v) / self.AC.Cf[1]) + / (1000 * 60) + ) ffnom = eta * T elif self.AC.engineType == "TURBOPROP": @@ -1865,39 +2085,51 @@ def ffnom(self, v, T): ) ffnom = eta * T - elif self.AC.engineType == "PISTON" or self.AC.engineType == "ELECTRIC": + elif ( + self.AC.engineType == "PISTON" or self.AC.engineType == "ELECTRIC" + ): ffnom = self.AC.Cf[0] / 60 return ffnom def ffMin(self, h): - """This function computes the minimum fuel flow + """ + Computes the minimum fuel flow based on altitude. - :param h: altitude [m]. - :type h: float. - :returns: Minimum fuel flow [kg s^-1]. + :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] * (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 def ff(self, h, v, T, config=None, flightPhase=None, adapted=False): - """This function computes the fuel flow based on the flight phase and flight situation - - :param h: altitude [m]. - :param rating: engine rating {MCMB,MCRZ,LIDL}. - :param v: true airspeed (TAS) [m s^-1]. - :param T: Thrust [N]. - :type h: float. - :type rating: string. - :type v: float. - :type T: float. - :returns: fuel flow [kg s^-1]. + """ + 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 """ @@ -1929,17 +2161,16 @@ def ff(self, h, v, T, config=None, flightPhase=None, adapted=False): return ff def reducedPower(self, h, mass, DeltaTemp): - """This function computes the reduced climb power coefficient + """ + Computes the reduced climb power coefficient based on altitude, mass, and temperature deviation. - :param h: altitude [m]. - :param DeltaTemp: deviation with respect to ISA [K] - :param mass: actual aircraft weight [kg] - :param hMax: aircraft flight envelope max Altitude [m] - :type h: float. - :type DeltaTemp: float. - :type mass: float. - :type hMax: float. - :returns: reduced climb power coefficient [-]. + :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 """ @@ -1951,19 +2182,31 @@ def reducedPower(self, h, mass, DeltaTemp): 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" + 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" + 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" + 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" + self.AC.GPFdata, + "C_red_elec", + engine="ELECTRIC", + phase="cl", ) else: CRed = 0 @@ -1972,25 +2215,26 @@ def reducedPower(self, h, mass, DeltaTemp): return CPowRed def ROCD(self, T, D, v, mass, ESF, h, DeltaTemp, reducedPower=False): - """This function computes the Rate of Climb or Descent - - :param h: altitude [m]. - :param T: aircraft thrust [N]. - :param D: aircraft drag [N]. - :param v: aircraft true airspeed [TAS] [m s^-1]. - :param mass: actual aircraft mass [kg]. - :param ESF: energy share factor [-]. - :param DeltaTemp: deviation with respect to ISA [K] - :param reducedPower: power reduction [-] - :type h: float. - :type T: float. - :type D: float. - :type v: float. - :type mass: float. - :type ESF: float. - :type DeltaTemp: float. - :type reducedPower: boolean. - :returns: rate of climb/descend [m/s]. + """ + 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 """ @@ -2002,7 +2246,12 @@ def ROCD(self, T, D, v, mass, ESF, h, DeltaTemp, reducedPower=False): CPowRed = self.reducedPower(h=h, mass=mass, DeltaTemp=DeltaTemp) ROCD = ( - ((temp - DeltaTemp) / temp) * (T - D) * v * ESF * CPowRed / (mass * const.g) + ((temp - DeltaTemp) / temp) + * (T - D) + * v + * ESF + * CPowRed + / (mass * const.g) ) return ROCD @@ -2020,14 +2269,19 @@ def __init__(self, AC): super().__init__(AC) def maxAltitude(self, mass, DeltaTemp): - """This function computes the maximum altitude for any given aircraft mass + """ + Computes the maximum altitude for a given aircraft mass and deviation from ISA. - :param DeltaTemp: deviation with respect to ISA [K] - :param mass: actual aircraft weight [kg] - :type DeltaTemp: float. - :type mass: float. - :returns: maximum altitude [m]. + :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 @@ -2047,18 +2301,21 @@ def maxAltitude(self, mass, DeltaTemp): if self.AC.Hmax == 0: hMax = self.AC.hmo else: - hMax = min(self.AC.hmo, self.AC.Hmax + Gt * var + Gw * (mMax - mass)) + hMax = min( + self.AC.hmo, self.AC.Hmax + Gt * var + Gw * (mMax - mass) + ) return conv.ft2m(hMax) def VStall(self, mass, config): - """This function computes the mass correction for stall speed calculation + """ + Computes the stall speed based on the aircraft configuration and mass. - :param config: aircraft configuration [CR/IC/TO/AP/LD][-] - :param mass: aircraft operating mass [kg] - :type config: string. - :type mass: string - :returns: aircraft stall speed [m s^-1] + :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 """ @@ -2074,18 +2331,25 @@ def VStall(self, mass, config): return vStall - def VMin(self, h, mass, config, DeltaTemp, nz=1.2, envelopeType="OPERATIONAL"): - """This function computes the minimum speed - - :param h: altitude [m]. - :param config: aircraft configuration [CR/IC/TO/AP/LD][-] - :param mass: aircraft operating mass [kg] - :param DeltaTemp: deviation with respect to ISA [K] - :type h: float. - :type config: string + 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 DeltaTemp: float. - :returns: minimum speed [m s^-1]. + :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 """ @@ -2108,7 +2372,10 @@ def VMin(self, h, mass, config, DeltaTemp, nz=1.2, envelopeType="OPERATIONAL"): 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": + if ( + self.AC.engineType == "JET" + or self.AC.engineType == "TURBOPROP" + ): [theta, delta, sigma] = atm.atmosphereProperties( h=h, DeltaTemp=DeltaTemp ) @@ -2121,32 +2388,41 @@ def VMin(self, h, mass, config, DeltaTemp, nz=1.2, envelopeType="OPERATIONAL"): Vmin = max( VminStall, atm.mach2Cas( - buffetLimit, theta=theta, delta=delta, sigma=sigma + buffetLimit, + theta=theta, + delta=delta, + sigma=sigma, ), ) - elif self.AC.engineType == "PISTON" or self.AC.engineType == "ELECTRIC": + elif ( + self.AC.engineType == "PISTON" + or self.AC.engineType == "ELECTRIC" + ): Vmin = VminStall return Vmin def Vmax_thrustLimited(self, h, mass, DeltaTemp, rating, config): - """This function computes the maximum CAS speed within the certified flight envelope taking into account the trust limitation. - - :param h: altitude [m]. - :param mass: aircraft operating mass [kg] - :param DeltaTemp: deviation with respect to ISA [K] - :param rating: aircraft engine rating [MTKF/MCMB/MCRZ][-] - :param config: aircraft configuration [TO/IC/CR][-] - :type h: float. + """ + 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 config: string - :type rating: string - :returns: maximum thrust lmited speed [m s^-1]. + :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) + [theta, delta, sigma] = atm.atmosphereProperties( + h=h, DeltaTemp=DeltaTemp + ) VmaxCertified = self.VMax(h=h, DeltaTemp=DeltaTemp) VminCertified = self.VMin( @@ -2164,7 +2440,9 @@ def Vmax_thrustLimited(self, h, mass, DeltaTemp, rating, config): CDvalue = None CASValue = None MValue = None - for CAS in np.linspace(VminCertified, VmaxCertified, num=200, endpoint=True): + 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( @@ -2188,23 +2466,26 @@ def Vmax_thrustLimited(self, h, mass, DeltaTemp, rating, config): return max(maxCASList) def Vx(self, h, mass, DeltaTemp, rating, config): - """This function computes the best angle of climb CAS speed. - - :param h: altitude [m]. - :param mass: aircraft operating mass [kg] - :param DeltaTemp: deviation with respect to ISA [K] - :param rating: aircraft engine rating [MTKF/MCMB/MCRZ][-] - :param config: aircraft configuration [TO/IC/CR][-] - :type h: float. + """ + 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 config: string - :type rating: string - :returns: Vx - best angle of climb speed [m s^-1]. + :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) + [theta, delta, sigma] = atm.atmosphereProperties( + h=h, DeltaTemp=DeltaTemp + ) VmaxCertified = self.VMax(h=h, DeltaTemp=DeltaTemp) VminCertified = self.VMin( @@ -2219,7 +2500,9 @@ def Vx(self, h, mass, DeltaTemp, rating, config): excessThrustList = [] VxList = [] - for CAS in np.linspace(VminCertified, VmaxCertified, num=200, endpoint=True): + 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 @@ -2236,36 +2519,46 @@ def Vx(self, h, mass, DeltaTemp, rating, config): return VxList[idx] def VMax(self, h, DeltaTemp): - """This function computes the maximum speed + """ + Computes the maximum speed based on altitude and temperature deviation. - :param h: altitude [m]. - :param DeltaTemp: deviation with respect to ISA [K] - :type h: float. - :type DeltaTemp: float. - :returns: maximum speed [m s^-1]. + :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) + 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) + [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 def lowSpeedBuffetLimit(self, h, mass, DeltaTemp, nz=1.2): - """This function computes the low speed Buffeting limit using numerical methods by numpy + """ + Computes the low-speed buffet limit using numerical methods. - :param h: altitude [m]. - :param mass: aircraft mass [kg] - :param DeltaTemp: deviation with respect to ISA [K] - :type h: float. - :type mass: float. - :type DeltaTemp: float. - :returns: low speed buffet limit as M [-]. + :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 """ @@ -2288,23 +2581,25 @@ def lowSpeedBuffetLimit(self, h, mass, DeltaTemp, nz=1.2): return min(Mb) def getConfig(self, phase, h, mass, v, DeltaTemp, hRWY=0.0, nz=1.2): - """This function returns the aircraft aerodynamic configuration - based on the aircraft altitude and speed and phase of flight - - :param hRWY: runway elevation AMSL [m]. - :param phase: aircraft phase of flight [cl/cr/des][-]. - :param h: altitude [m]. - :param v: calibrated airspeed (CAS) [m s^-1]. - :param mass: aircraft mass [kg] - :param DeltaTemp: deviation with respect to ISA [K] - :type hRWY: float. - :type phase: string. - :type h: float. - :type v: float. - :type mass: float. - :type DeltaTemp: float. - :returns: aircraft aerodynamic configuration [TO/IC/CR/AP/LD][-]. - :rtype: string + """ + 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 @@ -2313,19 +2608,27 @@ def getConfig(self, phase, h, mass, v, DeltaTemp, hRWY=0.0, nz=1.2): h_AGL = h - hRWY HmaxTO_AGL = ( - conv.ft2m(Parser.getGPFValue(self.AC.GPFdata, "H_max_to", phase="to")) + 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")) + 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")) + 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")) + conv.ft2m( + Parser.getGPFValue(self.AC.GPFdata, "H_max_ld", phase="lnd") + ) - hRWY ) @@ -2334,8 +2637,12 @@ def getConfig(self, phase, h, mass, v, DeltaTemp, hRWY=0.0, nz=1.2): 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) + 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" @@ -2375,13 +2682,13 @@ def getConfig(self, phase, h, mass, v, DeltaTemp, hRWY=0.0, nz=1.2): return config def getAeroConfig(self, config): - """This function returns the aircraft aerodynamic configuration - based on the aerodynamic configuration ID + """ + Returns the aerodynamic configuration ID for a given configuration. - :param config: aircraft configuration [CR/IC/TO/AP/LD][-] - :type config: string - :returns: aircraft aerodynamic configuration combination of HLID and LG [-]. - :rtype: [float, string] + :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"] @@ -2390,58 +2697,84 @@ def getAeroConfig(self, config): return [HLid, LG] def getHoldSpeed(self, h, theta, delta, sigma, DeltaTemp): - """This function returns the aircraft holding speed based on the altitude - - :param h: altitude [m]. - :param DeltaTemp: deviation with respect to ISA [K] - :type h: float. - :type DeltaTemp: float. - :returns: aircraft Holding calibrated airspeed (CAS) [m s^-1]. + """ + 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") + 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") + 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") + 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") + 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) def getGroundMovementSpeed(self, pos): - """This function returns the aircraft ground movement speed based on postion on the ground + """ + Returns the ground movement speed based on the aircraft's position on the ground. - :param pos: aircraft position on airport ground [backtrack/taxi/apron/gate][-]. - :type pos: string. - :returns: aircraft ground movement calibrated airspeed (CAS) [m s^-1]. + :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") + vGround = Parser.getGPFValue( + self.AC.GPFdata, "V_backtrack", phase="gnd" + ) elif pos == "taxi": - vGround = Parser.getGPFValue(self.AC.GPFdata, "V_taxi", phase="gnd") + vGround = Parser.getGPFValue( + self.AC.GPFdata, "V_taxi", phase="gnd" + ) elif pos == "apron": - vGround = Parser.getGPFValue(self.AC.GPFdata, "V_apron", phase="gnd") + vGround = Parser.getGPFValue( + self.AC.GPFdata, "V_apron", phase="gnd" + ) elif pos == "gate": - vGround = Parser.getGPFValue(self.AC.GPFdata, "V_gate", phase="gnd") + vGround = Parser.getGPFValue( + self.AC.GPFdata, "V_gate", phase="gnd" + ) return conv.kt2ms(vGround) def getBankAngle(self, phase, flightUnit, value): - """This function returns the aircraft bank angle based on phase of flight - - :param phase: aircraft phase of flight [to/ic/cl/cr/...][-]. - :param flightUnit: flight unit [civ/mil][-]. - :param value: nominal or maximum value [nom/max][-]. - :type phase: string. - :type flightUnit: string. - :type value: string. - :returns: bank angle [deg] + """ + 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 """ @@ -2458,22 +2791,21 @@ def getBankAngle(self, phase, flightUnit, value): return maxBankAngle def isAccOK(self, v1, v2, type="long", flightUnit="civ", deltaTime=1.0): - """This function checks the limits for longitudinal and normal acceleration - - :param type: logitudinal or normal acceleration [long/norm][-]. - :param flightUnit: flight unit [civ/mil][-]. - :param v1: (long) true airspeed (TAS) at step k-1 [m s^-1]. - :param v1: (norm) vertical airspeed (ROCD) at step k-1 [m s^-1]. - :param v2: (long) true airspeed (TAS) at step k [m s^-1]. - :param v2: (norm) vertical airspeed (ROCD) at step k [m s^-1]. - :param deltaTime: time interval between k and k-1 [s]. - :type type: string. - :type flightUnit: string. - :type v1: float. - :type v2: float. - :type deltaTime: float. - :returns: acceleration OK [True] or NOK [False] [-] - :rtype: boolean + """ + 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 @@ -2482,7 +2814,9 @@ def isAccOK(self, v1, v2, type="long", flightUnit="civ", deltaTime=1.0): if type == "long": if ( abs(v2 - v1) - <= conv.ft2m(Parser.getGPFValue(self.AC.GPFdata, "acc_long_max")) + <= conv.ft2m( + Parser.getGPFValue(self.AC.GPFdata, "acc_long_max") + ) * deltaTime ): OK = True @@ -2490,7 +2824,9 @@ def isAccOK(self, v1, v2, type="long", flightUnit="civ", deltaTime=1.0): elif type == "norm": if ( abs(v2 - v1) - <= conv.ft2m(Parser.getGPFValue(self.AC.GPFdata, "acc_norm_max")) + <= conv.ft2m( + Parser.getGPFValue(self.AC.GPFdata, "acc_norm_max") + ) * deltaTime ): OK = True @@ -2502,13 +2838,13 @@ def isAccOK(self, v1, v2, type="long", flightUnit="civ", deltaTime=1.0): return OK def getSpeedSchedule(self, phase): - """This function returns the speed schedule - based on the phase of flight {Climb, Cruise, Descent} + """ + Returns the speed schedule for a given phase of flight. - :param phase: aircraft phase of flight {Climb, Cruise, Descent} - :type phase: string - :returns: speed schedule combination of CAS1, CAS2 and M [m s^-1, m s^-1, -]. - :rtype: [float, float, float] + :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": @@ -2524,18 +2860,24 @@ def getSpeedSchedule(self, phase): return [CAS1, CAS2, M] - def checkConfigurationContinuity(self, phase, previousConfig, currentConfig): - """This function ensures the continuity of the configuration change, - so, the aerodynamic configuration does not change in the wrong direction based on the phase of the flight - - :param phase: aircraft phase of flight {Climb, Cruise, Descent} - :param previousConfig: aircraft previous aerodynamic configuration - :param currentConfig: aircraft current aerodynamic configuration - :type phase: string - :type previousConfig: string - :type currentConfig: string - :returns: speed new current configuration - :rtype: string + 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 = "" @@ -2599,24 +2941,47 @@ def climbSpeed( NADP1_ALT=3000, NADP2_ALT=[1000, 3000], ): - """This function computes the climb speed schedule CAS speed for any given altitude - - :param h: altitude [m]. - :param mass: aircraft mass [kg]. - :param theta: normalised air temperature [-]. - :param delta: normalised air pressure [-]. - :param DeltaTemp: deviation with respect to ISA [K] - :param speedSchedule_default: default speed schedule that will overwrite the BADA schedule [Vcl1, Vcl2, Mcl]. - :param applyLimits: apply min/max speed limitation [-]. - :type h: float. - :type mass: float. - :type theta: float. - :type delta: float. - :type DeltaTemp: float. - :type speedSchedule_default: [float, float, float]. - :type applyLimits: [boolean]. - :returns: climb calibrated airspeed (CAS) [m s^-1]. - :rtype: float + """ + 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" @@ -2645,31 +3010,41 @@ def climbSpeed( speed.append( Cvmin * VstallTO + conv.kt2ms( - Parser.getGPFValue(self.AC.GPFdata, "V_cl_5", phase=phase) + 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) + 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) + 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) + 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) + Parser.getGPFValue( + self.AC.GPFdata, "V_cl_1", phase=phase + ) ) ) @@ -2694,16 +3069,25 @@ def climbSpeed( elif h >= conv.ft2m(10000) and h < crossOverAlt: cas = Vcl2 elif h >= crossOverAlt: - cas = atm.mach2Cas(Mach=Mcl, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mcl, theta=theta, delta=delta, sigma=sigma + ) - elif acModel == "TURBOPROP" or acModel == "PISTON" or acModel == "ELECTRIC": + 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 + self.AC.GPFdata, + "V_cl_8", + engine="TURBOPROP", + phase=phase, ) ) ) @@ -2711,7 +3095,10 @@ def climbSpeed( Cvmin * VstallTO + conv.kt2ms( Parser.getGPFValue( - self.AC.GPFdata, "V_cl_7", engine="TURBOPROP", phase=phase + self.AC.GPFdata, + "V_cl_7", + engine="TURBOPROP", + phase=phase, ) ) ) @@ -2719,7 +3106,10 @@ def climbSpeed( Cvmin * VstallTO + conv.kt2ms( Parser.getGPFValue( - self.AC.GPFdata, "V_cl_6", engine="TURBOPROP", phase=phase + self.AC.GPFdata, + "V_cl_6", + engine="TURBOPROP", + phase=phase, ) ) ) @@ -2741,7 +3131,9 @@ def climbSpeed( elif h >= conv.ft2m(10000) and h < crossOverAlt: cas = Vcl2 elif h >= crossOverAlt: - cas = atm.mach2Cas(Mach=Mcl, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mcl, theta=theta, delta=delta, sigma=sigma + ) elif procedure == "NADP1": if acModel == "JET": @@ -2750,7 +3142,9 @@ def climbSpeed( speed.append( CvminTO * VstallTO + conv.kt2ms( - Parser.getGPFValue(self.AC.GPFdata, "V_cl_2", phase=phase) + Parser.getGPFValue( + self.AC.GPFdata, "V_cl_2", phase=phase + ) ) ) n = 1 @@ -2767,7 +3161,9 @@ def climbSpeed( cas = Vcl2 elif h >= crossOverAlt: sigma = atm.sigma(theta=theta, delta=delta) - cas = atm.mach2Cas(Mach=Mcl, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mcl, theta=theta, delta=delta, sigma=sigma + ) elif acModel == "TURBOPROP" or acModel == "PISTON": speed = list() @@ -2775,7 +3171,9 @@ def climbSpeed( speed.append( CvminTO * VstallTO + conv.kt2ms( - Parser.getGPFValue(self.AC.GPFdata, "V_cl_1", phase=phase) + Parser.getGPFValue( + self.AC.GPFdata, "V_cl_1", phase=phase + ) ) ) @@ -2793,7 +3191,9 @@ def climbSpeed( cas = Vcl2 elif h >= crossOverAlt: sigma = atm.sigma(theta=theta, delta=delta) - cas = atm.mach2Cas(Mach=Mcl, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mcl, theta=theta, delta=delta, sigma=sigma + ) elif procedure == "NADP2": if acModel == "JET": @@ -2802,13 +3202,17 @@ def climbSpeed( speed.append( Cvmin * VstallCR + conv.kt2ms( - Parser.getGPFValue(self.AC.GPFdata, "V_cl_2", phase=phase) + 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) + Parser.getGPFValue( + self.AC.GPFdata, "V_cl_2", phase=phase + ) ) ) @@ -2820,7 +3224,9 @@ def climbSpeed( if h < conv.ft2m(NADP2_ALT[0]): cas = speed[2] - elif h >= conv.ft2m(NADP2_ALT[0]) and h < conv.ft2m(NADP2_ALT[1]): + 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] @@ -2828,7 +3234,9 @@ def climbSpeed( cas = Vcl2 elif h >= crossOverAlt: sigma = atm.sigma(theta=theta, delta=delta) - cas = atm.mach2Cas(Mach=Mcl, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mcl, theta=theta, delta=delta, sigma=sigma + ) elif acModel == "TURBOPROP" or acModel == "PISTON": speed = list() @@ -2836,13 +3244,17 @@ def climbSpeed( speed.append( Cvmin * VstallCR + conv.kt2ms( - Parser.getGPFValue(self.AC.GPFdata, "V_cl_2", phase=phase) + 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) + Parser.getGPFValue( + self.AC.GPFdata, "V_cl_1", phase=phase + ) ) ) @@ -2854,7 +3266,9 @@ def climbSpeed( if h < conv.ft2m(NADP2_ALT[0]): cas = speed[2] - elif h >= conv.ft2m(NADP2_ALT[0]) and h < conv.ft2m(NADP2_ALT[1]): + 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] @@ -2862,7 +3276,9 @@ def climbSpeed( cas = Vcl2 elif h >= crossOverAlt: sigma = atm.sigma(theta=theta, delta=delta) - cas = atm.mach2Cas(Mach=Mcl, theta=theta, delta=delta, sigma=sigma) + 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 @@ -2904,24 +3320,36 @@ def cruiseSpeed( applyLimits=True, config=None, ): - """This function computes the cruise speed schedule CAS speed for any given altitude - - :param h: altitude [m]. - :param mass: aircraft mass [kg]. - :param theta: normalised air temperature [-]. - :param delta: normalised air pressure [-]. - :param DeltaTemp: deviation with respect to ISA [K] - :param speedSchedule_default: default speed schedule that will overwrite the BADA schedule [Vcr1, Vcr2, Mcr]. - :param applyLimits: apply min/max speed limitation [-]. - :type h: float. - :type mass: float. - :type theta: float. - :type delta: float. - :type DeltaTemp: float. - :type speedSchedule_default: [float, float, float]. - :type applyLimits: [boolean]. - :returns: climb calibrated airspeed (CAS) [m s^-1]. - :rtype: float + """ + 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" @@ -2949,9 +3377,15 @@ def cruiseSpeed( elif h >= conv.ft2m(14000) and h < crossOverAlt: cas = Vcr2 elif h >= crossOverAlt: - cas = atm.mach2Cas(Mach=Mcr, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mcr, theta=theta, delta=delta, sigma=sigma + ) - elif acModel == "TURBOPROP" or acModel == "PISTON" or acModel == "ELECTRIC": + 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): @@ -2961,7 +3395,9 @@ def cruiseSpeed( elif h >= conv.ft2m(10000) and h < crossOverAlt: cas = Vcr2 elif h >= crossOverAlt: - cas = atm.mach2Cas(Mach=Mcr, theta=theta, delta=delta, sigma=sigma) + 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 @@ -3004,24 +3440,36 @@ def descentSpeed( applyLimits=True, config=None, ): - """This function computes the descent speed schedule CAS speed for any given altitude - - :param h: altitude [m]. - :param mass: aircraft mass [kg]. - :param theta: normalised air temperature [-]. - :param delta: normalised air pressure [-]. - :param DeltaTemp: deviation with respect to ISA [K] - :param speedSchedule_default: default speed schedule that will overwrite the BADA schedule [Vdes1, Vdes2, Mdes]. - :param applyLimits: apply min/max speed limitation [-]. - :type h: float. - :type mass: float. - :type theta: float. - :type delta: float. - :type DeltaTemp: float. - :type speedSchedule_default: [float, float, float]. - :type applyLimits: [boolean]. - :returns: climb calibrated airspeed (CAS) [m s^-1]. - :rtype: float + """ + 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" @@ -3090,7 +3538,9 @@ def descentSpeed( elif h >= conv.ft2m(10000) and h < crossOverAlt: cas = Vdes2 elif h >= crossOverAlt: - cas = atm.mach2Cas(Mach=Mdes, theta=theta, delta=delta, sigma=sigma) + cas = atm.mach2Cas( + Mach=Mdes, theta=theta, delta=delta, sigma=sigma + ) elif acModel == "PISTON" or acModel == "ELECTRIC": speed = list() @@ -3099,7 +3549,10 @@ def descentSpeed( Cvmin * VstallDES + conv.kt2ms( Parser.getGPFValue( - self.AC.GPFdata, "V_des_7", engine="PISTON", phase=phase + self.AC.GPFdata, + "V_des_7", + engine="PISTON", + phase=phase, ) ) ) @@ -3107,7 +3560,10 @@ def descentSpeed( Cvmin * VstallDES + conv.kt2ms( Parser.getGPFValue( - self.AC.GPFdata, "V_des_6", engine="PISTON", phase=phase + self.AC.GPFdata, + "V_des_6", + engine="PISTON", + phase=phase, ) ) ) @@ -3115,7 +3571,10 @@ def descentSpeed( Cvmin * VstallDES + conv.kt2ms( Parser.getGPFValue( - self.AC.GPFdata, "V_des_5", engine="PISTON", phase=phase + self.AC.GPFdata, + "V_des_5", + engine="PISTON", + phase=phase, ) ) ) @@ -3137,7 +3596,9 @@ def descentSpeed( elif h >= conv.ft2m(10000) and h < crossOverAlt: cas = Vdes2 elif h >= crossOverAlt: - cas = atm.mach2Cas(Mach=Mdes, theta=theta, delta=delta, sigma=sigma) + 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 @@ -3184,13 +3645,17 @@ def __init__(self, AC): self.ARPM = ARPM(AC) def create(self, DeltaTemp, saveToPath): - """This function creates the BADA3 PTD file + """ + 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 saveToPath: path to directory where PTF should be stored [-] - :param DeltaTemp: deviation from ISA temperature [K] - :type saveToPath: string. + :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. - :returns: NONE + :type saveToPath: str. + :returns: None + :rtype: None """ # 3 different mass levels [kg] @@ -3199,7 +3664,11 @@ def create(self, DeltaTemp, saveToPath): else: massLow = 1.2 * self.AC.mass["minimum"] - massList = [massLow, self.AC.mass["reference"], self.AC.mass["maximum"]] + massList = [ + massLow, + self.AC.mass["reference"], + self.AC.mass["maximum"], + ] max_alt_ft = self.AC.hmo # original PTD altitude list @@ -3237,23 +3706,32 @@ def create(self, DeltaTemp, saveToPath): ) def save2PTD( - self, saveToPath, CLList_low, CLList_med, CLList_high, DESList_med, DeltaTemp + self, + saveToPath, + CLList_low, + CLList_med, + CLList_high, + DESList_med, + DeltaTemp, ): - """This function saves data to PTD file - - :param saveToPath: path to directory where PTD should be stored [-] - :param CLList_low: list of PTD data in CLIMB at low aircraft mass [-]. - :param CLList_med: list of PTD data in CLIMB at medium aircraft mass [-]. - :param CLList_high: list of PTD data in CLIMB at high aircraft mass [-]. - :param DESList_med: list of PTD data in DESCENT at medium aircraft mass [-]. - :param DeltaTemp: deviation from ISA temperature [K] - :type saveToPath: string. + """ + 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 + :returns: None """ def Nan2Zero(list): @@ -3285,7 +3763,9 @@ def Nan2Zero(list): file = open(filename, "w") file.write("BADA PERFORMANCE FILE RESULTS\n") file = open(filename, "a") - file.write("=============================\n=============================\n\n") + file.write( + "=============================\n=============================\n\n" + ) file.write("Low mass CLIMBS\n") file.write("===============\n\n") file.write( @@ -3411,15 +3891,16 @@ def Nan2Zero(list): file.write("\nTDC stands for (Thrust - Drag) * Cred\n") def PTD_climb(self, mass, altitudeList, DeltaTemp): - """This function calculates the BADA3 PTD data in CLIMB + """ + Calculates the BADA3 PTD data in climb phase. - :param mass: aircraft mass [kg] - :param altitudeList: aircraft altitude list [ft] - :param DeltaTemp: deviation from ISA temperature [K] - :type mass: float. - :type altitudeList: list of int. - :type DeltaTemp: float. - :returns: list of PTD CLIMB data [-] + :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 """ @@ -3451,7 +3932,9 @@ def PTD_climb(self, mass, altitudeList, DeltaTemp): for h in altitudeList: H_m = conv.ft2m(h) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) [cas, speedUpdated] = self.ARPM.climbSpeed( theta=theta, delta=delta, @@ -3547,16 +4030,20 @@ def PTD_climb(self, mass, altitudeList, DeltaTemp): return CLList def PTD_descent(self, mass, altitudeList, DeltaTemp): - """This function calculates the BADA3 PTD data in DESCENT + """ + 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 maximum altitude [ft] - :param DeltaTemp: deviation from ISA temperature [K] + :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 PTD DESCENT data [-] - :rtype: list + :returns: List of descent performance data. + :rtype: list. """ FL_complet = [] @@ -3587,7 +4074,9 @@ def PTD_descent(self, mass, altitudeList, DeltaTemp): for h in altitudeList: H_m = conv.ft2m(h) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) [cas, speedUpdated] = self.ARPM.descentSpeed( theta=theta, delta=delta, @@ -3609,10 +4098,17 @@ def PTD_descent(self, mass, altitudeList, 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": + 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 + rating="LIDL", + v=tas, + h=H_m, + config="CR", + DeltaTemp=DeltaTemp, ) ff = ( self.ff( @@ -3627,7 +4123,11 @@ def PTD_descent(self, mass, altitudeList, DeltaTemp): ) else: Thrust = self.Thrust( - rating="LIDL", v=tas, h=H_m, config=config, DeltaTemp=DeltaTemp + rating="LIDL", + v=tas, + h=H_m, + config=config, + DeltaTemp=DeltaTemp, ) ff = ( self.ff( @@ -3668,7 +4168,9 @@ def PTD_descent(self, mass, altitudeList, DeltaTemp): * 60 ) - tau_const = (theta * const.temp_0) / (theta * const.temp_0 - DeltaTemp) + tau_const = (theta * const.temp_0) / ( + theta * const.temp_0 - DeltaTemp + ) dhdt = (conv.ft2m(ROCD / 60)) * tau_const if self.AC.drone: @@ -3716,7 +4218,7 @@ def PTD_descent(self, mass, altitudeList, DeltaTemp): class PTF(BADA3): - """This class implements the PTD file creator for BADA3 aircraft following BADA3 manual. + """This class implements the PTF file creator for BADA3 aircraft following BADA3 manual. :param AC: parsed aircraft. :type AC: bada3.Parse. @@ -3729,13 +4231,17 @@ def __init__(self, AC): self.ARPM = ARPM(AC) def create(self, DeltaTemp, saveToPath): - """This function creates the BADA3 PTF file + """ + 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 saveToPath: path to directory where PTF should be stored [-] - :param DeltaTemp: deviation from ISA temperature [K] - :type saveToPath: string. + :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. - :returns: NONE + :type saveToPath: str. + :returns: None + :rtype: None """ # 3 different mass levels [kg] @@ -3744,7 +4250,11 @@ def create(self, DeltaTemp, saveToPath): else: massLow = 1.2 * self.AC.mass["minimum"] - massList = [massLow, self.AC.mass["reference"], self.AC.mass["maximum"]] + massList = [ + massLow, + self.AC.mass["reference"], + self.AC.mass["maximum"], + ] max_alt_ft = self.AC.hmo # original PTF altitude list @@ -3780,25 +4290,33 @@ def create(self, DeltaTemp, saveToPath): ) def save2PTF( - self, saveToPath, CRList, CLList, DESList, altitudeList, massList, DeltaTemp + self, + saveToPath, + CRList, + CLList, + DESList, + altitudeList, + massList, + DeltaTemp, ): - """This function saves data to PTF file - - :param saveToPath: path to directory where PTF should be stored [-] - :param altitudeList: aircraft altitude list [ft] - :param massList: aircraft mass list [kg] - :param CRList: list of PTF data in CRUISE [-]. - :param CLList: list of PTF data in CLIMB [-]. - :param DESList: list of PTF data in DESCENT [-]. - :param DeltaTemp: deviation from ISA temperature [K] + """ + 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 altitudeList: list of int. - :type massList: list of int. :type CRList: list. :type CLList: list. :type DESList: list. + :type altitudeList: list of int. + :type massList: list of int. :type DeltaTemp: float. - :returns: NONE + :returns: None """ def Nan2Zero(list): @@ -3844,7 +4362,8 @@ def Nan2Zero(list): file = open(filename, "w") file.write( - "BADA PERFORMANCE FILE %s\n\n" % (d3) + "BADA PERFORMANCE FILE %s\n\n" + % (d3) ) file = open(filename, "a") file.write("AC/Type: %s\n" % (acName)) @@ -3940,16 +4459,16 @@ def Nan2Zero(list): ) def PTF_cruise(self, massList, altitudeList, DeltaTemp): - """This function calculates the BADA3 PTF data in CRUISE + """Calculates BADA3 PTF data for the cruise phase. - :param massList: list of aircraft mass [kg] - :param altitudeList: aircraft altitude list [ft] - :param DeltaTemp: deviation from ISA temperature [K] - :type massList: list. + :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 of PTF CRUISE data [-] - :rtype: list + :returns: List containing cruise phase TAS and fuel flow data. + :rtype: list. """ TAS_CR_complet = [] @@ -3968,7 +4487,9 @@ def PTF_cruise(self, massList, altitudeList, DeltaTemp): for h in altitudeList: H_m = conv.ft2m(h) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) [cas, speedUpdated] = self.ARPM.cruiseSpeed( theta=theta, delta=delta, @@ -3996,28 +4517,35 @@ def PTF_cruise(self, massList, altitudeList, DeltaTemp): 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) + 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] + CRList = [ + TAS_CR_complet, + FF_CR_LO_complet, + FF_CR_NOM_complet, + FF_CR_HI_complet, + ] return CRList def PTF_climb(self, massList, altitudeList, DeltaTemp): - """This function calculates the BADA3 PTF data in CLIMB + """Calculates BADA3 PTF data for the climb phase. - :param massList: list of aircraft mass [kg] - :param altitudeList: aircraft altitude list [ft] - :param DeltaTemp: deviation from ISA temperature [K] - :type massList: list. + :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 of PTF CLIMB data [-] - :rtype: list + :returns: List containing climb phase TAS, ROCD, and fuel flow data. + :rtype: list. """ TAS_CL_complet = [] @@ -4038,7 +4566,9 @@ def PTF_climb(self, massList, altitudeList, DeltaTemp): for h in altitudeList: H_m = conv.ft2m(h) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) FL = h / 100 ROC = [] @@ -4058,22 +4588,36 @@ def PTF_climb(self, massList, altitudeList, DeltaTemp): 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 + 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 + 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 + h=H_m, + flightEvolution="constCAS", + M=M, + DeltaTemp=DeltaTemp, ) else: ESF = self.esf( - h=H_m, flightEvolution="constM", M=M, DeltaTemp=DeltaTemp + h=H_m, + flightEvolution="constM", + M=M, + DeltaTemp=DeltaTemp, ) # I think this should use all config, not just for nominal weight @@ -4117,16 +4661,16 @@ def PTF_climb(self, massList, altitudeList, DeltaTemp): return CLList def PTF_descent(self, massList, altitudeList, DeltaTemp): - """This function calculates the BADA3 PTF data in DESCENT + """Calculates BADA3 PTF data for the descent phase. - :param massList: list of aircraft mass [kg] - :param altitudeList: aircraft altitude list [ft] - :param DeltaTemp: deviation from ISA temperature [K] - :type massList: list. + :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 of PTF DESCENT data [-] - :rtype: list + :returns: List containing descent phase TAS, ROCD, and fuel flow data. + :rtype: list. """ TAS_DES_complet = [] @@ -4145,7 +4689,9 @@ def PTF_descent(self, massList, altitudeList, DeltaTemp): for h in altitudeList: H_m = conv.ft2m(h) # altitude [m] - [theta, delta, sigma] = atm.atmosphereProperties(h=H_m, DeltaTemp=DeltaTemp) + [theta, delta, sigma] = atm.atmosphereProperties( + h=H_m, DeltaTemp=DeltaTemp + ) [cas, speedUpdated] = self.ARPM.descentSpeed( theta=theta, delta=delta, @@ -4160,14 +4706,21 @@ def PTF_descent(self, massList, altitudeList, DeltaTemp): FL = h / 100 config = self.flightEnvelope.getConfig( - h=H_m, phase="Descent", v=cas, mass=massNominal, DeltaTemp=DeltaTemp + 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": + 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", @@ -4241,12 +4794,24 @@ def PTF_descent(self, massList, altitudeList, DeltaTemp): class Bada3Aircraft(BADA3): - """This class implements the BADA3 performance model following the BADA3 manual. - - :param filePath: path to the BADA3 ascii formatted file. - :param acName: ICAO aircraft designation - :type filePath: str. - :type acName: str + """ + 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): @@ -4281,7 +4846,9 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): ) self.ICAO = Parser.safe_get(filtered_df, "ICAO", None) - self.numberOfEngines = Parser.safe_get(filtered_df, "numberOfEngines", None) + self.numberOfEngines = Parser.safe_get( + filtered_df, "numberOfEngines", None + ) self.engineType = Parser.safe_get(filtered_df, "engineType", None) self.engines = Parser.safe_get(filtered_df, "engines", None) self.WTC = Parser.safe_get(filtered_df, "WTC", None) @@ -4327,7 +4894,9 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.drone = Parser.safe_get(filtered_df, "drone", None) self.DeltaCD = Parser.safe_get(filtered_df, "DeltaCD", None) - self.speedSchedule = Parser.safe_get(filtered_df, "speedSchedule", None) + self.speedSchedule = Parser.safe_get( + filtered_df, "speedSchedule", None + ) self.aeroConfig = Parser.safe_get(filtered_df, "aeroConfig", None) self.flightEnvelope = FlightEnvelope(self) @@ -4370,13 +4939,19 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): # check for existence of OPF and APF files OPFfile = ( os.path.join( - self.filePath, "BADA3", badaVersion, self.SearchedACName + self.filePath, + "BADA3", + badaVersion, + self.SearchedACName, ) + ".OPF" ) APFfile = ( os.path.join( - self.filePath, "BADA3", badaVersion, self.SearchedACName + self.filePath, + "BADA3", + badaVersion, + self.SearchedACName, ) + ".APF" ) @@ -4403,7 +4978,9 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): ) self.acName = Parser.safe_get(combined_df, "acName", None) - self.xmlFiles = Parser.safe_get(combined_df, "xmlFiles", None) + self.xmlFiles = Parser.safe_get( + combined_df, "xmlFiles", None + ) self.modificationDateOPF = Parser.safe_get( combined_df, "modificationDateOPF", None @@ -4416,8 +4993,12 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.numberOfEngines = Parser.safe_get( combined_df, "numberOfEngines", None ) - self.engineType = Parser.safe_get(combined_df, "engineType", None) - self.engines = Parser.safe_get(combined_df, "engines", None) + self.engineType = Parser.safe_get( + combined_df, "engineType", None + ) + self.engines = Parser.safe_get( + combined_df, "engines", None + ) self.WTC = Parser.safe_get(combined_df, "WTC", None) self.mass = Parser.safe_get(combined_df, "mass", None) @@ -4429,7 +5010,9 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.MMO = Parser.safe_get(combined_df, "MMO", None) self.hmo = Parser.safe_get(combined_df, "hmo", None) self.Hmax = Parser.safe_get(combined_df, "Hmax", None) - self.tempGrad = Parser.safe_get(combined_df, "tempGrad", None) + self.tempGrad = Parser.safe_get( + combined_df, "tempGrad", None + ) self.S = Parser.safe_get(combined_df, "S", None) self.Clbo = Parser.safe_get(combined_df, "Clbo", None) @@ -4439,10 +5022,18 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.CD2 = Parser.safe_get(combined_df, "CD2", None) self.HLids = Parser.safe_get(combined_df, "HLids", None) self.Ct = Parser.safe_get(combined_df, "Ct", None) - self.CTdeslow = Parser.safe_get(combined_df, "CTdeslow", None) - self.CTdeshigh = Parser.safe_get(combined_df, "CTdeshigh", None) - self.CTdesapp = Parser.safe_get(combined_df, "CTdesapp", None) - self.CTdesld = Parser.safe_get(combined_df, "CTdesld", None) + self.CTdeslow = Parser.safe_get( + combined_df, "CTdeslow", None + ) + self.CTdeshigh = Parser.safe_get( + combined_df, "CTdeshigh", None + ) + self.CTdesapp = Parser.safe_get( + combined_df, "CTdesapp", None + ) + self.CTdesld = Parser.safe_get( + combined_df, "CTdesld", None + ) self.HpDes = Parser.safe_get(combined_df, "HpDes", None) self.Cf = Parser.safe_get(combined_df, "Cf", None) self.CfDes = Parser.safe_get(combined_df, "CfDes", None) @@ -4456,15 +5047,21 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.V2 = Parser.safe_get(combined_df, "V2", None) self.M = Parser.safe_get(combined_df, "M", None) - self.GPFdata = Parser.safe_get(combined_df, "GPFdata", None) + self.GPFdata = Parser.safe_get( + combined_df, "GPFdata", None + ) self.drone = Parser.safe_get(combined_df, "drone", None) - self.DeltaCD = Parser.safe_get(combined_df, "DeltaCD", None) + self.DeltaCD = Parser.safe_get( + combined_df, "DeltaCD", None + ) self.speedSchedule = Parser.safe_get( combined_df, "speedSchedule", None ) - self.aeroConfig = Parser.safe_get(combined_df, "aeroConfig", None) + self.aeroConfig = Parser.safe_get( + combined_df, "aeroConfig", None + ) self.flightEnvelope = FlightEnvelope(self) self.ARPM = ARPM(self) @@ -4478,10 +5075,14 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.filePath, badaVersion, self.SearchedACName ) - combined_df = Parser.combineACDATA_GPF(XMLDataFrame, GPFDataFrame) + combined_df = Parser.combineACDATA_GPF( + XMLDataFrame, GPFDataFrame + ) self.acName = Parser.safe_get(combined_df, "acName", None) - self.xmlFiles = Parser.safe_get(combined_df, "xmlFiles", None) + self.xmlFiles = Parser.safe_get( + combined_df, "xmlFiles", None + ) self.modificationDateOPF = Parser.safe_get( combined_df, "modificationDateOPF", None @@ -4494,8 +5095,12 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.numberOfEngines = Parser.safe_get( combined_df, "numberOfEngines", None ) - self.engineType = Parser.safe_get(combined_df, "engineType", None) - self.engines = Parser.safe_get(combined_df, "engines", None) + self.engineType = Parser.safe_get( + combined_df, "engineType", None + ) + self.engines = Parser.safe_get( + combined_df, "engines", None + ) self.WTC = Parser.safe_get(combined_df, "WTC", None) self.mass = Parser.safe_get(combined_df, "mass", None) @@ -4507,7 +5112,9 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.MMO = Parser.safe_get(combined_df, "MMO", None) self.hmo = Parser.safe_get(combined_df, "hmo", None) self.Hmax = Parser.safe_get(combined_df, "Hmax", None) - self.tempGrad = Parser.safe_get(combined_df, "tempGrad", None) + self.tempGrad = Parser.safe_get( + combined_df, "tempGrad", None + ) self.S = Parser.safe_get(combined_df, "S", None) self.Clbo = Parser.safe_get(combined_df, "Clbo", None) @@ -4517,10 +5124,18 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.CD2 = Parser.safe_get(combined_df, "CD2", None) self.HLids = Parser.safe_get(combined_df, "HLids", None) self.Ct = Parser.safe_get(combined_df, "Ct", None) - self.CTdeslow = Parser.safe_get(combined_df, "CTdeslow", None) - self.CTdeshigh = Parser.safe_get(combined_df, "CTdeshigh", None) - self.CTdesapp = Parser.safe_get(combined_df, "CTdesapp", None) - self.CTdesld = Parser.safe_get(combined_df, "CTdesld", None) + self.CTdeslow = Parser.safe_get( + combined_df, "CTdeslow", None + ) + self.CTdeshigh = Parser.safe_get( + combined_df, "CTdeshigh", None + ) + self.CTdesapp = Parser.safe_get( + combined_df, "CTdesapp", None + ) + self.CTdesld = Parser.safe_get( + combined_df, "CTdesld", None + ) self.HpDes = Parser.safe_get(combined_df, "HpDes", None) self.Cf = Parser.safe_get(combined_df, "Cf", None) self.CfDes = Parser.safe_get(combined_df, "CfDes", None) @@ -4534,15 +5149,21 @@ def __init__(self, badaVersion, acName, filePath=None, allData=None): self.V2 = Parser.safe_get(combined_df, "V2", None) self.M = Parser.safe_get(combined_df, "M", None) - self.GPFdata = Parser.safe_get(combined_df, "GPFdata", None) + self.GPFdata = Parser.safe_get( + combined_df, "GPFdata", None + ) self.drone = Parser.safe_get(combined_df, "drone", None) - self.DeltaCD = Parser.safe_get(combined_df, "DeltaCD", None) + self.DeltaCD = Parser.safe_get( + combined_df, "DeltaCD", None + ) self.speedSchedule = Parser.safe_get( combined_df, "speedSchedule", None ) - self.aeroConfig = Parser.safe_get(combined_df, "aeroConfig", None) + self.aeroConfig = Parser.safe_get( + combined_df, "aeroConfig", None + ) self.flightEnvelope = FlightEnvelope(self) self.ARPM = ARPM(self) diff --git a/src/pyBADA/configuration.py b/src/pyBADA/configuration.py index 2024f68..ff53259 100644 --- a/src/pyBADA/configuration.py +++ b/src/pyBADA/configuration.py @@ -11,6 +11,17 @@ def getVersionsList(badaFamily): + """Retrieve a list of available BADA versions for a given BADA family. + + This function scans the directory corresponding to the specified BADA family + and returns a list of all subdirectories (which represent different versions of BADA). + + :param badaFamily: The BADA family (e.g., BADA3, BADA4) for which versions are being retrieved. + :type badaFamily: str. + :returns: List of available BADA versions. + :rtype: list of str. + """ + # list file and directories path = getBadaFamilyPath(badaFamily) items = os.listdir(path) @@ -24,6 +35,20 @@ def getVersionsList(badaFamily): def getAircraftList(badaFamily, badaVersion): + """Retrieve a list of available aircraft for a given BADA family and version. + + This function checks if the specified BADA family and version directory exists, and if so, + determines whether the aircraft data is stored in XML format or as standard ASCII files + (like OPF, APF, PTD, or PTF). It then returns a list of available aircraft. + + :param badaFamily: The BADA family (e.g., BADA3, BADA4) for which aircraft are being retrieved. + :param badaVersion: The specific version of the BADA family (e.g., 3.10, 4.2). + :type badaFamily: str. + :type badaVersion: str. + :returns: List of available aircraft names. + :rtype: list of str. + """ + path = getBadaVersionPath(badaFamily, badaVersion) if not os.path.exists(path): @@ -65,16 +90,43 @@ def getAircraftList(badaFamily, badaVersion): def getBadaFamilyPath(badaFamily): + """Get the full path to the specified BADA family directory. + + :param badaFamily: The BADA family (e.g., BADA3, BADA4) for which the path is required. + :type badaFamily: str. + :returns: The path to the BADA family directory. + :rtype: str. + """ + path = os.path.join(getAircraftPath(), badaFamily) return path def getBadaVersionPath(badaFamily, badaVersion): + """Get the full path to the specified BADA version directory. + + :param badaFamily: The BADA family (e.g., BADA3, BADA4) for which the path is required. + :param badaVersion: The specific version of the BADA family. + :type badaFamily: str. + :type badaVersion: str. + :returns: The path to the BADA version directory. + :rtype: str. + """ + path = os.path.join(getAircraftPath(), badaFamily, badaVersion) return path def getAircraftPath(): + """Get the path to the 'aircraft' resource directory. + + This function locates the 'aircraft' directory within the pyBADA package and + returns its absolute path. + + :returns: The absolute path to the 'aircraft' resource directory. + :rtype: str. + """ + package_name = "pyBADA" resource_name = "aircraft" @@ -86,6 +138,14 @@ def getAircraftPath(): def getDataPath(): + """Get the path to the 'data' resource directory. + + This function locates the 'data' directory within the pyBADA package and returns its absolute path. + + :returns: The absolute path to the 'data' resource directory. + :rtype: str. + """ + package_name = "pyBADA" resource_name = "data" diff --git a/src/pyBADA/flightTrajectory.py b/src/pyBADA/flightTrajectory.py index 1a7a381..6328573 100644 --- a/src/pyBADA/flightTrajectory.py +++ b/src/pyBADA/flightTrajectory.py @@ -24,30 +24,34 @@ def __init__(self): self.flightData = {} def createFT(self): - """This function creates a flightTrajectory and populate it with input data - - :param AC: aircraft {BADA3/4/H/E} - :param Hp: altitude - :param TAS: True Air Speed (TAS) - :param CAS: Calibrated Air Speed (CAS) - :param M: Mach speed (M) - :param ROCD: Rate of Climb/Descent - :param FUEL: fuel consumption - :param P: Power - :param slope: trajectory slope - :param acc: acceleration - :param THR: thrust - :param config: aerodynamic configuration - :param HLid: High Lift Device - Level of deployement - :param LG: Landing Gear level of deployment - :param mass: aircraft mass - :param LAT: Geographical Latitude - :param LON: Geographical Longitude - :param HDG: aircraft heading - :param time: time flown - :param dist: distance flown - :param comment: comment describing the trajectory segment - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. + """ + Creates and returns an empty DataFrame for storing aircraft flight trajectory data. This DataFrame includes various + flight parameters such as altitude, speed, fuel consumption, acceleration, and more. The columns are predefined to + match the data typically recorded during a flight. + + :param AC: Aircraft object from the BADA family (BADA3/4/H/E). + :param Hp: Pressure altitude [ft]. + :param TAS: True Airspeed [kt]. + :param CAS: Calibrated Airspeed [kt]. + :param M: Mach number [-]. + :param ROCD: Rate of Climb/Descent [ft/min]. + :param FUEL: Fuel consumption rate [kg/s]. + :param P: Power output of the engines [W]. + :param slope: Flight path slope [degrees]. + :param acc: Aircraft acceleration [m/s^2]. + :param THR: Thrust produced by the engines [N]. + :param config: Aerodynamic configuration of the aircraft (e.g., clean, takeoff, landing). + :param HLid: High Lift Device deployment level [-]. + :param LG: Landing gear deployment status (e.g., retracted, deployed). + :param mass: Aircraft mass [kg]. + :param LAT: Geographical latitude [degrees]. + :param LON: Geographical longitude [degrees]. + :param HDG: Aircraft heading [degrees]. + :param time: Elapsed flight time [s]. + :param dist: Distance traveled [NM]. + :param comment: Optional comment describing the trajectory segment. + + :type AC: BadaAircraft {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. :type Hp: float :type TAS: float :type CAS: float @@ -58,68 +62,71 @@ def createFT(self): :type slope: float :type acc: float :type THR: float - :type config: string + :type config: str :type HLid: float - :type LG: string + :type LG: str :type mass: float :type LAT: float :type LON: float :type HDG: float :type time: float :type dist: float - :type comment: string + :type comment: str - :returns: aircraft flight trajectory - :rtype: dict{list[float]}. + :returns: An empty DataFrame for flight trajectory data. + :rtype: pd.DataFrame """ - # Define the empty DataFrame with columns + # Create an empty DataFrame with the required flight parameters as columns flightTrajectory = pd.DataFrame( columns=[ - "Hp", - "TAS", - "CAS", - "GS", - "M", - "ROCD", - "ESF", - "FUEL", - "FUELCONSUMED", - "Preq", - "Peng", - "Pav", - "slope", - "acc", - "THR", - "DRAG", - "config", - "HLid", - "LG", - "mass", - "LAT", - "LON", - "HDGTrue", - "HDGMagnetic", - "time", - "dist", - "comment", - "BankAngle", - "ROT", + "Hp", # Pressure altitude [ft] + "TAS", # True Airspeed [kt] + "CAS", # Calibrated Airspeed [kt] + "GS", # Ground Speed [kt] + "M", # Mach number [-] + "ROCD", # Rate of Climb/Descent [ft/min] + "ESF", # Engine specific fuel consumption [-] + "FUEL", # Fuel flow rate [kg/s] + "FUELCONSUMED", # Total fuel consumed [kg] + "Preq", # Required power [W] + "Peng", # Generated power [W] + "Pav", # Available power [W] + "slope", # Flight path slope [degrees] + "acc", # Acceleration [m/s^2] + "THR", # Thrust [N] + "DRAG", # Drag force [N] + "config", # Aircraft aerodynamic configuration (clean, takeoff, etc.) + "HLid", # High Lift Device deployment level [-] + "LG", # Landing gear deployment status (e.g., up, down) + "mass", # Aircraft mass [kg] + "LAT", # Geographical latitude [degrees] + "LON", # Geographical longitude [degrees] + "HDGTrue", # True heading [degrees] + "HDGMagnetic", # Magnetic heading [degrees] + "time", # Time flown [s] + "dist", # Distance traveled [NM] + "comment", # Optional comment about the flight segment + "BankAngle", # Bank angle during the turn [degrees] + "ROT", # Rate of turn [degrees/s] ] ) return flightTrajectory @staticmethod def createFlightTrajectoryDataframe(flight_data): - """This function creates a pandas dataframe from the trajectory data in form of a list of data, - and fill in the missing values with None, to ensure the same size columns - - :param flight_data: trajectory data - :type flight_data: dict{list[float]}. - - :returns: aircraft flight trajectory - :rtype: pandas dataframe. """ + Creates a pandas DataFrame from flight trajectory data, ensuring that all lists of data have the + same length by padding shorter lists with None. This makes sure the resulting DataFrame has equal + column lengths for each parameter. + + :param flight_data: Dictionary containing flight trajectory data, where values are lists of float + values representing various parameters. + :type flight_data: dict{list[float]} + :returns: A pandas DataFrame representing the aircraft's flight trajectory. + :rtype: pandas.DataFrame + """ + # Find the maximum length of all lists in the flight data (ignore the Aircraft object) max_length = max( len(lst) if isinstance(lst, list) else 0 @@ -153,49 +160,46 @@ def pad_list(lst, max_length): return flightTrajectory_exploded def getACList(self): - """This function return the list of aircraft in the flightTrajectory object + """Returns a list of aircraft present in the flight trajectory object. - :returns: list of aircraft in the current flight trajectory object - :rtype: list[BadaAircraft]. + :returns: A list of BadaAircraft objects corresponding to the aircraft in the current flight trajectory. + :rtype: list[BadaAircraft] """ return list(self.flightData.keys()) def addFT(self, AC, flightTrajectory): - """This function adds the flight trajectory based on the aircraft + """Adds a flight trajectory for a specific aircraft to the internal data structure. - .. note::this will overwrite the stored data for the same aircraft + .. note:: This will overwrite any previously stored flight trajectory for the same aircraft. - :param AC: BadaAircraft {BADA3/4/H/E} - :param flightTrajectory: aircraft full flight trajectory - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type flightTrajectory: pandas dataframe. + :param AC: Aircraft object (BADA3/4/H/E) whose trajectory is being stored. + :param flightTrajectory: Pandas DataFrame containing the full flight trajectory for the aircraft. + :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft} + :type flightTrajectory: pandas.DataFrame """ self.flightData[AC] = flightTrajectory def getFT(self, AC): - """This function returns the flight trajectory based on the aircraft + """Returns the flight trajectory DataFrame for a specific aircraft. - :param AC: BadaAircraft {BADA3/4/H/E} - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - - :returns: aircraft flight trajectory - :rtype: pandas dataframe. + :param AC: Aircraft object (BADA3/4/H/E) whose flight trajectory is being retrieved. + :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft} + :returns: A pandas DataFrame containing the flight trajectory data of the aircraft. + :rtype: pandas.DataFrame """ return self.flightData.get(AC) def getAllValues(self, AC, parameter): - """This function returns the list of values corresponding to the aircarft trajectory - and defined parameter name - - :param AC: BadaAircraft {BADA3/4/H/E} - :param parameter: name of the parameter to search for in the flight trajectory - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type parameter: string + """Retrieves all values for a specific parameter from the aircraft's flight trajectory. - :returns: value of a selected parameter for the whole trajectory + :param AC: Aircraft object (BADA3/4/H/E) whose flight data is being queried. + :param parameter: The name of the parameter to retrieve values for (e.g., 'altitude', 'speed'). + :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft} + :type parameter: str + :returns: A list of values corresponding to the specified parameter throughout the flight. :rtype: list[float] """ @@ -207,16 +211,14 @@ def getAllValues(self, AC, parameter): return [] def getFinalValue(self, AC, parameter): - """This function returns last value corresponding to the aircarft trajectory - and defined parameter name - - :param AC: BadaAircraft {BADA3/4/H/E} - :param parameter: name of the parameter to search for in the flight trajectory - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type parameter: list[string] - - :returns: final value in the list of a selected parameter for the whole trajectory - :rtype: float or string + """Retrieves the last value for a specific parameter or a list of parameters from the aircraft's flight trajectory. + + :param AC: Aircraft object (BADA3/4/H/E) whose flight data is being queried. + :param parameter: The name or list of names of the parameter(s) to retrieve the final value(s) for. + :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft} + :type parameter: list[str] or str + :returns: The last value (or list of last values) for the specified parameter(s). + :rtype: float or list[float] """ if isinstance(parameter, list): @@ -238,13 +240,13 @@ def getFinalValue(self, AC, parameter): return self.getAllValues(AC, parameter)[-1] def append(self, AC, flightTrajectoryToAppend): - """This function will append data of 2 consecutive flight trajectories and merge them in terms of time and distance - if the aircraft is not in the list, the trajectory will be added to the flightTrajectory object + """Appends two consecutive flight trajectories and merges them, adjusting cumulative fields such as time, distance, + and fuel consumed. If the aircraft is not already present, the new trajectory will be added. - :param AC: BadaAircraft {BADA3/4/H/E} - :param flightTrajectoryToAppend: second flight trajectory to combine with original one [dict] - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type flightTrajectoryToAppend: dict{list[float]}. + :param AC: Aircraft object (BADA3/4/H/E) whose trajectory is being appended. + :param flightTrajectoryToAppend: The second flight trajectory to append, in the form of a DataFrame. + :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft} + :type flightTrajectoryToAppend: dict{list[float]} """ # retrieve the original trajectory @@ -294,18 +296,19 @@ def append(self, AC, flightTrajectoryToAppend): self.addFT(AC, flightTrajectoryCombined) def cut(self, AC, parameter, threshold, direction="BELOW"): - """This function cuts data from aircraft flight trajectory based on input field name and value. - - .. note::The value should be sorted to work as expected. - - :param AC: BadaAircraft {BADA3/4/H/E} - :param parameter: name of the parameter to take into account - :param threshold: value of the parameter where the cut shall be performed [] - :param direction: cut above or below set threshold value [] - :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. - :type parameter: string. - :type threshold: float. - :type direction: string {BELOW/ABOVE}. + """Cuts the aircraft's flight trajectory based on a specified parameter and threshold value, + keeping either the data above or below the threshold, depending on the direction. + + .. note:: The data must be sorted by the parameter for the cut to work as expected. + + :param AC: Aircraft object (BADA3/4/H/E) whose flight trajectory is being modified. + :param parameter: The name of the parameter (e.g., altitude, speed) used for filtering the data. + :param threshold: The value of the parameter that defines the cutting point. + :param direction: The direction of the cut. 'ABOVE' removes values above the threshold, 'BELOW' removes values below it. + :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft} + :type parameter: str + :type threshold: float + :type direction: str {'ABOVE', 'BELOW'} """ flightTrajectory = self.getFT(AC) @@ -323,12 +326,13 @@ def cut(self, AC, parameter, threshold, direction="BELOW"): def save2csv(self, saveToPath, separator=","): """ - This function saves the trajectory into a CSV file. + Saves the aircraft flight trajectory data into a CSV file with a custom header depending on the BADA family. + The CSV file will be saved with a timestamp in the filename. - :param saveToPath: Path to directory where the file should be stored. - :param separator: Separator to be used in the CSV file (only applicable for CSV). Default is a comma (','). - :type saveToPath: string - :type separator: string + :param saveToPath: Path to the directory where the file should be stored. + :param separator: Separator to be used in the CSV file. Default is a comma (','). + :type saveToPath: str + :type separator: str :returns: None """ @@ -592,12 +596,11 @@ def save2csv(self, saveToPath, separator=","): def save2xlsx(self, saveToPath): """ - This function saves the trajectory into a Excel/xlsx file. + Saves the aircraft flight trajectory data into an Excel (.xlsx) file with a custom header depending on the BADA family. + The Excel file will be saved with a timestamp in the filename. - :param saveToPath: Path to directory where the file should be stored. - :param separator: Separator to be used in the CSV file (only applicable for CSV). Default is a comma (','). - :type saveToPath: string - :type separator: string + :param saveToPath: Path to the directory where the file should be stored. + :type saveToPath: str :returns: None """ @@ -862,14 +865,14 @@ def save2xlsx(self, saveToPath): def save2kml(self, saveToPath): """ - This function saves the trajectory into a KML file. + Saves the aircraft flight trajectory data into a KML (Keyhole Markup Language) file for visualization in tools like Google Earth. + The KML file is generated with a timestamp in the filename and includes aircraft trajectory details with altitude extrusion. - :param saveToPath: Path to directory where the file should be stored. - :param separator: Separator to be used in the CSV file (only applicable for CSV). Default is a comma (','). - :type saveToPath: string - :type separator: string + :param saveToPath: Path to the directory where the file should be stored. + :type saveToPath: str :returns: None """ + # Create a KML object kml = simplekml.Kml() diff --git a/src/pyBADA/geodesic.py b/src/pyBADA/geodesic.py index 10fa8c8..47ee1fa 100644 --- a/src/pyBADA/geodesic.py +++ b/src/pyBADA/geodesic.py @@ -6,7 +6,20 @@ 2024 """ -from math import tan, atan2, sin, asin, cos, radians, degrees, sqrt, pi, log, log2, acos +from math import ( + tan, + atan2, + sin, + asin, + cos, + radians, + degrees, + sqrt, + pi, + log, + log2, + acos, +) from pyBADA.aircraft import Airplane as airplane from pyBADA import conversions as conv from pyBADA import constants as const @@ -24,19 +37,21 @@ def __init__(self): @staticmethod def distance(LAT_init, LON_init, LAT_final, LON_final): - """This function returns the great-circle distance between two point using haversine formula. - That is the shortest distance over the earth's surface (ignoring any hills on earth surface) + """Calculate the great-circle distance between two points on the Earth's surface using the haversine formula. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param LAT_final: final Latitude [deg] - :param LON_final: final Longitude [deg] + The great-circle distance is the shortest distance between two points over the Earth's surface, + ignoring elevation changes (i.e., hills or mountains). + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float - :returns: distance [m] - :rtype: float. + :returns: Great-circle distance between the two points in meters. + :rtype: float """ phi_init = radians(LAT_init) @@ -56,21 +71,23 @@ def distance(LAT_init, LON_init, LAT_final, LON_final): @staticmethod def destinationPoint(LAT_init, LON_init, distance, bearing): - """This function returns the destination point having travelled the given distance on the initial bearing + """Calculate the destination point given an initial point, distance, and bearing. - .. note:: - bearing normally varies around path followed + Given an initial latitude and longitude, this function calculates the destination point + after traveling the specified distance along the given initial bearing (direction). - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param distance: distance travelled from initial point at defined bearing [m] - :param bearing: [deg] + Note that the bearing may vary along the path, but this calculation assumes a constant bearing. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param distance: Distance to travel from the initial point in meters. + :param bearing: Initial bearing (direction) in degrees. :type LAT_init: float :type LON_init: float :type distance: float :type bearing: float - :returns: detination point ([deg],[deg]) - :rtype: (float, float). + :returns: Tuple containing the destination latitude and longitude in degrees. + :rtype: (float, float) """ delta = distance / (const.AVG_EARTH_RADIUS_KM * 1000) @@ -79,9 +96,9 @@ def destinationPoint(LAT_init, LON_init, distance, bearing): phi_init = radians(LAT_init) lambda_init = radians(LON_init) - sinPhi_final = sin(phi_init) * cos(delta) + cos(phi_init) * sin(delta) * cos( - theta - ) + sinPhi_final = sin(phi_init) * cos(delta) + cos(phi_init) * sin( + delta + ) * cos(theta) phi_final = asin(sinPhi_final) y = sin(theta) * sin(delta) * cos(phi_init) x = cos(delta) - sin(phi_init) * sinPhi_final @@ -94,23 +111,28 @@ def destinationPoint(LAT_init, LON_init, distance, bearing): @staticmethod def bearing(LAT_init, LON_init, LAT_final, LON_final): - """This function returns the initial bearing (sometimes referred to as forward azimuth), which if followed - in a straight line along the great-circle arc will take you from start point to the end point + """Calculate the initial bearing between two points along a great-circle path. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param LAT_final: final Latitude [deg] - :param LON_final: final Longitude [deg] + The initial bearing (forward azimuth) is the direction one would need to travel + in a straight line along the great-circle arc from the start point to the end point. + + This bearing is measured clockwise from true north. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float - :returns: initial bearing [deg] - :rtype: float. + :returns: Initial bearing in degrees (0° to 360°). + :rtype: float """ bearing = atan2( - sin(radians(LON_final) - radians(LON_init)) * cos(radians(LAT_final)), + sin(radians(LON_final) - radians(LON_init)) + * cos(radians(LAT_final)), cos(radians(LAT_init)) * sin(radians(LAT_final)) - sin(radians(LAT_init)) * cos(radians(LAT_final)) @@ -131,20 +153,21 @@ class Vincenty(object): @staticmethod def distance_bearing(LAT_init, LON_init, LAT_final, LON_final): - """This function returns the geodesic distance, initial and final bearing between - a pair of latitude/longitude points on the earth's surface using an accurate ellipsoidal - model of earth - - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param LAT_final: final Latitude [deg] - :param LON_final: final Longitude [deg] + """Calculate the geodesic distance, initial bearing, and final bearing between two points on the Earth's surface. + + This method uses the Vincenty formula to account for the Earth's ellipsoidal shape, providing accurate + calculations for long distances. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float - :returns: [distance, initial bearing, final bearing] [m, deg, deg] - :rtype: (float, float, float). + :returns: Tuple containing distance in meters, initial bearing in degrees, and final bearing in degrees. + :rtype: (float, float, float) """ LON2 = radians(LON_final) @@ -167,7 +190,9 @@ def distance_bearing(LAT_init, LON_init, LAT_final, LON_final): lambd = L lambd_new = 0.0 iterations = 0 - while iterations == 0 or (abs(lambd - lambd_new) > 1e-12 and iterations < 1000): + while iterations == 0 or ( + abs(lambd - lambd_new) > 1e-12 and iterations < 1000 + ): iterations += 1 sinlambda = sin(lambd) coslambda = cos(lambd) @@ -185,13 +210,21 @@ def distance_bearing(LAT_init, LON_init, LAT_final, LON_final): else: cos2sigmam = 0.0 - C = const.f / 16 * cosSqalpha * (4 + const.f * (4 - 3 * cosSqalpha)) + C = ( + const.f + / 16 + * cosSqalpha + * (4 + const.f * (4 - 3 * cosSqalpha)) + ) lambd_new = lambd lambd = L + (1 - C) * const.f * sinalpha * ( sigma + C * sinsigma - * (cos2sigmam + C * cossigma * (-1 + 2 * cos2sigmam * cos2sigmam)) + * ( + cos2sigmam + + C * cossigma * (-1 + 2 * cos2sigmam * cos2sigmam) + ) ) if antipodal: @@ -206,7 +239,9 @@ def distance_bearing(LAT_init, LON_init, LAT_final, LON_final): if iterations >= 1000: return [None, None, None] - uSq = cosSqalpha * (pow(const.a, 2) - pow(const.b, 2)) / pow(const.b, 2) + uSq = ( + cosSqalpha * (pow(const.a, 2) - pow(const.b, 2)) / pow(const.b, 2) + ) A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) deltaSigma = ( @@ -230,93 +265,111 @@ def distance_bearing(LAT_init, LON_init, LAT_final, LON_final): s = const.b * A * (sigma - deltaSigma) # initial bearing - alpha1 = atan2(cosU2 * sinlambda, cosU1 * sinU2 - sinU1 * cosU2 * coslambda) + alpha1 = atan2( + cosU2 * sinlambda, cosU1 * sinU2 - sinU1 * cosU2 * coslambda + ) alpha1 = (degrees(alpha1) + 360) % 360 # final bearing - alpha2 = atan2(cosU1 * sinlambda, -sinU1 * cosU2 + cosU1 * sinU2 * coslambda) + alpha2 = atan2( + cosU1 * sinlambda, -sinU1 * cosU2 + cosU1 * sinU2 * coslambda + ) alpha2 = (degrees(alpha2) + 360) % 360 return (s, alpha1, alpha2) @staticmethod def distance(LAT_init, LON_init, LAT_final, LON_final): - """This function returns the geodesic distance between a pair of latitude/longitude points - on the earth's surface using an accurate ellipsoidal model of earth + """Calculate the geodesic distance between two latitude/longitude points on the Earth's surface. + + This method uses an accurate ellipsoidal model (Vincenty's formula) for calculating the distance, + which is particularly useful for long distances across the globe. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param LAT_final: final Latitude [deg] - :param LON_final: final Longitude [deg] + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float - :returns: distance [m] - :rtype: float. + :returns: The geodesic distance in meters. + :rtype: float """ - dist_bearing = Vincenty.distance_bearing(LAT_init, LON_init, LAT_final, LON_final) + dist_bearing = Vincenty.distance_bearing( + LAT_init, LON_init, LAT_final, LON_final + ) return dist_bearing[0] @staticmethod def bearing_initial(LAT_init, LON_init, LAT_final, LON_final): - """This function returns the initial bearing between a pair of latitude/longitude points - on the earth's surface using an accurate ellipsoidal model of earth + """Calculate the initial bearing (forward azimuth) from the initial point to the final point. + + This function returns the initial bearing that, if followed in a straight line along a great-circle path, + will take you from the start point to the end point. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param LAT_final: final Latitude [deg] - :param LON_final: final Longitude [deg] + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float - :returns: initial bearing [deg] - :rtype: float. + :returns: The initial bearing in degrees. + :rtype: float """ - b_initial = Vincenty.distance_bearing(LAT_init, LON_init, LAT_final, LON_final) + b_initial = Vincenty.distance_bearing( + LAT_init, LON_init, LAT_final, LON_final + ) return b_initial[1] @staticmethod def bearing_final(LAT_init, LON_init, LAT_final, LON_final): - """This function returns the final bearing between a pair of latitude/longitude points - on the earth's surface using an accurate ellipsoidal model of earth + """Calculate the final bearing (reverse azimuth) from the final point to the initial point. + + This function calculates the final bearing at the destination point, which is the direction one + would need to take to return to the initial point along the great-circle path. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param LAT_final: final Latitude [deg] - :param LON_final: final Longitude [deg] + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :type LAT_init: float :type LON_init: float :type LAT_final: float :type LON_final: float - :returns: final bearing [deg] - :rtype: float. + :returns: The final bearing in degrees. + :rtype: float """ - b_final = Vincenty.distance_bearing(LAT_init, LON_init, LAT_final, LON_final) + b_final = Vincenty.distance_bearing( + LAT_init, LON_init, LAT_final, LON_final + ) return b_final[2] @staticmethod def destinationPoint_finalBearing(LAT_init, LON_init, distance, bearing): - """This function returns the destination point and final bearing having travelled the given distance on the initial bearing - from the initial point + """Calculate the destination point and final bearing given an initial point, distance, and bearing. + + This method calculates the latitude and longitude of the destination point after traveling a specified + distance along the given bearing from the starting point. It also returns the final bearing at the + destination point. - .. note:: - bearing normally varies around path followed + Note: The bearing normally varies along the path due to the Earth's curvature. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param distance: distance travelled from initial point at defined bearing [m] - :param bearing: initial bearing [deg] + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param distance: Distance traveled from the initial point in meters. + :param bearing: Initial bearing (direction) in degrees. :type LAT_init: float :type LON_init: float :type distance: float :type bearing: float - :returns: [detination point and final bearing] ([deg],[deg],[deg]) - :rtype: (float, float, float). + :returns: Tuple containing the destination latitude, destination longitude, and final bearing (degrees). + :rtype: (float, float, float) """ LON1 = radians(LON_init) @@ -332,7 +385,9 @@ def destinationPoint_finalBearing(LAT_init, LON_init, distance, bearing): sigma1 = atan2(tanU1, cosalpha1) sinalpha = cosU1 * sinalpha1 cosSqalpha = 1 - pow(sinalpha, 2) - uSq = cosSqalpha * (pow(const.a, 2) - pow(const.b, 2)) / pow(const.b, 2) + uSq = ( + cosSqalpha * (pow(const.a, 2) - pow(const.b, 2)) / pow(const.b, 2) + ) A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq))) B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq))) @@ -340,7 +395,9 @@ def destinationPoint_finalBearing(LAT_init, LON_init, distance, bearing): sigma_new = 0.0 iterations = 0 - while iterations == 0 or (abs(sigma - sigma_new) > 1e-12 and iterations < 1000): + while iterations == 0 or ( + abs(sigma - sigma_new) > 1e-12 and iterations < 1000 + ): iterations += 1 cos2sigmam = cos(2 * sigma1 + sigma) sinsigma = sin(sigma) @@ -376,7 +433,8 @@ def destinationPoint_finalBearing(LAT_init, LON_init, distance, bearing): (1 - const.f) * sqrt(sinalpha * sinalpha + x * x), ) lambd = atan2( - sinsigma * sinalpha1, cosU1 * cossigma - sinU1 * sinsigma * cosalpha1 + sinsigma * sinalpha1, + cosU1 * cossigma - sinU1 * sinsigma * cosalpha1, ) C = const.f / 16 * cosSqalpha * (4 + const.f * (4 - 3 * cosSqalpha)) L = lambd - (1 - C) * const.f * sinalpha * ( @@ -394,22 +452,21 @@ def destinationPoint_finalBearing(LAT_init, LON_init, distance, bearing): @staticmethod def destinationPoint(LAT_init, LON_init, distance, bearing): - """This function returns the destination point having travelled the given distance on the initial bearing - from the initial point + """Calculate the destination point after traveling a specified distance on a given bearing. - .. note:: - bearing normally varies around path followed + This method returns the latitude and longitude of the destination point after traveling the given + distance on the specified initial bearing, following a great-circle path. - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param distance: distance travelled from initial point at defined bearing [m] - :param bearing: initial bearing [deg] + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param distance: Distance to be traveled from the initial point in meters. + :param bearing: Initial bearing (direction) in degrees. :type LAT_init: float :type LON_init: float :type distance: float :type bearing: float - :returns: detination point ([deg],[deg]) - :rtype: (float, float). + :returns: Tuple containing the destination latitude and longitude in degrees. + :rtype: (float, float) """ dest = Vincenty.destinationPoint_finalBearing( @@ -430,9 +487,14 @@ class RhumbLine(object): @staticmethod def simple_project(latitiude: float) -> float: """ - Projects a point to its corrected latitude for the rhumbline calculations. - :param latitiude: A float in radians. - :return: The projected value in radians. + Applies a projection to the latitude for use in rhumbline calculations. + + The projection is based on the Mercator projection, where latitudes are projected + to account for the curvature of the Earth. This formula ensures that the calculations + along the rhumbline are accurate. + + :param latitiude: Latitude in radians. + :return: The projected latitude in radians. """ return tan(pi / 4 + latitiude / 2) @@ -440,9 +502,17 @@ def simple_project(latitiude: float) -> float: @staticmethod def distance(LAT_init, LON_init, LAT_final, LON_final) -> float: """ - Returns Rhumbline distance in [m] between two points. - :param point_a: Start point. Tuple of degrees. - :param point_b: End point. Tuple of degrees. + Calculates the rhumbline distance between two geographical points in meters. + + The rhumbline is a path of constant bearing that crosses all meridians at the same angle, + unlike a great-circle route which is the shortest distance between two points on the Earth's surface. + + This method adjusts for longitudes that span more than half of the globe. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. :return: The rhumbline distance in meters. """ @@ -478,10 +548,16 @@ def distance(LAT_init, LON_init, LAT_final, LON_final) -> float: @staticmethod def bearing(LAT_init, LON_init, LAT_final, LON_final) -> float: """ - Returns bearing between two points in degrees - :param point_a: Start point. Tuple of degrees. - :param point_b: End point. Tuple of degrees. - :return: The bearing in degrees. + Calculates the rhumbline bearing from the initial point to the final point. + + This returns the constant bearing (direction) required to travel along a rhumbline + between the two points. The bearing is adjusted for longitudes that cross the 180-degree meridian. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. + :return: The rhumbline bearing in degrees. """ lat_a = radians(LAT_init) @@ -506,11 +582,16 @@ def bearing(LAT_init, LON_init, LAT_final, LON_final) -> float: @staticmethod def destinationPoint(LAT_init, LON_init, bearing, distance) -> tuple: """ - Returns point B from point A, travelling at constant bearing θ in deg, and distance d in [m]. - :param point_a: Start point. Tuple of degrees. - :param bearing: The Bearing in degrees. - :param distance: The Distance in m. - :return: Point B coordinates. + Calculates the destination point given an initial point, a bearing, and a distance traveled. + + This method computes the final latitude and longitude after traveling along a rhumbline + for a given distance in meters from the initial point at a constant bearing. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param bearing: The constant bearing in degrees. + :param distance: The distance to travel from the initial point in meters. + :return: A tuple containing the destination latitude and longitude in degrees. """ lat_a = radians(LAT_init) @@ -545,12 +626,20 @@ def destinationPoint(LAT_init, LON_init, bearing, distance) -> tuple: return (lat_b, lon_b) @staticmethod - def loxodromic_mid_point(LAT_init, LON_init, LAT_final, LON_final) -> tuple: + def loxodromic_mid_point( + LAT_init, LON_init, LAT_final, LON_final + ) -> tuple: """ - Finds the rhumbline mid point between 2 points - :param point_a: Start point. Tuple of degrees. - :param point_b: End point. Tuple of degrees. - :return: The rhumbline midpoint tuple. + Calculates the midpoint along a rhumbline between two geographical points. + + The midpoint is calculated using the rhumbline path between the initial and final points. + This takes into account the Earth's curvature by projecting the latitudes. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. + :return: A tuple representing the midpoint's latitude and longitude in degrees. """ lat_a = radians(LAT_init) @@ -585,17 +674,24 @@ def loxodromic_power_interpolation( LAT_init, LON_init, LAT_final, LON_final, n_points: int ) -> list: """ - Returns n_points points between point_a and point_b - according to the rhumbline loxodromic interpolation, - using recursive programming. - :param point_a: Start point. Tuple of degrees. - :param point_b: End point. Tuple of degrees. - :param n_points: Number of midpoints. Needs to be a Power of 2 minus 1. - :returns The list of interpolation points from start to end. + Generates a specified number of points between two geographical locations along a rhumbline path. + + This method recursively calculates intermediate points between two points on the Earth's surface, + following a constant bearing rhumbline path. The number of points should be a power of 2 minus 1. + + :param LAT_init: Initial latitude in degrees. + :param LON_init: Initial longitude in degrees. + :param LAT_final: Final latitude in degrees. + :param LON_final: Final longitude in degrees. + :param n_points: Number of intermediate points to generate. Must be a power of 2 minus 1. + :return: A list of tuples, where each tuple represents an interpolated point's latitude and longitude in degrees. """ + n_points = int(n_points) if not log2(n_points + 1).is_integer(): - print("N_Points must be an power of 2 minus 1 Number! e.g. 1,3,7,15,...") + print( + "N_Points must be an power of 2 minus 1 Number! e.g. 1,3,7,15,..." + ) return [] lmp = RhumbLine.loxodromic_mid_point @@ -611,7 +707,9 @@ def solution(a, b, idx): solution(solution(a, b, 1), b, (idx - 1) / 2), ) - points = solution((LAT_init, LON_init), (LAT_final, LON_final), n_points) + points = solution( + (LAT_init, LON_init), (LAT_final, LON_final), n_points + ) # Decouple points decoupled_points = [] @@ -624,8 +722,7 @@ def solution(a, b, idx): class Turn(object): - """This class implements the calculations of geodesics turns - """ + """This class implements the calculations of geodesics turns""" @staticmethod def destinationPoint_finalBearing( @@ -638,31 +735,36 @@ def destinationPoint_finalBearing( directionOfTurn, centerPoint=None, ): - """This function returns the destination point and final bearing having travelled the given time Of Turn from the initial bearing - from the initial point while turning with Rate of Turn - - :param LAT_init: initial Latitude [deg] - :param LON_init: initial Longitude [deg] - :param timeOfTurn: time travelled from initial point from defined initial bearing [s] - :param bearingInit: initial bearing [deg] - :param TAS: aircraft True Airspeed TAS [m/s] - :param rateOfTurn: rate of turn [deg/s] - :param directionOfTurn: direction of turn {LEFT/RIGHT} - :param centerPoint: (LAT, LON) of point of rotation [deg, deg] - :type LAT_init: float - :type LON_init: float - :type timeOfTurn: float - :type bearing: float - :type TAS: float - :type rateOfTurn: float - :type directionOfTurn: string - :type centerPoint: tuple(float, float) - :returns: [detination point and final bearing] ([deg],[deg],[deg]) - :rtype: (float, float, float). + """Calculates the destination point and final bearing after traveling for a given time with a specified turn. + + This function computes the aircraft's final position and bearing after making a turn at a specified rate + of turn, direction, and true airspeed (TAS). If TAS is zero, the aircraft rotates in place. The calculation + accounts for turning radius and bank angle. + + :param LAT_init: Initial latitude [deg]. + :param LON_init: Initial longitude [deg]. + :param timeOfTurn: Time spent in turn [s]. + :param bearingInit: Initial bearing [deg]. + :param TAS: True Airspeed (TAS) [m/s]. + :param rateOfTurn: Rate of turn [deg/s]. + :param directionOfTurn: Direction of turn ('LEFT' or 'RIGHT'). + :param centerPoint: Optional latitude and longitude of the rotation center (defaults to None) [deg, deg]. + :type LAT_init: float. + :type LON_init: float. + :type timeOfTurn: float. + :type bearingInit: float. + :type TAS: float. + :type rateOfTurn: float. + :type directionOfTurn: str. + :type centerPoint: tuple(float, float). + :returns: Destination point's latitude, longitude, and final bearing [deg, deg, deg]. + :rtype: tuple(float, float, float). """ if TAS == 0: - arcLength = rateOfTurn * timeOfTurn # amount of degrees to do the rotation + arcLength = ( + rateOfTurn * timeOfTurn + ) # amount of degrees to do the rotation if directionOfTurn == "RIGHT": bearing_final = (bearingInit + arcLength) % 360 @@ -672,10 +774,16 @@ def destinationPoint_finalBearing( return (LAT_init, LON_init, bearing_final) else: - bankAngle = airplane.bankAngle(rateOfTurn=rateOfTurn, v=TAS) # [degrees] + bankAngle = airplane.bankAngle( + rateOfTurn=rateOfTurn, v=TAS + ) # [degrees] - arcLength = rateOfTurn * timeOfTurn # amount of degrees to do the rotation - turnRadius = airplane.turnRadius_bankAngle(v=TAS, ba=bankAngle) # [m] + arcLength = ( + rateOfTurn * timeOfTurn + ) # amount of degrees to do the rotation + turnRadius = airplane.turnRadius_bankAngle( + v=TAS, ba=bankAngle + ) # [m] # find center of rotation, which is at (bearingInit + 90 degrees) and distance of turnRadius if directionOfTurn == "RIGHT": @@ -721,16 +829,18 @@ def destinationPoint_finalBearing( @staticmethod def distance(rateOfTurn, TAS, timeOfTurn): - """This function returns the distance travelled the given time of turn - while turning with Rate of Turn and TAS - - :param timeOfTurn: time travelled from initial point from defined initial bearing [s] - :param TAS: aircraft True Airspeed TAS [m/s] - :param rateOfTurn: rate of turn [deg/s] - :type timeOfTurn: float - :type TAS: float - :type rateOfTurn: float - :returns: distance [m] + """Calculates the distance traveled during a turn based on the rate of turn, true airspeed, and time. + + This function computes the total distance traveled during a constant turn, based on the aircraft's rate + of turn, true airspeed, and the duration of the turn. + + :param rateOfTurn: Rate of turn [deg/s]. + :param TAS: True Airspeed (TAS) [m/s]. + :param timeOfTurn: Duration of the turn [s]. + :type rateOfTurn: float. + :type TAS: float. + :type timeOfTurn: float. + :returns: Distance traveled during the turn [m]. :rtype: float. """ @@ -741,7 +851,9 @@ def distance(rateOfTurn, TAS, timeOfTurn): arcLengthDegrees = ( rateOfTurn * timeOfTurn ) # amount of degrees to do the rotation - turnRadius = airplane.turnRadius_bankAngle(v=TAS, ba=bankAngle) # [m] + turnRadius = airplane.turnRadius_bankAngle( + v=TAS, ba=bankAngle + ) # [m] distance = radians(arcLengthDegrees) * turnRadius # arcLength [m] return distance diff --git a/src/pyBADA/magnetic.py b/src/pyBADA/magnetic.py index 720cb32..c9679ae 100644 --- a/src/pyBADA/magnetic.py +++ b/src/pyBADA/magnetic.py @@ -12,13 +12,24 @@ class Grid: - """This class implements the calculations on the magnetic declination using the grid data + """This class provides methods to calculate the magnetic declination at a given latitude and longitude + using pre-loaded grid data. + + The grid data is loaded from a JSON file, and the closest grid point is used to determine the + magnetic declination. """ def __init__(self, inputJSON=None): + """Initializes the grid with magnetic declination data. + + :param inputJSON: Path to the JSON file containing grid data. Defaults to a pre-configured path. + :type inputJSON: str, optional + """ + if inputJSON is None: inputJSON = ( - configuration.getDataPath() + "/magneticDeclinationGridData.json" + configuration.getDataPath() + + "/magneticDeclinationGridData.json" ) f = open(inputJSON) @@ -39,6 +50,14 @@ def __init__(self, inputJSON=None): self.gridData["declination"] = magneticDeclinationList def getClosestLatitude(self, LAT_target): + """Finds the closest latitude in the grid to the target latitude. + + :param LAT_target: Target latitude to search for. + :type LAT_target: float + :return: The closest latitude from the grid or None if the target is out of bounds. + :rtype: float or None + """ + latitudeList = sorted(self.gridData["LAT"]) if LAT_target < latitudeList[0] or LAT_target > latitudeList[-1]: @@ -52,11 +71,21 @@ def getClosestLatitude(self, LAT_target): else: before = latitudeList[index - 1] after = latitudeList[index] - closest = before if after - LAT_target > LAT_target - before else after + closest = ( + before if after - LAT_target > LAT_target - before else after + ) return closest def getClosestLongitude(self, LON_target): + """Finds the closest longitude in the grid to the target longitude. + + :param LON_target: Target longitude to search for. + :type LON_target: float + :return: The closest longitude from the grid or None if the target is out of bounds. + :rtype: float or None + """ + longitudeList = sorted(self.gridData["LON"]) if LON_target < longitudeList[0] or LON_target > longitudeList[-1]: @@ -70,11 +99,23 @@ def getClosestLongitude(self, LON_target): else: before = longitudeList[index - 1] after = longitudeList[index] - closest = before if after - LON_target > LON_target - before else after + closest = ( + before if after - LON_target > LON_target - before else after + ) return closest def getClosestIdx(self, LAT_target, LON_target): + """Finds the index of the closest grid point for a given latitude and longitude. + + :param LAT_target: Target latitude. + :param LON_target: Target longitude. + :type LAT_target: float + :type LON_target: float + :return: Index of the closest point in the grid or None if no point is found. + :rtype: int or None + """ + closestLAT = self.getClosestLatitude(LAT_target=LAT_target) closestLON = self.getClosestLongitude(LON_target=LON_target) @@ -96,6 +137,16 @@ def getClosestIdx(self, LAT_target, LON_target): return None def getMagneticDeclination(self, LAT_target, LON_target): + """Returns the magnetic declination for the closest grid point to the target coordinates. + + :param LAT_target: Target latitude. + :param LON_target: Target longitude. + :type LAT_target: float + :type LON_target: float + :return: Magnetic declination at the closest grid point or None if no point is found. + :rtype: float or None + """ + idx = self.getClosestIdx(LAT_target=LAT_target, LON_target=LON_target) if idx is None: diff --git a/src/pyBADA/trajectoryPrediction.py b/src/pyBADA/trajectoryPrediction.py index 3ab6ae3..c28ce46 100644 --- a/src/pyBADA/trajectoryPrediction.py +++ b/src/pyBADA/trajectoryPrediction.py @@ -27,25 +27,24 @@ def getInitialMass( fuelReserve=3600, flightPlanInitialMass=None, ): - """This function returns calculated (estimated) aircrat initial mass - derived from the breguet leduc formula - - :param AC: aircraft {BADA3/4/H/E} - :param distance: distance flown in [m] - :param altitude: cruising altitude in [m] - :param M: cruising speed in MACH [-] - :param payload: payload mass in [%] of maximum payload - :param fuelReserve: fuel reserve in [s] - :param flightPlanInitialMass: initial mass defined from the flight plan [kg] + """Calculates the estimated initial aircraft mass using the Breguet Leduc formula. + + :param AC: Aircraft object (BADA3/4/H/E). + :param distance: Distance to be flown in meters. + :param altitude: Cruising altitude in meters. + :param M: Mach number at cruising altitude. + :param payload: Percentage of the maximum payload mass (default is 60%). + :param fuelReserve: Fuel reserve in seconds (default is 3600s, or 1 hour). + :param flightPlanInitialMass: Optional initial mass from a flight plan, in kg. :type AC: {Bada3Aircraft, Bada4Aircraft, BadaEAircraft, BadaHAircraft}. :type distance: float :type altitude: float :type M: float :type payload: float :type fuelReserve: float - :type flightPlanInitialMass: float - :returns: initial mass [kg] - :rtype: float + :type flightPlanInitialMass: float, optional + :returns: Estimated initial aircraft mass in kg. + :rtype: float """ def initialMassCalculation( @@ -57,6 +56,8 @@ def initialMassCalculation( fuelReserve, flightPlanInitialMass, ): + """Helper function to calculate the initial mass based on aircraft type and flight conditions.""" + DeltaTemp = 0 [theta, delta, sigma] = atm.atmosphereProperties( h=altitude, DeltaTemp=DeltaTemp