示例#1
0
    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
示例#2
0
    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
示例#3
0
    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
示例#4
0
    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
示例#5
0
    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
示例#6
0
    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
示例#7
0
    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
示例#8
0
    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
示例#9
0
    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
示例#10
0
    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
示例#11
0
    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
示例#12
0
    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
示例#13
0
    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