def windcomponents(self, wind_dirs_deg, wind_speeds): """Resolves list of wind speeds and directions into runway/cross components""" speeds = mtools.recastasnpfloatarray(wind_speeds) # Wind speed is considered as a positive scalar speeds = np.abs(speeds) directions_deg = mtools.recastasnpfloatarray(wind_dirs_deg) relative_heading_rad = np.deg2rad(directions_deg - self.le_heading_degt) runway_component = speeds * np.cos(relative_heading_rad) # Headwind: + crosswind_component = speeds * np.sin(relative_heading_rad) # Right: + # Scalar output to a scalar input if isinstance(wind_dirs_deg, Number): return runway_component[0], crosswind_component[0] return runway_component, crosswind_component
def twrequired_sec(self, wingloading_pa): """T/W required for a service ceiling for a range of wing loadings""" if self.servceil_m == -1: secmsg = "Climb rate not specified in the designbrief dictionary." raise ValueError(secmsg) if self.secclimbspd_kias == -1: secmsg = "Best climb speed not specified in the designbrief dictionary." raise ValueError(secmsg) secclimbspeed_mpsias = co.kts2mps(self.secclimbspd_kias) secclimbspeed_mpstas = self.designatm.eas2tas(secclimbspeed_mpsias, self.servceil_m) wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) # W/S at the start of the service ceiling test point may be less than MTOW/S wingloading_pa = wingloading_pa * self.sec_weight_fraction inddragfact = self.induceddragfact(whichoswald=123) qclimb_pa = self.designatm.dynamicpressure_pa(secclimbspeed_mpstas, self.servceil_m) # Service ceiling typically defined in terms of climb rate (at best climb speed) of # dropping to 100feet/min ~ 0.508m/s climbrate_mps = co.fpm2mps(100) # What true climb rate does 100 feet/minute correspond to? climbrate_mpstroc = self.designatm.eas2tas(climbrate_mps, self.servceil_m) twratio = climbrate_mpstroc / secclimbspeed_mpstas + \ (1 / wingloading_pa) * qclimb_pa * self.cdminclean + \ (inddragfact / qclimb_pa) * wingloading_pa # What SL T/W will yield the required T/W at the actual altitude? temp_c = self.designatm.airtemp_c(self.servceil_m) pressure_pa = self.designatm.airpress_pa(self.servceil_m) density_kgpm3 = self.designatm.airdens_kgpm3(self.servceil_m) mach = self.designatm.mach(secclimbspeed_mpstas, self.servceil_m) corr = self._altcorr(temp_c, pressure_pa, mach, density_kgpm3) twratio = twratio / corr # Map back to T/MTOW if service ceiling test start weight is less than MTOW twratio = twratio * self.sec_weight_fraction if len(twratio) == 1: return twratio[0] return twratio
def bestclimbspeedprop(self, wingloading_pa, altitude_m): """The best rate of climb speed for a propeller aircraft""" wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) dragfactor = np.sqrt(self.induceddragfact(123) / (3 * self.cdminclean)) densfactor = 2 / self.designatm.airdens_kgpm3(altitude_m) # Gudmundsson, eq. (18-27) bestspeed_mps = np.sqrt(densfactor * wingloading_pa * dragfactor) if len(bestspeed_mps) == 1: return bestspeed_mps[0] return bestspeed_mps
def mach(self, airspeed_mps, altitude_m=0): """Mach number at a given speed (m/s) and altitude (m)""" airspeed_mps = mtools.recastasnpfloatarray(airspeed_mps) # Airspeed may be negative, e.g., when simulating a tailwind, but Mach must be >0 if airspeed_mps.any() < 0: negmsg = "Airspeed < 0. If intentional, ignore this. Positive Mach no. returned." warnings.warn(negmsg, RuntimeWarning) airspeed_mps = abs(airspeed_mps) # Check altitude range altitude_m = self._alttest(altitude_m) # Compute speed of sound at the given altitude(s) vs_mps = self.vsound_mps(altitude_m) return airspeed_mps / vs_mps
def twrequired_crs(self, wingloading_pa): """Calculate the T/W required for cruise for a range of wing loadings""" if self.cruisespeed_ktas == -1: cruisemsg = "Cruise speed not specified in the designbrief dictionary." raise ValueError(cruisemsg) cruisespeed_mps = co.kts2mps(self.cruisespeed_ktas) if self.cruisealt_m == -1: cruisemsg = "Cruise altitude not specified in the designbrief dictionary." raise ValueError(cruisemsg) wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) # W/S at the start of the cruise may be less than MTOW/S wingloading_pa = wingloading_pa * self.cruise_weight_fraction inddragfact = self.induceddragfact(whichoswald=123) qcruise_pa = self.designatm.dynamicpressure_pa(cruisespeed_mps, self.cruisealt_m) twratio = (1 / wingloading_pa) * qcruise_pa * self.cdminclean + \ (inddragfact / qcruise_pa) * wingloading_pa # What SL T/W will yield the required T/W at the actual altitude? temp_c = self.designatm.airtemp_c(self.cruisealt_m) pressure_pa = self.designatm.airpress_pa(self.cruisealt_m) density_kgpm3 = self.designatm.airdens_kgpm3(self.cruisealt_m) mach = self.designatm.mach(cruisespeed_mps, self.cruisealt_m) corr = self._altcorr(temp_c, pressure_pa, mach, density_kgpm3) twratio = twratio / corr # Map back to T/MTOW if cruise start weight is less than MTOW twratio = twratio * self.cruise_weight_fraction twratio = twratio * (1 / self.cruisethrustfact) if len(twratio) == 1: return twratio[0] return twratio
def windcomponents(self, wind_dirs_deg, wind_speeds): """Resolves list of wind speeds and directions into runway/cross components on the current runway. **Parameters:** wind_dirs_deg List of floats. Wind directions expressed in degrees true (e.g., directions specified in a METAR). wind_speeds List of floats. Wind_speeds (in the units in which the output is desired). **Outputs:** runway_component Scalar or numpy array. The runway direction component of the wind (sign convention: headwinds are positive). crosswind_component Scalar or numpy array. The cross component of the wind (sign convention: winds from the right are positive). **Example** :: # Given a METAR, calculate the wind components on Rwy 09 at Yeovilton from ADRpy import atmospheres as at from metar import Metar runway = at.Runway('EGDY', 1) egdywx = Metar.Metar('EGDY 211350Z 30017G25KT 9999 FEW028 BKN038 08/01 Q1031') direction_deg = egdywx.wind_dir.value() windspeed_kts = egdywx.wind_speed.value() rwy_knots, cross_knots = runway.windcomponents(direction_deg, windspeed_kts) print("Runway component:", rwy_knots) print("Cross component:", cross_knots) Output: :: Runway component: -13.5946391943 Cross component: -10.2071438305 """ speeds = mtools.recastasnpfloatarray(wind_speeds) # Wind speed is considered as a positive scalar speeds = np.abs(speeds) directions_deg = mtools.recastasnpfloatarray(wind_dirs_deg) relative_heading_rad = np.deg2rad(directions_deg - self.le_heading_degt) runway_component = speeds * np.cos(relative_heading_rad) # Headwind: + crosswind_component = speeds * np.sin(relative_heading_rad) # Right: + # Scalar output to a scalar input if isinstance(wind_dirs_deg, Number): return runway_component[0], crosswind_component[0] return runway_component, crosswind_component
def twrequired_clm(self, wingloading_pa): """Calculates the T/W required for climbing for a range of wing loadings. **Parameters** wingloading_pa float or numpy array, list of wing loading values in Pa. **Returns** twratio array, thrust to weight ratio required for the given wing loadings. **See also** ``twrequired`` **Notes** 1. Use `twrequired` if a full constraint analysis is desired, as this integrates the take-off, turn, climb, cruise, and service ceiling constraints, as well as computing the combined constraint boundary. 2. The calculation currently approximates climb performance on the constant TAS assumption (though note that the design brief dictionary variable must specify the climb speed as IAS, which is the operationally relevant figure) - a future version of the code will remove this approximation and assume constant IAS. **Example** Given a climb rate (in feet per minute) and a climb speed (KIAS), as well as an altitude (in a given atmosphere) where these must be achieved, as well as a set of basic geometrical and aerodynamic performance parameters, compute the necessary T/W ratio to hold the specified climb rate. :: from ADRpy import atmospheres as at from ADRpy import constraintanalysis as ca designbrief = {'climbalt_m': 0, 'climbspeed_kias': 101, 'climbrate_fpm': 1398} etap = {'climb': 0.8} designperformance = {'CDminclean': 0.0254, 'etaprop' :etap} designdef = {'aspectratio': 10.12, 'sweep_le_deg': 2, 'sweep_mt_deg': 0, 'bpr': -1} TOW_kg = 1542.0 designatm = at.Atmosphere() concept = ca.AircraftConcept(designbrief, designdef, designperformance, designatm) wingloadinglist_pa = [1250, 1500, 1750] twratio = concept.twrequired_clm(wingloadinglist_pa) print('T/W: ', twratio) Output: :: T/W: [ 0.20249491 0.2033384 0.20578177] """ if self.climbspeed_kias == -1: turnmsg = "Climb speed not specified in the designbrief dictionary." raise ValueError(turnmsg) climbspeed_mpsias = co.kts2mps(self.climbspeed_kias) # Assuming that the climb rate is 'indicated' if self.climbrate_fpm == -1: turnmsg = "Climb rate not specified in the designbrief dictionary." raise ValueError(turnmsg) climbrate_mps = co.fpm2mps(self.climbrate_fpm) climbspeed_mpstas = self.designatm.eas2tas(climbspeed_mpsias, self.servceil_m) climbrate_mpstroc = self.designatm.eas2tas(climbrate_mps, self.servceil_m) wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) # W/S at the start of the specified climb segment may be less than MTOW/S wingloading_pa = wingloading_pa * self.climb_weight_fraction inddragfact = self.induceddragfact(whichoswald=123) qclimb_pa = self.designatm.dynamicpressure_pa(climbspeed_mpstas, self.climbalt_m) cos_sq_theta = (1 - (climbrate_mpstroc / climbspeed_mpstas) ** 2) # To be implemented, as 1 + (V/g)*(dV/dh) accel_fact = 1.0 twratio = accel_fact * climbrate_mpstroc / climbspeed_mpstas + \ (1 / wingloading_pa) * qclimb_pa * self.cdminclean + \ (inddragfact / qclimb_pa) * wingloading_pa * cos_sq_theta # What SL T/W will yield the required T/W at the actual altitude? temp_c = self.designatm.airtemp_c(self.climbalt_m) pressure_pa = self.designatm.airpress_pa(self.climbalt_m) density_kgpm3 = self.designatm.airdens_kgpm3(self.climbalt_m) mach = self.designatm.mach(climbspeed_mpstas, self.climbalt_m) corr = self._altcorr(temp_c, pressure_pa, mach, density_kgpm3) twratio = twratio / corr # Map back to T/MTOW if climb start weight is less than MTOW twratio = twratio * self.climb_weight_fraction if len(twratio) == 1: return twratio[0] return twratio
def twrequired_trn(self, wingloading_pa): """Calculates the T/W required for turning for a range of wing loadings **Parameters** wingloading_pa float or numpy array, list of wing loading values in Pa. **Returns** twratio array, thrust to weight ratio required for the given wing loadings. clrequired array, lift coefficient values required for the turn (see notes). feasibletw as twratio, but contains NaNs in lieu of unachievable (CLmax exceeded) values. **See also** ``twrequired`` **Notes** 1. Use `twrequired` if a full constraint analysis is desired, as this integrates the take-off, turn, climb, cruise, and service ceiling constraints, as well as computing the combined constraint boundary. 2. At the higher end of the wing loading range (low wing area values) the CL required to achieve the required turn rate may exceed the maximum clean CL (as specified in the `CLmaxclean` entry in the `performance` dictionary argument of the `AircraftConcept` class object being used). This means that, whatever the T/W ratio, the wings will stall at this point. The basic T/W value will still be returned in `twratio`, but there is another output, `feasibletw`, which is an array of the same T/W values, with those values blanked out (replaced with NaN) that cannot be achieved due to CL exceeding the maximum clean lift coefficient. **Example** Given a load factor, an altitude (in a given atmosphere) and a true airspeed, as well as a set of basic geometrical and aerodynamic performance parameters, compute the necessary T/W ratio to hold that load factor in the turn. :: from ADRpy import atmospheres as at from ADRpy import constraintanalysis as ca from ADRpy import unitconversions as co designbrief = {'stloadfactor': 2, 'turnalt_m': co.feet2m(10000), 'turnspeed_ktas': 140} etap = {'turn': 0.85} designperformance = {'CLmaxclean': 1.45, 'CDminclean':0.02541, 'etaprop': etap} designdef = {'aspectratio': 10.12, 'sweep_le_deg': 2, 'sweep_mt_deg': 0, 'bpr': -1} designatm = at.Atmosphere() concept = ca.AircraftConcept(designbrief, designdef, designperformance, designatm) wingloadinglist_pa = [1250, 1500, 1750] twratio, clrequired, feasibletw = concept.twrequired_trn(wingloadinglist_pa) print('T/W: ', twratio) print('Only feasible T/Ws:', feasibletw) print('CL required: ', clrequired) print('CLmax clean: ', designperformance['CLmaxclean']) Output: :: T/W: [ 0.19920641 0.21420513 0.23243016] Only feasible T/Ws: [ 0.19920641 0.21420513 nan] CL required: [ 1.06552292 1.2786275 1.49173209] CLmax clean: 1.45 """ if self.turnspeed_ktas == -1: turnmsg = "Turn speed not specified in the designbrief dictionary." raise ValueError(turnmsg) if self.stloadfactor == -1: turnmsg = "Turn load factor not specified in the designbrief dictionary." raise ValueError(turnmsg) wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) # W/S at the start of the specified turn test may be less than MTOW/S wingloading_pa = wingloading_pa * self.turn_weight_fraction twratio, clrequired = self.thrusttoweight_sustainedturn(wingloading_pa) # What SL T/W will yield the required T/W at the actual altitude? temp_c = self.designatm.airtemp_c(self.turnalt_m) pressure_pa = self.designatm.airpress_pa(self.turnalt_m) density_kgpm3 = self.designatm.airdens_kgpm3(self.turnalt_m) turnspeed_mps = co.kts2mps(self.turnspeed_ktas) mach = self.designatm.mach(turnspeed_mps, self.turnalt_m) corr = self._altcorr(temp_c, pressure_pa, mach, density_kgpm3) twratio = twratio / corr # Map back to T/MTOW if turn start weight is less than MTOW twratio = twratio * self.turn_weight_fraction # Which of these points is actually reachable given the clean CLmax? feasibletw = np.copy(twratio) for idx, val in enumerate(clrequired): if val > self.clmaxclean: feasibletw[idx] = np.nan if len(twratio) == 1: return twratio[0], clrequired[0], feasibletw[0] return twratio, clrequired, feasibletw
def twrequired_to(self, wingloading_pa): """Calculate the T/W required for take-off for a range of wing loadings **Parameters** wingloading_pa float or numpy array, list of wing loading values in Pa. **Returns** twratio array, thrust to weight ratio required for the given wing loadings. liftoffspeed_mps array, liftoff speeds (TAS) in m/s. avspeed_mps average speed (TAS) during the take-off run, in m/s. **See also** ``twrequired`` **Notes** 1. The calculations here assume a 'no wind' take-off, conflating ground speed (GS) and true airspeed (TAS). 2. Use `twrequired` if a full constraint analysis is desired, as this integrates the take-off, turn, climb, cruise, and service ceiling constraints, as well as computing the combined constraint boundary. **Example** :: from ADRpy import atmospheres as at from ADRpy import constraintanalysis as ca designbrief = {'rwyelevation_m':1000, 'groundrun_m':1200} designdefinition = {'aspectratio':7.3, 'bpr':3.9, 'tr':1.05} designperformance = {'CDTO':0.04, 'CLTO':0.9, 'CLmaxTO':1.6, 'mu_R':0.02} wingloadinglist_pa = [2000, 3000, 4000, 5000] atm = at.Atmosphere() concept = ca.AircraftConcept(designbrief, designdefinition, designperformance, atm) tw_sl, liftoffspeed_mps, _ = concept.twrequired_to(wingloadinglist_pa) print(tw_sl) print(liftoffspeed_mps) Output: :: [ 0.19397876 0.26758006 0.33994772 0.41110154] [ 52.16511207 63.88895348 73.77260898 82.48028428] """ if self.groundrun_m == -1: tomsg = "Ground run not specified in the designbrief dictionary." raise ValueError(tomsg) wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) twratio, liftoffspeed_mps = self.thrusttoweight_takeoff(wingloading_pa) # What does this required T/W mean in terms of static T/W required? twratio = self.map2static() * twratio # What SL T/W will yield the required T/W at the actual altitude? temp_c = self.designatm.airtemp_c(self.rwyelevation_m) pressure_pa = self.designatm.airpress_pa(self.rwyelevation_m) density_kgpm3 = self.designatm.airdens_kgpm3(self.rwyelevation_m) for i, los_mps in enumerate(liftoffspeed_mps): mach = self.designatm.mach(los_mps, self.rwyelevation_m) corr = self._altcorr(temp_c, pressure_pa, mach, density_kgpm3) twratio[i] = twratio[i] / corr avspeed_mps = liftoffspeed_mps / np.sqrt(2) if len(twratio) == 1: return twratio[0], liftoffspeed_mps[0], avspeed_mps[0] return twratio, liftoffspeed_mps, avspeed_mps
def flightenvelope(self, wingloading_pa, category='norm', vd_keas=None, textsize=None, figsize_in=None, show=True): """EASA specification for CS-23.333(d) Flight Envelope (in cruising conditions). Calling this method will plot the flight envelope at a single wing-loading. **Parameters:** wingloading_pa float, single wingloading at which the flight envelope should be plotted for, in Pa. category string, choose from either :code:`'norm'` (normal), :code:`'util'` (utility), :code:`'comm'` (commuter), or :code:`'aero'` (aerobatic) categories of aircraft. Optional, defaults to :code:`'norm'`. vd_keas float, allows for specification of the design divespeed :code:`'vd_keas'` with units KEAS. If the desired speed does not fit in the bounds produced by CS-23.335, the minimum allowable airspeed is used instead. textsize integer, sets a representative reference fontsize that text in the output plot scale themselves in accordance to. Optional, defaults to 10. figsize_in list, used to specify custom dimensions of the output plot in inches. Image width must be specified as a float in the first entry of a two-item list, with height as the remaining item. Optional, defaults to 12 inches wide by 7.5 inches tall. show boolean, used to specify if the plot should be displayed. Optional, defaults to True. **Returns:** coords_poi dictionary, containing keys :code:`A` through :code:`G`, with values of coordinate tuples. These are "points of interest", the speed [KEAS] at which they occur, and the load factor they are attributed to. """ cs23categories_list = ['norm', 'util', 'comm', 'aero'] if category not in cs23categories_list: designmsg = "Valid aircraft category not specified, please select from '{0}', '{1}', '{2}', or '{3}'." \ .format(cs23categories_list[0], cs23categories_list[1], cs23categories_list[2], cs23categories_list[3]) raise ValueError(designmsg) catg_names = { 'norm': "Normal", 'util': "Utility", 'comm': "Commuter", 'aero': "Aerobatic" } if textsize is None: textsize = 10 default_figsize_in = [12, 7.5] if figsize_in is None: figsize_in = default_figsize_in elif type(figsize_in) == list: if len(figsize_in) != 2: argmsg = "Unsupported figure size, should be length 2, found {0} instead - using default parameters." \ .format(len(figsize_in)) warnings.warn(argmsg, RuntimeWarning) figsize_in = default_figsize_in if self.acobj.cruisealt_m is False: cruisemsg = "Cruise altitude not specified in the designbrief dictionary." raise ValueError(cruisemsg) rho_kgm3 = self.acobj.designatm.airdens_kgpm3(self.acobj.cruisealt_m) rho0_kgm3 = self.acobj.designatm.airdens_kgpm3() if self.acobj.cruisespeed_ktas is False: cruisemsg = "Cruise speed not specified in the designbrief dictionary." raise ValueError(cruisemsg) vc_ktas = self.acobj.cruisespeed_ktas if self.acobj.clmaxclean is False: perfmsg = "Clmaxclean not specified in the performance dictionary" raise ValueError(perfmsg) clmax = self.acobj.clmaxclean if self.acobj.clminclean is False: perfmsg = "Clminclean not specified in the performance dictionary" raise ValueError(perfmsg) clmin = self.acobj.clminclean wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) speedlimits_dict = self._paragraph335( wingloading_pa=wingloading_pa)[category] manoeuvreload_dict, gustspeeds_dict = self._paragraph333() # V_A, Manoeuvring Speed vamin_keas = speedlimits_dict['vamin_keas'] vamax_keas = speedlimits_dict['vamax_keas'] va_keas = np.interp(vamin_keas, [vamin_keas, vamax_keas], [vamin_keas, vamax_keas]) # V_B, Gust Penetration Speed vbmin_keas = float(speedlimits_dict['vbmin_keas']) vbpen_keas = float(speedlimits_dict['vbmin1_keas']) vbmax_keas = float(speedlimits_dict['vbmax_keas']) vb_keas = np.interp(vbpen_keas, [vbmin_keas, vbmax_keas], [vbmin_keas, vbmax_keas]) # V_C, Cruise Speed vcmin_keas = float(speedlimits_dict['vcmin_keas']) vc_keas = max(co.tas2eas(vc_ktas, rho_kgm3), vcmin_keas) # V_D, Dive Speed vdmin_keas = float(speedlimits_dict['vdmin_keas']) if vd_keas is None: vd_keas = vdmin_keas else: vd_keas = max(vd_keas, vdmin_keas) # V_S, Stall Speed vs_keas = self.vs_keas(loadfactor=1) # V_invS, Inverted Stalling Speed vis_keas = self.vs_keas(loadfactor=-1) if vis_keas < vs_keas: argmsg = "Inverted-flight stall speed < Level-flight stall speed, consider reducing design Manoeuvre Speed." warnings.warn(argmsg, RuntimeWarning) # V_invA, Inverted Manoeuvring Speed viamin_keas = vis_keas * math.sqrt( abs(manoeuvreload_dict[category]['nneg_C'])) # Gust coordinates airspeed_atgust_keas = {'Ub': vb_keas, 'Uc': vc_keas, 'Ud': vd_keas} gustloads, _, _ = self._paragraph341( wingloading_pa=wingloading_pa, speedatgust_keas=airspeed_atgust_keas) # Manoeuvre coordinates coords_manoeuvre = {} coordinate_list = ['x', 'y'] # Curve OA oa_x = np.linspace(0, va_keas, 100, endpoint=True) oa_y = rho0_kgm3 * (co.kts2mps(oa_x))**2 * clmax / wingloading_pa / 2 coords_manoeuvre.update( {'OA': dict(zip(coordinate_list, [list(oa_x), list(oa_y)]))}) # Points D, E, F coords_manoeuvre.update({ 'D': dict( zip(coordinate_list, [vd_keas, manoeuvreload_dict[category]['npos_D']])) }) coords_manoeuvre.update({ 'E': dict( zip(coordinate_list, [vd_keas, manoeuvreload_dict[category]['nneg_D']])) }) coords_manoeuvre.update({ 'F': dict( zip(coordinate_list, [vc_keas, manoeuvreload_dict[category]['nneg_C']])) }) # Curve GO go_x = np.linspace(viamin_keas, 0, 100, endpoint=True) go_y = 0.5 * rho0_kgm3 * co.kts2mps(go_x)**2 * clmin / wingloading_pa coords_manoeuvre.update( {'GO': dict(zip(coordinate_list, [list(go_x), list(go_y)]))}) # Flight Envelope coordinates coords_envelope = {} # Stall Line OS coords_envelope.update( {'OS': dict(zip(coordinate_list, [[vs_keas, vs_keas], [0, 1]]))}) # Curve+Line SC sc_x = np.linspace(vs_keas, vc_keas, 100, endpoint=True) sc_y = [] max_ygust = float(gustloads[category][list( gustloads[category].keys())[0]]) b_ygustpen = float(rho0_kgm3 * (co.kts2mps(vbpen_keas))**2 * clmax / wingloading_pa / 2) c_ygust = float(gustloads[category]['npos_Uc']) d_ymano = manoeuvreload_dict[category]['npos_D'] for speed in sc_x: # If below minimum manoeuvring speed or gust intersection speed, keep on the stall curve if (speed <= va_keas) or (speed <= vbpen_keas): sc_y.append(rho0_kgm3 * (co.kts2mps(speed))**2 * clmax / wingloading_pa / 2) # Else the flight envelope is the max of the gust/manoeuvre envelope sizes elif vbpen_keas > va_keas: sc_y.append( max( np.interp(speed, [vb_keas, vc_keas], [b_ygustpen, c_ygust]), d_ymano)) coords_envelope.update( {'SC': dict(zip(coordinate_list, [list(sc_x), sc_y]))}) # Line CD cd_x = np.linspace(vc_keas, vd_keas, 100, endpoint=True) cd_y = [] d_ygust = float(gustloads[category]['npos_Ud']) for speed in cd_x: cd_y.append( max( np.interp(speed, [vc_keas, vd_keas], [float(sc_y[-1]), d_ygust]), d_ymano)) coords_envelope.update( {'CD': dict(zip(coordinate_list, [list(cd_x), cd_y]))}) # Point E e_ygust = float(gustloads[category]['nneg_Ud']) e_ymano = manoeuvreload_dict[category]['nneg_D'] e_y = min(e_ygust, e_ymano) coords_envelope.update( {'E': dict(zip(coordinate_list, [vd_keas, e_y]))}) # Line EF ef_x = np.linspace(vd_keas, vc_keas, 100, endpoint=True) ef_y = [] f_ygust = float(gustloads[category]['nneg_Uc']) f_ymano = manoeuvreload_dict[category]['nneg_C'] for speed in ef_x: ef_y.append( min(np.interp(speed, [vc_keas, vd_keas], [f_ygust, e_ygust]), f_ymano)) coords_envelope.update( {'EF': dict(zip(coordinate_list, [list(ef_x), ef_y]))}) # Line FG fg_x = np.linspace(vc_keas, viamin_keas, 100, endpoint=True) fg_y = [] for speed in fg_x: fg_y.append( min(np.interp(speed, [0, vc_keas], [1, f_ygust]), f_ymano)) coords_envelope.update( {'FG': dict(zip(coordinate_list, [list(fg_x), fg_y]))}) # Curve GS gs_x = np.linspace(viamin_keas, vis_keas, 100, endpoint=True) gs_y = rho0_kgm3 * (co.kts2mps(gs_x))**2 * clmin / wingloading_pa / 2 coords_envelope.update( {'GS': dict(zip(coordinate_list, [list(gs_x), list(gs_y)]))}) # Stall Line iSO coords_envelope.update({ 'iSO': dict( zip(coordinate_list, [[vis_keas, vis_keas, vs_keas], [-1, 0, 0]])) }) # Points of Interest coordinates - These are points that appear in the CS-23.333(d) example coords_poi = {} coords_poi.update({ 'A': (va_keas, d_ymano), 'B': (vb_keas, b_ygustpen), 'C': (vc_keas, sc_y[-1]), 'D': (vd_keas, cd_y[-1]), 'E': (vd_keas, e_y), 'F': (vc_keas, ef_y[-1]), 'G': (viamin_keas, fg_y[-1]) }) if category == 'comm': coords_poi.update({'B': (vb_keas, b_ygustpen)}) if vbpen_keas > vc_keas: del coords_poi['B'] yposlim = max(max_ygust, coords_poi['C'][1], coords_poi['D'][1]) yneglim = min(coords_poi['E'][1], coords_poi['F'][1]) if show: # Plotting parameters fontsize_title = 1.20 * textsize fontsize_label = 1.05 * textsize fontsize_legnd = 1.00 * textsize fontsize_tick = 0.90 * textsize fig = plt.figure(figsize=figsize_in) fig.canvas.set_window_title('ADRpy airworthiness.py') ax = fig.add_axes([0.1, 0.1, 0.7, 0.8]) ax.set_title( "EASA CS-23 Amendment 4 - Flight Envelope ({0} Category)". format(catg_names[category]), fontsize=fontsize_title) ax.set_xlabel("Airspeed [KEAS]", fontsize=fontsize_label) ax.set_ylabel("Load Factor [-]", fontsize=fontsize_label) ax.tick_params(axis='x', labelsize=fontsize_tick) ax.tick_params(axis='y', labelsize=fontsize_tick) # Gust Lines plotting xlist = [] ylist = [] for gustindex, (gustloadkey, gustload) in enumerate( gustloads[category].items()): gusttype = gustloadkey.split('_')[1] gustspeed_mps = round( gustspeeds_dict[category][str(gusttype + '_mps')], 2) xlist += [0, airspeed_atgust_keas[gusttype]] ylist += [1, gustload] if gustload >= 0: # Calculate where gust speed annotations should point to xannotate = 0.15 * xlist[-1] yannotate = 0.15 * (ylist[-1] - 1) + 1 xyannotate = xannotate, yannotate # Calculate where the actual annotation text with the gust speed should be positioned offsetx = (abs(gustload)**2 * (15.24 - gustspeed_mps)) / 4 offsety = float((4 - gustindex + 1) * 3 * (12 if gustload < 0 else 10))**0.93 label = "$U_{de} = $" + str(gustspeed_mps) + " $ms^{-1}$" # Produce the annotation ax.annotate(label, xy=xyannotate, textcoords='offset points', xytext=(offsetx, offsety), fontsize=fontsize_legnd, arrowprops={ 'arrowstyle': '->', 'color': 'black', 'alpha': 0.8 }) # Plot the gust lines coords = np.array(xlist, dtype=object), np.array(ylist, dtype=object) for gustline_idx in range(len(gustloads[category])): # Individual gust line coordinates xcoord = coords[0][gustline_idx * 2:(gustline_idx * 2) + 2] ycoord = coords[1][gustline_idx * 2:(gustline_idx * 2) + 2] # Gust lines should be extended beyond the flight envelope, improving their visibility xcoord_ext = np.array([xcoord[0], xcoord[1] * 5], dtype=object) ycoord_ext = np.array([ycoord[0], ((ycoord[1] - 1) * 5) + 1], dtype=object) # Plot the gust lines, but make sure only one label appears in the legend if gustline_idx == 0: ax.plot(xcoord_ext, ycoord_ext, c='blue', ls='-.', lw=0.9, alpha=0.5, label='Gust Lines') else: ax.plot(xcoord_ext, ycoord_ext, c='blue', ls='-.', lw=0.9, alpha=0.5) # Flight Envelope plotting xlist = [] ylist = [] for _, (k, v) in enumerate(coords_envelope.items()): if type(v['x']) != list: xlist.append(v['x']) ylist.append(v['y']) else: xlist += v['x'] ylist += v['y'] coords = np.array(xlist, dtype=object), np.array(ylist, dtype=object) ax.plot(*coords, c='black', ls='-', lw=1.4, label='Flight Envelope') ax.fill(*coords, c='grey', alpha=0.10) # Points of Interest plotting - These are points that appear in the CS-23.333(d) example class AnyObject(object): def __init__(self, text, color): self.my_text = text self.my_color = color class AnyObjectHandler(object): def legend_artist(self, legend, orig_handle, fontsize, handlebox): patch = mpl_text.Text(x=0, y=0, text=orig_handle.my_text, color=orig_handle.my_color, verticalalignment=u'baseline', horizontalalignment=u'left', fontsize=fontsize_legnd) handlebox.add_artist(patch) return patch handles_objects_list = [] labels_list = [] handler_map = {} # First, annotate the V-n diagram with the points of interest clearly labelled for i, (k, v) in enumerate(coords_poi.items()): # If the speed to be annotated has a positive limit load, annotate with a green symbol, not red clr = 'green' if k in ['A', 'B', 'C', 'D'] else 'red' handles_objects_list.append(AnyObject(k, clr)) labels_list.append(("V: " + str(round(float(v[0]), 1))).ljust(10) + ("| n: " + str(round(float(v[1]), 2)))) handler_map.update( {handles_objects_list[-1]: AnyObjectHandler()}) offs_spd = vc_keas if c_ygust > max_ygust else vb_keas offset = (textsize / 2 if v[0] > offs_spd else -textsize, textsize / 10 if v[1] > 0 else -textsize) ax.annotate(k, xy=v, textcoords='offset points', xytext=offset, fontsize=fontsize_label, color=clr) plt.plot(*v, 'x', color=clr) # Second, create a legend which contains the V-n parameters of each point of interest vnlegend = ax.legend(handles_objects_list, labels_list, handler_map=handler_map, loc='center left', title="V-n Speed [KEAS]; Load [-]", bbox_to_anchor=(1, 0.4), title_fontsize=fontsize_label, prop={ 'size': fontsize_legnd, 'family': 'monospace' }) # Manoeuvre Envelope plotting xlist = [] ylist = [] for _, (k, v) in enumerate(coords_manoeuvre.items()): if type(v['x']) != list: xlist.append(v['x']) ylist.append(v['y']) else: xlist += v['x'] ylist += v['y'] coords = np.array(xlist, dtype=object), np.array(ylist, dtype=object) ax.plot(*coords, c='orange', ls='--', lw=1.4, alpha=0.9, label='Manoeuvre Envelope') # Create the primary legend ax.legend(loc='center left', bbox_to_anchor=(1, 0.75), prop={'size': fontsize_legnd}) # Add the secondary legend, without destroying the original ax.add_artist(vnlegend) ax.set_xlim(0, 1.1 * vd_keas) ax.set_ylim(1.3 * yneglim, 1.3 * yposlim) plt.grid() plt.show() plt.close(fig=fig) return coords_poi
def _paragraph341(self, wingloading_pa, speedatgust_keas): """EASA specification for CS-23 Gust load factors (in cruising conditions). **Parameters:** wingloading_pa float or array, list of wing-loading values in Pa. speedatgust_keas dictionary, containing the airspeeds at which each gust condition from CS-23.333 should be evaluated. The airspeed at each condition in KEAS should be passed as the value to one or more of the following keys: :code:`'Ub'`, :code:`'Uc'`, and :code:`'Ud'`. **Returns:** gustload_dict dictionary, with aircraft categories :code:`'norm'`,:code:`'util'`, :code:`'comm'`, and :code:`'aero'`. Contained within each category is another dictionary of the absolute maximum negative limit load, and minimum positive limit load due to gust, under keys :code:`'nposUb'`, :code:`'nposUc'`, :code:`'nposUd'`, :code:`'nnegUc'` and:code:`'nnegUd'`. k_g float, the gust alleviation factor liftslope float, the liftslope as calculated by the :code:`constraintanalysis` module, under cruise conditions. """ if self.acobj.cruisespeed_ktas is False: cruisemsg = "Cruise speed not specified in the designbrief dictionary." raise ValueError(cruisemsg) cruisespeed_mpstas = co.kts2mps(self.acobj.cruisespeed_ktas) if self.acobj.cruisealt_m is False: cruisemsg = "Cruise altitude not specified in the designbrief dictionary." raise ValueError(cruisemsg) altitude_m = self.acobj.cruisealt_m mach = self.acobj.designatm.mach(airspeed_mps=cruisespeed_mpstas, altitude_m=altitude_m) liftslope = self.acobj.liftslope(mach_inf=mach) rho_kgm3 = self.acobj.designatm.airdens_kgpm3(altitude_m) rho0_kgm3 = self.acobj.designatm.airdens_kgpm3() wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) cruiseloading_pa = wingloading_pa * self.acobj.cruise_weight_fraction _, gusts_mps = self._paragraph333() # Aeroplane mass ratio mu_g = (2 * cruiseloading_pa) / ( rho_kgm3 * self._meanchord_m()['SMC'] * liftslope * constants.g) # Gust alleviation factor k_g = 0.88 * mu_g / (5.3 + mu_g) # Gust load factors cs23categories_list = ['norm', 'util', 'comm', 'aero'] gustload_dict = dict( zip(cs23categories_list, [{} for _ in range(len(cs23categories_list))])) for category in cs23categories_list: for _, (gusttype, gustspeed_mps) in enumerate(gusts_mps[category].items()): suffix = gusttype.split('_')[0] if suffix in speedatgust_keas: airspeed_keas = [ value for key, value in speedatgust_keas.items() if suffix in key ][0] airspeed_mps = co.kts2mps(airspeed_keas) q = k_g * rho0_kgm3 * gustspeed_mps * airspeed_mps * liftslope / ( 2 * cruiseloading_pa) poskey = 'npos_' + suffix negkey = 'nneg_' + suffix gustload_dict[category].update({poskey: 1 + q}) gustload_dict[category].update({negkey: 1 - q}) return gustload_dict, k_g, liftslope
def _paragraph335(self, wingloading_pa): """EASA specification for CS-23 Design Airspeeds. For all categories of aircraft, this specification item produces limits for design airspeeds. **Parameters:** wingloading_pa float or array, list of wing-loading values in Pa. **Returns:** eas_dict dictionary, containing minimum and maximum allowable design airspeeds in KEAS. **Note:** This method is not fully implemented, a future revision would allow the cruise and design speeds to be selected as functions of Mach number based on compressibility limits. Furthermore, the cruise speed should be limited by maximum level flight speed but is not currently implemented. """ if self.acobj.cruisealt_m is False: cruisemsg = "Cruise altitude not specified in the designbrief dictionary." raise ValueError(cruisemsg) rho_kgpm3 = self.acobj.designatm.airdens_kgpm3(self.acobj.cruisealt_m) if self.acobj.cruisespeed_ktas is False: cruisemsg = "Cruise speed not specified in the designbrief dictionary." raise ValueError(cruisemsg) vc_keas = co.tas2eas(self.acobj.cruisespeed_ktas, rho_kgpm3) if self.acobj.clmaxclean is False: perfmsg = "CLmaxclean must be specified in the performance dictionary." raise ValueError(perfmsg) clmaxclean = self.acobj.clmaxclean wingloading_pa = actools.recastasnpfloatarray(wingloading_pa) wingloading_lbft2 = co.pa2lbfft2(wingloading_pa) # Create a dictionary of empty dictionaries for each aircraft category cs23categories_list = ['norm', 'util', 'comm', 'aero'] eas_dict = dict( zip(cs23categories_list, [{} for _ in range(len(cs23categories_list))])) # (a) Design cruising speed, V_C # (a)(1, 2) vcfactor_1i = np.interp(wingloading_lbft2, [20, 100], [33, 28.6]) vcfactor_1ii = np.interp(wingloading_lbft2, [20, 100], [36, 28.6]) eas_dict['norm'].update( {'vcmin_keas': vcfactor_1i * np.sqrt(wingloading_lbft2)}) eas_dict['util'].update( {'vcmin_keas': vcfactor_1i * np.sqrt(wingloading_lbft2)}) eas_dict['comm'].update( {'vcmin_keas': vcfactor_1i * np.sqrt(wingloading_lbft2)}) eas_dict['aero'].update( {'vcmin_keas': vcfactor_1ii * np.sqrt(wingloading_lbft2)}) # (a)(3) Requires vh_keas # (a)(4) Requires Mach # (b) Design dive speed, V_D # (b) (1, 2, 3) vdfactor_2i = np.interp(wingloading_lbft2, [20, 100], [1.4, 1.35]) vdfactor_2ii = np.interp(wingloading_lbft2, [20, 100], [1.5, 1.35]) vdfactor_2iii = np.interp(wingloading_lbft2, [20, 100], [1.55, 1.35]) eas_dict['norm'].update({ 'vdmin_keas': np.fmax(1.25 * vc_keas, vdfactor_2i * eas_dict['norm']['vcmin_keas']) }) eas_dict['util'].update({ 'vdmin_keas': np.fmax(1.25 * vc_keas, vdfactor_2ii * eas_dict['util']['vcmin_keas']) }) eas_dict['comm'].update({ 'vdmin_keas': np.fmax(1.25 * vc_keas, vdfactor_2i * eas_dict['comm']['vcmin_keas']) }) eas_dict['aero'].update({ 'vdmin_keas': np.fmax(1.25 * vc_keas, vdfactor_2iii * eas_dict['aero']['vcmin_keas']) }) # (b)(4) Requires Mach # (c) Design manoeuvring speed, V_A # (c)(1, 2) vs_keas = self.vs_keas(loadfactor=1) manoeuvrelimits = self._paragraph337() for category in cs23categories_list: eas_dict[category].update({ 'vamin_keas': vs_keas * math.sqrt(manoeuvrelimits[category]['npos_min']) }) eas_dict[category].update({'vamax_keas': vc_keas}) # (d) Design speed for maximum gust intensity, V_B # (d)(1) _, gustspeedsmps = self._paragraph333() _, k_g, liftslope = self._paragraph341( wingloading_pa, speedatgust_keas={'Uc': vc_keas}) cruisewfraction = self.acobj.cruise_weight_fraction vs1_keas = self.vs_keas(loadfactor=cruisewfraction) for category in cs23categories_list: if category == 'comm': gust_de_mps = gustspeedsmps[category]['Ub_mps'] else: gust_de_mps = gustspeedsmps[category]['Uc_mps'] cruiseloading_pa = wingloading_pa * self.acobj.cruise_weight_fraction rho0_kgm3 = self.acobj.designatm.airdens_kgpm3(altitudes_m=0) a = 1 b = -(liftslope / clmaxclean) * k_g * gust_de_mps c = -2 * cruiseloading_pa / (rho0_kgm3 * clmaxclean) vbmin1_keas = co.mps2kts((-b + (b**2 - 4 * a * c)**0.5) / (2 * a)) eas_dict[category].update({'vbmin1_keas': vbmin1_keas}) vbmin2_keas = vs1_keas * math.sqrt( manoeuvrelimits[category]['npos_min']) eas_dict[category].update({'vbmin2_keas': vbmin2_keas}) vbmin_keas = np.fmin(vbmin1_keas, vbmin2_keas) eas_dict[category].update({'vbmin_keas': vbmin_keas}) # (d)(2) eas_dict[category].update( {'vbmax_keas': np.fmax(vbmin_keas, vc_keas)}) return eas_dict