Module MAPLEAF.ENV.MeanWindModelling

Modeling of the mean / average component of wind velocity

Expand source code
''' Modeling of the mean / average component of wind velocity '''

import abc
import random
from math import cos, radians, sin

import numpy as np
import pandas as pd
from scipy.stats import rv_histogram

from MAPLEAF.IO import defaultConfigValues, getAbsoluteFilePath
from MAPLEAF.Motion import Vector, linInterp

__all__ = [ "meanWindModelFactory", "ConstantWind", "Hellman", "InterpolatedWind" ]

# Mean Wind Model Abstract Base Class / Interface Definition
class MeanWindModel(abc.ABC):
    ''' Defines the interface for Mean Wind Models '''

    @abc.abstractmethod
    def getMeanWind(self, AGLAltitude):
        pass 

# Mean Wind Model Factory
def meanWindModelFactory(envReader=None, silent=False) -> MeanWindModel:
    ''' Instantiates a mean wind model '''

    if envReader == None:
        # Return default zero-wind model
        constWind = Vector(defaultConfigValues["Environment.ConstantMeanWind.velocity"])
        return ConstantWind(constWind)

    meanWindModel = None
    meanWindModelType = envReader.getString("MeanWindModel")

    if meanWindModelType == "Constant":
        meanGroundWind = envReader.getVector("ConstantMeanWind.velocity")
        if not silent:
            print("Constant ground wind: {:1.2f} m/s".format(meanGroundWind))

        meanWindModel = ConstantWind(meanGroundWind)

    elif meanWindModelType in [ "SampledGroundWindData", "Hellman" ]:
        def getLocationSampledGroundWindVel():
            #### Get list of, and weights of, locations to sample wind rose from
            GroundWindLocationsSampled = envReader.getString("SampledGroundWindData.locationsToSample").split()
            locationsSampled = []
            locationWeights = []
            # Parse into locations (1st, 3rd, 5th... values) and weights (2nd, 4th, 6th... values)
            for i in range(0, len(GroundWindLocationsSampled), 2):
                locationsSampled.append(GroundWindLocationsSampled[i])
                locationWeights.append(float(GroundWindLocationsSampled[i+1]))
            launchMonth = envReader.getString("SampledGroundWindData.launchMonth")

            # Sample wind rose(s)
            sampler = WindRoseDataSampler(silent=silent)
            meanGroundWind = sampler.sampleWindRoses(locationsSampled, locationWeights, launchMonth)

            # Output choices parsed from input file and resulting wind
            if not silent:
                if envReader.simDefinition.monteCarloLogger != None:
                    envReader.simDefinition.monteCarloLogger.log("Sampling ground winds from: {}".format(locationsSampled))
                    envReader.simDefinition.monteCarloLogger.log("Sampling weights for each location: {}".format(locationWeights))
                    envReader.simDefinition.monteCarloLogger.log("Sampling wind distribution from month of: {}".format(launchMonth))
                    envReader.simDefinition.monteCarloLogger.log("Sampled mean ground wind: {:1.2f} m/s".format(meanGroundWind))
                else:
                    print("Sampling ground winds from: {}".format(locationsSampled))
                    print("Sampling weights for each location: {}".format(locationWeights))
                    print("Sampling wind distribution from month of: {}".format(launchMonth))
                    print("Sampled mean ground wind: {:1.2f} m/s".format(meanGroundWind))

            return meanGroundWind

        ### Create and return appropriate wind vs altitude model ###
        if meanWindModelType == "SampledGroundWindData":
            meanGroundWind = getLocationSampledGroundWindVel()
            if not silent:
                print("Wind is not a function of altitude")
            meanWindModel = ConstantWind(meanGroundWind)

        elif meanWindModelType == "Hellman":
            groundWindModel = envReader.getString("Hellman.groundWindModel")

            # Get ground wind
            if groundWindModel == "Constant":
                meanGroundWind = envReader.getVector("ConstantMeanWind.velocity")
            elif groundWindModel == "SampledGroundWindData":
                meanGroundWind = getLocationSampledGroundWindVel()

            HellmanAlphaCoeff = envReader.getFloat("Hellman.alphaCoeff")
            HellmanAltitudeLimit = envReader.getFloat("Hellman.altitudeLimit")

            if not silent:
                print("Constant ground wind: {:1.2f} m/s".format(meanGroundWind))
                print("Wind vs. altitude governed by Hellman law: v2 = v1*(z2/10)^a where a = {}".format(HellmanAlphaCoeff))
                print("Hellman law is taken to apply up to an altitude of {} m AGL".format(HellmanAltitudeLimit))

            meanWindModel = Hellman(meanGroundWind, HellmanAltitudeLimit, HellmanAlphaCoeff)
    
    elif meanWindModelType == "CustomWindProfile":
        meanWindProfileFilePath = envReader.getString("CustomWindProfile.filePath")
        meanWindModel = InterpolatedWind(windFilePath=meanWindProfileFilePath)

    elif meanWindModelType == "SampledRadioSondeData":
        # Get locations and location weights
        locationsAndWeights = envReader.getString("SampledRadioSondeData.locationsToSample").split()
        locations = []
        weights = []
        for i in range(0, len(locationsAndWeights), 2):
            locations.append(locationsAndWeights[i])
            weights.append(float(locationsAndWeights[i+1]))
            
        locationASLAltitudes = [ float(x) for x in envReader.getString("SampledRadioSondeData.locationASLAltitudes").split() ]

        # Get launch month
        launchMonth = envReader.getString("SampledRadioSondeData.launchMonth")

        # Get random seed (if provided)
        radioSondeRandomSeed = envReader.tryGetString("SampledRadioSondeData.randomSeed")

        # Select and parse radio sonde profile
        sampler = RadioSondeDataSampler(silent=silent)
        altitudes, windVectors = sampler.getRadioSondeWindProfile(locations, weights, locationASLAltitudes, launchMonth, radioSondeRandomSeed)
        
        meanWindModel = InterpolatedWind(windAltitudes=altitudes, winds=windVectors)

    else:
        raise ValueError("Unknown MeanWindModel: {}. Please see SimDefinitionTemplate.txt for available options.".format(meanWindModelType))

    return meanWindModel

#### Actual Mean Wind Model classes - these are instantiated by meanWindModelFactory ####
class ConstantWind(MeanWindModel):
    ''' Defines a constant wind speed at all altitudes  '''
    
    def __init__(self, wind):
        self.wind = wind

    def getMeanWind(self, AGLAltitude):
        return self.wind

class Hellman(MeanWindModel):
    '''
        Uses the Hellman law to scale a ground velocity using a power law as the atmospheric boundary layer is exited.
        Low-altitude only. https://en.wikipedia.org/wiki/Wind_gradient -> Engineering Section.

    '''
    def __init__(self, groundMeanWind=Vector(0,0,0), altitudeLimit=1500, hellmanAlphaCoeff=0.14):
        '''
            Arguments:
                groundMeanWind: Wind Vector at ground level (~10m above surface), in m/s
                altitudeLimit: Altitude above which the power law transitions into a constant profile (m, AGL)
                hellmanAlphaCoeff: Alpha in the Hellman model (1/7 is a common first guess, but can vary widely depending on terrain)
        '''
        self.groundMeanWind = groundMeanWind
        self.altitudeLimit = altitudeLimit
        self.hellmanAlphaCoeff = hellmanAlphaCoeff

    def getMeanWind(self, AGLAltitude):
        # Limit velocity scaling to up to the specified altitude limit
        HellmanAltitude = max(min(self.altitudeLimit, AGLAltitude), 10)
        # Assume initial winds come from a height of 10m
        return self.groundMeanWind * (HellmanAltitude/10)**self.hellmanAlphaCoeff

class InterpolatedWind(MeanWindModel):
    def __init__(self, windAltitudes=[], winds=[], windFilePath=None):
        '''
            Arguments:
                windAltitudes: list of altitudes at which wind vectors will be provided (m AGL)
                winds: list of wind vectors, matching the ordering of windAltitudes (m/s)

                windFilePath: string path to a text file containing a table, formatted similarly to the following:
                    AGlAltitude(m) WindX(m/s) WindY(m/s) WindZ(m/s)
                    a1              wx1         wy1     wz1
                    a2              wx2         wy2     wz2

            Notes:
                Provide either windAltitudes AND winds, OR windFilePath. If all are provided, windAltitudes/winds will be used
        '''
        if len(windAltitudes) + len(winds) > 0:
            self.windAltitudes = windAltitudes
            self.winds = winds

        elif windFilePath != None:
            self._readCustomWindProfile(windFilePath)

        else:
            raise ValueError("Incomplete initialization info provided. Provide either windAltitudes AND winds, OR windFilePath")

    def _readCustomWindProfile(self, filePath):
        windProfile = np.loadtxt(filePath, skiprows=1)
        self.windAltitudes = windProfile[:, 0]
        self.winds = windProfile[:, 1:]

    def getMeanWind(self, AGLAltitude):
        return Vector(*linInterp(self.windAltitudes, self.winds, AGLAltitude))

# Wind Data readers/samplers
class WindRoseDataSampler():
    ''' 
        Parses wind rose data files from Iowa State University's Environmental Mesonet.
    '''

    def __init__(self, silent=False):
        self.silent = silent

        # TODO: Generalize extreme winds outside Medecine Hat
        self.extremeWinds = [ 25, 31, 50 ] # mph = [ 40, 50, 80 ] km/h
        self.extremeWinds = [ x * 0.44704 for x in self.extremeWinds ] # convert to m/s
        self.extremeWindProbabilities = [ 0.01, 0.004, 0 ] # Decimal, chance of being at or higher than given wind speed, based on hourly peak gust data for Medecine Hat, AB

    def sampleWindRoses(self, locationsSampled, locationWeights, launchMonth):
        # Choose a wind rose based on the location weights
        averagedWindRose = self._getAveragedWindRose( Months=[launchMonth], locations=locationsSampled, locationWeights=locationWeights )
        speedrvHist = self._createWindSpeedCDF(averagedWindRose)
        
        # Choose a wind speed
        windSpeed = speedrvHist.ppf(random.random())
        
        # Construct heading probabilities based on that wind speed, then choose a wind heading
        heading_rvHist = self._createHeadingCDF(averagedWindRose, windSpeed=windSpeed)
        windHeading = heading_rvHist.ppf(random.random())
        windDirectionVector = _convertWindHeadingToXYPlaneWindDirection(windHeading)
        meanGroundWind = windDirectionVector * windSpeed

        return meanGroundWind

    def _readWindRose(self, filePath):
        '''
            Currently not general, will only work with Wind Rose data from Iowa State University - Iowa Environmental Mesonet
            http://mesonet.agron.iastate.edu/sites/windrose.phtml?network=WI_ASOS&station=CWA
        '''
        # Read data (Header = Direction,     Calm, 2.0  4.9, 5.0  6.9, 7.0  9.9,10.0 14.9,15.0 19.9,    20.0+)
        windRose = pd.read_csv(filePath, delimiter=",", comment="#")
        # Split direction column into min and max directions: ex. "000-010"
        directionMinMax = windRose["Direction"].str.split("-", n=1, expand=True)
        windRose.drop(columns=["Direction"], inplace=True)
        # Convert each of the split string data to floats
        directionMinMax = directionMinMax.astype(float)
        # Add new direction columns
        windRose["direction"] = (directionMinMax[0] + directionMinMax[1]) / 2
        # Set first direction to 0 degrees instead of 5 (assumed incorrect in windrose file)
        windRose.iloc[0,-1] = 0

        nameMap = {
            "     Calm": 1.0,
            " 2.0  4.9": 3.5,
            " 5.0  6.9": 6.0,
            " 7.0  9.9": 8.5,
            "10.0 14.9": 12.5,
            "15.0 19.9": 17.5,
            "    20.0+": 23.5
        }
        windRose.rename(mapper=nameMap, inplace=True, axis='columns')

        # Replace empty values in the first column with zeros
        windRose.replace(r'^\s*$', 0.0, regex=True, inplace=True)
        windRose[1.0] = windRose[1.0].astype(float)

        return windRose

    def _getAveragedWindRose(self, Months=["May"], MonthWeights=[1.0], locations=["MedecineHat"], locationWeights=[1.0]):
        windRose = self._readWindRose(getAbsoluteFilePath("MAPLEAF/Examples/Wind/WindroseAprMedecineHat.txt"))*0.0 # Initialize as zero
        for i in range(len(Months)):
            for a in range(len(locations)):
                windRoseFilePath = "MAPLEAF/Examples/Wind/Windrose{}{}.txt".format(Months[i], locations[a])
                windRoseFilePath = getAbsoluteFilePath(windRoseFilePath)
                windRose += self._readWindRose(windRoseFilePath) * MonthWeights[i] * locationWeights[a]

        return windRose.copy()

    def _createWindSpeedCDF(self, windRose):
        '''Calculates wind speed CDF from a windrose (wind rose can be generated using self.readWindRose())'''
        # Sum over headings to get probabilities for each speed
        likelyhoodBySpeed = windRose.sum()
        likelyhoodBySpeed.drop(index=["direction"], inplace=True)

        # Normalize to 0-1 probability range
        totalProb = likelyhoodBySpeed.sum()
        likelyhoodBySpeed = list(likelyhoodBySpeed / totalProb)

        # Bin boundaries
        cdfX = [ x * 0.44704 for x in [ 0, 2, 5, 7, 10, 15, 20, 23.5 ] ] # Convert to m/s from mph
        likelyhoodBySpeed[-1] -= sum(self.extremeWindProbabilities)
        cdfX += self.extremeWinds
        likelyhoodBySpeed += self.extremeWindProbabilities

        return rv_histogram((likelyhoodBySpeed, cdfX))

    def _createHeadingCDF(self, windRose, windSpeed=None):
        '''
            Calculates wind heading CDF from a windrose (wind rose can be generated using self.readWindRose())
            Pass in wind speed in m/s
        '''
        windRose = windRose.set_index("direction")
        windRose.loc[360] = windRose.loc[0]
        windRose.drop(index=[0], inplace=True)
        headings = list(windRose.index)
        headings.insert(0,0)
        
        if windSpeed == None:
            # Sum over all speeds to get overall probabilities for each heading
            likelyhoodByHeading = list(windRose.sum(axis=1))
        else:
            # Eliminate the empty first column, replace it with data from the 3.5 mph column
            windRose.iloc[:,0] = windRose.iloc[:,1]
            windRose.rename(columns={1.0:0.0}, inplace=True)

            # Convert from mph to m/s string
            windRose = windRose.rename(columns=lambda c: str(c * 0.44704))
            
            try:
                # If windSpeed exists already, use it
                likelyhoodByHeading = list(windRose[str(windSpeed)])
            except KeyError:
                #### Linearly interpolate to find wind speed probabilities at the given heading ####
                # Create new empty column at desired wind speed
                windRose[str(windSpeed)] = np.nan
                # Convert back to float names
                windRose = windRose.rename(columns=lambda c: float(c))
                # Sort the new column into position
                windRose = windRose.reindex(sorted(windRose.columns), axis=1)
                # Interpolate
                windRose = windRose.interpolate(method="index", axis=1, interpolate=True)            
                # Grab the new column as a list
                likelyhoodByHeading = list(windRose[windSpeed])

        #TODO: Need to de-rotate these headings by 5 degrees?
        return rv_histogram((likelyhoodByHeading, headings))

class RadioSondeDataSampler():
    ''' 
        Parses data from radio sonde data files from IGRA-2 after they've been post-processed by MAPLEAF/Examples/Wind/filterRadioSondeData.py
        IGRA-2 format info: https://www1.ncdc.noaa.gov/pub/data/igra/data/igra2-data-format.txt
    '''

    def __init__(self, silent=False, monteCarloDataLogger=None):
        self.silent = silent
        self.monteCarloDataLogger = monteCarloDataLogger

    def getRadioSondeWindProfile(self, locations=["Edmonton", "Glasgow"], locationWeights=[0.48, 0.52], locationASLAltitudes=[710, 638], launchMonth=None, randomSeed=None):
        '''
            Arguments:
                locations:              list of location names. Names must match those in radio sonde data file names (ex. "Edmonton" -> "RadioSondeEdmonton_filtered.txt")
                locationWeights:        list of numeric values, must add to one. Order should match list of locations.
                locationASLAltitudes:   list of ASL altitudes (m). Order should match list of locations.
                launchMonth:            3-letter string ("Jan", "Feb", "Mar", etc...) indicating month from which data is desired. Otherwise "Yearly" or None to select randomly from all months
                randomSeed:             Pass in the same random seed to pick the same radio sonde profile

            Returns:
                altitudes:              List of numeric altitudes (meters AGL) 
                windVectors:            Corresponding list of wind vectors (m/s)
        '''
        # Set random seed if provided
        random.seed(randomSeed)

        # Pick a location
        selectedLocation = random.choices(locations, locationWeights, k=1)[0]

        locationASLAltitude = locationASLAltitudes[locations.index(selectedLocation)]
        # Load radiosonde data for that location
        radioSondeFilePath = "MAPLEAF/Examples/Wind/RadioSonde{}_filtered.txt".format(selectedLocation)
        # Convert to absolute file path
        radioSondeFilePath = getAbsoluteFilePath(radioSondeFilePath)
        
        # Read datasets from fille
        if launchMonth != "Yearly":
            datasetStartLines, data = self._readRadioSondeDataFile(radioSondeFilePath, filterByMonth=launchMonth)
        else:
            datasetStartLines, data = self._readRadioSondeDataFile(radioSondeFilePath)

        # Pick one of it's datasets
        selectedDataset = random.randint(0, len(datasetStartLines)-1)
        
        # selectedDataset is an array of strings, header is a string
        # Example header: #CAM00071119 2000 01 01 12 1117   75 ncdc-gts ncdc-gts  535475 -1141083
        header, radioSondeData = self._extractRadioSondeDataSet(datasetStartLines, data, selectedDataset)
        stationID, year, month, day, hour = self._parseRadioSondeHeader(header)

        if not self.silent:
            if self.monteCarloDataLogger != None:
                self.monteCarloDataLogger.log("Wind data from radio sonde dataset: {}, stationID: {}, {}, {}, {}, {}".format(selectedLocation, stationID, year, month, day, hour))
            else:
                print("Wind data from radio sonde dataset: {}, stationID: {}, {}, {}, {}, {}".format(selectedLocation, stationID, year, month, day, hour))
        
        # Turn data from strings into a numpy array
        altitudes, windVectors = self._parseRadioSondeData(radioSondeData, locationASLAltitude)
        return altitudes, windVectors

    def _readRadioSondeDataFile(self, radioSondeFilePath, filterByMonth=None):
        '''
            Arguments:
                radioSondeFilePath: String File Path to radio sonde data file
                filterByMonth: 3-letter acronym of month from which radio sonde profiles are desired. Pass in "Yearly" or None to select all months

            Returns: datasetStartlines, data
                datasetStartlines is a list containing the numbers on which each data set starts - lines not from filterByMonth are ommitted
                data is a list of strings, each being a line of the original file
        '''
        
        with open(radioSondeFilePath, 'r') as file:
            data = file.readlines()

        # Yearly means no filter
        if filterByMonth == "Yearly":
            filterByMonth = None

        # Get Month number
        if filterByMonth != None:
            monthList = [ "noZerothMonth", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
            monthNumber = monthList.index(filterByMonth)
        
        datasetStartLines = []
        i = 0
        while i < len(data):
            # Radio sonde data header lines start with #
            # Check that profile month matches the desired month
            if data[i][0] == "#" and (filterByMonth == None or int(data[i].split()[2]) == monthNumber):
                datasetStartLines.append(i)
            i += 1
        
        return datasetStartLines, data

    def _extractRadioSondeDataSet(self, datasetStartLines, data, desiredDataSetNumber):
        ''' Returns header string and list of strings representing data for chosen dataset '''
        startLine = datasetStartLines[desiredDataSetNumber]
        header = data[startLine]

        # Collect the data, starting with the line after the header
        i = startLine + 1
        dataset = []

        # Stop at next header or end of file
            # Don't go to next dataset start  line, because the start lines from other months are potentially excluded from that list, but still need to be respected here
        while i < len(data) and data[i][0] != "#":
            dataset.append(data[i])
            i += 1

        return header, dataset

    def _parseRadioSondeHeader(self, headerString):
        '''
            Returns stationID, Year, Month, Day, Hour
            Example header: #CAM00071119 2000 01 01 12 1117   75 ncdc-gts ncdc-gts  535475 -1141083
            Further data format info: https://www1.ncdc.noaa.gov/pub/data/igra/data/igra2-data-format.txt
        '''
        
        data = headerString.split()
        stationID = data[0][1:]
        year = int(data[1])
        month = int(data[2])
        day = int(data[3])
        hour = int(data[4])
        return stationID, year, month, day, hour

    def _parseRadioSondeData(self, data, locationASLAltitude):
        '''
            Returns:
                altitudes: List of AGL altitudes (m)
                windVectors: List of corresponding mean wind vectors

            Both returned lists are sorted from lowest to highest altitude

            Further info about radio sonde input data at: https://www1.ncdc.noaa.gov/pub/data/igra/data/igra2-data-format.txt
            Input data expects that radio sonde data has already been reduced to just the altitude, wind heading and wind speed columsn using MAPLEAF/Examples/Wind/filterRadioSondeData.py
        '''
        result = []
        for line in data:
            result.append([ float(x) for x in line.split() ])
        
        result = np.array(result)
        result[:,2] = result[:,2] / 10 # Convert to m/s from m/s *10

        # Sort rows by altitude
        result = result[result[:,0].argsort()]
        altitudes = result[:,0] - locationASLAltitude # Convert altitudes to AGL

        # Turn heading and speed into a velocity vector
        windVectors = []
        for i in range(len(result)):
            windDirection = _convertWindHeadingToXYPlaneWindDirection(result[i,1])
            windVec = windDirection*result[i,2]
            windVectors.append(np.array((windVec.X, windVec.Y, windVec.Z)))

        return altitudes, windVectors

def _convertWindHeadingToXYPlaneWindDirection(heading, AngleBetweenYAxisandNorth=0):
    '''
        Reminder: wind rose indicates where wind is blowing FROM
        This function outputs a vector indicating where wind is blowing TOWARDS
        Heading angle of 90 degrees is defined to point east
        AnglBetweenYAxis and North defined in the same way, if that's 90 degrees, the Y axis is pointing east
    '''
    totalAngleFromYAxis = heading + AngleBetweenYAxisandNorth
    y = cos(radians(totalAngleFromYAxis))
    x = sin(radians(totalAngleFromYAxis))
    headingVector = Vector(x, y, 0)
    windDirectionVector = -1 * headingVector # Wind is in opposite direction of heading
    return windDirectionVector

Functions

def meanWindModelFactory(envReader=None, silent=False) ‑> MAPLEAF.ENV.MeanWindModelling.MeanWindModel

Instantiates a mean wind model

Expand source code
def meanWindModelFactory(envReader=None, silent=False) -> MeanWindModel:
    ''' Instantiates a mean wind model '''

    if envReader == None:
        # Return default zero-wind model
        constWind = Vector(defaultConfigValues["Environment.ConstantMeanWind.velocity"])
        return ConstantWind(constWind)

    meanWindModel = None
    meanWindModelType = envReader.getString("MeanWindModel")

    if meanWindModelType == "Constant":
        meanGroundWind = envReader.getVector("ConstantMeanWind.velocity")
        if not silent:
            print("Constant ground wind: {:1.2f} m/s".format(meanGroundWind))

        meanWindModel = ConstantWind(meanGroundWind)

    elif meanWindModelType in [ "SampledGroundWindData", "Hellman" ]:
        def getLocationSampledGroundWindVel():
            #### Get list of, and weights of, locations to sample wind rose from
            GroundWindLocationsSampled = envReader.getString("SampledGroundWindData.locationsToSample").split()
            locationsSampled = []
            locationWeights = []
            # Parse into locations (1st, 3rd, 5th... values) and weights (2nd, 4th, 6th... values)
            for i in range(0, len(GroundWindLocationsSampled), 2):
                locationsSampled.append(GroundWindLocationsSampled[i])
                locationWeights.append(float(GroundWindLocationsSampled[i+1]))
            launchMonth = envReader.getString("SampledGroundWindData.launchMonth")

            # Sample wind rose(s)
            sampler = WindRoseDataSampler(silent=silent)
            meanGroundWind = sampler.sampleWindRoses(locationsSampled, locationWeights, launchMonth)

            # Output choices parsed from input file and resulting wind
            if not silent:
                if envReader.simDefinition.monteCarloLogger != None:
                    envReader.simDefinition.monteCarloLogger.log("Sampling ground winds from: {}".format(locationsSampled))
                    envReader.simDefinition.monteCarloLogger.log("Sampling weights for each location: {}".format(locationWeights))
                    envReader.simDefinition.monteCarloLogger.log("Sampling wind distribution from month of: {}".format(launchMonth))
                    envReader.simDefinition.monteCarloLogger.log("Sampled mean ground wind: {:1.2f} m/s".format(meanGroundWind))
                else:
                    print("Sampling ground winds from: {}".format(locationsSampled))
                    print("Sampling weights for each location: {}".format(locationWeights))
                    print("Sampling wind distribution from month of: {}".format(launchMonth))
                    print("Sampled mean ground wind: {:1.2f} m/s".format(meanGroundWind))

            return meanGroundWind

        ### Create and return appropriate wind vs altitude model ###
        if meanWindModelType == "SampledGroundWindData":
            meanGroundWind = getLocationSampledGroundWindVel()
            if not silent:
                print("Wind is not a function of altitude")
            meanWindModel = ConstantWind(meanGroundWind)

        elif meanWindModelType == "Hellman":
            groundWindModel = envReader.getString("Hellman.groundWindModel")

            # Get ground wind
            if groundWindModel == "Constant":
                meanGroundWind = envReader.getVector("ConstantMeanWind.velocity")
            elif groundWindModel == "SampledGroundWindData":
                meanGroundWind = getLocationSampledGroundWindVel()

            HellmanAlphaCoeff = envReader.getFloat("Hellman.alphaCoeff")
            HellmanAltitudeLimit = envReader.getFloat("Hellman.altitudeLimit")

            if not silent:
                print("Constant ground wind: {:1.2f} m/s".format(meanGroundWind))
                print("Wind vs. altitude governed by Hellman law: v2 = v1*(z2/10)^a where a = {}".format(HellmanAlphaCoeff))
                print("Hellman law is taken to apply up to an altitude of {} m AGL".format(HellmanAltitudeLimit))

            meanWindModel = Hellman(meanGroundWind, HellmanAltitudeLimit, HellmanAlphaCoeff)
    
    elif meanWindModelType == "CustomWindProfile":
        meanWindProfileFilePath = envReader.getString("CustomWindProfile.filePath")
        meanWindModel = InterpolatedWind(windFilePath=meanWindProfileFilePath)

    elif meanWindModelType == "SampledRadioSondeData":
        # Get locations and location weights
        locationsAndWeights = envReader.getString("SampledRadioSondeData.locationsToSample").split()
        locations = []
        weights = []
        for i in range(0, len(locationsAndWeights), 2):
            locations.append(locationsAndWeights[i])
            weights.append(float(locationsAndWeights[i+1]))
            
        locationASLAltitudes = [ float(x) for x in envReader.getString("SampledRadioSondeData.locationASLAltitudes").split() ]

        # Get launch month
        launchMonth = envReader.getString("SampledRadioSondeData.launchMonth")

        # Get random seed (if provided)
        radioSondeRandomSeed = envReader.tryGetString("SampledRadioSondeData.randomSeed")

        # Select and parse radio sonde profile
        sampler = RadioSondeDataSampler(silent=silent)
        altitudes, windVectors = sampler.getRadioSondeWindProfile(locations, weights, locationASLAltitudes, launchMonth, radioSondeRandomSeed)
        
        meanWindModel = InterpolatedWind(windAltitudes=altitudes, winds=windVectors)

    else:
        raise ValueError("Unknown MeanWindModel: {}. Please see SimDefinitionTemplate.txt for available options.".format(meanWindModelType))

    return meanWindModel

Classes

class ConstantWind (wind)

Defines a constant wind speed at all altitudes

Expand source code
class ConstantWind(MeanWindModel):
    ''' Defines a constant wind speed at all altitudes  '''
    
    def __init__(self, wind):
        self.wind = wind

    def getMeanWind(self, AGLAltitude):
        return self.wind

Ancestors

  • MAPLEAF.ENV.MeanWindModelling.MeanWindModel
  • abc.ABC

Methods

def getMeanWind(self, AGLAltitude)
Expand source code
def getMeanWind(self, AGLAltitude):
    return self.wind
class Hellman (groundMeanWind=<MAPLEAF.Motion.CythonVector.Vector object>, altitudeLimit=1500, hellmanAlphaCoeff=0.14)

Uses the Hellman law to scale a ground velocity using a power law as the atmospheric boundary layer is exited. Low-altitude only. https://en.wikipedia.org/wiki/Wind_gradient -> Engineering Section.

Arguments

groundMeanWind: Wind Vector at ground level (~10m above surface), in m/s altitudeLimit: Altitude above which the power law transitions into a constant profile (m, AGL) hellmanAlphaCoeff: Alpha in the Hellman model (1/7 is a common first guess, but can vary widely depending on terrain)

Expand source code
class Hellman(MeanWindModel):
    '''
        Uses the Hellman law to scale a ground velocity using a power law as the atmospheric boundary layer is exited.
        Low-altitude only. https://en.wikipedia.org/wiki/Wind_gradient -> Engineering Section.

    '''
    def __init__(self, groundMeanWind=Vector(0,0,0), altitudeLimit=1500, hellmanAlphaCoeff=0.14):
        '''
            Arguments:
                groundMeanWind: Wind Vector at ground level (~10m above surface), in m/s
                altitudeLimit: Altitude above which the power law transitions into a constant profile (m, AGL)
                hellmanAlphaCoeff: Alpha in the Hellman model (1/7 is a common first guess, but can vary widely depending on terrain)
        '''
        self.groundMeanWind = groundMeanWind
        self.altitudeLimit = altitudeLimit
        self.hellmanAlphaCoeff = hellmanAlphaCoeff

    def getMeanWind(self, AGLAltitude):
        # Limit velocity scaling to up to the specified altitude limit
        HellmanAltitude = max(min(self.altitudeLimit, AGLAltitude), 10)
        # Assume initial winds come from a height of 10m
        return self.groundMeanWind * (HellmanAltitude/10)**self.hellmanAlphaCoeff

Ancestors

  • MAPLEAF.ENV.MeanWindModelling.MeanWindModel
  • abc.ABC

Methods

def getMeanWind(self, AGLAltitude)
Expand source code
def getMeanWind(self, AGLAltitude):
    # Limit velocity scaling to up to the specified altitude limit
    HellmanAltitude = max(min(self.altitudeLimit, AGLAltitude), 10)
    # Assume initial winds come from a height of 10m
    return self.groundMeanWind * (HellmanAltitude/10)**self.hellmanAlphaCoeff
class InterpolatedWind (windAltitudes=[], winds=[], windFilePath=None)

Defines the interface for Mean Wind Models

Arguments

windAltitudes: list of altitudes at which wind vectors will be provided (m AGL) winds: list of wind vectors, matching the ordering of windAltitudes (m/s)

windFilePath: string path to a text file containing a table, formatted similarly to the following: AGlAltitude(m) WindX(m/s) WindY(m/s) WindZ(m/s) a1 wx1 wy1 wz1 a2 wx2 wy2 wz2

Notes

Provide either windAltitudes AND winds, OR windFilePath. If all are provided, windAltitudes/winds will be used

Expand source code
class InterpolatedWind(MeanWindModel):
    def __init__(self, windAltitudes=[], winds=[], windFilePath=None):
        '''
            Arguments:
                windAltitudes: list of altitudes at which wind vectors will be provided (m AGL)
                winds: list of wind vectors, matching the ordering of windAltitudes (m/s)

                windFilePath: string path to a text file containing a table, formatted similarly to the following:
                    AGlAltitude(m) WindX(m/s) WindY(m/s) WindZ(m/s)
                    a1              wx1         wy1     wz1
                    a2              wx2         wy2     wz2

            Notes:
                Provide either windAltitudes AND winds, OR windFilePath. If all are provided, windAltitudes/winds will be used
        '''
        if len(windAltitudes) + len(winds) > 0:
            self.windAltitudes = windAltitudes
            self.winds = winds

        elif windFilePath != None:
            self._readCustomWindProfile(windFilePath)

        else:
            raise ValueError("Incomplete initialization info provided. Provide either windAltitudes AND winds, OR windFilePath")

    def _readCustomWindProfile(self, filePath):
        windProfile = np.loadtxt(filePath, skiprows=1)
        self.windAltitudes = windProfile[:, 0]
        self.winds = windProfile[:, 1:]

    def getMeanWind(self, AGLAltitude):
        return Vector(*linInterp(self.windAltitudes, self.winds, AGLAltitude))

Ancestors

  • MAPLEAF.ENV.MeanWindModelling.MeanWindModel
  • abc.ABC

Methods

def getMeanWind(self, AGLAltitude)
Expand source code
def getMeanWind(self, AGLAltitude):
    return Vector(*linInterp(self.windAltitudes, self.winds, AGLAltitude))