Module MAPLEAF.ENV.AtmosphereModelling

These classes model the change of air properties (Pressure, Density, etc… with altitude)

Expand source code
''' These classes model the change of air properties (Pressure, Density, etc... with altitude) '''

import abc
from bisect import bisect
from math import exp
from typing import Sequence

import numpy as np

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

__all__ = [ "atmosphericModelFactory", "ConstantAtmosphere", "TabulatedAtmosphere", "USStandardAtmosphere" ]

class AtmosphericModel(abc.ABC):
    ''' Interface for all atmosphere models '''
    
    @abc.abstractmethod
    def getAirProperties(self, ASLElevation: float, time: float) -> Sequence[float]:
        ''' 
            Return an iterable containing: 
                temp(K), 
                static pressure (Pa), 
                density (kg/m^3), 
                dynamic viscosity (Pa*s),
            in that order
        '''
        return

def atmosphericModelFactory(envDictReader=None) -> AtmosphericModel:
    ''' 
        Provide either an atmosphericModel name ('USStandardAtmosphere' is only option right now that doesn't require additional info,
            or provide an envDictReader (`MAPLEAF.IO.SubDictReader`)
    '''
    if envDictReader == None:
        modelType = defaultConfigValues["Environment.AtmosphericPropertiesModel"]
    else:
        modelType = envDictReader.getString("AtmosphericPropertiesModel")

    if modelType == "Constant":
        if envDictReader == None:
            raise ValueError("envDictReader required to initialize Constant atm properties model")

        # Get values from ConstantAtmosphere subDictionary
        envDictReader.simDefDictPathToReadFrom = "Environment.ConstantAtmosphere"

        constTemp = envDictReader.getFloat("temp") + 273.15 # Convert to Kelvin (Expecting Celsius input)
        constPressure = envDictReader.getFloat("pressure")
        constDensity = envDictReader.getFloat("density")
        constViscosity = envDictReader.getFloat("viscosity")

        # Return to reading from Environment for any subsequent parsing
        envDictReader.simDefDictPathToReadFrom = "Environment"

        return ConstantAtmosphere(constTemp, constPressure, constDensity, constViscosity)
    
    elif modelType == "TabulatedAtmosphere":
        try:
            tableFilePath = envDictReader.getString("TabulatedAtmosphere.filePath")
        except AttributeError:
            tableFilePath = defaultConfigValues["Environment.TabulatedAtmosphere.filePath"]

        tableFilePath = getAbsoluteFilePath(tableFilePath)
        return TabulatedAtmosphere(tableFilePath)

    elif modelType == "USStandardAtmosphere":
        return USStandardAtmosphere()

    else:
        raise ValueError("Atmospheric model: {} not implemented, try using 'USStandardAtmosphere'".format(modelType))
        
class ConstantAtmosphere(AtmosphericModel):
    def __init__(self, temp, pressure, density, viscosity):
        self.airProperties = [ temp, pressure, density, viscosity ]

    def getAirProperties(self, _, _2=None):
        return self.airProperties

class TabulatedAtmosphere(AtmosphericModel):
    '''
        Provides linearly-interpolated atmospheric properties from a table in a file
        Table columns are expected are:
            h(m ASL)    T(K)    P(Pa)   rho(kg/m^3)         mu(10^-5 Pa*s)
    '''
    def __init__(self, filePath):
        inputTable = np.loadtxt(filePath, skiprows=1)

        # Convert viscosity to Pa-s
        inputTable[:, 4] *= 1E-5 
        
        # Set up keys, values for interpolation of all properties at once
        self.keys = inputTable[:, 0] # Altitude (m ASL) is interpolation key
        self.values = inputTable[:, 1:] # Contains: T(K), P(Pa), rho(kg/m^3), dynamicViscosity(Pa*s), all to be interpolated over

    def getAirProperties(self, ASLElevation, _=None):
        return linInterp(self.keys, self.values, ASLElevation)

class USStandardAtmosphere(AtmosphericModel):
    '''
        Provides atmospheric properties calculated according to the model here: 
        https://nebula.wsimg.com/ab321c1edd4fa69eaa94b5e8e769b113?AccessKeyId=AF1D67CEBF3A194F66A3&disposition=0&alloworigin=1

    '''
    T0 = 288.15 # K (15 Celsius) @ ASL == 0
    p0 = 101325 # Pa @ ASL == 0
    M = 28.9644 # Molecular weight of air
    R = 8.31432 # J/molK
    earthRadius = 6356766 # m - used to convert to geopotential altitude
    G = 9.80665 # m/s^2 (sea level, 45 degree latitude)

    baseHeights = [ -2000, 11000, 20000, 32000, 47000, 51000, 71000, 84852 ] # m (Geopotential)
    dt_dh = [ -6.5e-3, 0, 1.0e-3, 2.8e-3, 0, -2.8e-3, -2.0e-3, 0 ] # K/m
    baseTemps = [] # Gets filled out in self.__init__
    basePressures = [] # Gets filled out in self.__init__

    def __init__(self):
        # Pre-Calculate base temperatures for each interval
        dt_dh = self.dt_dh[0]
        baseTemp1 = self.T0 + dt_dh*self.baseHeights[0]
        self.baseTemps.append(baseTemp1)

        Pb_over_P0 = ( (self.T0 + dt_dh*self.baseHeights[0]) / \
            self.T0) ** (-self.G*self.M/(self.R * dt_dh * 1000))
        self.basePressures.append(Pb_over_P0 * self.p0)

        # Loop over each layer, calculate temp & pressure at base
        for i in range(1, len(self.baseHeights)):
            # Calculate temperatures at base of each layer
            Tb, Pb, dt_dh = self.baseTemps[-1], self.basePressures[i-1], self.dt_dh[i-1]
            dh = float(self.baseHeights[i] - self.baseHeights[i-1])
            
            nextTemp = Tb + dt_dh*dh

            # Calculate pressures at the base of each layer
            if dt_dh == 0.0:
                P_over_Pb = exp(-self.G*self.M*dh / (self.R * Tb * 1000))
                nextPressure = P_over_Pb * Pb

            else:
                body = (Tb + dt_dh*dh) / Tb
                exponent = -self.G*self.M / (self.R*dt_dh*1000)
                nextPressure = body**exponent * Pb

            self.baseTemps.append(nextTemp)
            self.basePressures.append(nextPressure)

    def getAirProperties(self, ASLElevation, time):

        # Calculate geopotential altitude (H)
        H = (self.earthRadius * ASLElevation) / (self.earthRadius + ASLElevation)
        
        # Set density to zero above 86 km
        if H >= 86000:
            H = 86000

        # Figure out which interval we're in 
        baseIndex = bisect(self.baseHeights, H) - 1
        altitudeAboveBase = H - self.baseHeights[baseIndex] # Geopotential altitude above base
        dt_dh = self.dt_dh[baseIndex]
        
        # Calc temp
        Tb = self.baseTemps[baseIndex]
        temp = Tb + dt_dh * altitudeAboveBase

        # Calc pressure
        Pb = self.basePressures[baseIndex]
        if dt_dh == 0:
            P_over_Pb = exp(-self.G*self.M * altitudeAboveBase / (self.R * Tb * 1000))
            pressure = P_over_Pb * Pb
        else:
            body = (Tb + dt_dh*altitudeAboveBase) / Tb
            exponent = -self.G*self.M / (self.R*dt_dh*1000)
            pressure = body**exponent * Pb

        # Calculate density and viscosity from temperature and pressure   
        rho = self.M * pressure / (self.R * temp * 1000) # Ideal gas law
        viscosity = 1.457e-6 * temp**(1.5) / (temp + 110.4) # Sutherland's law

        if H >= 86000:
            rho = 0.0

        return [ temp, pressure, rho, viscosity ]

Functions

def atmosphericModelFactory(envDictReader=None) ‑> MAPLEAF.ENV.AtmosphereModelling.AtmosphericModel

Provide either an atmosphericModel name ('USStandardAtmosphere' is only option right now that doesn't require additional info, or provide an envDictReader (SubDictReader)

Expand source code
def atmosphericModelFactory(envDictReader=None) -> AtmosphericModel:
    ''' 
        Provide either an atmosphericModel name ('USStandardAtmosphere' is only option right now that doesn't require additional info,
            or provide an envDictReader (`MAPLEAF.IO.SubDictReader`)
    '''
    if envDictReader == None:
        modelType = defaultConfigValues["Environment.AtmosphericPropertiesModel"]
    else:
        modelType = envDictReader.getString("AtmosphericPropertiesModel")

    if modelType == "Constant":
        if envDictReader == None:
            raise ValueError("envDictReader required to initialize Constant atm properties model")

        # Get values from ConstantAtmosphere subDictionary
        envDictReader.simDefDictPathToReadFrom = "Environment.ConstantAtmosphere"

        constTemp = envDictReader.getFloat("temp") + 273.15 # Convert to Kelvin (Expecting Celsius input)
        constPressure = envDictReader.getFloat("pressure")
        constDensity = envDictReader.getFloat("density")
        constViscosity = envDictReader.getFloat("viscosity")

        # Return to reading from Environment for any subsequent parsing
        envDictReader.simDefDictPathToReadFrom = "Environment"

        return ConstantAtmosphere(constTemp, constPressure, constDensity, constViscosity)
    
    elif modelType == "TabulatedAtmosphere":
        try:
            tableFilePath = envDictReader.getString("TabulatedAtmosphere.filePath")
        except AttributeError:
            tableFilePath = defaultConfigValues["Environment.TabulatedAtmosphere.filePath"]

        tableFilePath = getAbsoluteFilePath(tableFilePath)
        return TabulatedAtmosphere(tableFilePath)

    elif modelType == "USStandardAtmosphere":
        return USStandardAtmosphere()

    else:
        raise ValueError("Atmospheric model: {} not implemented, try using 'USStandardAtmosphere'".format(modelType))

Classes

class ConstantAtmosphere (temp, pressure, density, viscosity)

Interface for all atmosphere models

Expand source code
class ConstantAtmosphere(AtmosphericModel):
    def __init__(self, temp, pressure, density, viscosity):
        self.airProperties = [ temp, pressure, density, viscosity ]

    def getAirProperties(self, _, _2=None):
        return self.airProperties

Ancestors

  • MAPLEAF.ENV.AtmosphereModelling.AtmosphericModel
  • abc.ABC

Methods

def getAirProperties(self, _)

Return an iterable containing: temp(K), static pressure (Pa), density (kg/m^3), dynamic viscosity (Pa*s), in that order

Expand source code
def getAirProperties(self, _, _2=None):
    return self.airProperties
class TabulatedAtmosphere (filePath)

Provides linearly-interpolated atmospheric properties from a table in a file Table columns are expected are: h(m ASL) T(K) P(Pa) rho(kg/m^3) mu(10^-5 Pa*s)

Expand source code
class TabulatedAtmosphere(AtmosphericModel):
    '''
        Provides linearly-interpolated atmospheric properties from a table in a file
        Table columns are expected are:
            h(m ASL)    T(K)    P(Pa)   rho(kg/m^3)         mu(10^-5 Pa*s)
    '''
    def __init__(self, filePath):
        inputTable = np.loadtxt(filePath, skiprows=1)

        # Convert viscosity to Pa-s
        inputTable[:, 4] *= 1E-5 
        
        # Set up keys, values for interpolation of all properties at once
        self.keys = inputTable[:, 0] # Altitude (m ASL) is interpolation key
        self.values = inputTable[:, 1:] # Contains: T(K), P(Pa), rho(kg/m^3), dynamicViscosity(Pa*s), all to be interpolated over

    def getAirProperties(self, ASLElevation, _=None):
        return linInterp(self.keys, self.values, ASLElevation)

Ancestors

  • MAPLEAF.ENV.AtmosphereModelling.AtmosphericModel
  • abc.ABC

Methods

def getAirProperties(self, ASLElevation)

Return an iterable containing: temp(K), static pressure (Pa), density (kg/m^3), dynamic viscosity (Pa*s), in that order

Expand source code
def getAirProperties(self, ASLElevation, _=None):
    return linInterp(self.keys, self.values, ASLElevation)
class USStandardAtmosphere
Expand source code
class USStandardAtmosphere(AtmosphericModel):
    '''
        Provides atmospheric properties calculated according to the model here: 
        https://nebula.wsimg.com/ab321c1edd4fa69eaa94b5e8e769b113?AccessKeyId=AF1D67CEBF3A194F66A3&disposition=0&alloworigin=1

    '''
    T0 = 288.15 # K (15 Celsius) @ ASL == 0
    p0 = 101325 # Pa @ ASL == 0
    M = 28.9644 # Molecular weight of air
    R = 8.31432 # J/molK
    earthRadius = 6356766 # m - used to convert to geopotential altitude
    G = 9.80665 # m/s^2 (sea level, 45 degree latitude)

    baseHeights = [ -2000, 11000, 20000, 32000, 47000, 51000, 71000, 84852 ] # m (Geopotential)
    dt_dh = [ -6.5e-3, 0, 1.0e-3, 2.8e-3, 0, -2.8e-3, -2.0e-3, 0 ] # K/m
    baseTemps = [] # Gets filled out in self.__init__
    basePressures = [] # Gets filled out in self.__init__

    def __init__(self):
        # Pre-Calculate base temperatures for each interval
        dt_dh = self.dt_dh[0]
        baseTemp1 = self.T0 + dt_dh*self.baseHeights[0]
        self.baseTemps.append(baseTemp1)

        Pb_over_P0 = ( (self.T0 + dt_dh*self.baseHeights[0]) / \
            self.T0) ** (-self.G*self.M/(self.R * dt_dh * 1000))
        self.basePressures.append(Pb_over_P0 * self.p0)

        # Loop over each layer, calculate temp & pressure at base
        for i in range(1, len(self.baseHeights)):
            # Calculate temperatures at base of each layer
            Tb, Pb, dt_dh = self.baseTemps[-1], self.basePressures[i-1], self.dt_dh[i-1]
            dh = float(self.baseHeights[i] - self.baseHeights[i-1])
            
            nextTemp = Tb + dt_dh*dh

            # Calculate pressures at the base of each layer
            if dt_dh == 0.0:
                P_over_Pb = exp(-self.G*self.M*dh / (self.R * Tb * 1000))
                nextPressure = P_over_Pb * Pb

            else:
                body = (Tb + dt_dh*dh) / Tb
                exponent = -self.G*self.M / (self.R*dt_dh*1000)
                nextPressure = body**exponent * Pb

            self.baseTemps.append(nextTemp)
            self.basePressures.append(nextPressure)

    def getAirProperties(self, ASLElevation, time):

        # Calculate geopotential altitude (H)
        H = (self.earthRadius * ASLElevation) / (self.earthRadius + ASLElevation)
        
        # Set density to zero above 86 km
        if H >= 86000:
            H = 86000

        # Figure out which interval we're in 
        baseIndex = bisect(self.baseHeights, H) - 1
        altitudeAboveBase = H - self.baseHeights[baseIndex] # Geopotential altitude above base
        dt_dh = self.dt_dh[baseIndex]
        
        # Calc temp
        Tb = self.baseTemps[baseIndex]
        temp = Tb + dt_dh * altitudeAboveBase

        # Calc pressure
        Pb = self.basePressures[baseIndex]
        if dt_dh == 0:
            P_over_Pb = exp(-self.G*self.M * altitudeAboveBase / (self.R * Tb * 1000))
            pressure = P_over_Pb * Pb
        else:
            body = (Tb + dt_dh*altitudeAboveBase) / Tb
            exponent = -self.G*self.M / (self.R*dt_dh*1000)
            pressure = body**exponent * Pb

        # Calculate density and viscosity from temperature and pressure   
        rho = self.M * pressure / (self.R * temp * 1000) # Ideal gas law
        viscosity = 1.457e-6 * temp**(1.5) / (temp + 110.4) # Sutherland's law

        if H >= 86000:
            rho = 0.0

        return [ temp, pressure, rho, viscosity ]

Ancestors

  • MAPLEAF.ENV.AtmosphereModelling.AtmosphericModel
  • abc.ABC

Class variables

var G
var M
var R
var T0
var baseHeights
var basePressures
var baseTemps
var dt_dh
var earthRadius
var p0

Methods

def getAirProperties(self, ASLElevation, time)

Return an iterable containing: temp(K), static pressure (Pa), density (kg/m^3), dynamic viscosity (Pa*s), in that order

Expand source code
def getAirProperties(self, ASLElevation, time):

    # Calculate geopotential altitude (H)
    H = (self.earthRadius * ASLElevation) / (self.earthRadius + ASLElevation)
    
    # Set density to zero above 86 km
    if H >= 86000:
        H = 86000

    # Figure out which interval we're in 
    baseIndex = bisect(self.baseHeights, H) - 1
    altitudeAboveBase = H - self.baseHeights[baseIndex] # Geopotential altitude above base
    dt_dh = self.dt_dh[baseIndex]
    
    # Calc temp
    Tb = self.baseTemps[baseIndex]
    temp = Tb + dt_dh * altitudeAboveBase

    # Calc pressure
    Pb = self.basePressures[baseIndex]
    if dt_dh == 0:
        P_over_Pb = exp(-self.G*self.M * altitudeAboveBase / (self.R * Tb * 1000))
        pressure = P_over_Pb * Pb
    else:
        body = (Tb + dt_dh*altitudeAboveBase) / Tb
        exponent = -self.G*self.M / (self.R*dt_dh*1000)
        pressure = body**exponent * Pb

    # Calculate density and viscosity from temperature and pressure   
    rho = self.M * pressure / (self.R * temp * 1000) # Ideal gas law
    viscosity = 1.457e-6 * temp**(1.5) / (temp + 110.4) # Sutherland's law

    if H >= 86000:
        rho = 0.0

    return [ temp, pressure, rho, viscosity ]