def sfc_at_max_thrust(self, atmosphere: Atmosphere, mach: Union[float, Sequence[float]]) -> np.ndarray: """ Computation of Specific Fuel Consumption at maximum thrust. Uses model described in :cite:`roux:2005`, p.41. :param atmosphere: Atmosphere instance at intended altitude :param mach: Mach number(s) :return: SFC (in kg/s/N) """ altitude = atmosphere.get_altitude(False) mach = np.asarray(mach) # Following coefficients are constant for alt<=0 and alt >=11000m. # We use numpy to implement that so we are safe if altitude is a sequence. bound_altitude = np.minimum(11000, np.maximum(0, altitude)) # pylint: disable=invalid-name # coefficients are named after model a1 = -7.44e-13 * bound_altitude + 6.54e-7 a2 = -3.32e-10 * bound_altitude + 8.54e-6 b1 = -3.47e-11 * bound_altitude - 6.58e-7 b2 = 4.23e-10 * bound_altitude + 1.32e-5 c = -1.05e-7 theta = atmosphere.temperature / ATM_SEA_LEVEL.temperature sfc = (mach * (a1 * self.bypass_ratio + a2) + (b1 * self.bypass_ratio + b2) * np.sqrt(theta) + ((7.4e-13 * (self.overall_pressure_ratio - 30) * altitude) + c) * (self.overall_pressure_ratio - 30)) return sfc
def max_thrust( self, atmosphere: Atmosphere, mach: Union[float, Sequence[float]], ) -> np.ndarray: """ Computation of maximum thrust. Uses model described in ... :param atmosphere: Atmosphere instance at intended altitude (should be <=20km) :param mach: Mach number(s) (should be between 0.05 and 1.0) :return: maximum thrust (in N) """ # Calculate maximum mechanical power @ given altitude altitude = atmosphere.get_altitude(altitude_in_feet=True) mach = np.asarray(mach) sigma = Atmosphere(altitude).density / Atmosphere(0.0).density max_power = self.max_power * (sigma - (1 - sigma) / 7.55) _, _, _, _, _, _ = self.compute_dimensions() thrust_1 = (self.propeller["thrust_SL"] * g) * sigma**( 1 / 3) # considered fixed point @altitude thrust_2 = max_power * PROPELLER_EFFICIENCY / np.maximum( mach * Atmosphere(altitude).speed_of_sound, 1e-20) return np.minimum(thrust_1, thrust_2)
def sfc_at_max_power(self, atmosphere: Atmosphere) -> Union[float, Sequence]: """ Computation of Specific Fuel Consumption at maximum power. :param atmosphere: Atmosphere instance at intended altitude :return: SFC_P (in kg/s/W) """ altitude = atmosphere.get_altitude(altitude_in_feet=True) sigma = Atmosphere(altitude).density / Atmosphere(0.0).density max_power = (self.max_power / 1e3) * (sigma - (1 - sigma) / 7.55 ) # max power in kW if self.fuel_type == 1.: if self.strokes_nb == 2.: # Gasoline 2-strokes sfc_p = 1125.9 * max_power**(-0.2441) else: # Gasoline 4-strokes sfc_p = -0.0011 * max_power**2 + 0.5905 * max_power + 228.58 elif self.fuel_type == 2.: if self.strokes_nb == 2.: # Diesel 2-strokes sfc_p = -0.765 * max_power + 334.94 else: # Diesel 4-strokes sfc_p = -0.964 * max_power + 231.91 else: warnings.warn( 'Propulsion layout {} not implemented in model, replaced by layout 1!' .format(self.fuel_type)) if self.strokes_nb == 2.: # Gasoline 2-strokes sfc_p = 1125.9 * max_power**(-0.2441) else: # Gasoline 4-strokes sfc_p = -0.0011 * max_power**2 + 0.5905 * max_power + 228.58 sfc_p = sfc_p / 1e6 / 3600.0 # change units to be in kg/s/W return sfc_p
def sfc_at_max_power(self, atmosphere: Atmosphere) -> Union[float, Sequence]: """ Computation of Specific Fuel Consumption at maximum power. :param atmosphere: Atmosphere instance at intended altitude :return: SFC_P (in kg/s/W) """ altitude = atmosphere.get_altitude(altitude_in_feet=True) sigma = Atmosphere(altitude).density / Atmosphere(0.0).density max_power = (self.max_power / 1e3) * (sigma - (1 - sigma) / 7.55 ) # max power in kW if self.fuel_type == 1.: if self.strokes_nb == 2.: # Gasoline 2-strokes sfc_p = 1125.9 * max_power**(-0.2441) else: # Gasoline 4-strokes sfc_p = -0.0011 * max_power**2 + 0.5905 * max_power + 228.58 elif self.fuel_type == 2.: if self.strokes_nb == 2.: # Diesel 2-strokes sfc_p = -0.765 * max_power + 334.94 else: # Diesel 4-strokes sfc_p = -0.964 * max_power + 231.91 else: raise FastBasicICEngineInconsistentInputParametersError( "Bad engine configuration: fuel type {0:f} model does not exist." .format(float(self.fuel_type))) sfc_p = sfc_p / 1e6 / 3600.0 # change units to be in kg/s/W return sfc_p
def max_thrust( self, atmosphere: Atmosphere, mach: Union[float, Sequence[float]], delta_t4: Union[float, Sequence[float]], ) -> np.ndarray: """ Computation of maximum thrust. Uses model described in :cite:`roux:2005`, p.57-58 :param atmosphere: Atmosphere instance at intended altitude (should be <=20km) :param mach: Mach number(s) (should be between 0.05 and 1.0) :param delta_t4: (unit=K) difference between operational and design values of turbine inlet temperature in K :return: maximum thrust (in N) """ altitude = atmosphere.get_altitude(altitude_in_feet=False) mach = np.asarray(mach) delta_t4 = np.asarray(delta_t4) def _mach_effect(): """Computation of Mach effect.""" vect = [ (self.overall_pressure_ratio - 30)**2, (self.overall_pressure_ratio - 30), 1.0, self.t_4, delta_t4, ] def _calc_coef(a_coeffs, b_coeffs): # We don't use np.dot because delta_t4 can be a sequence return (a_coeffs[0] * vect[0] + a_coeffs[1] * vect[1] + a_coeffs[2] + a_coeffs[3] * vect[3] + a_coeffs[4] * vect[4]) * self.bypass_ratio + ( b_coeffs[0] * vect[0] + b_coeffs[1] * vect[1] + b_coeffs[2] + b_coeffs[3] * vect[3] + b_coeffs[4] * vect[4]) f_ms = _calc_coef(ALPHA[0], BETA[0]) g_ms = _calc_coef(ALPHA[1], BETA[1]) f_fm = _calc_coef(ALPHA[2], BETA[2]) g_fm = _calc_coef(ALPHA[3], BETA[3]) ms_11000 = (A_MS * self.t_4 + B_MS * self.bypass_ratio + C_MS * (self.overall_pressure_ratio - 30) + D_MS * delta_t4 + E_MS) fm_11000 = (A_FM * self.t_4 + B_FM * self.bypass_ratio + C_FM * (self.overall_pressure_ratio - 30) + D_FM * delta_t4 + E_FM) # Following coefficients are constant for alt >=11000m. # We use numpy to implement that so we are safe if altitude is a sequence. bound_altitude = np.minimum(11000, altitude) m_s = ms_11000 + f_ms * (bound_altitude - 11000)**2 + g_ms * ( bound_altitude - 11000) f_m = fm_11000 + f_fm * (bound_altitude - 11000)**2 + g_fm * ( bound_altitude - 11000) alpha_mach_effect = (1 - f_m) / (m_s * m_s) return alpha_mach_effect * (mach - m_s)**2 + f_m def _altitude_effect(): """Computation of altitude effect.""" # pylint: disable=invalid-name # coefficients are named after model k = 1 + 1.2e-3 * delta_t4 nf = 0.98 + 8e-4 * delta_t4 def _troposhere_effect(density, altitude, k, nf): return (k * ((density / ATM_SEA_LEVEL.density)**nf) * (1 / (1 - (0.04 * np.sin( (np.pi * altitude) / 11000))))) def _stratosphere_effect(density, k, nf): return (k * ( (ATM_TROPOPAUSE.density / ATM_SEA_LEVEL.density)**nf) * density / ATM_TROPOPAUSE.density) if np.size(altitude) == 1: if altitude <= 11000: h = _troposhere_effect(atmosphere.density, altitude, k, nf) else: h = _stratosphere_effect(atmosphere.density, k, nf) else: h = np.empty(np.shape(altitude)) idx = altitude <= 11000 if np.size(delta_t4) == 1: h[idx] = _troposhere_effect(atmosphere.density[idx], altitude[idx], k, nf) idx = np.logical_not(idx) h[idx] = _stratosphere_effect(atmosphere.density[idx], k, nf) else: h[idx] = _troposhere_effect(atmosphere.density[idx], altitude[idx], k[idx], nf[idx]) idx = np.logical_not(idx) h[idx] = _stratosphere_effect(atmosphere.density[idx], k[idx], nf[idx]) return h def _residuals(): """Computation of residuals.""" return (-4.51e-3 * self.bypass_ratio + 2.19e-5 * self.t_4 - 3.09e-4 * (self.overall_pressure_ratio - 30) + 0.945) return self.f_0 * _mach_effect() * _altitude_effect() * _residuals()
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]]: """ 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 # Resetting the model is required to re-run from scratch the FMU self.model.reset() # Evaluate max power altitude = atmosphere.get_altitude(altitude_in_feet=True) sigma = Atmosphere(altitude).density / Atmosphere(0.0).density max_power = (self.max_power / 1e3) * (sigma - (1 - sigma) / 7.55 ) # max power in kW # Set all parameters before computation self.model.set("maximum_power", max_power) self.model.set("fuel_type", self.fuel_type) self.model.set("strokes_nb", self.strokes_nb) self.model.set("propeller_efficiency", PROPELLER_EFFICIENCY) self.model.set("thrust.k", out_thrust) self.model.set("mach.k", mach) self.model.initialize() result = self.model.simulate( start_time=0.0, final_time=0.0, options={"ncp": 2}, ) sfc = result['sfc.y'][-1] / np.maximum(out_thrust, 1e-6) # avoid 0 division return sfc, out_thrust_rate, out_thrust