示例#1
0
    def windcomponents(self, wind_dirs_deg, wind_speeds):
        """Resolves list of wind speeds and directions into runway/cross components"""

        speeds = mtools.recastasnpfloatarray(wind_speeds)

        # Wind speed is considered as a positive scalar
        speeds = np.abs(speeds)

        directions_deg = mtools.recastasnpfloatarray(wind_dirs_deg)

        relative_heading_rad = np.deg2rad(directions_deg -
                                          self.le_heading_degt)

        runway_component = speeds * np.cos(relative_heading_rad)  # Headwind: +
        crosswind_component = speeds * np.sin(relative_heading_rad)  # Right: +

        # Scalar output to a scalar input
        if isinstance(wind_dirs_deg, Number):
            return runway_component[0], crosswind_component[0]

        return runway_component, crosswind_component
示例#2
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
示例#3
0
    def bestclimbspeedprop(self, wingloading_pa, altitude_m):
        """The best rate of climb speed for a propeller aircraft"""

        wingloading_pa = actools.recastasnpfloatarray(wingloading_pa)
        dragfactor = np.sqrt(self.induceddragfact(123) / (3 * self.cdminclean))
        densfactor = 2 / self.designatm.airdens_kgpm3(altitude_m)

        # Gudmundsson, eq. (18-27)
        bestspeed_mps = np.sqrt(densfactor * wingloading_pa * dragfactor)

        if len(bestspeed_mps) == 1:
            return bestspeed_mps[0]

        return bestspeed_mps
示例#4
0
    def mach(self, airspeed_mps, altitude_m=0):
        """Mach number at a given speed (m/s) and altitude (m)"""

        airspeed_mps = mtools.recastasnpfloatarray(airspeed_mps)
        # Airspeed may be negative, e.g., when simulating a tailwind, but Mach must be >0
        if airspeed_mps.any() < 0:
            negmsg = "Airspeed < 0. If intentional, ignore this. Positive Mach no. returned."
            warnings.warn(negmsg, RuntimeWarning)
            airspeed_mps = abs(airspeed_mps)

        # Check altitude range
        altitude_m = self._alttest(altitude_m)

        # Compute speed of sound at the given altitude(s)
        vs_mps = self.vsound_mps(altitude_m)

        return airspeed_mps / vs_mps
示例#5
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
示例#6
0
    def windcomponents(self, wind_dirs_deg, wind_speeds):
        """Resolves list of wind speeds and directions into runway/cross components
        on the current runway.

        **Parameters:**

        wind_dirs_deg
            List of floats. Wind directions expressed in degrees true (e.g., directions
            specified in a METAR).

        wind_speeds
            List of floats. Wind_speeds (in the units in which the output is desired).

        **Outputs:**

        runway_component
            Scalar or numpy array. The runway direction component of the wind (sign
            convention: headwinds are positive).

        crosswind_component
            Scalar or numpy array. The cross component of the wind (sign convention:
            winds from the right are positive).


        **Example** ::

            # Given a METAR, calculate the wind components on Rwy 09 at Yeovilton

            from ADRpy import atmospheres as at
            from metar import Metar

            runway = at.Runway('EGDY', 1)

            egdywx = Metar.Metar('EGDY 211350Z 30017G25KT 9999 FEW028 BKN038 08/01 Q1031')

            direction_deg = egdywx.wind_dir.value()
            windspeed_kts = egdywx.wind_speed.value()

            rwy_knots, cross_knots = runway.windcomponents(direction_deg, windspeed_kts)

            print("Runway component:", rwy_knots)
            print("Cross component:", cross_knots)

        Output: ::

            Runway component: -13.5946391943
            Cross component: -10.2071438305
        """

        speeds = mtools.recastasnpfloatarray(wind_speeds)

        # Wind speed is considered as a positive scalar
        speeds = np.abs(speeds)

        directions_deg = mtools.recastasnpfloatarray(wind_dirs_deg)

        relative_heading_rad = np.deg2rad(directions_deg - self.le_heading_degt)

        runway_component = speeds * np.cos(relative_heading_rad) # Headwind: +
        crosswind_component = speeds * np.sin(relative_heading_rad) # Right: +

        # Scalar output to a scalar input
        if isinstance(wind_dirs_deg, Number):
            return runway_component[0], crosswind_component[0]

        return runway_component, crosswind_component
示例#7
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
示例#8
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
示例#9
0
    def twrequired_to(self, wingloading_pa):
        """Calculate the T/W required for take-off for a range of wing loadings

        **Parameters**

        wingloading_pa
            float or numpy array, list of wing loading values in Pa.

        **Returns**

        twratio
            array, thrust to weight ratio required for the given wing loadings.

        liftoffspeed_mps
            array, liftoff speeds (TAS) in m/s.

        avspeed_mps
            average speed (TAS) during the take-off run, in m/s.

        **See also** ``twrequired``

        **Notes**

        1. The calculations here assume a 'no wind' take-off, conflating ground speed (GS) and
        true airspeed (TAS).

        2. Use `twrequired` if a full constraint analysis is desired, as this integrates
        the take-off, turn, climb, cruise, and service ceiling constraints, as well as
        computing the combined constraint boundary.

        **Example** ::

            from ADRpy import atmospheres as at
            from ADRpy import constraintanalysis as ca

            designbrief = {'rwyelevation_m':1000, 'groundrun_m':1200}
            designdefinition = {'aspectratio':7.3, 'bpr':3.9, 'tr':1.05}
            designperformance = {'CDTO':0.04, 'CLTO':0.9, 'CLmaxTO':1.6, 'mu_R':0.02}

            wingloadinglist_pa = [2000, 3000, 4000, 5000]

            atm = at.Atmosphere()
            concept = ca.AircraftConcept(designbrief, designdefinition,
                                        designperformance, atm)

            tw_sl, liftoffspeed_mps, _ = concept.twrequired_to(wingloadinglist_pa)

            print(tw_sl)
            print(liftoffspeed_mps)

        Output: ::

            [ 0.19397876  0.26758006  0.33994772  0.41110154]
            [ 52.16511207  63.88895348  73.77260898  82.48028428]

        """
        if self.groundrun_m == -1:
            tomsg = "Ground run not specified in the designbrief dictionary."
            raise ValueError(tomsg)

        wingloading_pa = actools.recastasnpfloatarray(wingloading_pa)

        twratio, liftoffspeed_mps = self.thrusttoweight_takeoff(wingloading_pa)

        # What does this required T/W mean in terms of static T/W required?
        twratio = self.map2static() * twratio

        # What SL T/W will yield the required T/W at the actual altitude?
        temp_c = self.designatm.airtemp_c(self.rwyelevation_m)
        pressure_pa = self.designatm.airpress_pa(self.rwyelevation_m)
        density_kgpm3 = self.designatm.airdens_kgpm3(self.rwyelevation_m)

        for i, los_mps in enumerate(liftoffspeed_mps):
            mach = self.designatm.mach(los_mps, self.rwyelevation_m)
            corr = self._altcorr(temp_c, pressure_pa, mach, density_kgpm3)
            twratio[i] = twratio[i] / corr

        avspeed_mps = liftoffspeed_mps / np.sqrt(2)

        if len(twratio) == 1:
            return twratio[0], liftoffspeed_mps[0], avspeed_mps[0]

        return twratio, liftoffspeed_mps, avspeed_mps
示例#10
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
示例#11
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
示例#12
0
    def _paragraph335(self, wingloading_pa):
        """EASA specification for CS-23 Design Airspeeds.

        For all categories of aircraft, this specification item produces limits for design
        airspeeds.

        **Parameters:**

        wingloading_pa
            float or array, list of wing-loading values in Pa.

        **Returns:**

        eas_dict
            dictionary, containing minimum and maximum allowable design airspeeds in KEAS.

        **Note:**

        This method is not fully implemented, a future revision would allow the cruise and
        design speeds to be selected as functions of Mach number based on compressibility
        limits. Furthermore, the cruise speed should be limited by maximum level flight speed
        but is not currently implemented.

        """
        if self.acobj.cruisealt_m is False:
            cruisemsg = "Cruise altitude not specified in the designbrief dictionary."
            raise ValueError(cruisemsg)
        rho_kgpm3 = self.acobj.designatm.airdens_kgpm3(self.acobj.cruisealt_m)

        if self.acobj.cruisespeed_ktas is False:
            cruisemsg = "Cruise speed not specified in the designbrief dictionary."
            raise ValueError(cruisemsg)
        vc_keas = co.tas2eas(self.acobj.cruisespeed_ktas, rho_kgpm3)

        if self.acobj.clmaxclean is False:
            perfmsg = "CLmaxclean must be specified in the performance dictionary."
            raise ValueError(perfmsg)
        clmaxclean = self.acobj.clmaxclean

        wingloading_pa = actools.recastasnpfloatarray(wingloading_pa)
        wingloading_lbft2 = co.pa2lbfft2(wingloading_pa)

        # Create a dictionary of empty dictionaries for each aircraft category
        cs23categories_list = ['norm', 'util', 'comm', 'aero']
        eas_dict = dict(
            zip(cs23categories_list,
                [{} for _ in range(len(cs23categories_list))]))

        # (a) Design cruising speed, V_C

        # (a)(1, 2)
        vcfactor_1i = np.interp(wingloading_lbft2, [20, 100], [33, 28.6])
        vcfactor_1ii = np.interp(wingloading_lbft2, [20, 100], [36, 28.6])

        eas_dict['norm'].update(
            {'vcmin_keas': vcfactor_1i * np.sqrt(wingloading_lbft2)})
        eas_dict['util'].update(
            {'vcmin_keas': vcfactor_1i * np.sqrt(wingloading_lbft2)})
        eas_dict['comm'].update(
            {'vcmin_keas': vcfactor_1i * np.sqrt(wingloading_lbft2)})
        eas_dict['aero'].update(
            {'vcmin_keas': vcfactor_1ii * np.sqrt(wingloading_lbft2)})

        # (a)(3) Requires vh_keas
        # (a)(4) Requires Mach

        # (b) Design dive speed, V_D

        # (b) (1, 2, 3)
        vdfactor_2i = np.interp(wingloading_lbft2, [20, 100], [1.4, 1.35])
        vdfactor_2ii = np.interp(wingloading_lbft2, [20, 100], [1.5, 1.35])
        vdfactor_2iii = np.interp(wingloading_lbft2, [20, 100], [1.55, 1.35])

        eas_dict['norm'].update({
            'vdmin_keas':
            np.fmax(1.25 * vc_keas,
                    vdfactor_2i * eas_dict['norm']['vcmin_keas'])
        })
        eas_dict['util'].update({
            'vdmin_keas':
            np.fmax(1.25 * vc_keas,
                    vdfactor_2ii * eas_dict['util']['vcmin_keas'])
        })
        eas_dict['comm'].update({
            'vdmin_keas':
            np.fmax(1.25 * vc_keas,
                    vdfactor_2i * eas_dict['comm']['vcmin_keas'])
        })
        eas_dict['aero'].update({
            'vdmin_keas':
            np.fmax(1.25 * vc_keas,
                    vdfactor_2iii * eas_dict['aero']['vcmin_keas'])
        })

        # (b)(4) Requires Mach

        # (c) Design manoeuvring speed, V_A

        # (c)(1, 2)
        vs_keas = self.vs_keas(loadfactor=1)
        manoeuvrelimits = self._paragraph337()

        for category in cs23categories_list:
            eas_dict[category].update({
                'vamin_keas':
                vs_keas * math.sqrt(manoeuvrelimits[category]['npos_min'])
            })
            eas_dict[category].update({'vamax_keas': vc_keas})

        # (d) Design speed for maximum gust intensity, V_B

        # (d)(1)
        _, gustspeedsmps = self._paragraph333()
        _, k_g, liftslope = self._paragraph341(
            wingloading_pa, speedatgust_keas={'Uc': vc_keas})

        cruisewfraction = self.acobj.cruise_weight_fraction
        vs1_keas = self.vs_keas(loadfactor=cruisewfraction)

        for category in cs23categories_list:

            if category == 'comm':
                gust_de_mps = gustspeedsmps[category]['Ub_mps']
            else:
                gust_de_mps = gustspeedsmps[category]['Uc_mps']

            cruiseloading_pa = wingloading_pa * self.acobj.cruise_weight_fraction
            rho0_kgm3 = self.acobj.designatm.airdens_kgpm3(altitudes_m=0)

            a = 1
            b = -(liftslope / clmaxclean) * k_g * gust_de_mps
            c = -2 * cruiseloading_pa / (rho0_kgm3 * clmaxclean)
            vbmin1_keas = co.mps2kts((-b + (b**2 - 4 * a * c)**0.5) / (2 * a))
            eas_dict[category].update({'vbmin1_keas': vbmin1_keas})

            vbmin2_keas = vs1_keas * math.sqrt(
                manoeuvrelimits[category]['npos_min'])
            eas_dict[category].update({'vbmin2_keas': vbmin2_keas})

            vbmin_keas = np.fmin(vbmin1_keas, vbmin2_keas)
            eas_dict[category].update({'vbmin_keas': vbmin_keas})

            # (d)(2)
            eas_dict[category].update(
                {'vbmax_keas': np.fmax(vbmin_keas, vc_keas)})

        return eas_dict