def compute_from(self, start: FlightPoint) -> pd.DataFrame: self.complete_flight_point( start) # needed to ensure all speed values are computed. if self.target.altitude: if isinstance(self.target.altitude, str): # Target altitude will be modified along the process, so we keep track # of the original order in target CL, that is not used otherwise. self.target.CL = self.target.altitude # pylint: disable=invalid-name # let's put a numerical, negative value in self.target.altitude to # ensure there will be no problem in self._get_distance_to_target() self.target.altitude = -1000.0 self.interrupt_if_getting_further_from_target = False else: # Target altitude is fixed, back to original settings (in case # this instance is used more than once) self.target.CL = None self.interrupt_if_getting_further_from_target = True atm = AtmosphereSI(start.altitude) if self.target.equivalent_airspeed == self.CONSTANT_VALUE: start.true_airspeed = atm.get_true_airspeed( start.equivalent_airspeed) elif self.target.mach == self.CONSTANT_VALUE: start.true_airspeed = start.mach * atm.speed_of_sound return super().compute_from(start)
def _get_distance_to_target(self, flight_points: List[FlightPoint]) -> float: current = flight_points[-1] # Max flight level is first priority max_authorized_altitude = self.maximum_flight_level * 100.0 * foot if current.altitude >= max_authorized_altitude: return max_authorized_altitude - current.altitude if self.target.CL: # Optimal altitude is based on a target Mach number, though target speed # may be specified as TAS or EAS. If so, Mach number has to be computed # for target altitude and speed. # First, as target speed is expected to be set to self.CONSTANT_VALUE for one # parameter. Let's get the real value from start point. target_speed = copy(self.target) for speed_param in [ "true_airspeed", "equivalent_airspeed", "mach" ]: if isinstance(getattr(target_speed, speed_param), str): setattr(target_speed, speed_param, getattr(flight_points[0], speed_param)) # Now, let's compute target Mach number atm = AtmosphereSI(max(self.target.altitude, current.altitude)) if target_speed.equivalent_airspeed: target_speed.true_airspeed = atm.get_true_airspeed( target_speed.equivalent_airspeed) if target_speed.true_airspeed: target_speed.mach = target_speed.true_airspeed / atm.speed_of_sound # Now we compute optimal altitude optimal_altitude = self._get_optimal_altitude( current.mass, target_speed.mach, current.altitude) if self.target.CL == self.OPTIMAL_ALTITUDE: self.target.altitude = optimal_altitude else: # self.target.CL == self.OPTIMAL_FLIGHT_LEVEL: self.target.altitude = get_closest_flight_level( optimal_altitude, up_direction=False) if self.target.altitude is not None: return self.target.altitude - current.altitude if self.target.true_airspeed and self.target.true_airspeed != self.CONSTANT_VALUE: return self.target.true_airspeed - current.true_airspeed if (self.target.equivalent_airspeed and self.target.equivalent_airspeed != self.CONSTANT_VALUE): return self.target.equivalent_airspeed - current.equivalent_airspeed if self.target.mach is not None and self.target.mach != self.CONSTANT_VALUE: return self.target.mach - current.mach raise FastFlightSegmentIncompleteFlightPoint( "No valid target definition for altitude change.")
def distance_to_optimum(altitude): atm = AtmosphereSI(altitude) true_airspeed = mach * atm.speed_of_sound optimal_air_density = ( 2.0 * mass * g / (self.reference_area * true_airspeed ** 2 * self.polar.optimal_cl) ) return (atm.density - optimal_air_density) * 100.0
def complete_flight_point(self, flight_point: FlightPoint): """ Computes data for provided flight point. Assumes that it is already defined for time, altitude, mass, ground distance and speed (TAS, EAS, or Mach). :param flight_point: the flight point that will be completed in-place """ flight_point.engine_setting = self.engine_setting self._complete_speed_values(flight_point) # Mach number is capped by self.maximum_mach if flight_point.mach > self.maximum_mach: flight_point.mach = self.maximum_mach flight_point.true_airspeed = flight_point.equivalent_airspeed = None self._complete_speed_values(flight_point) atm = AtmosphereSI(flight_point.altitude) reference_force = 0.5 * atm.density * flight_point.true_airspeed ** 2 * self.reference_area if self.polar: flight_point.CL = flight_point.mass * g / reference_force flight_point.CD = self.polar.cd(flight_point.CL) else: flight_point.CL = flight_point.CD = 0.0 flight_point.drag = flight_point.CD * reference_force self._compute_propulsion(flight_point) flight_point.slope_angle, flight_point.acceleration = self._get_gamma_and_acceleration( flight_point.mass, flight_point.drag, flight_point.thrust )
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): atmosphere = AtmosphereSI( inputs["data:mission:sizing:main_route:cruise:altitude"]) cruise_speed = atmosphere.speed_of_sound * inputs[ "data:TLAR:cruise_mach"] cruise_distance = inputs[ "data:mission:sizing:main_route:cruise:distance"] ld_ratio = inputs["data:aerodynamics:aircraft:cruise:L_D_max"] sfc = inputs["data:propulsion:SFC"] range_factor = cruise_speed * ld_ratio / g / sfc # During first iterations, SFC will be incorrect and range_factor may be too low, # resulting in null or too small cruise_mass_ratio. # Forcing cruise_mass_ratio to a minimum of 0.3 avoids problems and should not # harm (no airplane loses 70% of its weight from fuel consumption) cruise_mass_ratio = np.maximum( 0.3, 1.0 / np.exp(cruise_distance / range_factor)) outputs[ "data:mission:sizing:main_route:cruise:mass_ratio"] = cruise_mass_ratio
def compute_from(self, start: FlightPoint) -> pd.DataFrame: start = FlightPoint(start) self.complete_flight_point(start) # needed to ensure all speed values are computed. if self.target.altitude and self.target.altitude < 0.0: # Target altitude will be modified along the process, so we keep track # of the original order in target CL, that is not used otherwise. self.target.CL = self.target.altitude self.interrupt_if_getting_further_from_target = False atm = AtmosphereSI(start.altitude) if self.target.equivalent_airspeed == "constant": start.true_airspeed = atm.get_true_airspeed(start.equivalent_airspeed) elif self.target.mach == "constant": start.true_airspeed = start.mach * atm.speed_of_sound return super().compute_from(start)
def _get_distance_to_target(self, flight_points: List[FlightPoint]) -> bool: current = flight_points[-1] if self.target.CL: # Optimal altitude is based on a target Mach number, though target speed # may be specified as TAS or EAS. If so, Mach number has to be computed # for target altitude and speed. # First, as target speed is expected to be set to "constant" for one # parameter. Let's get the real value from start point. target_speed = FlightPoint(self.target) for speed_param in ["true_airspeed", "equivalent_airspeed", "mach"]: if isinstance(target_speed.get(speed_param), str): target_speed[speed_param] = flight_points[0][speed_param] # Now, let's compute target Mach number atm = AtmosphereSI(max(self.target.altitude, current.altitude)) if target_speed.equivalent_airspeed: target_speed.true_airspeed = atm.get_true_airspeed(target_speed.equivalent_airspeed) if target_speed.true_airspeed: target_speed.mach = target_speed.true_airspeed / atm.speed_of_sound # Mach number has to be capped by self.maximum_mach target_mach = min(target_speed.mach, self.maximum_mach) # Now we compute optimal altitude optimal_altitude = self._get_optimal_altitude( current.mass, target_mach, current.altitude ) if self.target.CL == self.OPTIMAL_ALTITUDE: self.target.altitude = optimal_altitude else: # self.target.CL == self.OPTIMAL_FLIGHT_LEVEL: flight_level = 1000 * foot self.target.altitude = flight_level * np.floor(optimal_altitude / flight_level) if self.target.altitude: return self.target.altitude - current.altitude elif self.target.true_airspeed: return self.target.true_airspeed - current.true_airspeed elif self.target.equivalent_airspeed: return self.target.equivalent_airspeed - current.equivalent_airspeed elif self.target.mach: return self.target.mach - current.mach
def _complete_speed_values(flight_point: FlightPoint): """ Computes consistent values between TAS, EAS and Mach, assuming one of them is defined. """ atm = AtmosphereSI(flight_point.altitude) if flight_point.true_airspeed is None: if flight_point.mach is not None: flight_point.true_airspeed = flight_point.mach * atm.speed_of_sound elif flight_point.equivalent_airspeed is not None: flight_point.true_airspeed = atm.get_true_airspeed( flight_point.equivalent_airspeed) else: raise FastFlightSegmentIncompleteFlightPoint( "Flight point should be defined for true_airspeed, " "equivalent_airspeed, or mach.") if flight_point.mach is None: flight_point.mach = flight_point.true_airspeed / atm.speed_of_sound if flight_point.equivalent_airspeed is None: flight_point.equivalent_airspeed = atm.get_equivalent_airspeed( flight_point.true_airspeed)
def compute(self, inputs, outputs): if self.low_speed_aero: mach = inputs["data:aerodynamics:aircraft:takeoff:mach"] altitude = 0.0 else: mach = inputs["data:TLAR:cruise_mach"] altitude = inputs["data:mission:sizing:main_route:cruise:altitude"] reynolds = AtmosphereSI(altitude).get_unitary_reynolds(mach) if self.low_speed_aero: outputs["data:aerodynamics:wing:low_speed:reynolds"] = reynolds else: outputs["data:aerodynamics:wing:cruise:reynolds"] = reynolds
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): F = inputs["data:propeller:force:hover:z"] D = inputs["data:propeller:diameter"] Ct = inputs["data:propeller:Ct"] altitude = inputs["settings:altitude"] atm = AtmosphereSI(altitude) rho = atm.density n = np.sqrt(F / (Ct * rho * D**4.0)) omega = n * 2.0 * pi outputs["data:propeller:frequency:hover"] = n outputs["data:propeller:speed:hover"] = omega
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): F_pro = inputs["data:propeller:force:takeoff:z"] Ct = inputs["data:propeller:Ct"] altitude = inputs["settings:altitude"] k_ND = inputs["data:propeller:ND:k"] ND_max = inputs["data:propeller:ND:max"] atm = AtmosphereSI(altitude) rho = atm.density ND = ND_max / k_ND D_pro = (F_pro / (Ct * rho * ND**2.0))**0.5 outputs["data:propeller:ND"] = ND outputs["data:propeller:diameter"] = D_pro
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): D = inputs["data:propeller:diameter"] n = inputs["data:propeller:frequency:hover"] omega = inputs["data:propeller:speed:hover"] Cp = inputs["data:propeller:Cp"] altitude = inputs["settings:altitude"] atm = AtmosphereSI(altitude) rho = atm.density P = Cp * rho * n**3.0 * D**5.0 T = P / omega outputs["data:propeller:power:hover"] = P outputs["data:propeller:torque:hover"] = T
def compute_cruise_mass_ratio(self, initial_cruise_mass, cruise_distance): """ :param initial_cruise_mass: :param cruise_distance: :return: (mass at end of cruise) / (mass at start of cruise) """ self.thrust = initial_cruise_mass / self.lift_drag_ratio * g flight_point = FlightPoint( mach=self.cruise_mach, altitude=self.cruise_altitude, engine_setting=EngineSetting.CRUISE, thrust=self.thrust, ) self.propulsion.compute_flight_points(flight_point) self.sfc = flight_point.sfc self.thrust_rate = flight_point.thrust_rate atmosphere = AtmosphereSI(self.cruise_altitude) cruise_speed = atmosphere.speed_of_sound * self.cruise_mach range_factor = cruise_speed * self.lift_drag_ratio / g / self.sfc return 1.0 / np.exp(cruise_distance / range_factor)
# Copyright (C) 2020 ONERA & ISAE-SUPAERO # FAST is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see <https://www.gnu.org/licenses/>. from fastoad.utils.physics import AtmosphereSI # Atmosphere at limits of troposhere ATM_SEA_LEVEL = AtmosphereSI(0) ATM_TROPOPAUSE = AtmosphereSI(11000) # Constants for computation of maximum thrust --------------------------------- # (see E. Roux model definition in roux:2005) A_MS = -2.74e-4 A_FM = 2.67e-4 B_MS = 1.91e-2 B_FM = -2.35e-2 C_MS = 1.21e-3 C_FM = -1.32e-3 D_MS = -8.48e-4 D_FM = 3.14e-4 E_MS = 8.96e-1 E_FM = 5.22e-1