def powerrequired(self, wingloadinglist_pa, tow_kg, feasibleonly=True): """Calculate the power (in HP) required for t/o, trn, clm, crs, sec.""" if self.etadefaultflag > 0: etamsg = str(self.etadefaultflag) + " prop etas set to defaults." warnings.warn(etamsg, RuntimeWarning) twreq = self.twrequired(wingloadinglist_pa, feasibleonly) # Take-off power required pw_to_wpn = tw2pw(twreq['take-off'], twreq['avspeed_mps'], self.etaprop_to) pw_to_hpkg = co.wn2hpkg(pw_to_wpn) p_to_hp = pw_to_hpkg * tow_kg # Turn power required trnspeed_mpstas = co.kts2mps(self.turnspeed_ktas) if feasibleonly: pw_trn_wpn = tw2pw(twreq['turnfeasible'], trnspeed_mpstas, self.etaprop_turn) else: pw_trn_wpn = tw2pw(twreq['turn'], trnspeed_mpstas, self.etaprop_turn) pw_trn_hpkg = co.wn2hpkg(pw_trn_wpn) p_trn_hp = pw_trn_hpkg * tow_kg # Climb power # Conversion to TAS, IAS and EAS conflated, safe for typical prop speeds climbspeed_ktas = self.designatm.eas2tas(self.climbspeed_kias, self.climbalt_m) clmspeed_mpstas = co.kts2mps(climbspeed_ktas) pw_clm_wpn = tw2pw(twreq['climb'], clmspeed_mpstas, self.etaprop_climb) pw_clm_hpkg = co.wn2hpkg(pw_clm_wpn) p_clm_hp = pw_clm_hpkg * tow_kg # Power for cruise crsspeed_mpstas = co.kts2mps(self.cruisespeed_ktas) pw_crs_wpn = tw2pw(twreq['cruise'], crsspeed_mpstas, self.etaprop_cruise) pw_crs_hpkg = co.wn2hpkg(pw_crs_wpn) p_crs_hp = pw_crs_hpkg * tow_kg # Power for service ceiling # Conversion to TAS, IAS and EAS conflated, safe for typical prop speeds secclmbspeed_ktas = self.designatm.eas2tas(self.secclimbspd_kias, self.servceil_m) secclmspeed_mpstas = co.kts2mps(secclmbspeed_ktas) pw_sec_wpn = tw2pw(twreq['servceil'], secclmspeed_mpstas, self.etaprop_sec) pw_sec_hpkg = co.wn2hpkg(pw_sec_wpn) p_sec_hp = pw_sec_hpkg * tow_kg p_combined_hp = np.amax([p_to_hp, p_trn_hp, p_clm_hp, p_crs_hp, p_sec_hp], 0) preq_hp = { 'take-off': p_to_hp, 'liftoffspeed_mps': twreq['liftoffspeed_mps'], 'avspeed_mps': twreq['avspeed_mps'], 'turn': p_trn_hp, 'turncl': twreq['turncl'], 'climb': p_clm_hp, 'cruise': p_crs_hp, 'servceil': p_sec_hp, 'combined': p_combined_hp} return preq_hp
def bank2turnradius(self, bankangle_deg): """Calculates the turn radius in m, given the turn TAS and the bank angle""" bankangle_rad = math.radians(bankangle_deg) v_mps = co.kts2mps(self.turnspeed_ktas) r_m = (v_mps ** 2) / (constants.g * math.tan(bankangle_rad)) return r_m
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 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 wsmaxcleanstall_pa(self): """Maximum wing loading defined by the clean stall Clmax""" # (W/S)_max = q_vstall * CLmaxclean if self.clmaxclean == -1: clmaxmsg = "CLmaxclean must be specified in the performance dictionary." raise ValueError(clmaxmsg) if self.vstallclean_kcas == -1: vstallmsg = "Clean stall speed must be specified in the design brief dictionary." raise ValueError(vstallmsg) # We do the q calculation at SL conditions, TAS ~= EAS ~= CAS # (conflating CAS and EAS on the basis that the stall Mach number is likely v small) stallspeed_mpstas = co.kts2mps(self.vstallclean_kcas) q_pa = self.designatm.dynamicpressure_pa(stallspeed_mpstas, 0) return q_pa * self.clmaxclean
def thrusttoweight_sustainedturn(self, wingloading_pa): """Baseline T/W req'd for sustaining a given load factor at a certain altitude""" nturn = self.stloadfactor turnalt_m = self.turnalt_m turnspeed_mps = co.kts2mps(self.turnspeed_ktas) qturn = self.designatm.dynamicpressure_pa(airspeed_mps=turnspeed_mps, altitudes_m=turnalt_m) inddragfact = self.induceddragfact(whichoswald=123) cdmin = self.cdminclean twreqtrn = qturn * \ (cdmin / wingloading_pa + inddragfact * ((nturn / qturn) ** 2) * wingloading_pa) # What cl is required to actually reach the target load factor clrequired = nturn * wingloading_pa / qturn return twreqtrn, clrequired
def keas2kcas(self, keas, altitude_m): """Converts equivalent airspeed into calibrated airspeed. The relationship between the two depends on the Mach number :math:`M` and the ratio :math:`\\delta` of the pressure at the current altitude :math:`P_\\mathrm{alt}` and the sea level pressure :math:`P_\\mathrm{0}`. We approximate this relationship with the expression: .. math:: \\mathrm{CAS}\\approx\\mathrm{EAS}\\left[1 + \\frac{1}{8}(1-\\delta)M^2 + \\frac{3}{640}\\left(1-10\\delta+9\\delta^2 \\right)M^4 \\right] **Parameters** keas float or numpy array, equivalent airspeed in knots. altitude_m float, altitude in metres. **Returns** kcas float or numpy array, calibrated airspeed in knots. mach float, Mach number. **See also** ``mpseas2mpscas`` **Notes** The reverse conversion is slightly more complicated, as their relationship depends on the Mach number. This, in turn, requires the computation of the true airspeed and that can only be computed from EAS, not CAS. The unit- specific nature of the function is also the result of the need for computing the Mach number. **Example** :: import numpy as np from ADRpy import atmospheres as at from ADRpy import unitconversions as co isa = at.Atmosphere() keas = np.array([100, 200, 300]) altitude_m = co.feet2m(40000) kcas, mach = isa.keas2kcas(keas, altitude_m) print(kcas) Output: :: [ 101.25392563 209.93839073 333.01861569] """ # Note: unit specific, as the calculation requires Mach no. np.asarray(keas) mpseas = co.kts2mps(keas) mpscas, machno = self.mpseas2mpscas(mpseas, altitude_m) kcas = co.mps2kts(mpscas) return kcas, machno
def flightenvelope(self, textsize=None, figsize_in=None, show=True): """Construction of the flight envelope, as per CS-23.333(d), see also 14 CFR 23.333. Calling this method will plot the flight envelope at a single wing-loading. For examples, see below and in the Jupyter notebook :code:`Constructing V-n diagrams.ipynb` included in :code:`docs/ADRpy/notebooks`. Note that this V-n diagram should only be seen as indicative. When preparing the documentation for establishing the airworthiness of an aircraft, the engineer responsible for the structural aspects of the airworthiness must conduct his/her own calculations in establishing the flight envelope. **Parameters:** 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. **Outputs:** 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. **Example** :: from ADRpy import airworthiness as aw from ADRpy import unitconversions as co from ADRpy import atmospheres as at designbrief = {} designdef = {'aspectratio': 11.1, 'wingarea_m2': 12.1, 'weight_n': 5872} designperf = {'CLmaxclean': 1.45, 'CLminclean': -1, 'CLslope': 6.28} designpropulsion = "piston" # not specifically needed for the V-n diagram here, required simply for # consistency with other classes and to support features included in later releases designatm = at.Atmosphere() # set the design atmosphere to a zero-offset ISA csbrief={'cruisespeed_keas': 107, 'divespeed_keas': 150, 'altitude_m': 0, 'weightfraction': 1, 'certcat': 'norm'} concept = aw.CertificationSpecifications(designbrief, designdef, designperf, designatm, designpropulsion, csbrief) """ category = self.category 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 rho0_kgm3 = self.acobj.designatm.airdens_kgpm3() if self.cruisespeed_keas is False: cruisemsg = 'Cruise speed not specified in the csbrief or designbrief dictionary.' raise ValueError(cruisemsg) vc_keas = self.cruisespeed_keas if self.divespeed_keas is False: divemsg = 'Dive speed not specified in the csbrief or designbrief dictionary, using minimum allowable ' \ 'speed as specified in CS 23.335(b)(2).' warnings.warn(divemsg, RuntimeWarning) 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 if self.acobj.weight_n is False: designmsg = 'Maximum take-off weight must be specified in the design dictionary.' raise ValueError(designmsg) if self.acobj.wingarea_m2 is False: designmsg = 'Reference wing area must be specified in the design dictionary.' raise ValueError(designmsg) wingloading_pa = self.acobj.weight_n / self.acobj.wingarea_m2 wfract = self.weightfraction trueloading_pa = wingloading_pa * wfract speedlimits_dict = self._paragraph335()[category] manoeuvreload_dict, gustspeeds_dict = self._paragraph333() # V_C, Cruise Speed vcmin_keas = float(speedlimits_dict['vcmin_keas']) vcmax_keas = speedlimits_dict['vcsoftmax_keas'] vc_keas = max(vc_keas, vcmin_keas) if (vc_keas > vcmax_keas) and vcmax_keas is not False: info = "CS 23.335(a)(3): V_C need not exceed 0.9 * V_H Sea Level." warnings.warn(info, UserWarning) # V_D, Dive Speed vdmin_keas = float(speedlimits_dict['vdmin_keas']) if self.divespeed_keas is False: vd_keas = vdmin_keas else: vd_keas = max(self.divespeed_keas, vdmin_keas) # V_A, Manoeuvring Speed vamin_keas = speedlimits_dict['vamin_keas'] # vamax_keas = speedlimits_dict['vasoftmax_keas'] va_keas = vamin_keas if va_keas > vc_keas: info = "CS 23.335(c)(2): V_A need not exceed V_C used in design." warnings.warn(info, UserWarning) # 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 = vbpen_keas # V_S, Stall Speed vs_keas = self.vs_keas(loadfactor=(1 * wfract)) # V_invS, Inverted Stalling Speed vis_keas = self.vs_keas(loadfactor=(-1 * wfract)) 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( 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 / trueloading_pa / 2 / wfract 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'] * (wingloading_pa / trueloading_pa) ])) }) coords_manoeuvre.update({ 'E': dict( zip(coordinate_list, [ vd_keas, manoeuvreload_dict[category]['nneg_D'] * (wingloading_pa / trueloading_pa) ])) }) coords_manoeuvre.update({ 'F': dict( zip(coordinate_list, [ vc_keas, manoeuvreload_dict[category]['nneg_C'] * (wingloading_pa / trueloading_pa) ])) }) # Curve GO go_x = np.linspace(viamin_keas, 0, 100, endpoint=True) go_y = 0.5 * rho0_kgm3 * co.kts2mps( go_x)**2 * clmin / trueloading_pa / wfract 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 / wfract)]])) }) # 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 / trueloading_pa / 2 / wfract) c_ygust = float(gustloads[category]['npos_Uc']) d_ymano = float(manoeuvreload_dict[category]['npos_D'] / wfract) 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 / trueloading_pa / 2 / wfract) # Else the flight envelope is the max of the gust/manoeuvre envelope sizes else: # 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( float( 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'] * wfract 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'] / wfract for speed in ef_x: ef_y.append( min(np.interp(speed, [vc_keas, vd_keas], [f_ygust, e_ygust]), e_ymano)) coords_envelope.update( {'EF': dict(zip(coordinate_list, [list(ef_x), ef_y]))}) # Curve+Line FS fs_x = np.linspace(vc_keas, vis_keas, 100, endpoint=True) fs_y = [] for speed in fs_x: fs_ystall = rho0_kgm3 * ( co.kts2mps(speed))**2 * clmin / trueloading_pa / 2 / wfract # If below minimum manoeuvring speed or gust intersection speed, keep on the stall line if speed < viamin_keas: fs_y.append(fs_ystall) else: fs_y.append( max( min(np.interp(speed, [0, vc_keas], [1, f_ygust]), f_ymano), fs_ystall)) coords_envelope.update( {'FS': dict(zip(coordinate_list, [list(fs_x), fs_y]))}) # Stall Line iSO coords_envelope.update({ 'iSO': dict( zip(coordinate_list, [[vis_keas, vis_keas, vs_keas], [-1 / wfract, 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, go_y[0]) }) 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], coords_poi['G'][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-25 - Flight Envelope (Large Aeroplane 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(4) * (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.8, label='Gust Lines') else: ax.plot(xcoord_ext, ycoord_ext, c='blue', ls='-.', lw=0.9, alpha=0.8) # 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.20) # 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 _, (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 (0 - textsize), textsize / 10 if v[1] > 0 else (0 - 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, speedatgust_keas): """Gust load factors (in cruising conditions), as per CS-23.341 (see also 14 CFR 23.341). **Parameters:** 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'`. **Outputs:** 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:`'npos_Ub'`, :code:`'npos_Uc'`, :code:`'npos_Ud'`, :code:`'nneg_Uc'` and:code:`'nneg_Ud'`. k_g float, the gust alleviation factor liftslope_prad float, the liftslope_prad as calculated by the :code:`constraintanalysis` module, under cruise conditions. """ altitude_m = self.altitude_m rho_kgm3 = self.acobj.designatm.airdens_kgpm3(altitude_m) if self.cruisespeed_keas is False: cruisemsg = 'Cruise speed not specified in the csbrief or designbrief dictionary.' raise ValueError(cruisemsg) cruisespeed_mpstas = co.kts2mps( co.eas2tas(self.cruisespeed_keas, localairdensity_kgm3=rho_kgm3)) mach = self.acobj.designatm.mach(airspeed_mps=cruisespeed_mpstas, altitude_m=altitude_m) if self.acobj.weight_n is False: designmsg = 'Maximum take-off weight must be specified in the design dictionary.' raise ValueError(designmsg) if self.acobj.wingarea_m2 is False: designmsg = 'Reference wing area must be specified in the design dictionary.' raise ValueError(designmsg) wingloading_pa = self.acobj.weight_n / self.acobj.wingarea_m2 liftslope_prad = self.acobj.liftslope_prad(mach_inf=mach) rho0_kgm3 = self.acobj.designatm.airdens_kgpm3() wfract = self.weightfraction trueloading_pa = wingloading_pa * wfract _, gusts_mps = self._paragraph333() # Aeroplane mass ratio mu_g = (2 * trueloading_pa) / (rho_kgm3 * self._meanchord_m()['SMC'] * liftslope_prad * 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_prad / ( 2 * trueloading_pa) / wfract 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_prad
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 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