class ComputeEngineWeight(ExplicitComponent): """ Engine weight estimation calling wrapper """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_output("data:weight:propulsion:engine:mass", units="lb") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) b1 = propulsion_model.compute_weight() outputs["data:weight:propulsion:engine:mass"] = b1
class _compute_taxi(om.ExplicitComponent): """ Compute the fuel consumption for taxi based on speed and duration. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) self.options.declare("taxi_out", default=True, types=bool) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) if self.options["taxi_out"]: self.add_input("data:mission:sizing:taxi_out:thrust_rate", np.nan) self.add_input("data:mission:sizing:taxi_out:duration", np.nan, units="s") self.add_input("data:mission:sizing:taxi_out:speed", np.nan, units="m/s") self.add_output("data:mission:sizing:taxi_out:fuel", units='kg') else: self.add_input("data:mission:sizing:taxi_in:thrust_rate", np.nan) self.add_input("data:mission:sizing:taxi_in:duration", np.nan, units="s") self.add_input("data:mission:sizing:taxi_in:speed", np.nan, units="m/s") self.add_output("data:mission:sizing:taxi_in:fuel", units='kg') self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) if self.options["taxi_out"]: thrust_rate = inputs["data:mission:sizing:taxi_out:thrust_rate"] duration = inputs["data:mission:sizing:taxi_out:duration"] mach = inputs["data:mission:sizing:taxi_out:speed"]/Atmosphere(0.0).speed_of_sound else: thrust_rate = inputs["data:mission:sizing:taxi_in:thrust_rate"] duration = inputs["data:mission:sizing:taxi_in:duration"] mach = inputs["data:mission:sizing:taxi_in:speed"] / Atmosphere(0.0).speed_of_sound # FIXME: no specific settings for taxi (to be changed in fastoad\constants.py) flight_point = FlightPoint( mach=mach, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=thrust_rate ) propulsion_model.compute_flight_points(flight_point) fuel_mass = propulsion_model.get_consumed_mass(flight_point, duration) if self.options["taxi_out"]: outputs["data:mission:sizing:taxi_out:fuel"] = fuel_mass else: outputs["data:mission:sizing:taxi_in:fuel"] = fuel_mass
class ComputeUnusableFuelWeight(ExplicitComponent): """ Weight estimation for motor oil Based on : Wells, Douglas P., Bryce L. Horvath, and Linwood A. McCullers. "The Flight Optimization System Weights Estimation Method." (2017). Equation 121 """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input("data:geometry:wing:area", val=np.nan, units="ft**2") self.add_input("data:weight:aircraft:MFW", val=np.nan, units="lb") self.add_output("data:weight:propulsion:unusable_fuel:mass", units="lb") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): n_eng = inputs["data:geometry:propulsion:count"] wing_area = inputs["data:geometry:wing:area"] mfw = inputs["data:weight:aircraft:MFW"] n_tank = 2.0 propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), n_eng) flight_point = FlightPoint( mach=0.0, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) sl_thrust_newton = float(flight_point.thrust) sl_thrust_lbs = sl_thrust_newton / lbf sl_thrust_lbs_per_engine = sl_thrust_lbs / n_eng b3 = 11.5 * n_eng * sl_thrust_lbs_per_engine ** 0.2 + \ 0.07 * wing_area + \ 1.6 * n_tank * mfw ** 0.28 outputs["data:weight:propulsion:unusable_fuel:mass"] = b3
class ComputeOilWeight(ExplicitComponent): """ Weight estimation for motor oil Based on : Wells, Douglas P., Bryce L. Horvath, and Linwood A. McCullers. "The Flight Optimization System Weights Estimation Method." (2017). Equation 123 Not used since already included in the engine installed weight but left there in case """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_output("data:weight:propulsion:engine_oil:mass", units="lb") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): n_eng = inputs["data:geometry:propulsion:count"] propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), n_eng) flight_point = FlightPoint( mach=0.0, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) # This should give the UNINSTALLED weight sl_thrust_newton = float(flight_point.thrust) sl_thrust_lbs = sl_thrust_newton / lbf b1_2 = 0.082 * n_eng * sl_thrust_lbs**0.65 outputs["data:weight:propulsion:engine_oil:mass"] = b1_2
class ComputeEngineWeight(ExplicitComponent): """ Engine weight estimation calling wrapper Based on : Raymer Daniel. Aircraft Design: A Conceptual Approach. AIAA Education Series 1996 for installed engine weight, table 15.2 """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_output("data:weight:propulsion:engine:mass", units="lb") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) # This should give the UNINSTALLED weight uninstalled_engine_weight = propulsion_model.compute_weight() b1 = 1.4 * uninstalled_engine_weight outputs["data:weight:propulsion:engine:mass"] = b1
class ComputeFlightCGCase(ExplicitComponent): """ Center of gravity estimation for all load cases in flight""" def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:cabin:luggage:mass_max", val=np.nan, units="kg") self.add_input("data:geometry:wing:area", val=np.nan, units="m**2") self.add_input("data:aerodynamics:aircraft:cruise:CD0", val=np.nan) self.add_input( "data:aerodynamics:wing:cruise:induced_drag_coefficient", val=np.nan) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input("data:geometry:cabin:seats:passenger:NPAX_max", val=np.nan) self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m") self.add_input("data:geometry:fuselage:front_length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:pilot:length", val=np.nan, units="m") self.add_input("data:geometry:fuselage:PAX_length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:count_by_row", val=np.nan) self.add_input("data:geometry:cabin:seats:passenger:length", val=np.nan, units="m") self.add_input("data:weight:payload:rear_fret:CG:x", val=np.nan, units="m") self.add_input("data:weight:aircraft_empty:CG:x", val=np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg") self.add_input("data:weight:aircraft_empty:mass", val=np.nan, units="kg") self.add_input("data:weight:propulsion:unusable_fuel:mass", val=np.nan, units="kg") self.add_input("data:weight:propulsion:tank:CG:x", val=np.nan, units="m") self.add_input("data:weight:aircraft:MFW", val=np.nan, units="kg") self.add_output( "data:weight:aircraft:CG:flight_condition:max:MAC_position") self.add_output( "data:weight:aircraft:CG:flight_condition:min:MAC_position") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): luggage_mass_max = float( inputs["data:geometry:cabin:luggage:mass_max"]) n_pax_max = inputs["data:geometry:cabin:seats:passenger:NPAX_max"] l0_wing = inputs["data:geometry:wing:MAC:length"] fa_length = inputs["data:geometry:wing:MAC:at25percent:x"] lav = inputs["data:geometry:fuselage:front_length"] l_pax = inputs["data:geometry:fuselage:PAX_length"] l_pilot_seat = inputs["data:geometry:cabin:seats:pilot:length"] count_by_row = inputs[ "data:geometry:cabin:seats:passenger:count_by_row"] l_pass_seat = inputs["data:geometry:cabin:seats:passenger:length"] cg_rear_fret = inputs["data:weight:payload:rear_fret:CG:x"] x_cg_plane_aft = inputs["data:weight:aircraft_empty:CG:x"] m_empty = inputs["data:weight:aircraft_empty:mass"] m_unusable_fuel = inputs["data:weight:propulsion:unusable_fuel:mass"] cg_tank = inputs["data:weight:propulsion:tank:CG:x"] mfw = inputs["data:weight:aircraft:MFW"] l_instr = 0.7 cg_pilot = lav + l_instr + l_pilot_seat / 2.0 n_pax_array = np.linspace(0., n_pax_max, int(n_pax_max) + 1) m_pilot_single = 77. m_pilot_array = np.array( [2. * m_pilot_single]) # Without the pilots and with the 2 pilots m_fuel_min = m_unusable_fuel + self.min_in_flight_fuel(inputs) m_fuel_array = np.array([m_fuel_min, mfw]) m_lug_array = np.array([0.0, luggage_mass_max]) cg_list = [] for m_pilot in m_pilot_array: for m_fuel in m_fuel_array: for m_lug in m_lug_array: for n_pax in n_pax_array: n_row = np.ceil(n_pax / count_by_row) x_cg_pax_fwd = 0.0 for idx in range(int(n_row)): row_cg = (idx + 0.5) * l_pass_seat nb_pers = min(count_by_row, n_pax_max - idx * count_by_row) x_cg_pax_fwd += row_cg * nb_pers / n_pax_max x_cg_pax_aft = 0.0 for idx in range(int(n_row)): row_cg = l_pax - l_pilot_seat - (idx + 0.5) * l_pass_seat nb_pers = min(count_by_row, n_pax_max - idx * count_by_row) x_cg_pax_aft += row_cg * nb_pers / n_pax_max cg_pax_array = np.array([ lav + l_instr + l_pilot_seat + x_cg_pax_fwd, lav + l_instr + l_pilot_seat + x_cg_pax_aft ]) for cg_pax in cg_pax_array: m_pax_array = np.array([n_pax * 80., n_pax * 90.]) for m_pax in m_pax_array: mass = m_pax + m_pilot + m_fuel + m_lug + m_empty cg = (m_empty * x_cg_plane_aft + m_pax * cg_pax + m_pilot * cg_pilot + m_fuel * cg_tank + m_lug * cg_rear_fret) / mass cg_list.append(cg) cg_aft = max(cg_list) cg_fwd = min(cg_list) cg_fwd_ratio_pl = (cg_fwd - fa_length + 0.25 * l0_wing) / l0_wing cg_aft_ratio_pl = (cg_aft - fa_length + 0.25 * l0_wing) / l0_wing outputs[ "data:weight:aircraft:CG:flight_condition:max:MAC_position"] = cg_aft_ratio_pl outputs[ "data:weight:aircraft:CG:flight_condition:min:MAC_position"] = cg_fwd_ratio_pl def min_in_flight_fuel(self, inputs): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) # noinspection PyTypeChecker mtow = inputs["data:weight:aircraft:MTOW"] vh = self.max_speed(inputs, 0.0, mtow) atm = Atmosphere(0.0, altitude_in_feet=False) flight_point = FlightPoint(mach=vh / atm.speed_of_sound, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) propulsion_model.compute_flight_points(flight_point) m_fuel = propulsion_model.get_consumed_mass(flight_point, 30. * 60.) # Fuel necessary for a half-hour at max continuous power return m_fuel def max_speed(self, inputs, altitude, mass): # noinspection PyTypeChecker roots = optimize.fsolve(self.delta_axial_load, 300.0, args=(inputs, altitude, mass))[0] return np.max(roots[roots > 0.0]) def delta_axial_load(self, air_speed, inputs, altitude, mass): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) wing_area = inputs["data:geometry:wing:area"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k = inputs[ "data:aerodynamics:wing:cruise:induced_drag_coefficient"] # Get the available thrust from propulsion system atm = Atmosphere(altitude, altitude_in_feet=False) flight_point = FlightPoint(mach=air_speed / atm.speed_of_sound, altitude=altitude, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Get the necessary thrust to overcome cl = (mass * g) / (0.5 * atm.density * wing_area * air_speed**2.0) cd = cd0 + coef_k * cl**2.0 drag = 0.5 * atm.density * wing_area * cd * air_speed**2.0 return thrust - drag
class _compute_cruise(om.ExplicitComponent): """ Compute the fuel consumption on cruise segment with constant VTAS and altitude. The hypothesis of small alpha/gamma angles is done. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("data:aerodynamics:aircraft:cruise:CD0", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:holding:fuel", 0.0, units="kg") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:initial_climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:distance", np.nan, units="m") self.add_input("data:mission:sizing:main_route:descent:distance", np.nan, units="m") self.add_output("data:mission:sizing:main_route:cruise:fuel", units="kg") self.add_output("data:mission:sizing:main_route:cruise:distance", units="m") self.add_output("data:mission:sizing:main_route:cruise:duration", units="s") self.declare_partials( "*", [ "data:aerodynamics:aircraft:cruise:CD0", "data:aerodynamics:aircraft:cruise:induced_drag_coefficient", "data:geometry:wing:area", "data:weight:aircraft:MTOW", "data:mission:sizing:taxi_out:fuel", "data:mission:sizing:holding:fuel", "data:mission:sizing:takeoff:fuel", "data:mission:sizing:initial_climb:fuel", "data:mission:sizing:main_route:climb:fuel", "data:mission:sizing:main_route:climb:distance", "data:mission:sizing:main_route:descent:distance", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) v_tas = inputs["data:TLAR:v_cruise"] cruise_distance = max( 0.0, ( inputs["data:TLAR:range"] - inputs["data:mission:sizing:main_route:climb:distance"] - inputs["data:mission:sizing:main_route:descent:distance"] ) ) cruise_altitude = inputs["data:mission:sizing:main_route:cruise:altitude"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k = inputs["data:aerodynamics:aircraft:cruise:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] m_to = inputs["data:mission:sizing:taxi_out:fuel"] m_ho = inputs["data:mission:sizing:holding:fuel"] m_tk = inputs["data:mission:sizing:takeoff:fuel"] m_ic = inputs["data:mission:sizing:initial_climb:fuel"] m_cl = inputs["data:mission:sizing:main_route:climb:fuel"] # Define specific time step ~1000 points for calculation time_step = (cruise_distance / v_tas) / 1000.0 # Define initial conditions distance_t = 0.0 time_t = 0.0 mass_fuel_t = 0.0 mass_t = mtow - (m_to + m_ho + m_tk + m_ic + m_cl) atm_0 = Atmosphere(0.0) atm = Atmosphere(cruise_altitude, altitude_in_feet=False) v_cas = v_tas / math.sqrt(atm_0.density / atm.density) while distance_t < cruise_distance: # Calculate Cl - Cd and corresponding drag cl = mass_t * g / (0.5 * atm.density * wing_area * v_tas ** 2) cd = cd0 + coef_k * cl ** 2 drag = 0.5 * atm.density * wing_area * cd * v_tas ** 2 # Evaluate sfc mach = math.sqrt( 5 * ((atm_0.pressure / atm.pressure * ( (1 + 0.2 * (v_cas / atm_0.speed_of_sound) ** 2) ** 3.5 - 1 ) + 1) ** (1 / 3.5) - 1) ) flight_point = FlightPoint( mach=mach, altitude=cruise_altitude, engine_setting=EngineSetting.CRUISE, thrust_is_regulated=True, thrust=drag, ) propulsion_model.compute_flight_points(flight_point) # If thrust exceed max thrust exit cruise calculation if float(flight_point.thrust_rate) > 1.0: warnings.warn("The cruise strategy exceeds propulsion power!") mass_fuel_t = 0.0 time_t = 0.0 break # Calculate distance increase distance_t += v_tas * min(time_step, (cruise_distance - distance_t) / v_tas) # Estimate mass evolution and update time mass_fuel_t += propulsion_model.get_consumed_mass( flight_point, min(time_step, (cruise_distance - distance_t) / v_tas) ) mass_t = mass_t - propulsion_model.get_consumed_mass( flight_point, min(time_step, (cruise_distance - distance_t) / v_tas) ) time_t += min(time_step, (cruise_distance - distance_t) / v_tas) outputs["data:mission:sizing:main_route:cruise:fuel"] = mass_fuel_t outputs["data:mission:sizing:main_route:cruise:distance"] = distance_t outputs["data:mission:sizing:main_route:cruise:duration"] = time_t
class _vr_from_v2(om.ExplicitComponent): """ Search VR for given lift-off conditions by doing reverted simulation. The error introduced comes from acceleration acc(t)~acc(t+dt) => v(t-dt)~V(t)-acc(t)*dt. Time step has been reduced by 1/5 to limit integration error. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CL", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_alpha", np.nan, units="rad**-1") self.add_input("data:aerodynamics:aircraft:low_speed:CD0", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CD", np.nan) self.add_input("data:aerodynamics:wing:low_speed:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:geometry:wing:span", np.nan, units="m") self.add_input("data:geometry:landing_gear:height", np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:takeoff:thrust_rate", np.nan) self.add_input("data:mission:sizing:takeoff:friction_coefficient_no_brake", np.nan) self.add_input("vloff:speed", np.nan, units='m/s') self.add_input("vloff:angle", np.nan, units='rad') self.add_output("vr:speed", units='m/s') self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) cl0 = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] + inputs["data:aerodynamics:flaps:takeoff:CL"] cl_alpha = inputs["data:aerodynamics:wing:low_speed:CL_alpha"] cd0 = inputs["data:aerodynamics:aircraft:low_speed:CD0"] + inputs["data:aerodynamics:flaps:takeoff:CD"] coef_k = inputs["data:aerodynamics:wing:low_speed:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] wing_span = inputs["data:geometry:wing:span"] lg_height = inputs["data:geometry:landing_gear:height"] mtow = inputs["data:weight:aircraft:MTOW"] thrust_rate = inputs["data:mission:sizing:takeoff:thrust_rate"] friction_coeff = inputs["data:mission:sizing:takeoff:friction_coefficient_no_brake"] v_t = float(inputs["vloff:speed"]) alpha_t = float(inputs["vloff:angle"]) # Define ground factor effect on Drag k_ground = 33. * (lg_height / wing_span) ** 1.5 / (1. + 33. * (lg_height / wing_span) ** 1.5) # Start reverted calculation of flight from lift-off to 0° alpha angle atm = Atmosphere(0.0) while (alpha_t != 0.0) and (v_t != 0.0): # Estimation of thrust flight_point = FlightPoint( mach=v_t / atm.speed_of_sound, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=thrust_rate ) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculate lift and drag cl = cl0 + cl_alpha * alpha_t lift = 0.5 * atm.density * wing_area * cl * v_t ** 2 cd = cd0 + k_ground * coef_k * cl ** 2 drag = 0.5 * atm.density * wing_area * cd * v_t ** 2 # Calculate rolling resistance load friction = (mtow * g - lift - thrust * math.sin(alpha_t)) * friction_coeff # Calculate acceleration acc_x = (thrust * math.cos(alpha_t) - drag - friction) / mtow # Speed and angle update (feedback) dt = min(TIME_STEP / 5, alpha_t / ALPHA_RATE, v_t / acc_x) v_t = v_t - acc_x * dt alpha_t = alpha_t - ALPHA_RATE * dt outputs["vr:speed"] = v_t
class ComputeVh(om.ExplicitComponent): def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg") self.add_input("data:geometry:wing:area", val=np.nan, units="m**2") self.add_input("data:aerodynamics:aircraft:cruise:CD0", val=np.nan) self.add_input( "data:aerodynamics:wing:cruise:induced_drag_coefficient", val=np.nan) self.add_output("data:TLAR:v_max_sl", units="kn") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): # The maximum Sea Level flight velocity is computed using a method which finds for which speed # the thrust required for flight (drag) is equal to the thrust available design_mass = inputs["data:weight:aircraft:MTOW"] Vh = self.max_speed(inputs, 0.0, design_mass) outputs["data:TLAR:v_max_sl"] = Vh def max_speed(self, inputs, altitude, mass): # noinspection PyTypeChecker roots = optimize.fsolve(self.delta_axial_load, 300.0, args=(inputs, altitude, mass))[0] return np.max(roots[roots > 0.0]) def delta_axial_load(self, air_speed, inputs, altitude, mass): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) wing_area = inputs["data:geometry:wing:area"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k = inputs[ "data:aerodynamics:wing:cruise:induced_drag_coefficient"] # Get the available thrust from propulsion system atm = Atmosphere(altitude, altitude_in_feet=False) flight_point = FlightPoint(mach=air_speed / atm.speed_of_sound, altitude=altitude, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Get the necessary thrust to overcome cl = (mass * g) / (0.5 * atm.density * wing_area * air_speed**2.0) cd = cd0 + coef_k * cl**2.0 drag = 0.5 * atm.density * wing_area * cd * air_speed**2.0 return thrust - drag
class _v2(om.ExplicitComponent): """ Calculate V2 safety speed @ defined altitude considering a 30% safety margin on max lift capability (alpha imposed). Find corresponding climb rate margin for imposed thrust rate. Fuel burn is neglected : mass = MTOW. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CL", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_alpha", np.nan, units="rad**-1") self.add_input("data:aerodynamics:aircraft:low_speed:CD0", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CD", np.nan) self.add_input("data:aerodynamics:wing:low_speed:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:geometry:wing:span", np.nan, units="m") self.add_input("data:geometry:landing_gear:height", np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_output("v2:speed", units='m/s') self.add_output("v2:angle", units='rad') self.add_output("v2:climb_rate") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) cl_max_clean = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"] cl0 = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] + inputs["data:aerodynamics:flaps:takeoff:CL"] cl_alpha = inputs["data:aerodynamics:wing:low_speed:CL_alpha"] cd0 = inputs["data:aerodynamics:aircraft:low_speed:CD0"] + inputs["data:aerodynamics:flaps:takeoff:CD"] coef_k = inputs["data:aerodynamics:wing:low_speed:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] wing_span = inputs["data:geometry:wing:span"] lg_height = inputs["data:geometry:landing_gear:height"] mtow = inputs["data:weight:aircraft:MTOW"] # Define atmospheric condition for safety height atm = Atmosphere(SAFETY_HEIGHT, altitude_in_feet=False) # Define Cl considering 30% margin and estimate alpha cl = cl_max_clean / 1.2 ** 2 # V2>=1.2*VS1 alpha_interp = np.linspace(0.0, 30.0, 31) * math.pi / 180.0 cl_interp = cl0 + alpha_interp * cl_alpha alpha = np.interp(cl, cl_interp, alpha_interp) # Calculate drag coefficient k_ground = ( 33. * ((lg_height + SAFETY_HEIGHT) / wing_span) ** 1.5 / (1. + 33. * ((lg_height + SAFETY_HEIGHT) / wing_span) ** 1.5) ) cd = cd0 + k_ground * coef_k * cl ** 2 # Find v2 safety speed for 0% climb rate v2 = math.sqrt((mtow * g) / (0.5 * atm.density * wing_area * cl)) # Estimate climb rate considering alpha~0° and max thrust rate for CS23.65 (loop on error) flight_point = FlightPoint( mach=v2 / atm.speed_of_sound, altitude=SAFETY_HEIGHT, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0 ) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) gamma = math.asin(thrust / (mtow * g) - cd / cl) rel_error = 0.1 while rel_error > 0.05: new_gamma = math.asin(thrust / (mtow * g) - cd / cl * math.cos(gamma)) rel_error = abs((new_gamma - gamma) / new_gamma) gamma = new_gamma outputs["v2:speed"] = v2 outputs["v2:angle"] = alpha outputs["v2:climb_rate"] = math.sin(gamma)
class ComputeTORotationLimit(om.ExplicitComponent): """ Computes area of horizontal tail plane (internal function) """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input("data:geometry:wing:area", val=np.nan, units="m**2") self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m") self.add_input("data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input("data:geometry:horizontal_tail:area", val=np.nan, units="m**2") self.add_input("data:geometry:propulsion:nacelle:height", val=np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg") self.add_input("data:weight:airframe:landing_gear:main:CG:x", val=np.nan, units="m") self.add_input("data:weight:aircraft_empty:CG:z", val=np.nan, units="m") self.add_input("data:weight:propulsion:engine:CG:z", val=np.nan, units="m") self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", val=np.nan) self.add_input("data:aerodynamics:aircraft:takeoff:CL_max", val=np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", val=np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CL", val=np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CM", val=np.nan) self.add_input("data:aerodynamics:horizontal_tail:low_speed:CL_alpha_isolated", val=np.nan, units="rad**-1") self.add_input("data:aerodynamics:horizontal_tail:efficiency", val=np.nan) self.add_input("takeoff:cl_htp", val=np.nan) self.add_input("takeoff:cm_wing", val=np.nan) self.add_input("low_speed:cl_alpha_htp", val=np.nan) self.add_output("data:handling_qualities:to_rotation_limit:x", units="m") self.add_output("data:handling_qualities:to_rotation_limit:MAC_position") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): cl_max_takeoff = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"] cl0_clean = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] cl_flaps_takeoff = inputs["data:aerodynamics:flaps:takeoff:CL"] cm_takeoff = inputs["takeoff:cm_wing"] cl_alpha_htp_isolated = inputs["data:aerodynamics:horizontal_tail:low_speed:CL_alpha_isolated"] cl_htp = inputs["takeoff:cl_htp"] tail_efficiency_factor = inputs["data:aerodynamics:horizontal_tail:efficiency"] n_engines = inputs["data:geometry:propulsion:count"] x_wing_aero_center = inputs["data:geometry:wing:MAC:at25percent:x"] wing_area = inputs["data:geometry:wing:area"] wing_mac = inputs["data:geometry:wing:MAC:length"] ht_area = inputs["data:geometry:horizontal_tail:area"] lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"] mtow = inputs["data:weight:aircraft:MTOW"] x_lg = inputs["data:weight:airframe:landing_gear:main:CG:x"] z_cg_aircraft = inputs["data:weight:aircraft_empty:CG:z"] z_cg_engine = inputs["data:weight:propulsion:engine:CG:z"] propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) # Conditions for calculation atm = Atmosphere(0.0) rho = atm.density sos = atm.speed_of_sound # Calculation of take-off minimum speed weight = mtow * g vs1 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_takeoff)) if n_engines == 1.0: vr = 1.10 * vs1 else: vr = 1.0 * vs1 mach_r = vr / sos flight_point = FlightPoint( mach=mach_r, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0 ) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) x_ht = x_wing_aero_center + lp_ht # Compute aerodynamic coefficients for takeoff @ 0° aircraft angle cl0_takeoff = cl0_clean + cl_flaps_takeoff eta_q = 1. + cl_alpha_htp_isolated / cl_htp * _ANG_VEL * (x_ht - x_lg) / vr eta_h = (x_ht - x_lg) / lp_ht * tail_efficiency_factor k_cl = cl_max_takeoff / (eta_q * eta_h * cl_htp) tail_volume_coefficient = ht_area * lp_ht / (wing_area * wing_mac) zt = z_cg_aircraft - z_cg_engine engine_contribution = zt * thrust / weight x_cg = ( 1. / k_cl * ( tail_volume_coefficient - cl0_takeoff / cl_htp * (x_lg / wing_mac - 0.25) ) - cm_takeoff / cl_max_takeoff ) * (vr / vs1) ** 2.0 + x_lg - engine_contribution outputs["data:handling_qualities:to_rotation_limit:x"] = x_cg x_cg_ratio = (x_cg - x_wing_aero_center + 0.25 * wing_mac) / wing_mac outputs["data:handling_qualities:to_rotation_limit:MAC_position"] = x_cg_ratio
class ComputeVTArea(om.ExplicitComponent): """ Computes needed vt area to: - have enough rotational moment/controllability during cruise - compensate 1-failed engine linear trajectory at limited altitude (5000ft) """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input("data:geometry:wing:area", val=np.nan, units="m**2") self.add_input("data:geometry:wing:span", val=np.nan, units="m") self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") self.add_input("data:weight:aircraft:CG:aft:MAC_position", val=np.nan) self.add_input("data:aerodynamics:fuselage:cruise:CnBeta", val=np.nan) self.add_input("data:aerodynamics:vertical_tail:cruise:CL_alpha", val=np.nan, units="rad**-1") self.add_input("data:TLAR:v_approach", val=np.nan, units="m/s") self.add_input("data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input("data:geometry:propulsion:nacelle:wet_area", val=np.nan, units="m**2") self.add_input("data:geometry:propulsion:nacelle:y", val=np.nan, units="m") self.add_output("data:geometry:vertical_tail:area", val=2.5, units="m**2") self.declare_partials( "*", [ "data:geometry:wing:area", "data:geometry:wing:span", "data:geometry:wing:MAC:length", "data:weight:aircraft:CG:aft:MAC_position", "data:aerodynamics:fuselage:cruise:CnBeta", "data:aerodynamics:vertical_tail:cruise:CL_alpha", "data:TLAR:v_cruise", "data:TLAR:v_approach", "data:mission:sizing:main_route:cruise:altitude", "data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25", "data:geometry:propulsion:nacelle:wet_area", "data:geometry:propulsion:nacelle:y", "data:geometry:vertical_tail:area", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): # Sizing constraints for the vertical tail. # Limiting cases: rotating torque objective (cn_beta_goal) during cruise, and # compensation of engine failure induced torque at approach speed/altitude. # Returns maximum area. propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) engine_number = inputs["data:geometry:propulsion:count"] wing_area = inputs["data:geometry:wing:area"] span = inputs["data:geometry:wing:span"] l0_wing = inputs["data:geometry:wing:MAC:length"] cg_mac_position = inputs["data:weight:aircraft:CG:aft:MAC_position"] cn_beta_fuselage = inputs["data:aerodynamics:fuselage:cruise:CnBeta"] cl_alpha_vt = inputs["data:aerodynamics:vertical_tail:cruise:CL_alpha"] cruise_speed = inputs["data:TLAR:v_cruise"] approach_speed = inputs["data:TLAR:v_approach"] cruise_altitude = inputs["data:mission:sizing:main_route:cruise:altitude"] wing_htp_distance = inputs["data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25"] nac_wet_area = inputs["data:geometry:propulsion:nacelle:wet_area"] y_nacelle = inputs["data:geometry:propulsion:nacelle:y"] # CASE1: OBJECTIVE TORQUE @ CRUISE ############################################################################# atm = Atmosphere(cruise_altitude) speed_of_sound = atm.speed_of_sound cruise_mach = cruise_speed / speed_of_sound # Matches suggested goal by Raymer, Fig 16.20 cn_beta_goal = 0.0569 - 0.01694 * cruise_mach + 0.15904 * cruise_mach ** 2 required_cnbeta_vtp = cn_beta_goal - cn_beta_fuselage distance_to_cg = wing_htp_distance + 0.25 * l0_wing - cg_mac_position * l0_wing area_1 = required_cnbeta_vtp / (distance_to_cg / wing_area / span * cl_alpha_vt) # CASE2: ENGINE FAILURE COMPENSATION DURING CLIMB ############################################################## failure_altitude = 5000.0 # CS23 for Twin engine - at 5000ft atm = Atmosphere(failure_altitude) speed_of_sound = atm.speed_of_sound pressure = atm.pressure if engine_number == 2.0: stall_speed = approach_speed / 1.3 mc_speed = 1.2 * stall_speed # Flights mechanics from GA - Serge Bonnet CS23 mc_mach = mc_speed / speed_of_sound # Calculation of engine power for given conditions flight_point = FlightPoint( mach=mc_mach, altitude=failure_altitude, engine_setting=EngineSetting.CLIMB, thrust_rate=1.0 ) # forced to maximum thrust propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculation of engine thrust and nacelle drag (failed one) nac_drag = 0.07 * nac_wet_area # FIXME: the form factor should not be fixed outside propulsion module! # Torque compensation area_2 = ( 2 * (y_nacelle / wing_htp_distance) * (thrust + nac_drag) / (pressure * mc_mach ** 2 * 0.9 * 0.42 * 10) ) else: area_2 = 0.0 outputs["data:geometry:vertical_tail:area"] = max(area_1, area_2)
class _compute_descent(AircraftEquilibrium): """ Compute the fuel consumption on descent segment with constant VCAS and descent rate. The hypothesis of small alpha angle is done. Warning: Descent rate is reduced if cd/cl < abs(desc_rate)! """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): super().setup() self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:mission:sizing:main_route:descent:descent_rate", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:optimal_CL", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:CD0", np.nan) self.add_input( "data:aerodynamics:wing:cruise:induced_drag_coefficient", np.nan) self.add_input( "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient", np.nan) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:holding:fuel", 0.0, units="kg") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:initial_climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:cruise:fuel", np.nan, units="kg") self.add_output("data:mission:sizing:main_route:descent:fuel", units="kg") self.add_output("data:mission:sizing:main_route:descent:distance", 0.0, units="m") self.add_output("data:mission:sizing:main_route:descent:duration", units="s") self.declare_partials( "*", [ "data:aerodynamics:aircraft:cruise:optimal_CL", "data:aerodynamics:aircraft:cruise:CD0", "data:aerodynamics:wing:cruise:induced_drag_coefficient", "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient", "data:geometry:wing:area", "data:weight:aircraft:MTOW", "data:mission:sizing:taxi_out:fuel", "data:mission:sizing:holding:fuel", "data:mission:sizing:takeoff:fuel", "data:mission:sizing:initial_climb:fuel", "data:mission:sizing:main_route:climb:fuel", "data:mission:sizing:main_route:cruise:fuel", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) cruise_altitude = inputs[ "data:mission:sizing:main_route:cruise:altitude"] descent_rate = -abs( inputs["data:mission:sizing:main_route:descent:descent_rate"]) cl = inputs["data:aerodynamics:aircraft:cruise:optimal_CL"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k_wing = inputs[ "data:aerodynamics:wing:cruise:induced_drag_coefficient"] coef_k_htp = inputs[ "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] m_to = inputs["data:mission:sizing:taxi_out:fuel"] m_ho = inputs["data:mission:sizing:holding:fuel"] m_tk = inputs["data:mission:sizing:takeoff:fuel"] m_ic = inputs["data:mission:sizing:initial_climb:fuel"] m_cl = inputs["data:mission:sizing:main_route:climb:fuel"] m_cr = inputs["data:mission:sizing:main_route:cruise:fuel"] # Define initial conditions t_start = time.time() gamma = math.asin(descent_rate) altitude_t = copy.deepcopy(cruise_altitude) distance_t = 0.0 time_t = 0.0 mass_fuel_t = 0.0 mass_t = mtow - (m_to + m_ho + m_tk + m_ic + m_cl + m_cr) atm_0 = Atmosphere(0.0) warning = False # Calculate defined VCAS at the beginning of descent (cos(gamma)~1) v_cas = math.sqrt((mass_t * g) * math.cos(descent_rate) / (0.5 * atm_0.density * wing_area * cl)) # Define specific time step ~POINTS_NB_CLIMB points for calculation (with ground conditions) time_step = ( -cruise_altitude / (v_cas * math.sin(descent_rate))) / float(POINTS_NB_DESCENT) while altitude_t > 0.0: # Define air properties and calculate VTAS atm = Atmosphere(altitude_t, altitude_in_feet=False) mach = math.sqrt( 5 * ((atm_0.pressure / atm.pressure * ((1 + 0.2 * (v_cas / atm_0.speed_of_sound)**2)**3.5 - 1) + 1)** (1 / 3.5) - 1)) v_tas = mach * atm.speed_of_sound # Calculate equilibrium and induced drag cl_wing, cl_htp_only, cl_elevator, _ = self.found_cl_repartition( inputs, 1.0, mass_t, (0.5 * atm.density * v_tas**2), False) cd = cd0 + coef_k_wing * cl_wing**2 + coef_k_htp * (cl_htp_only + cl_elevator)**2 cl = ((mass_t * g) * math.cos(descent_rate) / (0.5 * atm.density * wing_area * v_tas**2)) cl_cd = cl / cd drag = 0.5 * atm.density * wing_area * cd * v_tas**2 # Calculate necessary Thrust to maintain VCAS and descent rate # if T<0N, VCAS is maintained reducing gamma/descent rate and engine in IDLE condition thrust = drag + (mass_t * g) * math.sin(gamma) if thrust <= 0.0: flight_point = FlightPoint( mach=mach, altitude=altitude_t, engine_setting=EngineSetting.IDLE, thrust_rate=0.2) # FIXME: define IDLE maybe? descent_rate = -1 / cl_cd gamma = math.asin(descent_rate) warning = True else: # FIXME: DESCENT setting on engine does not exist, replaced by CRUISE for test flight_point = FlightPoint( mach=mach, altitude=altitude_t, engine_setting=EngineSetting.CRUISE, thrust_is_regulated=True, thrust=thrust, ) propulsion_model.compute_flight_points(flight_point) # Calculate distance increase v_x = v_tas * math.cos(descent_rate) v_z = v_tas * math.sin(descent_rate) time_step = min(time_step, -altitude_t / v_z) distance_t += v_x * time_step altitude_t += v_z * time_step # Estimate mass evolution and update time mass_fuel_t += propulsion_model.get_consumed_mass( flight_point, time_step) mass_t = mass_t - propulsion_model.get_consumed_mass( flight_point, time_step) time_t += time_step # Check calculation duration if (time.time() - t_start) > MAX_CALCULATION_TIME: raise Exception( "Time calculation duration for descent phase [{}s] exceeded!" .format(MAX_CALCULATION_TIME)) if warning: warnings.warn("Descent rate has been reduced!") outputs["data:mission:sizing:main_route:descent:fuel"] = mass_fuel_t outputs["data:mission:sizing:main_route:descent:distance"] = distance_t outputs["data:mission:sizing:main_route:descent:duration"] = time_t
class _compute_cruise(AircraftEquilibrium): """ Compute the fuel consumption on cruise segment with constant VTAS and altitude. The hypothesis of small alpha/gamma angles is done. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): super().setup() self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("data:aerodynamics:aircraft:cruise:CD0", np.nan) self.add_input( "data:aerodynamics:wing:cruise:induced_drag_coefficient", np.nan) self.add_input( "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient", np.nan) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:holding:fuel", 0.0, units="kg") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:initial_climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:distance", np.nan, units="m") self.add_input("data:mission:sizing:main_route:descent:distance", np.nan, units="m") self.add_output("data:mission:sizing:main_route:cruise:fuel", units="kg") self.add_output("data:mission:sizing:main_route:cruise:distance", units="m") self.add_output("data:mission:sizing:main_route:cruise:duration", units="s") self.declare_partials( "*", [ "data:aerodynamics:aircraft:cruise:CD0", "data:aerodynamics:wing:cruise:induced_drag_coefficient", "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient", "data:geometry:wing:area", "data:weight:aircraft:MTOW", "data:mission:sizing:taxi_out:fuel", "data:mission:sizing:holding:fuel", "data:mission:sizing:takeoff:fuel", "data:mission:sizing:initial_climb:fuel", "data:mission:sizing:main_route:climb:fuel", "data:mission:sizing:main_route:climb:distance", "data:mission:sizing:main_route:descent:distance", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) v_tas = inputs["data:TLAR:v_cruise"] cruise_distance = max( 0.0, (inputs["data:TLAR:range"] - inputs["data:mission:sizing:main_route:climb:distance"] - inputs["data:mission:sizing:main_route:descent:distance"])) cruise_altitude = inputs[ "data:mission:sizing:main_route:cruise:altitude"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k_wing = inputs[ "data:aerodynamics:wing:cruise:induced_drag_coefficient"] coef_k_htp = inputs[ "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] m_to = inputs["data:mission:sizing:taxi_out:fuel"] m_tk = inputs["data:mission:sizing:takeoff:fuel"] m_ic = inputs["data:mission:sizing:initial_climb:fuel"] m_cl = inputs["data:mission:sizing:main_route:climb:fuel"] # Define specific time step ~POINTS_NB_CRUISE points for calculation time_step = (cruise_distance / v_tas) / float(POINTS_NB_CRUISE) # Define initial conditions t_start = time.time() distance_t = 0.0 time_t = 0.0 mass_fuel_t = 0.0 mass_t = mtow - (m_to + m_tk + m_ic + m_cl) atm = Atmosphere(cruise_altitude, altitude_in_feet=False) while distance_t < cruise_distance: # Calculate equilibrium and induced drag cl_wing, cl_htp_only, cl_elevator, _ = self.found_cl_repartition( inputs, 1.0, mass_t, (0.5 * atm.density * v_tas**2), False) cd = cd0 + coef_k_wing * cl_wing**2 + coef_k_htp * (cl_htp_only + cl_elevator)**2 drag = 0.5 * atm.density * wing_area * cd * v_tas**2 # Evaluate sfc mach = v_tas / atm.speed_of_sound flight_point = FlightPoint( mach=mach, altitude=cruise_altitude, engine_setting=EngineSetting.CRUISE, thrust_is_regulated=True, thrust=drag, ) propulsion_model.compute_flight_points(flight_point) # If thrust exceed max thrust exit cruise calculation if float(flight_point.thrust_rate) > 1.0: warnings.warn("The cruise strategy exceeds propulsion power!") mass_fuel_t = 0.0 time_t = 0.0 break # Calculate distance increase distance_t += v_tas * min(time_step, (cruise_distance - distance_t) / v_tas) # Estimate mass evolution and update time mass_fuel_t += propulsion_model.get_consumed_mass( flight_point, min(time_step, (cruise_distance - distance_t) / v_tas)) mass_t = mass_t - propulsion_model.get_consumed_mass( flight_point, min(time_step, (cruise_distance - distance_t) / v_tas)) time_t += min(time_step, (cruise_distance - distance_t) / v_tas) # Check calculation duration if (time.time() - t_start) > MAX_CALCULATION_TIME: raise Exception( "Time calculation duration for cruise phase [{}s] exceeded!" .format(MAX_CALCULATION_TIME)) outputs["data:mission:sizing:main_route:cruise:fuel"] = mass_fuel_t outputs["data:mission:sizing:main_route:cruise:distance"] = distance_t outputs["data:mission:sizing:main_route:cruise:duration"] = time_t
class _compute_climb(AircraftEquilibrium): """ Compute the fuel consumption on climb segment with constant VCAS and fixed thrust ratio. The hypothesis of small alpha/gamma angles is done. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): super().setup() self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:CD0", np.nan) self.add_input( "data:aerodynamics:wing:cruise:induced_drag_coefficient", np.nan) self.add_input( "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient", np.nan) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:holding:fuel", 0.0, units="kg") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:initial_climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:thrust_rate", np.nan) self.add_output("data:mission:sizing:main_route:climb:fuel", units="kg") self.add_output("data:mission:sizing:main_route:climb:distance", units="m") self.add_output("data:mission:sizing:main_route:climb:duration", units="s") self.add_output("data:mission:sizing:main_route:climb:v_cas", units="m/s") self.declare_partials( "*", [ "data:aerodynamics:aircraft:cruise:CD0", "data:aerodynamics:wing:cruise:induced_drag_coefficient", "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient", "data:geometry:wing:area", "data:weight:aircraft:MTOW", "data:mission:sizing:taxi_out:fuel", "data:mission:sizing:holding:fuel", "data:mission:sizing:takeoff:fuel", "data:mission:sizing:initial_climb:fuel", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) cruise_altitude = inputs[ "data:mission:sizing:main_route:cruise:altitude"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k_wing = inputs[ "data:aerodynamics:wing:cruise:induced_drag_coefficient"] coef_k_htp = inputs[ "data:aerodynamics:horizontal_tail:cruise:induced_drag_coefficient"] cl_max_clean = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"] wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] m_to = inputs["data:mission:sizing:taxi_out:fuel"] m_tk = inputs["data:mission:sizing:takeoff:fuel"] m_ic = inputs["data:mission:sizing:initial_climb:fuel"] thrust_rate = inputs[ "data:mission:sizing:main_route:climb:thrust_rate"] # Define initial conditions t_start = time.time() altitude_t = SAFETY_HEIGHT # conversion to m distance_t = 0.0 time_t = 0.0 mass_t = mtow - (m_to + m_tk + m_ic) mass_fuel_t = 0.0 atm_0 = Atmosphere(0.0) # FIXME: VCAS strategy is specific to ICE-propeller configuration, should be an input cl = math.sqrt(3 * cd0 / coef_k_wing) atm = Atmosphere(altitude_t, altitude_in_feet=False) v_cas = math.sqrt((mass_t * g) / (0.5 * atm.density * wing_area * cl)) vs1 = math.sqrt( (mass_t * g) / (0.5 * atm.density * wing_area * cl_max_clean)) mach = math.sqrt(5 * ((atm_0.pressure / atm.pressure * ( (1 + 0.2 * (v_cas / atm_0.speed_of_sound)**2)**3.5 - 1) + 1)**(1 / 3.5) - 1)) v_tas = max(mach * atm.speed_of_sound, 1.3 * vs1) mach = v_tas / atm.speed_of_sound # Define specific time step ~POINTS_NB_CLIMB points for calculation (with ground conditions) cl_wing, cl_htp_only, cl_elevator, _ = self.found_cl_repartition( inputs, 1.0, mass_t, (0.5 * atm.density * v_tas**2), False) cd = cd0 + coef_k_wing * cl_wing**2 + coef_k_htp * (cl_htp_only + cl_elevator)**2 flight_point = FlightPoint( mach=mach, altitude=SAFETY_HEIGHT, engine_setting=EngineSetting.CLIMB, thrust_rate=thrust_rate) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) climb_rate = thrust / (mass_t * g) - cd / (cl_wing + cl_htp_only + cl_elevator) time_step = ((cruise_altitude - SAFETY_HEIGHT) / (v_tas * math.sin(climb_rate))) / float(POINTS_NB_CLIMB) while altitude_t < cruise_altitude: # Define air properties atm = Atmosphere(altitude_t, altitude_in_feet=False) vs1 = math.sqrt( (mass_t * g) / (0.5 * atm.density * wing_area * cl_max_clean)) # Evaluate thrust and sfc mach = math.sqrt( 5 * ((atm_0.pressure / atm.pressure * ((1 + 0.2 * (v_cas / atm_0.speed_of_sound)**2)**3.5 - 1) + 1)** (1 / 3.5) - 1)) v_tas = max(mach * atm.speed_of_sound, 1.3 * vs1) mach = v_tas / atm.speed_of_sound flight_point = FlightPoint( mach=mach, altitude=altitude_t, engine_setting=EngineSetting.CLIMB, thrust_rate=thrust_rate ) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculate equilibrium and induced drag cl_wing, cl_htp_only, cl_elevator, _ = self.found_cl_repartition( inputs, 1.0, mass_t, (0.5 * atm.density * v_tas**2), False) cd = cd0 + coef_k_wing * cl_wing**2 + coef_k_htp * (cl_htp_only + cl_elevator)**2 # Calculate climb rate and height increase climb_rate = thrust / (mass_t * g) - cd / (cl_wing + cl_htp_only + cl_elevator) v_z = v_tas * math.sin(climb_rate) v_x = v_tas * math.cos(climb_rate) time_step = min(time_step, (cruise_altitude - altitude_t) / v_z) altitude_t += v_z * time_step distance_t += v_x * time_step # Estimate mass evolution and update time mass_fuel_t += propulsion_model.get_consumed_mass( flight_point, time_step) mass_t = mass_t - propulsion_model.get_consumed_mass( flight_point, time_step) time_t += time_step # Check calculation duration if (time.time() - t_start) > MAX_CALCULATION_TIME: raise Exception( "Time calculation duration for climb phase [{}s] exceeded!" .format(MAX_CALCULATION_TIME)) outputs["data:mission:sizing:main_route:climb:fuel"] = mass_fuel_t outputs["data:mission:sizing:main_route:climb:distance"] = distance_t outputs["data:mission:sizing:main_route:climb:duration"] = time_t outputs["data:mission:sizing:main_route:climb:v_cas"] = v_cas
class SizingFlight(om.ExplicitComponent): """ Simulates a complete flight mission with diversion. """ def __init__(self, **kwargs): """ Computes thrust, SFC and thrust rate by direct call to engine model. Options: - propulsion_id: (mandatory) the identifier of the propulsion wrapper. - out_file: if provided, a csv file will be written at provided path with all computed flight points. If path is relative, it will be resolved from working directory """ super().__init__(**kwargs) self.flight_points = None self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) self.options.declare("out_file", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) # Inputs ----------------------------------------------------------------------------------- self.add_input("data:TLAR:cruise_mach", np.nan) self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("data:geometry:propulsion:engine:count", 2) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:aerodynamics:aircraft:cruise:CL", np.nan, shape=POLAR_POINT_COUNT) self.add_input("data:aerodynamics:aircraft:cruise:CD", np.nan, shape=POLAR_POINT_COUNT) self.add_input("data:aerodynamics:aircraft:takeoff:CL", np.nan, shape=POLAR_POINT_COUNT) self.add_input("data:aerodynamics:aircraft:takeoff:CD", np.nan, shape=POLAR_POINT_COUNT) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:takeoff:V2", np.nan, units="m/s") self.add_input("data:mission:sizing:takeoff:altitude", np.nan, units="m") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:climb:thrust_rate", np.nan) self.add_input("data:mission:sizing:descent:thrust_rate", np.nan) self.add_input("data:mission:sizing:diversion:distance", np.nan, units="m") self.add_input("data:mission:sizing:holding:duration", np.nan, units="s") self.add_input("data:mission:sizing:taxi_in:duration", np.nan, units="s") self.add_input("data:mission:sizing:taxi_in:speed", np.nan, units="m/s") self.add_input("data:mission:sizing:taxi_in:thrust_rate", np.nan) # Outputs ---------------------------------------------------------------------------------- self.add_output("data:mission:sizing:initial_climb:fuel", units="kg") self.add_output("data:mission:sizing:main_route:climb:fuel", units="kg") self.add_output("data:mission:sizing:main_route:cruise:fuel", units="kg") self.add_output("data:mission:sizing:main_route:descent:fuel", units="kg") self.add_output("data:mission:sizing:initial_climb:distance", units="m") self.add_output("data:mission:sizing:main_route:climb:distance", units="m") self.add_output("data:mission:sizing:main_route:cruise:distance", units="m") self.add_output("data:mission:sizing:main_route:descent:distance", units="m") self.add_output("data:mission:sizing:initial_climb:duration", units="s") self.add_output("data:mission:sizing:main_route:climb:duration", units="s") self.add_output("data:mission:sizing:main_route:cruise:duration", units="s") self.add_output("data:mission:sizing:main_route:descent:duration", units="s") self.add_output("data:mission:sizing:diversion:climb:fuel", units="kg") self.add_output("data:mission:sizing:diversion:cruise:fuel", units="kg") self.add_output("data:mission:sizing:diversion:descent:fuel", units="kg") self.add_output("data:mission:sizing:diversion:climb:distance", units="m") self.add_output("data:mission:sizing:diversion:cruise:distance", units="m") self.add_output("data:mission:sizing:diversion:descent:distance", units="m") self.add_output("data:mission:sizing:diversion:climb:duration", units="s") self.add_output("data:mission:sizing:diversion:cruise:duration", units="s") self.add_output("data:mission:sizing:diversion:descent:duration", units="s") self.add_output("data:mission:sizing:holding:fuel", units="kg") self.add_output("data:mission:sizing:taxi_in:fuel", units="kg") self.add_output("data:mission:sizing:ZFW", units="kg") self.add_output("data:mission:sizing:fuel", units="kg") self.declare_partials(["*"], ["*"]) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): try: self.compute_mission(inputs, outputs) except IndexError: self.compute_breguet(inputs, outputs) def compute_breguet(self, inputs, outputs): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"] ) high_speed_polar = Polar( inputs["data:aerodynamics:aircraft:cruise:CL"], inputs["data:aerodynamics:aircraft:cruise:CD"], ) breguet = Breguet( propulsion_model, max( 10.0, high_speed_polar.optimal_cl / high_speed_polar.cd(high_speed_polar.optimal_cl) ), inputs["data:TLAR:cruise_mach"], 10000.0, ) breguet.compute( inputs["data:weight:aircraft:MTOW"], inputs["data:TLAR:range"], ) outputs["data:mission:sizing:ZFW"] = breguet.zfw outputs["data:mission:sizing:fuel"] = breguet.mission_fuel 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"])
class _compute_descent(om.ExplicitComponent): """ Compute the fuel consumption on descent segment with constant VCAS and descent rate. The hypothesis of small alpha angle is done. Warning: Descent rate is reduced if cd/cl < abs(desc_rate)! """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:mission:sizing:main_route:descent:descent_rate", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:optimal_CL", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:CD0", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:holding:fuel", 0.0, units="kg") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:initial_climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:cruise:fuel", np.nan, units="kg") self.add_output("data:mission:sizing:main_route:descent:fuel", units="kg") self.add_output("data:mission:sizing:main_route:descent:distance", 0.0, units="m") self.add_output("data:mission:sizing:main_route:descent:duration", units="s") self.declare_partials( "*", [ "data:aerodynamics:aircraft:cruise:optimal_CL", "data:aerodynamics:aircraft:cruise:CD0", "data:aerodynamics:aircraft:cruise:induced_drag_coefficient", "data:geometry:wing:area", "data:weight:aircraft:MTOW", "data:mission:sizing:taxi_out:fuel", "data:mission:sizing:holding:fuel", "data:mission:sizing:takeoff:fuel", "data:mission:sizing:initial_climb:fuel", "data:mission:sizing:main_route:climb:fuel", "data:mission:sizing:main_route:cruise:fuel", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) cruise_altitude = inputs["data:mission:sizing:main_route:cruise:altitude"] descent_rate = -abs(inputs["data:mission:sizing:main_route:descent:descent_rate"]) cl = inputs["data:aerodynamics:aircraft:cruise:optimal_CL"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k = inputs["data:aerodynamics:aircraft:cruise:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] m_to = inputs["data:mission:sizing:taxi_out:fuel"] m_ho = inputs["data:mission:sizing:holding:fuel"] m_tk = inputs["data:mission:sizing:takeoff:fuel"] m_ic = inputs["data:mission:sizing:initial_climb:fuel"] m_cl = inputs["data:mission:sizing:main_route:climb:fuel"] m_cr = inputs["data:mission:sizing:main_route:cruise:fuel"] # Define initial conditions gamma = math.asin(descent_rate) altitude_t = copy.deepcopy(cruise_altitude) distance_t = 0.0 time_t = 0.0 mass_fuel_t = 0.0 mass_t = mtow - (m_to + m_ho + m_tk + m_ic + m_cl + m_cr) atm_0 = Atmosphere(0.0) warning = False # Calculate defined VCAS at the beginning of descent (cos(gamma)~1) v_cas = math.sqrt((mass_t * g) * math.cos(descent_rate) / (0.5 * atm_0.density * wing_area * cl)) while altitude_t > SAFETY_HEIGHT: # Define air properties and calculate VTAS atm = Atmosphere(altitude_t, altitude_in_feet=False) v_tas = v_cas * math.sqrt(atm_0.density / atm.density) # Calculate lift and drag coefficients changes to maintain speed (cos(gamma)~1) cl = ((mass_t * g) * math.cos(descent_rate) / (0.5 * atm.density * wing_area * v_tas**2)) cd = cd0 + coef_k * cl**2 cl_cd = cl/cd drag = 0.5 * atm.density * wing_area * cd * v_tas**2 # Evaluate mach mach = math.sqrt( 5 * ((atm_0.pressure / atm.pressure * ( (1 + 0.2 * (v_cas / atm_0.speed_of_sound) ** 2) ** 3.5 - 1 ) + 1) ** (1 / 3.5) - 1) ) # Calculate necessary Thrust to maintain VCAS and descent rate # if T<0N, VCAS is maintained reducing gamma/descent rate and engine in IDLE condition thrust = drag + (mass_t * g) * math.sin(gamma) if thrust <= 0.0: flight_point = FlightPoint( mach=mach, altitude=altitude_t, engine_setting=EngineSetting.IDLE, thrust_rate=0.2) # FIXME: define IDLE maybe? descent_rate = -1/cl_cd gamma = math.asin(descent_rate) warning = True else: # FIXME: DESCENT setting on engine does not exist, replaced by CRUISE for test flight_point = FlightPoint( mach=mach, altitude=altitude_t, engine_setting=EngineSetting.CRUISE, thrust_is_regulated=True, thrust=thrust, ) propulsion_model.compute_flight_points(flight_point) # Calculate distance increase v_x = v_tas * math.cos(descent_rate) v_z = v_tas * math.sin(descent_rate) distance_t += v_x * TIME_STEP altitude_t += v_z * TIME_STEP # Estimate mass evolution and update time mass_fuel_t += propulsion_model.get_consumed_mass(flight_point, TIME_STEP) mass_t = mass_t - propulsion_model.get_consumed_mass(flight_point, TIME_STEP) time_t += TIME_STEP if warning: warnings.warn("Descent rate has been reduced!") outputs["data:mission:sizing:main_route:descent:fuel"] = mass_fuel_t outputs["data:mission:sizing:main_route:descent:distance"] = distance_t outputs["data:mission:sizing:main_route:descent:duration"] = time_t
class SizingMission(om.ExplicitComponent): def __init__(self, **kwargs): """ Computes thrust, SFC and thrust rate by direct call to engine model. Options: - propulsion_id: (mandatory) the identifier of the propulsion wrapper. - out_file: if provided, a csv file will be written at provided path with all computed flight points. If path is relative, it will be resolved from working directory """ super().__init__(**kwargs) self.flight_points = None self._engine_wrapper = None self._mission_input = None self._mission: MissionWrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) self.options.declare("out_file", default="", types=str) self.options.declare("breguet_iterations", default=2, types=int) self.options.declare("mission_file_path", types=str, allow_none=True, default=None) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self._mission_input = self.options["mission_file_path"] if not self._mission_input: with path(resources, "sizing_mission.yml") as mission_input_file: self._mission_input = MissionDefinition(mission_input_file) self._mission = MissionWrapper(self._mission_input) self._mission.setup(self) self.add_input("data:geometry:propulsion:engine:count", 2) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:takeoff:altitude", np.nan, units="m") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:takeoff:V2", np.nan, units="m/s") self.add_output("data:mission:sizing:ZFW", units="kg") self.declare_partials(["*"], ["*"]) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): if self.iter_count_without_approx < self.options["breguet_iterations"]: _LOGGER.info("Using Breguet for computing sizing mission.") self.compute_breguet(inputs, outputs) else: _LOGGER.info( "Using time-step integration for computing sizing mission.") self.compute_mission(inputs, outputs) def compute_breguet(self, inputs, outputs): """ Computes mission using simple Breguet formula at altitude==10000m Useful for initiating the computation. :param inputs: OpenMDAO input vector :param outputs: OpenMDAO output vector """ propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"]) high_speed_polar = Polar( inputs["data:aerodynamics:aircraft:cruise:CL"], inputs["data:aerodynamics:aircraft:cruise:CD"], ) breguet = Breguet( propulsion_model, max( 10.0, high_speed_polar.optimal_cl / high_speed_polar.cd(high_speed_polar.optimal_cl)), inputs["data:TLAR:cruise_mach"], 10000.0, ) breguet.compute( inputs["data:weight:aircraft:MTOW"], inputs["data:TLAR:range"], ) outputs["data:mission:sizing:ZFW"] = breguet.zfw outputs["data:mission:sizing:fuel"] = breguet.mission_fuel def compute_mission(self, inputs, outputs): """ Computes mission using time-step integration. :param inputs: OpenMDAO input vector :param outputs: OpenMDAO output vector """ propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"]) reference_area = inputs["data:geometry:wing:area"] self._mission.propulsion = propulsion_model self._mission.reference_area = reference_area end_of_takeoff = FlightPoint( time=0.0, # FIXME: legacy FAST was considering that aircraft was at MTOW before taxi out, # though it supposed to be at MTOW at takeoff. We keep this logic for sake of # non-regression, but it should be corrected later. mass=inputs["data:weight:aircraft:MTOW"] - inputs["data:mission:sizing:takeoff:fuel"] - inputs["data:mission:sizing:taxi_out:fuel"], true_airspeed=inputs["data:mission:sizing:takeoff:V2"], altitude=inputs["data:mission:sizing:takeoff:altitude"] + 35 * foot, ground_distance=0.0, ) self.flight_points = self._mission.compute(inputs, outputs, end_of_takeoff) # Final ================================================================ end_of_descent = FlightPoint.create(self.flight_points.loc[ self.flight_points.name == "sizing:main_route:descent"].iloc[-1]) end_of_taxi_in = FlightPoint.create(self.flight_points.iloc[-1]) 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"]) if self.options["out_file"]: self.flight_points.to_csv(self.options["out_file"])
class ComputeFuselageGeometryCabinSizingFL(ExplicitComponent): # TODO: Document equations. Cite sources """ Geometry of fuselage - Cabin is sized based on layout (seats, aisle...) and additional rear length (Fixed Length). """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:cabin:seats:passenger:NPAX_max", val=np.nan) self.add_input("data:geometry:cabin:seats:pilot:length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:pilot:width", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:width", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:count_by_row", val=np.nan) self.add_input("data:geometry:cabin:aisle_width", val=np.nan, units="m") self.add_input("data:geometry:cabin:luggage:mass_max", val=np.nan, units="kg") self.add_input("data:geometry:fuselage:rear_length", units="m") self.add_output("data:geometry:cabin:NPAX") self.add_output("data:geometry:fuselage:length", val=10.0, units="m") self.add_output("data:geometry:fuselage:maximum_width", units="m") self.add_output("data:geometry:fuselage:maximum_height", units="m") self.add_output("data:geometry:fuselage:front_length", units="m") self.add_output("data:geometry:fuselage:PAX_length", units="m") self.add_output("data:geometry:cabin:length", units="m") self.add_output("data:geometry:fuselage:wet_area", units="m**2") self.add_output("data:geometry:fuselage:luggage_length", units="m") self.declare_partials( "*", "*", method="fd") # FIXME: declare proper partials without int values def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), 1.0) npax_max = inputs["data:geometry:cabin:seats:passenger:NPAX_max"] l_pilot_seats = inputs["data:geometry:cabin:seats:pilot:length"] w_pilot_seats = inputs["data:geometry:cabin:seats:pilot:width"] l_pass_seats = inputs["data:geometry:cabin:seats:passenger:length"] w_pass_seats = inputs["data:geometry:cabin:seats:passenger:width"] seats_p_row = inputs[ "data:geometry:cabin:seats:passenger:count_by_row"] w_aisle = inputs["data:geometry:cabin:aisle_width"] luggage_mass_max = inputs["data:geometry:cabin:luggage:mass_max"] prop_layout = inputs["data:geometry:propulsion:layout"] lar = inputs["data:geometry:fuselage:rear_length"] # Length of instrument panel l_instr = 0.7 # Length of pax cabin # noinspection PyBroadException npax = math.ceil( float(npax_max) / float(seats_p_row)) * float(seats_p_row) n_rows = npax / float(seats_p_row) lpax = l_pilot_seats + n_rows * l_pass_seats # Cabin width considered is for side by side seats wcabin = max(2 * w_pilot_seats, seats_p_row * w_pass_seats + w_aisle) r_i = wcabin / 2 radius = 1.06 * r_i # Cylindrical fuselage b_f = 2 * radius # 0.14m is the distance between both lobe centers of the fuselage h_f = b_f + 0.14 # Luggage length (80% of internal radius section can be filled with luggage) luggage_density = 161.0 # In kg/m3 l_lug = (luggage_mass_max / luggage_density) / (0.8 * math.pi * r_i**2) # Cabin total length cabin_length = l_instr + lpax + l_lug # Calculate nose length if prop_layout == 3.0: # engine located in nose _, _, propulsion_length, _, _, _ = propulsion_model.compute_dimensions( ) lav = propulsion_length else: lav = 1.7 * h_f # Calculate fuselage length fus_length = lav + cabin_length + lar # Calculate wet area fus_dia = math.sqrt(b_f * h_f) # equivalent diameter of the fuselage cyl_length = fus_length - lav - lar wet_area_nose = 2.45 * fus_dia * lav wet_area_cyl = 3.1416 * fus_dia * cyl_length wet_area_tail = 2.3 * fus_dia * lar wet_area_fus = (wet_area_nose + wet_area_cyl + wet_area_tail) outputs["data:geometry:cabin:NPAX"] = npax outputs["data:geometry:fuselage:length"] = fus_length outputs["data:geometry:fuselage:maximum_width"] = b_f outputs["data:geometry:fuselage:maximum_height"] = h_f outputs["data:geometry:fuselage:front_length"] = lav outputs["data:geometry:fuselage:PAX_length"] = lpax outputs["data:geometry:cabin:length"] = cabin_length outputs["data:geometry:fuselage:wet_area"] = wet_area_fus outputs["data:geometry:fuselage:luggage_length"] = l_lug
class ComputeFuselageGeometryCabinSizing(ExplicitComponent): # TODO: Document equations. Cite sources """ Geometry of fuselage part A - Cabin (Commercial) estimation """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:TLAR:NPAX", val=np.nan) self.add_input("data:geometry:cabin:seats:pilot:length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:pilot:width", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:width", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:count_by_row", val=np.nan) self.add_input("data:geometry:cabin:aisle_width", val=np.nan, units="m") self.add_input("data:geometry:propulsion:layout", val=np.nan) self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m") self.add_input( "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input( "data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input("data:geometry:horizontal_tail:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:vertical_tail:MAC:length", val=np.nan, units="m") self.add_output("data:geometry:cabin:NPAX") self.add_output("data:geometry:fuselage:length", units="m") self.add_output("data:geometry:fuselage:maximum_width", units="m") self.add_output("data:geometry:fuselage:maximum_height", units="m") self.add_output("data:geometry:fuselage:front_length", units="m") self.add_output("data:geometry:fuselage:rear_length", units="m") self.add_output("data:geometry:fuselage:PAX_length", units="m") self.add_output("data:geometry:cabin:length", units="m") self.add_output("data:geometry:fuselage:wet_area", units="m**2") self.add_output("data:geometry:fuselage:luggage_length", units="m") self.declare_partials( "*", "*", method="fd") # FIXME: declare proper partials without int values def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), 1.0) npax = inputs["data:TLAR:NPAX"] l_pilot_seats = inputs["data:geometry:cabin:seats:pilot:length"] w_pilot_seats = inputs["data:geometry:cabin:seats:pilot:width"] l_pass_seats = inputs["data:geometry:cabin:seats:passenger:length"] w_pass_seats = inputs["data:geometry:cabin:seats:passenger:width"] seats_p_row = inputs[ "data:geometry:cabin:seats:passenger:count_by_row"] w_aisle = inputs["data:geometry:cabin:aisle_width"] prop_layout = inputs["data:geometry:propulsion:layout"] fa_length = inputs["data:geometry:wing:MAC:at25percent:x"] ht_lp = inputs[ "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"] vt_lp = inputs[ "data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25"] ht_length = inputs["data:geometry:horizontal_tail:MAC:length"] vt_length = inputs["data:geometry:vertical_tail:MAC:length"] # Length of instrument panel l_instr = 0.7 # Length of pax cabin # noinspection PyBroadException try: npax_1 = math.ceil(npax / seats_p_row) * seats_p_row except: npax_1 = npax n_rows = npax_1 / seats_p_row lpax = l_pilot_seats + n_rows * l_pass_seats # Cabin width considered is for side by side seats wcabin = max(2 * w_pilot_seats, seats_p_row * w_pass_seats + w_aisle) r_i = wcabin / 2 radius = 1.06 * r_i # Cylindrical fuselage b_f = 2 * radius # 0.14m is the distance between both lobe centers of the fuselage h_f = b_f + 0.14 # Luggage length l_lug = npax_1 * 0.20 / (math.pi * radius**2) # Cabin total length cabin_length = l_instr + lpax + l_lug # Calculate nose length if prop_layout == 3.0: # engine located in nose _, _, propulsion_length, _ = propulsion_model.compute_dimensions() lav = propulsion_length else: lav = 1.7 * h_f # Calculate fuselage length fus_length = fa_length + max(ht_lp + 0.75 * ht_length, vt_lp + 0.75 * vt_length) lar = fus_length - (lav + cabin_length) # Calculate wet area fus_dia = math.sqrt(b_f * h_f) # equivalent diameter of the fuselage cyl_length = fus_length - lav - lar wet_area_nose = 2.45 * fus_dia * lav wet_area_cyl = 3.1416 * fus_dia * cyl_length wet_area_tail = 2.3 * fus_dia * lar wet_area_fus = (wet_area_nose + wet_area_cyl + wet_area_tail) outputs["data:geometry:cabin:NPAX"] = npax_1 outputs["data:geometry:fuselage:length"] = fus_length outputs["data:geometry:fuselage:maximum_width"] = b_f outputs["data:geometry:fuselage:maximum_height"] = h_f outputs["data:geometry:fuselage:front_length"] = lav outputs["data:geometry:fuselage:rear_length"] = lar outputs["data:geometry:fuselage:PAX_length"] = lpax outputs["data:geometry:cabin:length"] = cabin_length outputs["data:geometry:fuselage:wet_area"] = wet_area_fus outputs["data:geometry:fuselage:luggage_length"] = l_lug
class ComputeFuselageGeometryCabinSizingFD(ExplicitComponent): # TODO: Document equations. Cite sources """ Geometry of fuselage - Cabin is sized based on layout (seats, aisle...) and HTP/VTP position (Fixed tail Distance). """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:cabin:seats:passenger:NPAX_max", val=np.nan) self.add_input("data:geometry:cabin:seats:pilot:length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:pilot:width", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:length", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:width", val=np.nan, units="m") self.add_input("data:geometry:cabin:seats:passenger:count_by_row", val=np.nan) self.add_input("data:geometry:cabin:aisle_width", val=np.nan, units="m") self.add_input("data:geometry:cabin:luggage:mass_max", val=np.nan, units="kg") self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m") self.add_input( "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input( "data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input("data:geometry:horizontal_tail:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:vertical_tail:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:horizontal_tail:sweep_25", val=np.nan, units="deg") self.add_input("data:geometry:horizontal_tail:span", val=np.nan, units="m") self.add_input("data:geometry:vertical_tail:sweep_25", val=np.nan, units="deg") self.add_input("data:geometry:vertical_tail:span", val=np.nan, units="m") self.add_output("data:geometry:cabin:NPAX") self.add_output("data:geometry:plane:length", units="m") self.add_output("data:geometry:fuselage:length", val=10.0, units="m") self.add_output("data:geometry:fuselage:maximum_width", units="m") self.add_output("data:geometry:fuselage:maximum_height", units="m") self.add_output("data:geometry:fuselage:front_length", units="m") self.add_output("data:geometry:fuselage:rear_length", units="m") self.add_output("data:geometry:fuselage:PAX_length", units="m") self.add_output("data:geometry:cabin:length", units="m") self.add_output("data:geometry:fuselage:wet_area", units="m**2") self.add_output("data:geometry:fuselage:luggage_length", units="m") self.declare_partials( "*", "*", method="fd") # FIXME: declare proper partials without int values def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), 1.0) npax_max = inputs["data:geometry:cabin:seats:passenger:NPAX_max"] l_pilot_seats = inputs["data:geometry:cabin:seats:pilot:length"] w_pilot_seats = inputs["data:geometry:cabin:seats:pilot:width"] l_pass_seats = inputs["data:geometry:cabin:seats:passenger:length"] w_pass_seats = inputs["data:geometry:cabin:seats:passenger:width"] seats_p_row = inputs[ "data:geometry:cabin:seats:passenger:count_by_row"] w_aisle = inputs["data:geometry:cabin:aisle_width"] luggage_mass_max = inputs["data:geometry:cabin:luggage:mass_max"] prop_layout = inputs["data:geometry:propulsion:layout"] fa_length = inputs["data:geometry:wing:MAC:at25percent:x"] ht_lp = inputs[ "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"] vt_lp = inputs[ "data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25"] ht_length = inputs["data:geometry:horizontal_tail:MAC:length"] vt_length = inputs["data:geometry:vertical_tail:MAC:length"] sweep_25_vt = inputs["data:geometry:vertical_tail:sweep_25"] b_v = inputs["data:geometry:vertical_tail:span"] sweep_25_ht = inputs["data:geometry:horizontal_tail:sweep_25"] b_h = inputs["data:geometry:horizontal_tail:span"] # Length of instrument panel l_instr = 0.7 # Length of pax cabin npax = math.ceil( float(npax_max) / float(seats_p_row)) * float(seats_p_row) n_rows = npax / float(seats_p_row) lpax = l_pilot_seats + n_rows * l_pass_seats # Cabin width considered is for side by side seats wcabin = max(2 * w_pilot_seats, seats_p_row * w_pass_seats + w_aisle) r_i = wcabin / 2 radius = 1.06 * r_i # Cylindrical fuselage b_f = 2 * radius # 0.14m is the distance between both lobe centers of the fuselage h_f = b_f + 0.14 # Luggage length (80% of internal radius section can be filled with luggage) luggage_density = 161.0 # In kg/m3 l_lug = (luggage_mass_max / luggage_density) / (0.8 * math.pi * r_i**2) # Cabin total length cabin_length = l_instr + lpax + l_lug # Calculate nose length if prop_layout == 3.0: # engine located in nose _, _, propulsion_length, _, _, spinner_length = propulsion_model.compute_dimensions( ) lav = propulsion_length + spinner_length else: lav = 1.40 * h_f # Used to be 1.7, supposedly as an A320 according to FAST legacy. Results on the BE76 tend to say it is # around 1.40, though it varies a lot depending on the airplane and its use # Calculate fuselage length fus_length = fa_length + max(ht_lp + 0.75 * ht_length, vt_lp + 0.75 * vt_length) plane_length = fa_length + max( ht_lp + 0.75 * ht_length + b_h / 2.0 * math.tan(sweep_25_ht * math.pi / 180), vt_lp + 0.75 * vt_length + b_v * math.tan(sweep_25_vt * math.pi / 180)) lar = fus_length - (lav + cabin_length) # Calculate wet area fus_dia = math.sqrt(b_f * h_f) # equivalent diameter of the fuselage cyl_length = fus_length - lav - lar wet_area_nose = 2.45 * fus_dia * lav wet_area_cyl = 3.1416 * fus_dia * cyl_length wet_area_tail = 2.3 * fus_dia * lar wet_area_fus = (wet_area_nose + wet_area_cyl + wet_area_tail) outputs["data:geometry:cabin:NPAX"] = npax outputs["data:geometry:fuselage:length"] = fus_length outputs["data:geometry:plane:length"] = plane_length outputs["data:geometry:fuselage:maximum_width"] = b_f outputs["data:geometry:fuselage:maximum_height"] = h_f outputs["data:geometry:fuselage:front_length"] = lav outputs["data:geometry:fuselage:rear_length"] = lar outputs["data:geometry:fuselage:PAX_length"] = lpax outputs["data:geometry:cabin:length"] = cabin_length outputs["data:geometry:fuselage:wet_area"] = wet_area_fus outputs["data:geometry:fuselage:luggage_length"] = l_lug
class _UpdateArea(om.ExplicitComponent): """ Computes area of horizontal tail plane (internal function) """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input("settings:weight:aircraft:CG:range", val=0.3) self.add_input("data:mission:sizing:takeoff:thrust_rate", val=np.nan) self.add_input("data:geometry:wing:area", val=np.nan, units="m**2") self.add_input("data:geometry:wing:MAC:at25percent:x", val=np.nan, units="m") self.add_input( "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25", val=np.nan, units="m") self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:propulsion:nacelle:height", val=np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg") self.add_input("data:weight:aircraft:MLW", val=np.nan, units="kg") self.add_input("data:weight:aircraft:CG:aft:x", val=np.nan, units="m") self.add_input("data:weight:airframe:landing_gear:main:CG:x", val=np.nan, units="m") self.add_input("data:weight:aircraft_empty:CG:z", val=np.nan, units="m") self.add_input("data:weight:propulsion:engine:CG:z", val=np.nan, units="m") self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", val=np.nan) self.add_input("data:aerodynamics:wing:low_speed:CM0_clean", val=np.nan) self.add_input("data:aerodynamics:aircraft:landing:CL_max", val=np.nan) self.add_input("data:aerodynamics:aircraft:takeoff:CL_max", val=np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", val=np.nan) self.add_input("data:aerodynamics:flaps:landing:CL", val=np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CL", val=np.nan) self.add_input("data:aerodynamics:flaps:landing:CM", val=np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CM", val=np.nan) self.add_input("data:aerodynamics:horizontal_tail:low_speed:CL_alpha", val=np.nan, units="rad**-1") self.add_input("data:aerodynamics:horizontal_tail:efficiency", val=np.nan) self.add_input("landing:cl_htp", val=np.nan) self.add_input("takeoff:cl_htp", val=np.nan) self.add_input("low_speed:cl_alpha_htp_isolated", val=np.nan) self.add_output("data:geometry:horizontal_tail:area", val=4.0, units="m**2") self.declare_partials( "*", "*", method="fd") # FIXME: write partial avoiding discrete parameters def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): # Sizing constraints for the horizontal tail (methods from Torenbeek). # Limiting cases: Rotating power at takeoff/landing, with the most # forward CG position. Returns maximum area. propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) n_engines = inputs["data:geometry:propulsion:count"] cg_range = inputs["settings:weight:aircraft:CG:range"] takeoff_t_rate = inputs["data:mission:sizing:takeoff:thrust_rate"] wing_area = inputs["data:geometry:wing:area"] x_wing_aero_center = inputs["data:geometry:wing:MAC:at25percent:x"] lp_ht = inputs[ "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"] wing_mac = inputs["data:geometry:wing:MAC:length"] mtow = inputs["data:weight:aircraft:MTOW"] mlw = inputs["data:weight:aircraft:MLW"] x_cg_aft = inputs["data:weight:aircraft:CG:aft:x"] z_cg_aircraft = inputs["data:weight:aircraft_empty:CG:z"] z_cg_engine = inputs["data:weight:propulsion:engine:CG:z"] x_lg = inputs["data:weight:airframe:landing_gear:main:CG:x"] cl0_clean = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] cl_max_clean = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"] cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"] cl_max_takeoff = inputs["data:aerodynamics:aircraft:takeoff:CL_max"] cl_flaps_landing = inputs["data:aerodynamics:flaps:landing:CL"] cl_flaps_takeoff = inputs["data:aerodynamics:flaps:takeoff:CL"] tail_efficiency_factor = inputs[ "data:aerodynamics:horizontal_tail:efficiency"] cl_htp_landing = inputs["landing:cl_htp"] cl_htp_takeoff = inputs["takeoff:cl_htp"] cm_landing = inputs[ "data:aerodynamics:wing:low_speed:CM0_clean"] + inputs[ "data:aerodynamics:flaps:landing:CM"] cm_takeoff = inputs[ "data:aerodynamics:wing:low_speed:CM0_clean"] + inputs[ "data:aerodynamics:flaps:takeoff:CM"] cl_alpha_htp_isolated = inputs["low_speed:cl_alpha_htp_isolated"] z_eng = z_cg_aircraft - z_cg_engine # Conditions for calculation atm = Atmosphere(0.0) rho = atm.density # CASE1: TAKE-OFF ############################################################################################## # method extracted from Torenbeek 1982 p325 # Calculation of take-off minimum speed weight = mtow * g vs0 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_takeoff)) vs1 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_clean)) # Rotation speed requirement from FAR 23.51 (depends on number of engines) if n_engines == 1: v_r = vs1 * 1.0 else: v_r = vs1 * 1.1 # Definition of max forward gravity center position x_cg = x_cg_aft - cg_range * wing_mac # Definition of horizontal tail global position x_ht = x_wing_aero_center + lp_ht # Calculation of wheel factor flight_point = FlightPoint(mach=v_r / atm.speed_of_sound, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=takeoff_t_rate) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) fact_wheel = ( (x_lg - x_cg - z_eng * thrust / weight) / wing_mac * (vs0 / v_r)**2 ) # FIXME: not clear if vs0 or vs1 should be used in formula # Compute aerodynamic coefficients for takeoff @ 0° aircraft angle cl0_takeoff = cl0_clean + cl_flaps_takeoff # Calculation of correction coefficient n_h and n_q n_h = ( x_ht - x_lg ) / lp_ht * tail_efficiency_factor # tail_efficiency_factor: dynamic pressure reduction at # tail (typical value) n_q = 1 + cl_alpha_htp_isolated / cl_htp_takeoff * _ANG_VEL * ( x_ht - x_lg) / v_r # Calculation of volume coefficient based on Torenbeek formula coef_vol = (cl_max_takeoff / (n_h * n_q * cl_htp_takeoff) * (cm_takeoff / cl_max_takeoff - fact_wheel) + cl0_takeoff / cl_htp_takeoff * (x_lg - x_wing_aero_center) / wing_mac) # Calculation of equivalent area area_1 = coef_vol * wing_area * wing_mac / lp_ht # CASE2: LANDING ############################################################################################### # method extracted from Torenbeek 1982 p325 # Calculation of take-off minimum speed weight = mlw * g vs0 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_landing)) # Rotation speed requirement from FAR 23.73 v_r = vs0 * 1.3 # Calculation of wheel factor flight_point = FlightPoint( mach=v_r / atm.speed_of_sound, altitude=0.0, engine_setting=EngineSetting.IDLE, thrust_rate=0.1 ) # FIXME: fixed thrust rate (should depend on wished descent rate) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) fact_wheel = ( (x_lg - x_cg - z_eng * thrust / weight) / wing_mac * (vs0 / v_r)**2 ) # FIXME: not clear if vs0 or vs1 should be used in formula # Evaluate aircraft overall angle (aoa) cl0_landing = cl0_clean + cl_flaps_landing # Calculation of correction coefficient n_h and n_q n_h = ( x_ht - x_lg ) / lp_ht * tail_efficiency_factor # tail_efficiency_factor: dynamic pressure reduction at # tail (typical value) n_q = 1 + cl_alpha_htp_isolated / cl_htp_landing * _ANG_VEL * ( x_ht - x_lg) / v_r # Calculation of volume coefficient based on Torenbeek formula coef_vol = (cl_max_landing / (n_h * n_q * cl_htp_landing) * (cm_landing / cl_max_landing - fact_wheel) + cl0_landing / cl_htp_landing * (x_lg - x_wing_aero_center) / wing_mac) # Calculation of equivalent area area_2 = coef_vol * wing_area * wing_mac / lp_ht if max(area_1, area_2) < 0.0: print( "Warning: HTP area estimated negative (in ComputeHTArea) forced to 1m²!" ) outputs["data:geometry:horizontal_tail:area"] = 1.0 else: outputs["data:geometry:horizontal_tail:area"] = max(area_1, area_2)
class ComputeBalkedLandingLimit(aircraft_equilibrium_limit): """ Computes fwd limit position of cg in case of a balked landing """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): super().setup() self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:weight:aircraft:MTOW", val=np.nan, units="kg") self.add_input("data:weight:aircraft:MLW", val=np.nan, units="kg") self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input( "data:aerodynamics:wing:low_speed:induced_drag_coefficient", val=np.nan) self.add_input( "data:aerodynamics:horizontal_tail:low_speed:induced_drag_coefficient", val=np.nan) self.add_input("data:aerodynamics:aircraft:landing:CL_max", val=np.nan) self.add_input("data:aerodynamics:aircraft:low_speed:CD0", val=np.nan) self.add_output("data:handling_qualities:balked_landing_limit:x", val=4.0, units="m") self.add_output( "data:handling_qualities:balked_landing_limit:MAC_position", val=np.nan) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): mlw = inputs["data:weight:aircraft:MLW"] cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"] wing_area = inputs["data:geometry:wing:area"] fa_length = inputs["data:geometry:wing:MAC:at25percent:x"] l0_wing = inputs["data:geometry:wing:MAC:length"] rho = Atmosphere(0.0).density v_s0 = math.sqrt( (mlw * 9.81) / (0.5 * rho * wing_area * cl_max_landing)) v_ref = 1.3 * v_s0 propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) x_cg = float(fa_length) increment = l0_wing / 100. equilibrium_found = True climb_gradient_achieved = True while equilibrium_found and climb_gradient_achieved: climb_gradient, equilibrium_found = self.delta_climb_rate( x_cg, v_ref, mlw, propulsion_model, inputs) if climb_gradient < 0.033: climb_gradient_achieved = False x_cg -= increment outputs["data:handling_qualities:balked_landing_limit:x"] = x_cg x_cg_ratio = (x_cg - fa_length + 0.25 * l0_wing) / l0_wing outputs[ "data:handling_qualities:balked_landing_limit:MAC_position"] = x_cg_ratio def delta_climb_rate(self, x_cg, v_ref, mass, propulsion_model, inputs): coeff_k_wing = inputs[ "data:aerodynamics:wing:low_speed:induced_drag_coefficient"] coeff_k_htp = inputs[ "data:aerodynamics:horizontal_tail:low_speed:induced_drag_coefficient"] cl_alpha_wing = inputs["data:aerodynamics:wing:low_speed:CL_alpha"] cl_alpha_htp = inputs[ "data:aerodynamics:horizontal_tail:low_speed:CL_alpha"] cl_delta_htp = inputs["data:aerodynamics:elevator:low_speed:CL_delta"] cd_flaps = inputs["data:aerodynamics:flaps:landing:CD"] cl_flaps = inputs["data:aerodynamics:flaps:landing:CL"] cd_0 = inputs["data:aerodynamics:aircraft:low_speed:CD0"] rho = Atmosphere(0.0).density sos = Atmosphere(0.0).speed_of_sound dynamic_pressure = 1. / 2. * rho * v_ref**2.0 alpha_ac, delta_e, equilibrium_found = self.found_cl_repartition( inputs, 1.0, mass, dynamic_pressure, x_cg) cl_AOA_wing = cl_alpha_wing * alpha_ac cl_AOA_htp = cl_alpha_htp * alpha_ac cl_elevator = cl_delta_htp * delta_e cd_min = cd_0 + cd_flaps cl = cl_AOA_wing + cl_AOA_htp + cl_elevator + cl_flaps cd = cd_min + \ coeff_k_wing * (cl_AOA_wing + cl_flaps) ** 2.0 + \ coeff_k_htp * (cl_AOA_htp + cl_elevator) ** 2.0 flight_point = FlightPoint( mach=v_ref / sos, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) propeller_advance_ratio = v_ref / (2700. / 60. * 1.97) propeller_efficiency_reduction = math.sin(propeller_advance_ratio * math.pi / 2.) climb_angle = math.asin(propeller_efficiency_reduction * thrust / (mass * 9.81) - cd / cl) climb_gradient = math.tan(climb_angle) return climb_gradient, equilibrium_found
class _vloff_from_v2(om.ExplicitComponent): """ Search alpha-angle<=alpha(v2) at which Vloff is operated such that aircraft reaches v>=v2 speed @ safety height with imposed rotation speed. Fuel burn is neglected : mass = MTOW. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CL", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_alpha", np.nan, units="rad**-1") self.add_input("data:aerodynamics:aircraft:low_speed:CD0", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CD", np.nan) self.add_input("data:aerodynamics:wing:low_speed:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:geometry:wing:span", np.nan, units="m") self.add_input("data:geometry:landing_gear:height", np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:takeoff:thrust_rate", np.nan) self.add_input("v2:speed", np.nan, units='m/s') self.add_input("v2:angle", np.nan, units='rad') self.add_output("vloff:speed", units='m/s') self.add_output("vloff:angle", units='rad') self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) cl0 = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] + inputs["data:aerodynamics:flaps:takeoff:CL"] cl_alpha = inputs["data:aerodynamics:wing:low_speed:CL_alpha"] cd0 = inputs["data:aerodynamics:aircraft:low_speed:CD0"] + inputs["data:aerodynamics:flaps:takeoff:CD"] coef_k = inputs["data:aerodynamics:wing:low_speed:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] wing_span = inputs["data:geometry:wing:span"] lg_height = inputs["data:geometry:landing_gear:height"] mtow = inputs["data:weight:aircraft:MTOW"] thrust_rate = inputs["data:mission:sizing:takeoff:thrust_rate"] v2_target = float(inputs["v2:speed"]) alpha_v2 = float(inputs["v2:angle"]) # Define ground factor effect on Drag k_ground = lambda altitude: ( 33. * ((lg_height + altitude) / wing_span) ** 1.5 / (1. + 33. * ((lg_height + altitude) / wing_span) ** 1.5) ) # Calculate v2 speed @ safety height for different alpha lift-off alpha = np.linspace(0.0, min(ALPHA_LIMIT, alpha_v2), num=10) vloff = np.zeros(np.size(alpha)) v2 = np.zeros(np.size(alpha)) atm_0 = Atmosphere(0.0) for i in range(len(alpha)): # Calculate lift coefficient cl = cl0 + cl_alpha * alpha[i] # Loop on estimated lift-off speed error induced by thrust estimation rel_error = 0.1 vloff[i] = math.sqrt((mtow * g) / (0.5 * atm_0.density * wing_area * cl)) while rel_error > 0.05: # Update thrust with vloff flight_point = FlightPoint( mach=vloff[i] / atm_0.speed_of_sound, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=thrust_rate ) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculate vloff necessary to overcome weight if thrust * math.sin(alpha[i]) > mtow * g: break else: v = math.sqrt((mtow * g - thrust * math.sin(alpha[i])) / (0.5 * atm_0.density * wing_area * cl)) rel_error = abs(v - vloff[i]) / v vloff[i] = v # Perform climb with imposed rotational speed till reaching safety height alpha_t = alpha[i] gamma_t = 0.0 v_t = float(vloff[i]) altitude_t = 0.0 distance_t = 0.0 while altitude_t < SAFETY_HEIGHT: # Estimation of thrust atm = Atmosphere(altitude_t, altitude_in_feet=False) flight_point = FlightPoint( mach=v_t / atm.speed_of_sound, altitude=altitude_t, engine_setting=EngineSetting.TAKEOFF, thrust_rate=thrust_rate ) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculate lift and drag cl = cl0 + cl_alpha * alpha_t lift = 0.5 * atm.density * wing_area * cl * v_t ** 2 cd = cd0 + k_ground(altitude_t) * coef_k * cl ** 2 drag = 0.5 * atm.density * wing_area * cd * v_t ** 2 # Calculate acceleration on x/z air axis weight = mtow * g acc_x = (thrust * math.cos(alpha_t) - weight * math.sin(gamma_t) - drag) / mtow acc_z = (lift + thrust * math.sin(alpha_t) - weight * math.cos(gamma_t)) / mtow # Calculate gamma change and new speed delta_gamma = math.atan((acc_z * TIME_STEP) / (v_t + acc_x * TIME_STEP)) v_t_new = math.sqrt((acc_z * TIME_STEP) ** 2 + (v_t + acc_x * TIME_STEP) ** 2) # Trapezoidal integration on distance/altitude delta_altitude = (v_t_new * math.sin(gamma_t + delta_gamma) + v_t * math.sin(gamma_t)) / 2 * TIME_STEP delta_distance = (v_t_new * math.cos(gamma_t + delta_gamma) + v_t * math.cos(gamma_t)) / 2 * TIME_STEP # Update temporal values alpha_t = min(alpha_v2, alpha_t + ALPHA_RATE * TIME_STEP) gamma_t = gamma_t + delta_gamma altitude_t = altitude_t + delta_altitude distance_t = distance_t + delta_distance v_t = v_t_new # Save obtained v2 v2[i] = v_t # If v2 target speed not reachable maximum lift-off speed chosen (alpha=0°) if sum(v2 > v2_target) == 0: alpha = 0.0 vloff = vloff[0] # FIXME: not reachable v2 warnings.warn("V2 @ 50ft requirement not reachable with max lift-off speed!") else: # If max alpha angle lead to v2 > v2 target take it if v2[-1] > v2_target: alpha = alpha[-1] vloff = vloff[-1] else: alpha = np.interp(v2_target, v2, alpha) vloff = np.interp(v2_target, v2, vloff) outputs["vloff:speed"] = vloff outputs["vloff:angle"] = alpha
class Cd0Nacelle(ExplicitComponent): def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("low_speed_aero", default=False, types=bool) self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", val=np.nan) self.add_input("data:geometry:wing:MAC:length", val=np.nan, units="m") self.add_input("data:geometry:wing:area", val=np.nan, units="m**2") if self.options["low_speed_aero"]: self.add_input("data:aerodynamics:low_speed:mach", val=np.nan) self.add_input("data:aerodynamics:low_speed:unit_reynolds", val=np.nan, units="m**-1") self.add_output("data:aerodynamics:nacelles:low_speed:CD0") else: self.add_input("data:aerodynamics:cruise:mach", val=np.nan) self.add_input("data:aerodynamics:cruise:unit_reynolds", val=np.nan, units="m**-1") self.add_output("data:aerodynamics:nacelles:cruise:CD0") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), 1.0) engine_number = inputs["data:geometry:propulsion:count"] prop_layout = inputs["data:geometry:propulsion:layout"] l0_wing = inputs["data:geometry:wing:MAC:length"] wing_area = inputs["data:geometry:wing:area"] if self.options["low_speed_aero"]: mach = inputs["data:aerodynamics:low_speed:mach"] unit_reynolds = inputs["data:aerodynamics:low_speed:unit_reynolds"] else: mach = inputs["data:aerodynamics:cruise:mach"] unit_reynolds = inputs["data:aerodynamics:cruise:unit_reynolds"] drag_force = propulsion_model.compute_drag(mach, unit_reynolds, l0_wing) if (prop_layout == 1.0) or (prop_layout == 2.0): cd0 = drag_force / wing_area * engine_number elif prop_layout == 3.0: cd0 = 0.0 else: cd0 = 0.0 warnings.warn( 'Propulsion layout {} not implemented in model, replaced by layout 1!' .format(prop_layout)) if self.options["low_speed_aero"]: outputs["data:aerodynamics:nacelles:low_speed:CD0"] = cd0 else: outputs["data:aerodynamics:nacelles:cruise:CD0"] = cd0
class _simulate_takeoff(om.ExplicitComponent): """ Simulate take-off from 0m/s speed to safety height using input VR. Fuel burn is supposed negligible : mass = MTOW. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_max_clean", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL0_clean", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CL", np.nan) self.add_input("data:aerodynamics:wing:low_speed:CL_alpha", np.nan, units="rad**-1") self.add_input("data:aerodynamics:aircraft:low_speed:CD0", np.nan) self.add_input("data:aerodynamics:flaps:takeoff:CD", np.nan) self.add_input("data:aerodynamics:wing:low_speed:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:geometry:wing:span", np.nan, units="m") self.add_input("data:geometry:landing_gear:height", np.nan, units="m") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:takeoff:thrust_rate", np.nan) self.add_input("data:mission:sizing:takeoff:friction_coefficient_no_brake", np.nan) self.add_input("vr:speed", np.nan, units='m/s') self.add_input("v2:angle", np.nan, units='rad') self.add_output("data:mission:sizing:takeoff:VR", units='m/s') self.add_output("data:mission:sizing:takeoff:VLOF", units='m/s') self.add_output("data:mission:sizing:takeoff:V2", units='m/s') self.add_output("data:mission:sizing:takeoff:TOFL", units='m') self.add_output("data:mission:sizing:takeoff:duration", units='s') self.add_output("data:mission:sizing:takeoff:fuel", units='kg') self.add_output("data:mission:sizing:initial_climb:fuel", units='kg') self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) cl_max_clean = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"] cl0 = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] + inputs["data:aerodynamics:flaps:takeoff:CL"] cl_alpha = inputs["data:aerodynamics:wing:low_speed:CL_alpha"] cd0 = inputs["data:aerodynamics:aircraft:low_speed:CD0"] + inputs["data:aerodynamics:flaps:takeoff:CD"] coef_k = inputs["data:aerodynamics:wing:low_speed:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] wing_span = inputs["data:geometry:wing:span"] lg_height = inputs["data:geometry:landing_gear:height"] mtow = inputs["data:weight:aircraft:MTOW"] thrust_rate = inputs["data:mission:sizing:takeoff:thrust_rate"] friction_coeff = inputs["data:mission:sizing:takeoff:friction_coefficient_no_brake"] alpha_v2 = float(inputs["v2:angle"]) # Define ground factor effect on Drag k_ground = lambda altitude: ( 33. * ((lg_height + altitude) / wing_span) ** 1.5 / (1. + 33. * ((lg_height + altitude) / wing_span) ** 1.5) ) # Determine rotation speed from regulation CS23.51 vs1 = math.sqrt((mtow * g) / (0.5 * Atmosphere(0).density * wing_area * cl_max_clean)) if inputs["data:geometry:propulsion:count"] == 1.0: k = 1.0 else: k = 1.1 vr = max(k * vs1, float(inputs["vr:speed"])) # Start calculation of flight from null speed to 35ft high alpha_t = 0.0 gamma_t = 0.0 v_t = 0.0 altitude_t = 0.0 distance_t = 0.0 mass_fuel1_t = 0.0 mass_fuel2_t = 0.0 time_t = 0.0 vloff = 0.0 climb = False while altitude_t < SAFETY_HEIGHT: # Estimation of thrust atm = Atmosphere(altitude_t, altitude_in_feet=False) flight_point = FlightPoint( mach=max(v_t, vr) / atm.speed_of_sound, altitude=altitude_t, engine_setting=EngineSetting.TAKEOFF, thrust_rate=thrust_rate ) # FIXME: (speed increased to vr to have feasible consumptions) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculate lift and drag cl = cl0 + cl_alpha * alpha_t lift = 0.5 * atm.density * wing_area * cl * v_t ** 2 cd = cd0 + k_ground(altitude_t) * coef_k * cl ** 2 drag = 0.5 * atm.density * wing_area * cd * v_t ** 2 # Check if lift-off condition reached if ((lift + thrust * math.sin(alpha_t) - mtow * g * math.cos(gamma_t)) >= 0.0) and not climb: climb = True vloff = v_t # Calculate acceleration on x/z air axis if climb: acc_z = (lift + thrust * math.sin(alpha_t) - mtow * g * math.cos(gamma_t)) / mtow acc_x = (thrust * math.cos(alpha_t) - mtow * g * math.sin(gamma_t) - drag) / mtow else: friction = (mtow * g - lift - thrust * math.sin(alpha_t)) * friction_coeff acc_z = 0.0 acc_x = (thrust * math.cos(alpha_t) - drag - friction) / mtow # Calculate gamma change and new speed delta_gamma = math.atan((acc_z * TIME_STEP) / (v_t + acc_x * TIME_STEP)) v_t_new = math.sqrt((acc_z * TIME_STEP) ** 2 + (v_t + acc_x * TIME_STEP) ** 2) # Trapezoidal integration on distance/altitude delta_altitude = (v_t_new * math.sin(gamma_t + delta_gamma) + v_t * math.sin(gamma_t)) / 2 * TIME_STEP delta_distance = (v_t_new * math.cos(gamma_t + delta_gamma) + v_t * math.cos(gamma_t)) / 2 * TIME_STEP # Update temporal values if v_t >= vr: alpha_t = min(alpha_v2, alpha_t + ALPHA_RATE * TIME_STEP) gamma_t = gamma_t + delta_gamma altitude_t = altitude_t + delta_altitude if not climb: mass_fuel1_t += propulsion_model.get_consumed_mass(flight_point, TIME_STEP) distance_t = distance_t + delta_distance time_t = time_t + TIME_STEP else: mass_fuel2_t += propulsion_model.get_consumed_mass(flight_point, TIME_STEP) time_t = time_t + TIME_STEP v_t = v_t_new outputs["data:mission:sizing:takeoff:VR"] = vr outputs["data:mission:sizing:takeoff:VLOF"] = vloff outputs["data:mission:sizing:takeoff:V2"] = v_t outputs["data:mission:sizing:takeoff:TOFL"] = distance_t outputs["data:mission:sizing:takeoff:duration"] = time_t outputs["data:mission:sizing:takeoff:fuel"] = mass_fuel1_t outputs["data:mission:sizing:initial_climb:fuel"] = mass_fuel2_t
class BreguetWithPropulsion(om.ExplicitComponent): def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:mission:sizing:main_route:cruise:altitude", np.nan, units="m") self.add_input("data:TLAR:cruise_mach", np.nan) self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:aerodynamics:aircraft:cruise:L_D_max", np.nan) self.add_input("data:geometry:propulsion:engine:count", 2) self.add_input("settings:mission:sizing:breguet:climb:mass_ratio", 0.97) self.add_input("settings:mission:sizing:breguet:descent:mass_ratio", 0.98) self.add_input("settings:mission:sizing:breguet:reserve:mass_ratio", 0.06) self.add_input("data:TLAR:range", np.nan, units="m") self.add_input("settings:mission:sizing:breguet:climb_descent_distance", 500.0e3, units="m") self.add_output("data:mission:sizing:main_route:climb:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:main_route:cruise:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:main_route:descent:distance", units="m", ref=1e3) self.add_output("data:mission:sizing:ZFW", units="kg", ref=1e4) self.add_output("data:mission:sizing:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:climb:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:cruise:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:main_route:descent:fuel", units="kg", ref=1e4) self.add_output("data:mission:sizing:fuel_reserve", units="kg", ref=1e4) self.add_output("data:propulsion:SFC", units="kg/s/N", ref=1e-4) self.add_output("data:propulsion:thrust_rate", lower=0.0, upper=1.0) self.add_output("data:propulsion:thrust", units="N", ref=1e5) self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"] ) breguet = Breguet( propulsion_model, inputs["data:aerodynamics:aircraft:cruise:L_D_max"], inputs["data:TLAR:cruise_mach"], inputs["data:mission:sizing:main_route:cruise:altitude"], inputs["settings:mission:sizing:breguet:climb:mass_ratio"], inputs["settings:mission:sizing:breguet:descent:mass_ratio"], inputs["settings:mission:sizing:breguet:reserve:mass_ratio"], inputs["settings:mission:sizing:breguet:climb_descent_distance"], ) breguet.compute( inputs["data:weight:aircraft:MTOW"], inputs["data:TLAR:range"], ) outputs["data:propulsion:SFC"] = breguet.sfc outputs["data:propulsion:thrust_rate"] = breguet.thrust_rate outputs["data:propulsion:thrust"] = breguet.thrust outputs["data:mission:sizing:ZFW"] = breguet.zfw outputs["data:mission:sizing:fuel"] = breguet.mission_fuel outputs["data:mission:sizing:main_route:fuel"] = breguet.flight_fuel outputs["data:mission:sizing:main_route:climb:fuel"] = breguet.climb_fuel outputs["data:mission:sizing:main_route:cruise:fuel"] = breguet.cruise_fuel outputs["data:mission:sizing:main_route:descent:fuel"] = breguet.descent_fuel outputs["data:mission:sizing:main_route:climb:distance"] = breguet.climb_distance outputs["data:mission:sizing:main_route:cruise:distance"] = breguet.cruise_distance outputs["data:mission:sizing:main_route:descent:distance"] = breguet.descent_distance outputs["data:mission:sizing:fuel_reserve"] = breguet.reserve_fuel
class ComputeNacelleGeometry(om.ExplicitComponent): # TODO: Document equations. Cite sources """ Nacelle and pylon geometry estimation """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component( self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:wing:span", val=np.nan, units="m") self.add_input("data:geometry:propulsion:y_ratio", val=np.nan) self.add_input("data:geometry:fuselage:maximum_width", val=np.nan, units="m") self.add_output("data:geometry:propulsion:nacelle:length", units="m") self.add_output("data:geometry:propulsion:nacelle:height", units="m") self.add_output("data:geometry:propulsion:nacelle:width", units="m") self.add_output("data:geometry:propulsion:nacelle:wet_area", units="m**2") self.add_output("data:geometry:propulsion:propeller:depth", units="m") self.add_output("data:geometry:propulsion:propeller:diameter", units="m") self.add_output("data:geometry:landing_gear:height", units="m") self.add_output("data:geometry:propulsion:nacelle:y", units="m") self.declare_partials("*", "*", method="fd") def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), 1.0) prop_layout = inputs["data:geometry:propulsion:layout"] span = inputs["data:geometry:wing:span"] y_ratio = inputs["data:geometry:propulsion:y_ratio"] b_f = inputs["data:geometry:fuselage:maximum_width"] nac_height, nac_width, nac_length, nac_wet_area, prop_dia, prop_depth = propulsion_model.compute_dimensions( ) if prop_layout == 1.0: y_nacelle = y_ratio * span / 2 elif prop_layout == 2.0: y_nacelle = b_f / 2 + 0.8 * nac_width elif prop_layout == 3.0: y_nacelle = 0.0 else: y_nacelle = y_ratio * span / 2 warnings.warn( 'Propulsion layout {} not implemented in model, replaced by layout 1!' .format(prop_layout)) lg_height = 0.41 * prop_dia outputs["data:geometry:propulsion:nacelle:length"] = nac_length outputs["data:geometry:propulsion:nacelle:height"] = nac_height outputs["data:geometry:propulsion:nacelle:width"] = nac_width outputs["data:geometry:propulsion:nacelle:wet_area"] = nac_wet_area outputs["data:geometry:propulsion:propeller:depth"] = prop_depth outputs["data:geometry:propulsion:propeller:diameter"] = prop_dia outputs["data:geometry:landing_gear:height"] = lg_height outputs["data:geometry:propulsion:nacelle:y"] = y_nacelle
class _compute_climb(om.ExplicitComponent): """ Compute the fuel consumption on climb segment with constant VCAS and fixed thrust ratio. The hypothesis of small alpha/gamma angles is done. """ def __init__(self, **kwargs): super().__init__(**kwargs) self._engine_wrapper = None def initialize(self): self.options.declare("propulsion_id", default="", types=str) def setup(self): self._engine_wrapper = BundleLoader().instantiate_component(self.options["propulsion_id"]) self._engine_wrapper.setup(self) self.add_input("data:geometry:propulsion:count", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:CD0", np.nan) self.add_input("data:aerodynamics:aircraft:cruise:induced_drag_coefficient", np.nan) self.add_input("data:geometry:wing:area", np.nan, units="m**2") self.add_input("data:weight:aircraft:MTOW", np.nan, units="kg") self.add_input("data:mission:sizing:taxi_out:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:holding:fuel", 0.0, units="kg") self.add_input("data:mission:sizing:takeoff:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:initial_climb:fuel", np.nan, units="kg") self.add_input("data:mission:sizing:main_route:climb:thrust_rate", np.nan) self.add_output("data:mission:sizing:main_route:climb:fuel", units="kg") self.add_output("data:mission:sizing:main_route:climb:distance", units="m") self.add_output("data:mission:sizing:main_route:climb:duration", units="s") self.add_output("data:mission:sizing:main_route:climb:v_cas", units="m/s") self.declare_partials( "*", [ "data:aerodynamics:aircraft:cruise:CD0", "data:aerodynamics:aircraft:cruise:induced_drag_coefficient", "data:geometry:wing:area", "data:weight:aircraft:MTOW", "data:mission:sizing:taxi_out:fuel", "data:mission:sizing:holding:fuel", "data:mission:sizing:takeoff:fuel", "data:mission:sizing:initial_climb:fuel", ], method="fd", ) def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"] ) cruise_altitude = inputs["data:mission:sizing:main_route:cruise:altitude"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k = inputs["data:aerodynamics:aircraft:cruise:induced_drag_coefficient"] wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] m_to = inputs["data:mission:sizing:taxi_out:fuel"] m_ho = inputs["data:mission:sizing:holding:fuel"] m_tk = inputs["data:mission:sizing:takeoff:fuel"] m_ic = inputs["data:mission:sizing:initial_climb:fuel"] thrust_rate = inputs["data:mission:sizing:main_route:climb:thrust_rate"] # Define initial conditions altitude_t = SAFETY_HEIGHT # conversion to m distance_t = 0.0 time_t = 0.0 mass_t = mtow - (m_to + m_ho + m_tk + m_ic) mass_fuel_t = 0.0 atm_0 = Atmosphere(0.0) # FIXME: VCAS strategy is specific to ICE-propeller configuration, should be an input cl = math.sqrt(3*cd0/coef_k) atm = Atmosphere(altitude_t, altitude_in_feet=False) v_cas = math.sqrt((mass_t * g) / (0.5 * atm.density * wing_area * cl)) while altitude_t < cruise_altitude: # Define air properties atm = Atmosphere(altitude_t, altitude_in_feet=False) v_tas = v_cas * math.sqrt(atm_0.density / atm.density) # Evaluate thrust and sfc mach = math.sqrt( 5 * ((atm_0.pressure / atm.pressure * ( (1 + 0.2 * (v_cas / atm_0.speed_of_sound) ** 2) ** 3.5 - 1 ) + 1) ** (1 / 3.5) - 1) ) flight_point = FlightPoint( mach=mach, altitude=altitude_t, engine_setting=EngineSetting.CLIMB, thrust_rate=thrust_rate ) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Calculates cl and drag considering constant climb rate cl = mass_t * g / (0.5 * atm.density * wing_area * v_tas**2) cd = cd0 + coef_k * cl**2 # Calculate climb rate and height increase climb_rate = thrust / (mass_t * g) - cd / cl vz = v_tas * math.sin(climb_rate) vx = v_tas * math.cos(climb_rate) altitude_t += vz * TIME_STEP distance_t += vx * TIME_STEP # Estimate mass evolution and update time mass_fuel_t += propulsion_model.get_consumed_mass(flight_point, TIME_STEP) mass_t = mass_t - propulsion_model.get_consumed_mass(flight_point, TIME_STEP) time_t += TIME_STEP outputs["data:mission:sizing:main_route:climb:fuel"] = mass_fuel_t outputs["data:mission:sizing:main_route:climb:distance"] = distance_t outputs["data:mission:sizing:main_route:climb:duration"] = time_t outputs["data:mission:sizing:main_route:climb:v_cas"] = v_cas