def compute_dimensions(self) -> (float, float, float, float, float, float): """ Computes propulsion dimensions (engine/nacelle/propeller) from maximum power. Model from :... """ # Compute engine dimensions self.engine.length = self.ref["length"] * ( self.max_power / self.ref["max_power"])**(1 / 3) self.engine.height = self.ref["height"] * ( self.max_power / self.ref["max_power"])**(1 / 3) self.engine.width = self.ref["width"] * ( self.max_power / self.ref["max_power"])**(1 / 3) if self.prop_layout == 3.0: nacelle_length = 1.15 * self.engine.length # Based on the length between nose and firewall for TB20 and SR22 else: nacelle_length = 1.50 * self.engine.length # Compute nacelle dimensions self.nacelle = Nacelle( height=self.engine.height * 1.1, width=self.engine.width * 1.1, length=nacelle_length, ) self.nacelle.wet_area = 2 * (self.nacelle.height + self.nacelle.width) * self.nacelle.length # Compute propeller dimensions (2-blades) w_propeller = 2500 # regulated propeller speed in RPM v_sound = Atmosphere(self.design_altitude, altitude_in_feet=False).speed_of_sound d_max = (((v_sound * 0.85)**2 - self.design_speed**2) / ((w_propeller * math.pi / 30) / 2)**2)**0.5 d_opt = 1.04**2 * ((self.max_power / 735.5) * 1e8 / (w_propeller**2 * self.design_speed * 3.6))**(1 / 4) d = min(d_max, d_opt) t_0 = 7.4 * ((self.max_power / 735.5) * d)**(2 / 3) area = 13307 * t_0 / (d**2 * w_propeller**2) chord = area / d self.propeller = Propeller( area=area, depth=chord * 1.1, diameter=d, thrust_SL=t_0, ) propeller_depth = max(chord * 1.1, 0.2 * d) # For clarity purposes, it has been assimilated as the spinner length return self.nacelle["height"], self.nacelle["width"], self.nacelle[ "length"], self.nacelle["wet_area"], self.propeller[ "diameter"], self.propeller["depth"]
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): x0_wing = inputs["data:geometry:wing:MAC:leading_edge:x:local"] l0_wing = inputs["data:geometry:wing:MAC:length"] l1_wing = inputs["data:geometry:wing:root:virtual_chord"] width_max = inputs["data:geometry:fuselage:maximum_width"] fa_length = inputs["data:geometry:wing:MAC:at25percent:x"] fus_length = inputs["data:geometry:fuselage:length"] wing_area = inputs["data:geometry:wing:area"] aspect_ratio = inputs["data:geometry:wing:aspect_ratio"] lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"] cl_alpha_wing = inputs["data:aerodynamics:wing:cruise:CL_alpha"] cl_alpha_ht = inputs["data:aerodynamics:horizontal_tail:cruise:CL_alpha"] cl_delta_ht = inputs["data:aerodynamics:elevator:low_speed:CL_delta"] ch_alpha_3d = inputs["data:aerodynamics:horizontal_tail:cruise:hinge_moment:CH_alpha"] ch_delta_3d = inputs["data:aerodynamics:horizontal_tail:cruise:hinge_moment:CH_delta"] tail_efficiency = inputs["data:aerodynamics:horizontal_tail:efficiency"] v_cruise = inputs["data:TLAR:v_cruise"] alt_cruise = inputs["data:mission:sizing:main_route:cruise:altitude"] # TODO: make variable name in computation sequence more english x0_25 = fa_length - 0.25 * l0_wing - x0_wing + 0.25 * l1_wing ratio_x025 = x0_25 / fus_length # fitting result of Raymer book, figure 16.14 k_h = 0.01222 - 7.40541e-4 * ratio_x025 * 100 + 2.1956e-5 * (ratio_x025 * 100) ** 2 # equation from Raymer book, eqn 16.22 # FIXME: introduce cm_alpha_wing to the equation (non-symmetrical profile) cm_alpha_fus = k_h * width_max ** 2 * fus_length / (l0_wing * wing_area) * 180.0 / np.pi x_ca_plane = (tail_efficiency * cl_alpha_ht * lp_ht - cm_alpha_fus * l0_wing) / \ (cl_alpha_wing + tail_efficiency * cl_alpha_ht) x_aero_center = x_ca_plane / l0_wing + 0.25 outputs["data:aerodynamics:cruise:neutral_point:stick_fixed:x"] = x_aero_center sos = Atmosphere(alt_cruise).speed_of_sound mach = v_cruise / sos beta = math.sqrt(1. - mach ** 2.0) cl_delta_ht_cruise = cl_delta_ht / beta # The cl_alpha_ht in the formula for the free_elevator_factor is defined with respect to the tail angle of # attack, the one we compute is wth respect to the plane so it includes downwash, as a consequence we must # correct it influence for this specific calculation. We will use the formula for elliptical wing as it is well # known downwash_effect = (1. - 2. * cl_alpha_wing / (math.pi * aspect_ratio)) cl_alpha_ht_ht = cl_alpha_ht / downwash_effect free_elevator_factor = 1. - (cl_delta_ht_cruise/cl_alpha_ht_ht)*(ch_alpha_3d/ch_delta_3d) outputs["data:aerodynamics:cruise:neutral_point:free_elevator_factor"] = free_elevator_factor x_ca_plane_free = (tail_efficiency * free_elevator_factor * cl_alpha_ht * lp_ht - cm_alpha_fus * l0_wing) / \ (cl_alpha_wing + tail_efficiency * free_elevator_factor * cl_alpha_ht) x_aero_center_free = x_ca_plane_free / l0_wing + 0.25 outputs["data:aerodynamics:cruise:neutral_point:stick_free:x"] = x_aero_center_free
def compute_flight_points(self, flight_points: Union[FlightPoint, pd.DataFrame]): altitude = float(Atmosphere(np.array(flight_points.altitude)).get_altitude(altitude_in_feet=True)) mach = np.array(flight_points.mach) thrust = np.array(flight_points.thrust) sigma = Atmosphere(altitude).density / Atmosphere(0.0).density max_power = self.max_power * (sigma - (1 - sigma) / 7.55) max_thrust = min( self.max_thrust * sigma ** (1. / 3.), max_power * 0.8 / np.maximum(mach * Atmosphere(altitude).speed_of_sound, 1e-20) ) if flight_points.thrust_rate is None: flight_points.thrust = min(max_thrust, float(thrust)) flight_points.thrust_rate = float(thrust) / max_thrust else: flight_points.thrust = max_thrust * np.array(flight_points.thrust_rate) sfc_pmax = 8.5080e-08 # fixed whatever the thrust ratio, sfc for ONE 130kW engine ! sfc = sfc_pmax * flight_points.thrust_rate * mach * Atmosphere(altitude).speed_of_sound flight_points['sfc'] = sfc
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): fus_length = inputs["data:geometry:fuselage:length"] lav = inputs["data:geometry:fuselage:front_length"] lar = inputs["data:geometry:fuselage:rear_length"] maximum_width = inputs["data:geometry:fuselage:maximum_width"] maximum_height = inputs["data:geometry:fuselage:maximum_height"] wet_area_fus = inputs["data:geometry:fuselage:wet_area"] sizing_factor_ultimate = inputs["data:mission:sizing:cs23:sizing_factor_ultimate"] mtow = inputs["data:weight:aircraft:MTOW"] lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"] cruise_alt = inputs["data:mission:sizing:main_route:cruise:altitude"] v_cruise = inputs["data:TLAR:v_cruise"] * 0.5144 atm_cruise = Atmosphere(cruise_alt) rho_cruise = atm_cruise.density pressure_cruise = atm_cruise.pressure atm_sl = Atmosphere(0.0) pressure_sl = atm_sl.pressure dynamic_pressure = 1. / 2. * rho_cruise * v_cruise ** 2. * 0.020885434273039 if cruise_alt > 10000.: fus_dia = (maximum_height + maximum_width) / 2. v_press = (fus_length - lar - lav) * math.pi * (fus_dia / 2.) ** 2.0 delta_p = (pressure_sl - pressure_cruise) * 0.000145038 else: v_press = 0.0 delta_p = 0.0 a2 = 0.052 * ( wet_area_fus ** 1.086 * (sizing_factor_ultimate * mtow) ** 0.177 * lp_ht ** (-0.051) * ((fus_length - lar - lav) / maximum_height) ** (-0.072) * dynamic_pressure ** 0.241 + 11.9 * (v_press * delta_p) ** 0.271 ) outputs["data:weight:airframe:fuselage:mass_raymer"] = a2
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): wing_area = inputs["data:geometry:wing:area"] ht_area = inputs["data:geometry:horizontal_tail:area"] mlw = inputs["data:weight:aircraft:MLW"] cl0_htp = inputs["data:aerodynamics:horizontal_tail:low_speed:CL0"] cl_alpha_htp_isolated = inputs[ "data:aerodynamics:horizontal_tail:low_speed:CL_alpha_isolated"] cl_alpha_htp = inputs[ "data:aerodynamics:horizontal_tail:low_speed:CL_alpha"] cl0_clean_wing = inputs["data:aerodynamics:wing:low_speed:CL0_clean"] cl_alpha_wing = inputs["data:aerodynamics:wing:low_speed:CL_alpha"] cl_flaps_landing = inputs["data:aerodynamics:flaps:landing:CL"] cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"] cl_delta_elev = inputs["data:aerodynamics:elevator:low_speed:CL_delta"] # Conditions for calculation atm = Atmosphere(0.0) rho = atm.density # Calculate elevator max. additional lift if self.options["landing"]: elev_angle = inputs["data:mission:sizing:landing:elevator_angle"] else: elev_angle = inputs["data:mission:sizing:takeoff:elevator_angle"] cl_elev = cl_delta_elev * elev_angle # Define alpha angle depending on phase if self.options["landing"]: # Calculation of take-off minimum speed weight = mlw * g vs0 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_landing)) # Rotation speed correction v_r = vs0 * 1.3 # Evaluate aircraft overall angle (aoa) cl0_landing = cl0_clean_wing + cl_flaps_landing cl_landing = weight / (0.5 * rho * v_r**2 * wing_area) alpha = (cl_landing - cl0_landing) / cl_alpha_wing * 180 / math.pi else: # Define aircraft overall angle (aoa) alpha = 0.0 # Interpolate cl/cm and define with ht reference surface cl_htp = ((cl0_htp + (alpha * math.pi / 180) * cl_alpha_htp + cl_elev) * wing_area / ht_area) # Define Cl_alpha with htp reference surface cl_alpha_htp_isolated = cl_alpha_htp_isolated * wing_area / ht_area outputs["cl_htp"] = cl_htp outputs["cl_alpha_htp_isolated"] = cl_alpha_htp_isolated
def sfc_ratio( self, altitude: Union[float, Sequence[float]], thrust_rate: Union[float, Sequence[float]], mach: Union[float, Sequence[float]] = 0.8, ) -> Tuple[np.ndarray, np.ndarray]: """ Computation of ratio :math:`\\frac{SFC(P)}{SFC(Pmax)}`, given altitude and thrust_rate :math:`\\frac{F}{Fmax}`. Warning: this model is very limited :param altitude: :param thrust_rate: :param mach: Mach number(s) :return: SFC ratio and Power (in W) """ altitude = np.asarray(altitude) thrust_rate = np.asarray(thrust_rate) mach = np.asarray(mach) mach = mach + (mach == 0) * 1e-12 max_thrust = self.max_thrust( Atmosphere(altitude, altitude_in_feet=False), mach) sigma = Atmosphere( altitude, altitude_in_feet=False).density / Atmosphere(0.0).density max_power = np.minimum( self.max_power * (sigma - (1 - sigma) / 7.55), max_thrust * mach * Atmosphere(altitude).speed_of_sound / PROPELLER_EFFICIENCY) prop_power = (max_thrust * thrust_rate * mach * Atmosphere(altitude).speed_of_sound) mech_power = prop_power / PROPELLER_EFFICIENCY # FIXME: low speed efficiency should drop down leading to high mechanical power! power_rate = mech_power / max_power sfc_ratio = (-0.9976 * power_rate**2 + 1.9964 * power_rate) return sfc_ratio, (power_rate * max_power)
def test_sfc_at_max_thrust(): """ Checks model against values from :cite:`roux:2005` p.40 (only for ground/Mach=0 values, as cruise values of the report look flawed) .. bibliography:: ../refs.bib """ # Check with arrays cfm56_3c1 = RubberEngine(6, 25.7, 0, 0, 0, 0) atm = Atmosphere([0, 10668, 13000], altitude_in_feet=False) sfc = cfm56_3c1.sfc_at_max_thrust(atm, [0, 0.8, 0.8]) # Note: value for alt==10668 is different from PhD report # alt=13000 is here just for testing in stratosphere np.testing.assert_allclose(sfc, [0.97035e-5, 1.7756e-5, 1.7711e-5], rtol=1e-4) # Check with scalars trent900 = RubberEngine(7.14, 41, 0, 0, 0, 0) atm = Atmosphere(0, altitude_in_feet=False) sfc = trent900.sfc_at_max_thrust(atm, 0) np.testing.assert_allclose(sfc, 0.73469e-5, rtol=1e-4) atm = Atmosphere(9144, altitude_in_feet=False) sfc = trent900.sfc_at_max_thrust(atm, 0.8) np.testing.assert_allclose(sfc, 1.6766e-5, rtol=1e-4) # value is different from PhD report # Check with arrays pw2037 = RubberEngine(6, 31.8, 0, 0, 0, 0) atm = Atmosphere(0, altitude_in_feet=False) sfc = pw2037.sfc_at_max_thrust(atm, 0) np.testing.assert_allclose(sfc, 0.9063e-5, rtol=1e-4) atm = Atmosphere(10668, altitude_in_feet=False) sfc = pw2037.sfc_at_max_thrust(atm, 0.85) np.testing.assert_allclose(sfc, 1.7439e-5, rtol=1e-4) # value is different from PhD report
def test_sfc_at_max_thrust(): """ Checks model against values from :... .. bibliography:: ../refs.bib """ # Check with arrays # BasicICEngine(max_power(W), design_altitude(m), design_speed(m/s), fuel_type, strokes_nb) _50kw_engine = BasicICEngine(50000.0, 2400.0, 81.0, 1.0, 4.0) atm = Atmosphere([0, 10668, 13000], altitude_in_feet=False) sfc = _50kw_engine.sfc_at_max_power(atm) # Note: value for alt==10668 is different from PhD report # alt=13000 is here just for testing in stratosphere np.testing.assert_allclose( sfc, [7.09319444e-08, 6.52497276e-08, 6.44112478e-08], rtol=1e-4) # Check with scalars # BasicICEngine(max_power(W), design_altitude(m), design_speed(m/s), fuel_type, strokes_nb) _250kw_engine = BasicICEngine(250000.0, 2400.0, 81.0, 1.0, 4.0) atm = Atmosphere(0, altitude_in_feet=False) sfc = _250kw_engine.sfc_at_max_power(atm) np.testing.assert_allclose(sfc, 8.540416666666667e-08, rtol=1e-4)
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): l0_wing = inputs["data:geometry:wing:MAC:length"] speed = inputs["data:TLAR:approach_speed"] atm = Atmosphere(0.0, 15.0) mach = speed / atm.speed_of_sound reynolds = atm.get_unitary_reynolds(mach) * l0_wing outputs["data:aerodynamics:aircraft:landing:mach"] = mach outputs["data:aerodynamics:aircraft:landing:reynolds"] = reynolds
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): sizing_factor_ultimate = inputs[ "data:mission:sizing:cs23:sizing_factor_ultimate"] mtow = inputs["data:weight:aircraft:MTOW"] v_cruise_ktas = inputs["data:TLAR:v_cruise"] cruise_alt = inputs["data:mission:sizing:main_route:cruise:altitude"] area_ht = inputs["data:geometry:horizontal_tail:area"] t_c_ht = inputs["data:geometry:horizontal_tail:thickness_ratio"] sweep_25_ht = inputs["data:geometry:horizontal_tail:sweep_25"] ar_ht = inputs["data:geometry:horizontal_tail:aspect_ratio"] taper_ht = inputs["data:geometry:horizontal_tail:taper_ratio"] rho_cruise = Atmosphere(cruise_alt).density dynamic_pressure = 1. / 2. * rho_cruise * (v_cruise_ktas * 0.5144)**2. * 0.0208854 # In lb/ft2 a31 = 0.016 * ( (sizing_factor_ultimate * mtow)**0.414 * dynamic_pressure**0.168 * area_ht**0.896 * (100 * t_c_ht / math.cos(sweep_25_ht * math.pi / 180.))**-0.12 * (ar_ht / (math.cos(sweep_25_ht * math.pi / 180.))**2.0)**0.043 * taper_ht**-0.02) # Mass formula in lb outputs["data:weight:airframe:horizontal_tail:mass"] = a31 has_t_tail = inputs["data:geometry:has_T_tail"] area_vt = inputs["data:geometry:vertical_tail:area"] t_c_vt = inputs["data:geometry:vertical_tail:thickness_ratio"] sweep_25_vt = inputs["data:geometry:vertical_tail:sweep_25"] ar_vt = inputs["data:geometry:vertical_tail:aspect_ratio"] taper_vt = inputs["data:geometry:vertical_tail:taper_ratio"] a32 = 0.073 * (1. + 0.2 * has_t_tail) * ( (sizing_factor_ultimate * mtow)**0.376 * dynamic_pressure**0.122 * area_vt**0.873 * (100 * t_c_vt / math.cos(sweep_25_vt * math.pi / 180.))**-0.49 * (ar_vt / (math.cos(sweep_25_vt * math.pi / 180.))**2.0)**0.357 * taper_vt**0.039) # Mass formula in lb outputs["data:weight:airframe:vertical_tail:mass"] = a32
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): mtow = inputs["data:weight:aircraft:MTOW"] npax = inputs["data:TLAR:NPAX"] m_iae = inputs["data:weight:systems:navigation:mass"] limit_speed = inputs["data:TLAR:v_limit"] atm = Atmosphere(0.0) limit_speed = limit_speed / atm.speed_of_sound # converted to mach c22 = 0.261 * mtow**.52 * npax**0.68 * m_iae**0.17 * limit_speed**0.08 # mass formula in lb outputs["data:weight:systems:life_support:air_conditioning:mass"] = c22
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): # Get inputs b_f = inputs['data:geometry:fuselage:maximum_width'] span = inputs['data:geometry:wing:span'] aspect_ratio = inputs['data:geometry:wing:aspect_ratio'] wing_area = inputs['data:geometry:wing:area'] if self.options["low_speed_aero"]: mach = inputs["data:aerodynamics:low_speed:mach"] v_inf = max(Atmosphere(0.0).speed_of_sound * mach, 0.01) # avoid V=0 m/s crashes cl_clean = inputs["data:aerodynamics:wing:low_speed:CL"] cdp_clean = inputs["data:aerodynamics:wing:low_speed:CDp"] else: mach = inputs["data:aerodynamics:cruise:mach"] v_inf = inputs["data:TLAR:v_cruise"] cl_clean = inputs["data:aerodynamics:wing:cruise:CL"] cdp_clean = inputs["data:aerodynamics:wing:cruise:CDp"] super()._run(inputs) cl, _, oswald, _ = super().compute_wing(inputs, _INPUT_AOAList, v_inf, flaps_angle=0.0, use_airfoil=True) k_fus = 1 - 2 * (b_f / span)**2 oswald = oswald[0] * k_fus # Fuselage correction if mach > 0.4: oswald = oswald * (-0.001521 * ( (mach - 0.05) / 0.3 - 1)**10.82 + 1) # Mach correction cdp_foil = self._interpolate_cdp(cl_clean, cdp_clean, cl[0]) cdi = (1.05 * cl[0])**2 / ( math.pi * aspect_ratio * oswald) + cdp_foil # Aircraft cor.: Wing = 105% total lift. coef_e = cl[0]**2 / (math.pi * aspect_ratio * cdi) coef_k = 1. / (math.pi * span**2 / wing_area * coef_e) if self.options["low_speed_aero"]: outputs[ 'data:aerodynamics:aircraft:low_speed:induced_drag_coefficient'] = coef_k else: outputs[ 'data:aerodynamics:aircraft:cruise:induced_drag_coefficient'] = coef_k
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)
def compute_dimensions(self) -> (float, float, float, float): """ Computes propulsion dimensions (engine/nacelle/propeller) from maximum power. Model from :... """ # Compute engine dimensions self.engine.length = self.ref["length"] * ( self.max_power / self.ref["max_power"])**(1 / 3) self.engine.height = self.ref["height"] * ( self.max_power / self.ref["max_power"])**(1 / 3) self.engine.width = self.ref["width"] * ( self.max_power / self.ref["max_power"])**(1 / 3) # Compute nacelle dimensions self.nacelle = Nacelle( height=self.engine.height * 1.1, width=self.engine.width * 1.1, length=1.5 * self.engine.length, ) self.nacelle.wet_area = 2 * (self.nacelle.height + self.nacelle.width) * self.nacelle.length # Compute propeller dimensions (2-blades) w_propeller = 2500 # regulated propeller speed in RPM v_sound = Atmosphere(self.design_altitude, altitude_in_feet=False).speed_of_sound d_max = (((v_sound * 0.85)**2 - self.design_speed**2) / ((w_propeller * math.pi / 30) / 2)**2)**0.5 d_opt = 1.04**2 * ((self.max_power / 735.5) * 1e8 / (w_propeller**2 * self.design_speed * 3.6))**(1 / 4) d = min(d_max, d_opt) t_0 = 7.4 * ((self.max_power / 735.5) * d)**(2 / 3) area = 13307 * t_0 / (d**2 * w_propeller**2) chord = area / d self.propeller = Propeller( area=area, depth=chord * 1.1, diameter=d, thrust_SL=t_0, ) return self.nacelle["height"], self.nacelle["width"], self.nacelle[ "length"], self.nacelle["wet_area"]
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
def get_cd0(alt, mach, cl, low_speed_aero): reynolds = Atmosphere(alt).get_unitary_reynolds(mach) ivc = get_indep_var_comp(input_list) if low_speed_aero: ivc.add_output("data:aerodynamics:aircraft:takeoff:mach", mach) ivc.add_output("data:aerodynamics:wing:low_speed:reynolds", reynolds) ivc.add_output("data:aerodynamics:aircraft:low_speed:CL", 150 * [cl]) # needed because size of input array is fixed else: ivc.add_output("data:TLAR:cruise_mach", mach) ivc.add_output("data:aerodynamics:wing:cruise:reynolds", reynolds) ivc.add_output("data:aerodynamics:aircraft:cruise:CL", 150 * [cl]) # needed because size of input array is fixed problem = run_system(CD0(low_speed_aero=low_speed_aero), ivc) if low_speed_aero: return problem["data:aerodynamics:aircraft:low_speed:CD0"][0] else: return problem["data:aerodynamics:aircraft:cruise:CD0"][0]
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 compute(self, inputs, outputs): v_tas = inputs["data:TLAR:v_cruise"] cruise_altitude = inputs["data:mission:sizing:main_route:cruise:altitude"] design_mass = inputs["data:weight:aircraft:MTOW"] design_vc = Atmosphere(cruise_altitude, altitude_in_feet=False).get_equivalent_airspeed(v_tas) velocity_array, load_factor_array, _ = self.flight_domain(inputs, outputs, design_mass, cruise_altitude, design_vc, design_n_ps=0.0, design_n_ng=0.0) if DOMAIN_PTS_NB < len(velocity_array): velocity_array = velocity_array[0:DOMAIN_PTS_NB - 1] load_factor_array = load_factor_array[0:DOMAIN_PTS_NB - 1] warnings.warn("Defined maximum stored domain points in fast compute_vn.py exceeded!") else: additional_zeros = list(np.zeros(DOMAIN_PTS_NB - len(velocity_array))) velocity_array.extend(additional_zeros) load_factor_array.extend(additional_zeros) outputs["data:flight_domain:velocity"] = np.array(velocity_array) outputs["data:flight_domain:load_factor"] = np.array(load_factor_array)
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 compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): mtow = inputs["data:weight:aircraft:MTOW"] n_pax = inputs["data:geometry:cabin:seats:passenger:NPAX_max"] m_iae = inputs["data:weight:systems:navigation:mass"] limit_speed = inputs["data:TLAR:v_limit"] cruise_alt = inputs["data:mission:sizing:main_route:cruise:altitude"] n_occ = n_pax + 2. # Because there are two pilots that needs to be taken into account atm = Atmosphere(cruise_alt) limit_mach = limit_speed / atm.speed_of_sound # converted to mach c21 = 0.0 c22 = 0.265 * mtow**.52 * n_occ**0.68 * m_iae**0.17 * limit_mach**0.08 # mass formula in lb c23 = 0.0 c24 = 0.0 c25 = 0.0 c26 = 7. * n_occ**0.702 c27 = 0.0 outputs["data:weight:systems:life_support:insulation:mass"] = c21 outputs["data:weight:systems:life_support:air_conditioning:mass"] = c22 outputs["data:weight:systems:life_support:de_icing:mass"] = c23 outputs[ "data:weight:systems:life_support:internal_lighting:mass"] = c24 outputs[ "data:weight:systems:life_support:seat_installation:mass"] = c25 outputs["data:weight:systems:life_support:fixed_oxygen:mass"] = c26 outputs["data:weight:systems:life_support:security_kits:mass"] = c27
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
def _compute_flight_points( self, mach: Union[float, Sequence], altitude: Union[float, Sequence], engine_setting: Union[float, Sequence], thrust_is_regulated: Optional[Union[bool, Sequence]] = None, thrust_rate: Optional[Union[float, Sequence]] = None, thrust: Optional[Union[float, Sequence]] = None, ) -> Tuple[Union[float, Sequence], Union[float, Sequence], Union[ float, Sequence]]: """ Same as :meth:`compute_flight_points`. :param mach: Mach number :param altitude: (unit=m) altitude w.r.t. to sea level :param engine_setting: define engine settings :param thrust_is_regulated: tells if thrust_rate or thrust should be used (works element-wise) :param thrust_rate: thrust rate (unit=none) :param thrust: required thrust (unit=N) :return: SFC (in kg/s/N), thrust rate, thrust (in N) """ """ Computes the Specific Fuel Consumption based on aircraft trajectory conditions. :param flight_points.mach: Mach number :param flight_points.altitude: (unit=m) altitude w.r.t. to sea level :param flight_points.engine_setting: define :param flight_points.thrust_is_regulated: tells if thrust_rate or thrust should be used (works element-wise) :param flight_points.thrust_rate: thrust rate (unit=none) :param flight_points.thrust: required thrust (unit=N) :return: SFC (in kg/s/N), thrust rate, thrust (in N) """ # Treat inputs (with check on thrust rate <=1.0) mach = np.asarray(mach) altitude = np.asarray(altitude) if thrust_is_regulated is not None: thrust_is_regulated = np.asarray(np.round(thrust_is_regulated, 0), dtype=bool) thrust_is_regulated, thrust_rate, thrust = self._check_thrust_inputs( thrust_is_regulated, thrust_rate, thrust) thrust_is_regulated = np.asarray(np.round(thrust_is_regulated, 0), dtype=bool) thrust_rate = np.asarray(thrust_rate) thrust = np.asarray(thrust) # Get maximum thrust @ given altitude atmosphere = Atmosphere(altitude, altitude_in_feet=False) max_thrust = self.max_thrust(atmosphere, mach) # We compute thrust values from thrust rates when needed idx = np.logical_not(thrust_is_regulated) if np.size(max_thrust) == 1: maximum_thrust = max_thrust out_thrust_rate = thrust_rate out_thrust = thrust else: out_thrust_rate = (np.full(np.shape(max_thrust), thrust_rate.item()) if np.size(thrust_rate) == 1 else thrust_rate) out_thrust = (np.full(np.shape(max_thrust), thrust.item()) if np.size(thrust) == 1 else thrust) maximum_thrust = max_thrust[idx] if np.any(idx): out_thrust[idx] = out_thrust_rate[idx] * maximum_thrust if np.any(thrust_is_regulated): out_thrust[thrust_is_regulated] = np.minimum( out_thrust[thrust_is_regulated], max_thrust[thrust_is_regulated]) # thrust_rate is obtained from entire thrust vector (could be optimized if needed, # as some thrust rates that are computed may have been provided as input) out_thrust_rate = out_thrust / max_thrust # Now SFC can be computed sfc_pmax = self.sfc_at_max_power(atmosphere) sfc_ratio, mech_power = self.sfc_ratio(altitude, out_thrust_rate, mach) sfc = (sfc_pmax * sfc_ratio * mech_power) / np.maximum( out_thrust, 1e-6) # avoid 0 division return sfc, out_thrust_rate, out_thrust
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
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
def flight_domain(self, inputs, outputs, mass, altitude, design_vc, design_n_ps=0.0, design_n_ng=0.0): # Get necessary inputs wing_area = inputs["data:geometry:wing:area"] mtow = inputs["data:weight:aircraft:MTOW"] category = inputs[ "data:TLAR:category"] # Aerobatic = 1.0, Utility = 2.0, Normal = 3.0, Commuter = 4.0 level = inputs["data:TLAR:level"] Vh = inputs["data:TLAR:v_max_sl"] root_chord = inputs["data:geometry:wing:root:chord"] tip_chord = inputs["data:geometry:wing:tip:chord"] cl_max_flaps = inputs["data:aerodynamics:aircraft:landing:CL_max"] cl_max = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"] cl_min = inputs["data:aerodynamics:wing:low_speed:CL_min_clean"] mean_chord = (root_chord + tip_chord) / 2.0 atm_0 = Atmosphere(0.0) atm = Atmosphere(altitude, altitude_in_feet=False) # Initialise the lists in which we will store the data velocity_array = [] load_factor_array = [] # For some of the correlation presented in the regulation, we need to convert the data # of the airplane to imperial units weight_lbf = (mass * g) / self.lbf_to_N mtow_lbf = (mtow * g) / self.lbf_to_N wing_area_sft = wing_area / (self.ft_to_m**2.0) mtow_loading_psf = mtow_lbf / wing_area_sft # [lbf/ft**2] # We can now start computing the values of the different air-speeds given in the regulation # as well as the load factors. We will here make the choice to stick with the limits given # in the certifications even though they sometimes allow to choose design speeds and loads # over the values written in the documents. # Lets start by computing the 1g/-1g stall speeds using the usual formulations Vs_1g_ps = math.sqrt( (2. * mass * g) / (atm_0.density * wing_area * cl_max)) # [m/s] Vs_1g_ng = math.sqrt( (2. * mass * g) / (atm_0.density * wing_area * abs(cl_min))) # [m/s] velocity_array.append(float(Vs_1g_ps)) load_factor_array.append(1.0) velocity_array.append(float(Vs_1g_ng)) load_factor_array.append(-1.0) # As we will consider all the calculated speed to be Vs_1g_ps < V < 1.4*Vh, we will compute cl_alpha for N # points equally spaced on log scale (to take into account the high non-linearity effect). # If the option is not selected, we will only consider low_speed and cruise cl_alpha points and consider a # square regression between both. if self.options["compute_cl_alpha"]: mach_interp = inputs[ "data:aerodynamics:aircraft:mach_interpolation:mach_vector"] v_interp = [] for mach in mach_interp: v_interp.append(float(mach * atm.speed_of_sound)) cl_alpha_interp = inputs[ "data:aerodynamics:aircraft:mach_interpolation:CL_alpha_vector"] cl_alpha_fct = interpolate.interp1d(v_interp, cl_alpha_interp, fill_value="extrapolate", kind="quadratic") else: v_interp = np.array([ float(inputs["data:TLAR:v_approach"]), float(inputs["data:TLAR:v_cruise"]) ]) cl_alpha_1 = float( inputs["data:aerodynamics:wing:low_speed:CL_alpha"] + inputs["data:aerodynamics:horizontal_tail:low_speed:CL_alpha"]) cl_alpha_2 = float( inputs["data:aerodynamics:wing:cruise:CL_alpha"] + inputs["data:aerodynamics:horizontal_tail:cruise:CL_alpha"]) cl_alpha_interp = np.array([cl_alpha_1, cl_alpha_2]) cl_alpha_fct = interpolate.interp1d(v_interp, cl_alpha_interp, fill_value="extrapolate", kind="linear") # We will now establish the minimum limit maneuvering load factors outside of gust load # factors. Th designer can take higher load factor if he so wish. As will later be done for the # the cruising speed, we will simply ensure that the designer choice agrees with certifications # The limit load factor can be found in section CS 23.337 (a) and (b) # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm if category == 1.0: n_lim_1 = 6.0 # For aerobatic GA aircraft else: n_lim_1 = 3.80 # For non aerobatic GA aircraft n_lim_2 = 2.1 + 24000. / (mtow_lbf + 10000.) # CS 23.337 (a) n_lim_ps_min = min(n_lim_1, n_lim_2) # CS 23.337 (a) n_lim_ps = max(n_lim_ps_min, design_n_ps) if category == 1.0: n_lim_ng_max = -0.5 * n_lim_ps # CS 23.337 (b) else: n_lim_ng_max = -0.4 * n_lim_ps # CS 23.337 (b) n_lim_ng = min(n_lim_ng_max, design_n_ng) load_factor_array.append(float(n_lim_ps)) load_factor_array.append(float(n_lim_ng)) # Starting from there, we need to compute the gust lines as it can have an impact on the choice # of the maneuvering speed. We will also compute the maximum intensity gust line for later # use but keep in mind that this is specific for commuter or level 4 aircraft # The values used to compute the gust lines can be found in CS 23.341 # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # We first compute the gust velocities as presented in CS 23.333 (c), for now, we don't take into account # the case of the commuter nor do we implement the reduction of gust intensity with the location # of the gust center if altitude < 20000.0: U_de_Vc = 50. # [ft/s] U_de_Vd = 25. # [ft/s] U_de_Vmg = 66. # [ft/s] elif 20000.0 < altitude < 50000.0: U_de_Vc = 66.7 - 0.000833 * altitude # [ft/s] U_de_Vd = 33.4 - 0.000417 * altitude # [ft/s] U_de_Vmg = 84.7 - 0.000933 * altitude # [ft/s] else: U_de_Vc = 25. # [ft/s] U_de_Vd = 12.5 # [ft/s] U_de_Vmg = 38. # [ft/s] # Let us define aeroplane mass ratio formula and alleviation factor formula mu_g = lambda x: (2.0 * mass * g / wing_area) / ( atm.density * mean_chord * x * g) # [x = cl_alpha] K_g = lambda x: (0.88 * x) / (5.3 + x) # [x = mu_g] # Now, define the gust function load_factor_gust_p = lambda u_de_v, x: float( 1 + K_g(mu_g(cl_alpha_fct(x))) * atm_0.density * u_de_v * self. ft_to_m * x * cl_alpha_fct(x) / (2.0 * weight_lbf / wing_area_sft * self.lbf_to_N / self.ft_to_m**2)) load_factor_gust_n = lambda u_de_v, x: float( 1 - K_g(mu_g(cl_alpha_fct(x))) * atm_0.density * u_de_v * self. ft_to_m * x * cl_alpha_fct(x) / (2.0 * weight_lbf / wing_area_sft * self.lbf_to_N / self.ft_to_m**2)) load_factor_stall_p = lambda x: (x / Vs_1g_ps)**2.0 load_factor_stall_n = lambda x: -(x / Vs_1g_ng)**2.0 # We can now go back to the computation of the maneuvering speeds, we will first compute it # "traditionally" and should we find out that the line limited by the Cl max is under the gust # line, we will adjust it (see Step 10. of section 16.4.1 of (1)). As for the traditional # computation they can be found in CS 23.335 (c) # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm Vma_ps = Vs_1g_ps * math.sqrt(n_lim_ps) # [m/s] Vma_ng = Vs_1g_ng * math.sqrt(abs(n_lim_ng)) # [m/s] velocity_array.append(float(Vma_ps)) velocity_array.append(float(Vma_ng)) # We now need to check if we are in the aforementioned case (usually happens for low design wing # loading aircraft and/or mission wing loading) n_ma_ps = load_factor_gust_p(U_de_Vc, Vma_ps) if n_ma_ps > n_lim_ps: # In case the gust line load factor is above the maneuvering load factor, we need to solve the difference # between both curve to be 0.0 to find intersect delta = lambda x: load_factor_gust_p(U_de_Vc, x ) - load_factor_stall_p(x) Vma_ps = max(optimize.fsolve(delta, np.array(1000.0))) n_ma_ps = load_factor_gust_p(U_de_Vc, Vma_ps) # [-] velocity_array.append(float(Vma_ps)) load_factor_array.append(float(n_ma_ps)) else: velocity_array.append(0.0) load_factor_array.append(0.0) # We now need to do the same thing for the negative maneuvering speed n_ma_ng = load_factor_gust_n(U_de_Vc, Vma_ng) # [-] if n_ma_ng < n_lim_ng: # In case the gust line load factor is above the maneuvering load factor, we need to solve the difference # between both curve to be 0.0 to find intersect delta = lambda x: load_factor_gust_n(U_de_Vc, x ) - load_factor_stall_n(x) Vma_ng = max(optimize.fsolve(delta, np.array(1000.0))[0]) n_ma_ng = load_factor_gust_n(U_de_Vc, Vma_ng) # [-] velocity_array.append(float(Vma_ng)) load_factor_array.append(float(n_ma_ng)) else: velocity_array.append(0.0) load_factor_array.append(0.0) # For the cruise velocity, things will be different since it is an entry choice. As such we will # simply check that it complies with the values given in the certification papers and re-adjust # it if necessary. For airplane certified for aerobatics, the coefficient in front of the wing # loading in psf is slightly different than for normal aircraft but for either case it becomes # 28.6 at wing loading superior to 100 psf # Values and methodology used can be found in CS 23.335 (a) # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm if category == 1.0: if mtow_loading_psf < 20.0: k_c = 36.0 elif mtow_loading_psf < 100.: # Linear variation from 33.0 to 28.6 k_c = 36.0 + (mtow_loading_psf - 20.0) * (28.6 - 36.0) / (100.0 - 20.0) else: k_c = 28.6 else: if mtow_loading_psf < 20.0: k_c = 33.0 elif mtow_loading_psf < 100.: # Linear variation from 33.0 to 28.6 k_c = 33.0 + (mtow_loading_psf - 20.0) * (28.6 - 33.0) / (100.0 - 20.0) else: k_c = 28.6 Vc_min_1 = k_c * math.sqrt( weight_lbf / wing_area_sft) * self.kts_to_ms # [m/s] # This second constraint rather refers to the paragraph on maneuvering speeds, which needs to be chosen # so that they are smaller than cruising speeds Vc_min_2 = Vma_ps # [m/s] Vc_min = max(Vc_min_1, Vc_min_2) # [m/s] # The certifications specifies that Vc need not be more than 0.9 Vh so we will simply take the # minimum value between the Vc_min and this value Vc_min_fin = min(Vc_min, 0.9 * Vh) # [m/s] # The constraint regarding the maximum velocity for cruise does not appear in the certifications but # from a physics point of view we can easily infer that the cruise speed will never be greater than # the maximum level velocity at sea level hence Vc = max(min(design_vc, Vh), Vc_min_fin) # [m/s] velocity_array.append(float(Vc)) load_factor_array.append(float(n_lim_ng)) # Lets now look at the load factors associated with the Vc, since it is here that the greatest # load factors can appear n_Vc_ps = max(load_factor_gust_p(U_de_Vc, Vc), n_lim_ps) # [-] n_Vc_ng = min(load_factor_gust_n(U_de_Vc, Vc), n_lim_ng) # [-] velocity_array.append(float(Vc)) load_factor_array.append(float(n_Vc_ps)) velocity_array.append(float(Vc)) load_factor_array.append(float(n_Vc_ng)) # We now compute the diving speed, methods are described in CS 23.335 (b). We will take the minimum # diving speed allowable as our design diving speed. We need to keep in mind that this speed could # be greater if the designer was willing to show that the structure holds for the wanted Vd. For # airplane that needs to be certified for aerobatics use, the factor between Vd_min and Vc_min is # slightly different, but they both become 1.35 for wing loading higher than 100 psf # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm Vd_min_1 = 1.25 * Vc # [m/s] if category == 1.0: if mtow_loading_psf < 20.0: k_d = 1.55 elif mtow_loading_psf < 100.: # Linear variation from 1.55 to 1.35 k_d = 1.55 + (mtow_loading_psf - 20.0) * (1.35 - 1.55) / (100.0 - 20.0) else: k_d = 1.35 elif category == 2.0: if mtow_loading_psf < 20.0: k_d = 1.50 elif mtow_loading_psf < 100.: # Linear variation from 1.5 to 1.35 k_d = 1.50 + (mtow_loading_psf - 20.0) * (1.35 - 1.50) / (100.0 - 20.0) else: k_d = 1.35 else: if mtow_loading_psf < 20.0: k_d = 1.4 elif mtow_loading_psf < 100.: # Linear variation from 1.4 to 1.35 k_d = 1.4 + (mtow_loading_psf - 20.0) * (1.35 - 1.4) / (100.0 - 20.0) else: k_d = 1.35 Vd_min_2 = k_d * Vc_min_fin # [m/s] Vd = max(Vd_min_1, Vd_min_2) # [m/s] velocity_array.append(float(Vd)) load_factor_array.append(float(n_lim_ps)) velocity_array.append(float(Vd)) load_factor_array.append(0.0) # Similarly to what was done for the design cruising speed we will explore the load factors # associated with the diving speed since gusts are likely to broaden the flight domain around # these points n_Vd_ps = load_factor_gust_p(U_de_Vd, Vd) # [-] # For the negative load factor at the diving speed, it seems that for non_aerobatic airplanes, it is # always sized according to the gust lines, regardless of the negative design load factor. For aerobatic # airplanes however, it seems as if it is sized for a greater value (more negative) but it does not look # to be equal to the negative diving factor as can be seen in figure 16-13 of (1). No information was # found for the location of this precises point, so the choice was made to take it as the negative # design load factor or the load factor given by the gust, whichever is the greatest (most negative). # This way, for non aerobatic airplane, we ensure to be conservative. n_Vd_ng = load_factor_gust_n(U_de_Vd, Vd) # [-] velocity_array.append(float(Vd)) load_factor_array.append(float(n_Vd_ps)) velocity_array.append(float(Vd)) load_factor_array.append(float(n_Vd_ng)) # We have now calculated all the velocities need to plot the flight domain. For the sake of # thoroughness we will also compute the maximal structural cruising speed and cruise never-exceed # speed. The computation for these two can be found in CS 23.1505 # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm # Let us start, as presented in the certifications papers with the never-exceed speed # Since we made the choice to take the Vd as the minimum value allowed by certifications, the V_ne # will have a fixed value and not a range as one would have expect. Indeed if Vd = Vd_min and since # V_ne has to be greater or equal to 0.9 x Vd_min and smaller or equal to 0.9 x Vd, V_ne will be equal # to 0.9 Vd. For future implementations, it should be noted that this section will need to be rewritten # should the Vd become a design parameter like what was made on Vc. Additionally the effect of # buffeting which serves as an additional upper limit is not included but should be taken into # account in detailed analysis phases V_ne = 0.9 * Vd # [m/s] velocity_array.append(float(V_ne)) load_factor_array.append(0.0) V_no_min = Vc_min # [m/s] V_no_max = min(Vc, 0.89 * V_ne) # [m/s] # Again we need to make a choice for this speed : what value would be retained. We will take the # highest speed acceptable for certification, i.e V_no = max(V_no_min, V_no_max) # [m/s] velocity_array.append(float(V_no)) load_factor_array.append(0.0) # One additional velocity needs to be computed if we are talking about commuter aircraft. It is # the maximum gust intensity velocity. Due to the way we are returning the values, even if we are not # investigating a commuter aircraft we need to return a value for Vmg so we will put it to 0.0. If we # are investigating a commuter aircraft, we will compute it according ot the guidelines from CS 23.335 (d) # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm # We decided to put this computation here as we may need the gust load factor in cruise conditions for # one of the possible candidates for the Vmg. While writing this program, the writer realized they were # no paragraph that impeach the Vc from being at a value such that one one of the conditions for the # minimum speed was above the Vc creating a problem with point (2). This case may however never appear # in practice as it would suppose that the Vc chosen is above the stall line which is more than certainly # avoided by the correlation between Vc_min and W/S in CS 23.335 (a) if (level == 4.0) or (category == 4.0): # We first need to compute the intersection of the stall line with the gust line given by the # gust of maximum intensity. Similar calculation were already done in case the maneuvering speed # is dictated by the Vc gust line so the computation will be very similar delta = lambda x: load_factor_gust_p(U_de_Vmg, x ) - load_factor_stall_p(x) Vmg_min_1 = max(optimize.fsolve(delta, np.array(1000.0))[0]) # The second candidate for the Vmg is given by the stall speed and the load factor at the cruise # speed Vmg_min_2 = Vs_1g_ps * math.sqrt(load_factor_gust_p(U_de_Vc, Vc)) # [m/s] Vmg = min(Vmg_min_1, Vmg_min_2) # [m/s] # As for the computation of the associated load factor, no source were found for any formula or # hint as to its computation. It can however be guessed that depending on the minimum value found # above, it will either be on the stall line or at the maximum design load factor if Vmg == Vmg_min_1: # On the gust line n_Vmg = load_factor_gust_n(U_de_Vmg, Vmg_min_1) # [-] else: n_Vmg = n_Vc_ps # [-] else: Vmg = 0.0 # [m/s] n_Vmg = 0.0 velocity_array.append(float(Vmg)) load_factor_array.append(float(n_Vmg)) # Let us now look at the flight domain in the flap extended configuration. For the computation of these # speeds and load factors, we will use the formula provided in CS 23.1511 # https://www.easa.europa.eu/sites/default/files/dfu/CS-23%20Amendment%204.pdf # https://www.astm.org/Standards/F3116.htm # For the computation of the Vfe CS 23.1511, refers to CS 23.345 but there only seems to be a # requirement for the lowest the Vfe can be, hence we will take this speed as the Vfe. As for the # load factors that are prescribed we will use the guidelines provided in CS 23.345 (b) # Let us start by computing the Vfe Vsfe_1g_ps = math.sqrt( (2. * mass * g) / (atm_0.density * wing_area * cl_max_flaps)) # [m/s] Vfe_min_1 = 1.4 * Vs_1g_ps # [m/s] Vfe_min_2 = 1.8 * Vsfe_1g_ps # [m/s] Vfe_min = max(Vfe_min_1, Vfe_min_2) # [m/s] Vfe = Vfe_min # [m/s] velocity_array.append(float(Vsfe_1g_ps)) load_factor_array.append(1.0) # We can then move on to the computation of the load limitation of the flapped flight domain, which # must be equal to either a constant load factor of 2 or a load factor dictated by a gust of 25 fps. # Also since the use of flaps is limited to take-off, approach and landing, we will use the SL density # and a constant gust velocity U_de_fe = 25. # [ft/s] n_lim_ps_fe = 2.0 n_Vfe = max(n_lim_ps_fe, load_factor_gust_n(U_de_fe, Vfe)) velocity_array.append(float(Vsfe_1g_ps * math.sqrt(n_Vfe))) load_factor_array.append(float(n_Vfe)) velocity_array.append(float(Vfe)) load_factor_array.append(float(n_Vfe)) # We also store the conditions in which the values were computed so that we can easily access # them when drawing the flight domains conditions = [mass, altitude] return velocity_array, load_factor_array, conditions
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)
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): sweep_25_wing = float(inputs["data:geometry:wing:sweep_25"]) aspect_ratio_wing = float(inputs["data:geometry:wing:aspect_ratio"]) taper_ratio_wing = float(inputs["data:geometry:wing:taper_ratio"]) area_wing = float(inputs["data:geometry:wing:area"]) span_wing = float(inputs["data:geometry:wing:span"]) sweep_25_htp = float(inputs["data:geometry:horizontal_tail:sweep_25"]) aspect_ratio_htp = float( inputs["data:geometry:horizontal_tail:aspect_ratio"]) efficiency_htp = float( inputs["data:aerodynamics:horizontal_tail:efficiency"]) area_htp = float(inputs["data:geometry:horizontal_tail:area"]) lp_ht = float(inputs[ "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]) delta_z_htp = float( inputs["data:geometry:horizontal_tail:z:from_wingMAC25"]) fuselage_width = float(inputs["data:geometry:fuselage:maximum_width"]) fuselage_height = float( inputs["data:geometry:fuselage:maximum_height"]) fuselage_diameter = np.sqrt(fuselage_width * fuselage_height) area_ratio = area_htp / area_wing sos_cruise = Atmosphere( inputs["data:mission:sizing:main_route:cruise:altitude"], altitude_in_feet=False).speed_of_sound mach_cruise = float(inputs["data:TLAR:v_cruise"]) / float(sos_cruise) wing_cl = inputs["xfoil:wing:CL"] wing_alpha = inputs["xfoil:wing:alpha"] index_1 = np.where(wing_alpha == 1.0) index_2 = np.where(wing_alpha == 11.0) wing_airfoil_cl_alpha = (wing_cl[index_2] - wing_cl[index_1]) / (10. * math.pi / 180.) htp_cl = inputs["xfoil:horizontal_tail:CL"] htp_alpha = inputs["xfoil:horizontal_tail:alpha"] index_3 = np.where(htp_alpha == 1.0) index_4 = np.where(htp_alpha == 11.0) htp_airfoil_cl_alpha = (htp_cl[index_4] - htp_cl[index_3]) / (10. * math.pi / 180.) mach_array = np.linspace(0., 1.55 * mach_cruise, MACH_NB_PTS + 1) beta = np.sqrt(1. - mach_array**2.) k_wing = wing_airfoil_cl_alpha / beta / (2. * math.pi) tan_sweep_wing = math.tan(sweep_25_wing * math.pi / 180.) cos_sweep_wing = math.cos(sweep_25_wing * math.pi / 180.) wing_cl_alpha = (2. * math.pi * aspect_ratio_wing) / ( 2. + np.sqrt(aspect_ratio_wing**2. * beta**2. / k_wing**2. * (1. + tan_sweep_wing**2. / beta**2.) + 4.)) k_htp = htp_airfoil_cl_alpha / beta / (2. * math.pi) tan_sweep_htp = math.tan(sweep_25_htp * math.pi / 180.) # Computing the fuselage interference factor k_wf = 1 + 0.025 * (fuselage_diameter / span_wing) - 0.25 * ( fuselage_diameter / span_wing)**2.0 htp_cl_alpha = (2. * math.pi * aspect_ratio_htp) / ( 2. + np.sqrt(aspect_ratio_htp**2. * beta**2. / k_htp**2. * (1. + tan_sweep_htp**2. / beta**2.) + 4.)) k_a = 1. / aspect_ratio_wing - 1. / (1. + aspect_ratio_wing**1.7) k_lambda = (10. - 3. * taper_ratio_wing) / 7. k_h = (1. - delta_z_htp / span_wing) / (2. * lp_ht / span_wing)**(1. / 3.) downwash_gradient = 4.44 * ( k_a * k_lambda * k_h * np.sqrt(cos_sweep_wing))**1.19 * wing_cl_alpha / wing_cl_alpha[0] aircraft_cl_alpha = k_wf * wing_cl_alpha + htp_cl_alpha * efficiency_htp * area_ratio * ( 1. - downwash_gradient) outputs[ "data:aerodynamics:aircraft:mach_interpolation:CL_alpha_vector"] = aircraft_cl_alpha outputs[ "data:aerodynamics:aircraft:mach_interpolation:mach_vector"] = mach_array
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): elevator_chord_ratio = inputs[ "data:geometry:horizontal_tail:elevator_chord_ratio"] tail_thickness_ratio = inputs[ "data:geometry:horizontal_tail:thickness_ratio"] cl_alpha_ht = inputs[ "data:aerodynamics:horizontal_tail:cruise:CL_alpha"] wing_area = inputs["data:geometry:wing:area"] ht_area = inputs["data:geometry:horizontal_tail:area"] v_cruise = inputs["data:TLAR:v_cruise"] cruise_alt = inputs["data:mission:sizing:main_route:cruise:altitude"] # Section 10.4.1.1 # Step 1. y = lambda x: tail_thickness_ratio / 0.2 * ( 0.2969 * x**0.5 - 0.126 * x - 0.3516 * x**2.0 + 0.2843 * x**3.0 - 0.105 * x**4.0) y_90 = y(0.90) y_95 = y(0.95) y_99 = y(0.99) tan_0_5_phi_te = (y(1. - 1.e-6) - y(1.)) / 1e-6 tan_0_5_phi_te_prime = (y_90 / 2.0 - y_99 / 2.0) / 9.0 tan_0_5_phi_te_prime_prime = (y_95 / 2.0 - y_99 / 2.0) / 9.0 if (tan_0_5_phi_te == tan_0_5_phi_te_prime) and ( tan_0_5_phi_te_prime == tan_0_5_phi_te_prime_prime) and (tan_0_5_phi_te_prime_prime == tail_thickness_ratio): condition = True else: condition = False # Step 2. cl_alpha_ht *= wing_area / ht_area cl_alpha_ht_th = 6.3 + tail_thickness_ratio / 0.2 * (7.3 - 6.3) k_cl_alpha_inter = inter.interp1d([0., 0.2], [0.9, 0.69]) k_cl_alpha = k_cl_alpha_inter(tail_thickness_ratio)[0] k_cl_alpha_array = [ 0.7, 0.72, 0.74, 0.76, 0.78, 0.8, 0.82, 0.84, 0.86, 0.88, 0.9, 0.92, 0.94, 0.96, 0.98, 1.0 ] k_ch_alpha_min_array = [ -0.1, 0.0, 0.08, 0.19, 0.28, 0.36, 0.43, 0.5, 0.58, 0.65, 0.70, 0.75, 0.82, 0.87, 0.93, 1.0 ] k_ch_alpha_max_array = [ 0.12, 0.22, 0.30, 0.38, 0.46, 0.53, 0.6, 0.65, 0.70, 0.75, 0.80, 0.84, 0.88, 0.92, 0.96, 1.0 ] k_ch_alpha_min_inter = inter.interp1d(k_cl_alpha_array, k_ch_alpha_min_array) k_ch_alpha_max_inter = inter.interp1d(k_cl_alpha_array, k_ch_alpha_max_array) k_ch_alpha_min = k_ch_alpha_min_inter(k_cl_alpha) k_ch_alpha_max = k_ch_alpha_max_inter(k_cl_alpha) if elevator_chord_ratio < 0.1: k_ch_alpha = k_ch_alpha_min elif elevator_chord_ratio > 0.4: k_ch_alpha = k_ch_alpha_max else: k_ch_alpha_inter = inter.interp1d([0.1, 0.4], [k_ch_alpha_min, k_ch_alpha_max]) k_ch_alpha = k_ch_alpha_inter(elevator_chord_ratio) thickness_ratio_array = [0., 0.4, 0.6, 0.8, 0.10, 0.12, 0.16] ch_alpha_min_array = [ -0.225, -0.25, -0.27, -0.295, -0.305, -0.315, -0.3 ] ch_alpha_max_array = [-0.65, -0.67, -0.685, -0.7, -0.71, -0.72, -0.75] ch_alpha_min_inter = inter.interp1d(thickness_ratio_array, ch_alpha_min_array) ch_alpha_max_inter = inter.interp1d(thickness_ratio_array, ch_alpha_max_array) ch_alpha_min = ch_alpha_min_inter(tail_thickness_ratio)[0] ch_alpha_max = ch_alpha_max_inter(tail_thickness_ratio)[0] if elevator_chord_ratio < 0.1: ch_alpha = ch_alpha_min elif elevator_chord_ratio > 0.4: ch_alpha = ch_alpha_max else: ch_alpha_inter = inter.interp1d([0.1, 0.4], [ch_alpha_min, ch_alpha_max]) ch_alpha = ch_alpha_inter(elevator_chord_ratio) ch_prime_alpha = k_ch_alpha * ch_alpha # Step 3. if condition: ch_prime_prime_alpha = ch_prime_alpha else: ch_prime_prime_alpha = ch_prime_alpha + \ 2. * cl_alpha_ht_th * (1. - k_cl_alpha) * \ (tan_0_5_phi_te_prime_prime - tail_thickness_ratio) # Step 4. # The overhang (cb/cf will we taken equal to 3 as we assume a 1/4, 3/4 chord repartition for the hinge line) # We will also assume that the thickness ratio of the elevator is the same as the tail and that it has a # round nose balance_ratio = math.sqrt((1. / 3.)**2.0 - (tail_thickness_ratio * 5. / 4)**2.0) if balance_ratio < 0.15: balance_ratio = 0.15 elif balance_ratio > 0.5: balance_ratio = 0.5 k_ch_alpha_balance_inter = inter.interp1d([0.15, 0.50], [0.93, 0.2]) k_ch_alpha_balance = k_ch_alpha_balance_inter(balance_ratio) ch_alpha_balance = k_ch_alpha_balance * ch_prime_prime_alpha # Step 5. sos_cruise = Atmosphere(cruise_alt, altitude_in_feet=False).speed_of_sound mach = v_cruise / sos_cruise beta = math.sqrt(1. - mach**2.0) ch_alpha_fin = ch_alpha_balance / beta # Section 10.4.1.2 # Step 1., same as step 1. in previous section # Step 2. k_cl_alpha_array = [0.6, 0.65, 0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 1.0] k_ch_delta_min = [0.64, 0.70, 0.75, 0.80, 0.84, 0.88, 0.92, 0.96, 1.0] k_ch_delta_avg = [ 0.56, 0.65, 0.725, 0.775, 0.83, 0.87, 0.91, 0.955, 1.0 ] k_ch_delta_max = [ 0.415, 0.55, 0.66, 0.745, 0.80, 0.85, 0.90, 0.95, 1.0 ] k_ch_delta_min_inter = inter.interp1d(k_cl_alpha_array, k_ch_delta_min) k_ch_delta_avg_inter = inter.interp1d(k_cl_alpha_array, k_ch_delta_avg) k_ch_delta_max_inter = inter.interp1d(k_cl_alpha_array, k_ch_delta_max) k_ch_delta_min = k_ch_delta_min_inter(k_cl_alpha) k_ch_delta_avg = k_ch_delta_avg_inter(k_cl_alpha) k_ch_delta_max = k_ch_delta_max_inter(k_cl_alpha) if elevator_chord_ratio < 0.1: k_ch_delta = k_ch_delta_min elif elevator_chord_ratio > 0.4: k_ch_delta = k_ch_delta_max else: k_ch_delta_inter = inter.interp1d( [0.1, 0.25, 0.4], [k_ch_delta_min, k_ch_delta_avg, k_ch_delta_max], kind='quadratic') k_ch_delta = k_ch_delta_inter(elevator_chord_ratio) thickness_ratio_array = [0., 0.4, 0.6, 0.8, 0.10, 0.12, 0.16] ch_delta_min_array = [-0.88, -0.83, -0.8, -0.77, -0.735, -0.7, -0.64] ch_delta_max_array = [ -1.02, -0.995, -0.985, -0.97, -0.955, -0.94, -0.925 ] ch_delta_min_inter = inter.interp1d(thickness_ratio_array, ch_delta_min_array) ch_delta_max_inter = inter.interp1d(thickness_ratio_array, ch_delta_max_array) ch_delta_min = ch_delta_min_inter(tail_thickness_ratio)[0] ch_delta_max = ch_delta_max_inter(tail_thickness_ratio)[0] if elevator_chord_ratio < 0.1: ch_delta = ch_delta_min elif elevator_chord_ratio > 0.4: ch_delta = ch_delta_max else: ch_delta_inter = inter.interp1d([0.1, 0.4], [ch_delta_min, ch_delta_max]) ch_delta = ch_delta_inter(elevator_chord_ratio) ch_prime_delta = k_ch_delta * ch_delta # Step 3. if condition: ch_prime_prime_delta = ch_prime_delta else: thickness_ratio_array = [ 0.0, 0.02, 0.04, 0.06, 0.08, 0.1, 0.12, 0.15 ] cl_delta_th_min_array = [ 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75, 1.75 ] cl_delta_th_avg_array = [ 4.125, 4.1875, 4.25, 4.3125, 4.375, 4.4375, 4.5, 4.5625 ] cl_delta_th_max_array = [ 5.15, 5.25, 5.35, 5.45, 5.55, 5.7, 5.8, 5.95 ] cl_delta_th_min_inter = inter.interp1d(thickness_ratio_array, cl_delta_th_min_array) cl_delta_th_avg_inter = inter.interp1d(thickness_ratio_array, cl_delta_th_avg_array) cl_delta_th_max_inter = inter.interp1d(thickness_ratio_array, cl_delta_th_max_array) cl_delta_th_min = cl_delta_th_min_inter(tail_thickness_ratio)[0] cl_delta_th_avg = cl_delta_th_avg_inter(tail_thickness_ratio)[0] cl_delta_th_max = cl_delta_th_max_inter(tail_thickness_ratio)[0] if elevator_chord_ratio < 0.05: cl_delta_th = cl_delta_th_min elif elevator_chord_ratio > 0.5: cl_delta_th = cl_delta_th_max else: cl_delta_th_inter = inter.interp1d( [0.05, 0.3, 0.5], [cl_delta_th_min, cl_delta_th_avg, cl_delta_th_max], kind='quadratic') cl_delta_th = cl_delta_th_inter(elevator_chord_ratio) k_cl_alpha_array = [ 0.7, 0.72, 0.74, 0.76, 0.78, 0.8, 0.82, 0.84, 0.86, 0.88, 0.9, 0.92, 0.94, 0.96, 0.98, 1.0 ] k_cl_delta_min_array = [ 0.36, 0.4, 0.44, 0.48, 0.53, 0.575, 0.615, 0.655, 0.7, 0.735, 0.78, 0.83, 0.86, 0.91, 0.95, 1.0 ] k_cl_delta_max_array = [ 0.55, 0.59, 0.62, 0.655, 0.695, 0.733, 0.766, 0.8, 0.82, 0.845, 0.87, 0.9, 0.925, 0.95, 0.975, 1.0 ] k_cl_delta_min_array_inter = inter.interp1d( k_cl_alpha_array, k_cl_delta_min_array) k_cl_delta_max_array_inter = inter.interp1d( k_cl_alpha_array, k_cl_delta_max_array) k_cl_delta_min = k_cl_delta_min_array_inter(k_cl_alpha) k_cl_delta_max = k_cl_delta_max_array_inter(k_cl_alpha) if elevator_chord_ratio < 0.05: k_cl_delta = k_cl_delta_min elif elevator_chord_ratio > 0.5: k_cl_delta = k_cl_delta_max else: k_cl_delta_inter = inter.interp1d( [0.05, 0.5], [k_cl_delta_min, k_cl_delta_max]) k_cl_delta = k_cl_delta_inter(elevator_chord_ratio) ch_prime_prime_delta = ch_prime_delta + ( 2. * cl_delta_th * (1. - k_cl_delta) * (tan_0_5_phi_te_prime_prime - tail_thickness_ratio)) # Step 4. Same assumption as the step 4. of the previous section. Only this time we only take the curve for # the NACA 0009 k_ch_delta_balance = inter.interp1d([0.15, 0.50], [0.93, 0.2])(balance_ratio) ch_delta_balance = k_ch_delta_balance * ch_prime_prime_delta # Step 5. ch_delta_fin = ch_delta_balance / beta outputs[ "data:aerodynamics:horizontal_tail:cruise:hinge_moment:CH_alpha_2D"] = ch_alpha_fin outputs[ "data:aerodynamics:horizontal_tail:cruise:hinge_moment:CH_delta_2D"] = ch_delta_fin
def compute_flight_points_from_dt4( self, mach: Union[float, Sequence], altitude: Union[float, Sequence], delta_t4: Union[float, Sequence], thrust_is_regulated: Optional[Union[bool, Sequence]] = None, thrust_rate: Optional[Union[float, Sequence]] = None, thrust: Optional[Union[float, Sequence]] = None, ) -> Tuple[Union[float, Sequence], Union[float, Sequence], Union[ float, Sequence]]: # pylint: disable=too-many-arguments # they define the trajectory """ Same as :meth:`compute_flight_points` except that delta_t4 is used directly instead of specifying flight engine_setting. :param mach: Mach number :param altitude: (unit=m) altitude w.r.t. to sea level :param delta_t4: (unit=K) difference between operational and design values of turbine inlet temperature in K :param thrust_is_regulated: tells if thrust_rate or thrust should be used (works element-wise) :param thrust_rate: thrust rate (unit=none) :param thrust: required thrust (unit=N) :return: SFC (in kg/s/N), thrust rate, thrust (in N) """ mach = np.asarray(mach) altitude = np.asarray(altitude) delta_t4 = np.asarray(delta_t4) if thrust_is_regulated is not None: thrust_is_regulated = np.asarray(np.round(thrust_is_regulated, 0), dtype=bool) thrust_is_regulated, thrust_rate, thrust = self._check_thrust_inputs( thrust_is_regulated, thrust_rate, thrust) thrust_is_regulated = np.asarray(np.round(thrust_is_regulated, 0), dtype=bool) thrust_rate = np.asarray(thrust_rate) thrust = np.asarray(thrust) atmosphere = Atmosphere(altitude, altitude_in_feet=False) max_thrust = self.max_thrust(atmosphere, mach, delta_t4) # We compute thrust values from thrust rates when needed idx = np.logical_not(thrust_is_regulated) if np.size(max_thrust) == 1: maximum_thrust = max_thrust out_thrust_rate = thrust_rate out_thrust = thrust else: out_thrust_rate = (np.full(np.shape(max_thrust), thrust_rate.item()) if np.size(thrust_rate) == 1 else thrust_rate) out_thrust = (np.full(np.shape(max_thrust), thrust.item()) if np.size(thrust) == 1 else thrust) maximum_thrust = max_thrust[idx] if np.any(idx): out_thrust[idx] = out_thrust_rate[idx] * maximum_thrust # thrust_rate is obtained from entire thrust vector (could be optimized if needed, # as some thrust rates that are computed may have been provided as input) out_thrust_rate = out_thrust / max_thrust # Now SFC can be computed sfc_0 = self.sfc_at_max_thrust(atmosphere, mach) sfc = sfc_0 * self.sfc_ratio(altitude, out_thrust_rate) return sfc, out_thrust_rate, out_thrust
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)