def compute_next_flight_point( self, flight_points: List[FlightPoint], time_step: float ) -> FlightPoint: """ Computes time, altitude, speed, mass and ground distance of next flight point. :param flight_points: previous flight points :param time_step: time step for computing next point :return: the computed next flight point """ start = flight_points[0] previous = flight_points[-1] next_point = FlightPoint() next_point.mass = previous.mass - self.propulsion.get_consumed_mass(previous, time_step) next_point.time = previous.time + time_step next_point.ground_distance = ( previous.ground_distance + previous.true_airspeed * time_step * np.cos(previous.slope_angle) ) self._compute_next_altitude(next_point, previous) if self.target.true_airspeed == "constant": next_point.true_airspeed = previous.true_airspeed elif self.target.equivalent_airspeed == "constant": next_point.equivalent_airspeed = start.equivalent_airspeed elif self.target.mach == "constant": next_point.mach = start.mach else: next_point.true_airspeed = previous.true_airspeed + time_step * previous.acceleration # The naming is not done in complete_flight_point for not naming the start point next_point.name = self.name return next_point
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 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 _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 _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 compute_mission(self, inputs, outputs): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"] ) reference_area = inputs["data:geometry:wing:area"] cruise_mach = inputs["data:TLAR:cruise_mach"] flight_distance = inputs["data:TLAR:range"] thrust_rates = { FlightPhase.CLIMB: inputs["data:mission:sizing:climb:thrust_rate"], FlightPhase.DESCENT: inputs["data:mission:sizing:descent:thrust_rate"], } high_speed_polar = Polar( inputs["data:aerodynamics:aircraft:cruise:CL"], inputs["data:aerodynamics:aircraft:cruise:CD"], ) low_speed_climb_polar = Polar( inputs["data:aerodynamics:aircraft:takeoff:CL"], inputs["data:aerodynamics:aircraft:takeoff:CD"], ) base_flight_calculator = RangedFlight( StandardFlight( propulsion=propulsion_model, reference_area=reference_area, low_speed_climb_polar=low_speed_climb_polar, high_speed_polar=high_speed_polar, cruise_mach=cruise_mach, thrust_rates=thrust_rates, ), flight_distance, ) end_of_takeoff = FlightPoint( mass=inputs["data:weight:aircraft:MTOW"] - inputs["data:mission:sizing:takeoff:fuel"], true_airspeed=inputs["data:mission:sizing:takeoff:V2"], altitude=inputs["data:mission:sizing:takeoff:altitude"] + 35 * foot, ground_distance=0.0, ) flight_points = base_flight_calculator.compute_from(end_of_takeoff) # Update start flight point with computed (non initialized) parameters end_of_takeoff = FlightPoint(flight_points.iloc[0]) # Get flight points for each end of phase end_of_initial_climb = FlightPoint( flight_points.loc[flight_points.name == FlightPhase.INITIAL_CLIMB.value].iloc[-1] ) end_of_climb = FlightPoint( flight_points.loc[flight_points.name == FlightPhase.CLIMB.value].iloc[-1] ) end_of_cruise = FlightPoint( flight_points.loc[flight_points.name == FlightPhase.CRUISE.value].iloc[-1] ) end_of_descent = FlightPoint( flight_points.loc[flight_points.name == FlightPhase.DESCENT.value].iloc[-1] ) # Set OpenMDAO outputs outputs["data:mission:sizing:initial_climb:fuel"] = ( end_of_takeoff.mass - end_of_initial_climb.mass ) outputs["data:mission:sizing:main_route:climb:fuel"] = ( end_of_initial_climb.mass - end_of_climb.mass ) outputs["data:mission:sizing:main_route:cruise:fuel"] = ( end_of_climb.mass - end_of_cruise.mass ) outputs["data:mission:sizing:main_route:descent:fuel"] = ( end_of_cruise.mass - end_of_descent.mass ) outputs["data:mission:sizing:initial_climb:distance"] = ( end_of_initial_climb.ground_distance - end_of_takeoff.ground_distance ) outputs["data:mission:sizing:main_route:climb:distance"] = ( end_of_climb.ground_distance - end_of_initial_climb.ground_distance ) outputs["data:mission:sizing:main_route:cruise:distance"] = ( end_of_cruise.ground_distance - end_of_climb.ground_distance ) outputs["data:mission:sizing:main_route:descent:distance"] = ( end_of_descent.ground_distance - end_of_cruise.ground_distance ) outputs["data:mission:sizing:initial_climb:duration"] = ( end_of_initial_climb.time - end_of_takeoff.time ) outputs["data:mission:sizing:main_route:climb:duration"] = ( end_of_climb.time - end_of_initial_climb.time ) outputs["data:mission:sizing:main_route:cruise:duration"] = ( end_of_cruise.time - end_of_climb.time ) outputs["data:mission:sizing:main_route:descent:duration"] = ( end_of_descent.time - end_of_cruise.time ) # Diversion flight ===================================================== diversion_distance = inputs["data:mission:sizing:diversion:distance"] if diversion_distance <= 200 * nautical_mile: diversion_cruise_altitude = 22000 * foot else: diversion_cruise_altitude = 31000 * foot diversion_flight_calculator = RangedFlight( StandardFlight( propulsion=propulsion_model, reference_area=reference_area, low_speed_climb_polar=low_speed_climb_polar, high_speed_polar=high_speed_polar, cruise_mach=cruise_mach, thrust_rates=thrust_rates, climb_target_altitude=diversion_cruise_altitude, ), diversion_distance, ) diversion_flight_points = diversion_flight_calculator.compute_from(end_of_descent) # Get flight points for each end of phase end_of_diversion_climb = FlightPoint( diversion_flight_points.loc[ diversion_flight_points.name == FlightPhase.CLIMB.value ].iloc[-1] ) end_of_diversion_cruise = FlightPoint( diversion_flight_points.loc[ diversion_flight_points.name == FlightPhase.CRUISE.value ].iloc[-1] ) end_of_diversion_descent = FlightPoint( diversion_flight_points.loc[ diversion_flight_points.name == FlightPhase.DESCENT.value ].iloc[-1] ) # rename phases because all flight points will be concatenated later. diversion_flight_points.name = "diversion_" + diversion_flight_points.name # Set OpenMDAO outputs outputs["data:mission:sizing:diversion:climb:fuel"] = ( end_of_descent.mass - end_of_diversion_climb.mass ) outputs["data:mission:sizing:diversion:cruise:fuel"] = ( end_of_diversion_climb.mass - end_of_diversion_cruise.mass ) outputs["data:mission:sizing:diversion:descent:fuel"] = ( end_of_diversion_cruise.mass - end_of_diversion_descent.mass ) outputs["data:mission:sizing:diversion:climb:distance"] = ( end_of_diversion_climb.ground_distance - end_of_descent.ground_distance ) outputs["data:mission:sizing:diversion:cruise:distance"] = ( end_of_diversion_cruise.ground_distance - end_of_diversion_climb.ground_distance ) outputs["data:mission:sizing:diversion:descent:distance"] = ( end_of_diversion_descent.ground_distance - end_of_diversion_cruise.ground_distance ) outputs["data:mission:sizing:diversion:climb:duration"] = ( end_of_diversion_climb.time - end_of_descent.time ) outputs["data:mission:sizing:diversion:cruise:duration"] = ( end_of_diversion_cruise.time - end_of_diversion_climb.time ) outputs["data:mission:sizing:diversion:descent:duration"] = ( end_of_diversion_descent.time - end_of_diversion_cruise.time ) # Holding ============================================================== holding_duration = inputs["data:mission:sizing:holding:duration"] holding_calculator = HoldSegment( target=FlightPoint(time=holding_duration), propulsion=propulsion_model, reference_area=reference_area, polar=high_speed_polar, name="holding", ) holding_flight_points = holding_calculator.compute_from(end_of_diversion_descent) end_of_holding = FlightPoint(holding_flight_points.iloc[-1]) outputs["data:mission:sizing:holding:fuel"] = ( end_of_diversion_descent.mass - end_of_holding.mass ) # Taxi-in ============================================================== taxi_in_duration = inputs["data:mission:sizing:taxi_in:duration"] taxi_in_thrust_rate = inputs["data:mission:sizing:taxi_in:thrust_rate"] taxi_in_calculator = TaxiSegment( target=FlightPoint(time=taxi_in_duration), propulsion=propulsion_model, thrust_rate=taxi_in_thrust_rate, name=FlightPhase.TAXI_IN.value, ) start_of_taxi_in = FlightPoint(end_of_holding) start_of_taxi_in.true_airspeed = inputs["data:mission:sizing:taxi_in:speed"] taxi_in_flight_points = taxi_in_calculator.compute_from(end_of_holding) end_of_taxi_in = FlightPoint(taxi_in_flight_points.iloc[-1]) outputs["data:mission:sizing:taxi_in:fuel"] = end_of_holding.mass - end_of_taxi_in.mass # Final ================================================================ fuel_route = inputs["data:weight:aircraft:MTOW"] - end_of_descent.mass outputs["data:mission:sizing:ZFW"] = end_of_taxi_in.mass - 0.03 * fuel_route outputs["data:mission:sizing:fuel"] = ( inputs["data:weight:aircraft:MTOW"] - outputs["data:mission:sizing:ZFW"] ) self.flight_points = ( pd.concat( [ flight_points, diversion_flight_points, holding_flight_points, taxi_in_flight_points, ] ) .reset_index(drop=True) .applymap(lambda x: np.asscalar(np.asarray(x))) ) if self.options["out_file"]: self.flight_points.to_csv(self.options["out_file"])