Module MAPLEAF.ENV.TurbulenceModelling
Modeling of turbulent, fluctuating component of wind velocity
Expand source code
''' Modeling of turbulent, fluctuating component of wind velocity '''
import abc
import random
from math import cos, pi
from MAPLEAF.Motion import Vector
__all__ = [ "turbulenceModelFactory", "PinkNoiseGenerator" ]
#TODO: Implement vonKarman or Dryden model with std deviation / length scale that varies with altitude as per NASA HDBK-1001
class TurbulenceModel(abc.ABC):
''' Interface for all turbulence models '''
@abc.abstractmethod
def getTurbVelocity(self, altitude, meanWindVelocity, time):
pass
def turbulenceModelFactory(envReader, silent=False):
''' Reads data from simDefinition, initializes and returns the appropriate TurbulenceModel '''
turbModelType = envReader.getString("Environment.TurbulenceModel")
if not silent:
print("Turbulence/Gust Model: {}".format(turbModelType))
if turbModelType== "None":
turbulenceModel = NoTurb()
elif "PinkNoise" in turbModelType:
measuredPinkNoiseStdDev = 2.26 # For a 2-pole PinkNoiseGenerator - re-measure if the number of poles is changed
turbulenceIntensity = envReader.tryGetInt("PinkNoiseModel.turbulenceIntensity")
if turbulenceIntensity != None:
turbulenceIntensity /= (measuredPinkNoiseStdDev * 100)
velocityStDev = envReader.tryGetInt("PinkNoiseModel.velocityStdDeviation")
if velocityStDev != None:
velocityStDev /= measuredPinkNoiseStdDev
pinkNoiseRandomSeed1 = envReader.tryGetInt("PinkNoiseModel.randomSeed1")
pinkNoiseRandomSeed2 = envReader.tryGetInt("PinkNoiseModel.randomSeed2")
pinkNoiseRandomSeed3 = envReader.tryGetInt("PinkNoiseModel.randomSeed3")
if turbModelType == "PinkNoise1D":
turbulenceModel = PinkNoise1D(turbulenceIntensity, velocityStDev, pinkNoiseRandomSeed1)
if not silent:
print("Random seed 1: {}".format(turbulenceModel.png1.seed))
elif turbModelType == "PinkNoise2D":
turbulenceModel = PinkNoise2D(turbulenceIntensity, velocityStDev, pinkNoiseRandomSeed1, pinkNoiseRandomSeed2)
if not silent:
print("Random seed 1: {}".format(turbulenceModel.png1.seed))
print("Random seed 2: {}".format(turbulenceModel.png2.seed))
elif turbModelType == "PinkNoise3D":
turbulenceModel = PinkNoise3D(turbulenceIntensity, velocityStDev, pinkNoiseRandomSeed1, pinkNoiseRandomSeed2, pinkNoiseRandomSeed3)
if not silent:
print("Random seed 1: {}".format(turbulenceModel.png1.seed))
print("Random seed 2: {}".format(turbulenceModel.png2.seed))
print("Random seed 2: {}".format(turbulenceModel.png3.seed))
elif turbModelType == "customSineGust":
GustStartAltitude = envReader.getFloat("CustomSineGust.startAltitude")
GustMagnitude = envReader.getFloat("CustomSineGust.magnitude")
GustSineBlendDistance = envReader.getFloat("CustomSineGust.sineBlendDistance")
GustLayerThickness = envReader.getFloat("CustomSineGust.thickness")
GustDirection = envReader.getVector("CustomSineGust.direction").normalize()
turbulenceModel = CustomSineGust(GustStartAltitude, GustSineBlendDistance, GustLayerThickness, GustMagnitude, GustDirection)
if not silent:
print("Gust Altitude: {}".format(GustStartAltitude))
print("Gust Vector: {}".format(GustMagnitude * GustDirection))
else:
raise ValueError("Turbulence Model {} not found. Please choose from: None, PinkNoise1/2/3D or customSineGust")
return turbulenceModel
class NoTurb(TurbulenceModel):
def getTurbVelocity(self, _, __, ___):
return Vector(0,0,0)
class _PinkNoiseTurbModel(TurbulenceModel):
def __init__(self, turbulenceIntensity, velocityStdDev, randomSeed):
self.velStdDev = velocityStdDev
self.turbulenceIntensity = turbulenceIntensity
self.png1 = PinkNoiseGenerator(seed=randomSeed)
# Dummy function, will be replaced by subclasses
def getTurbVelocity(self, altitude, meanWindVelocity, time):
return Vector(0,0,0)
def _getDesiredTurbulenceStdDev(self, altitude, meanWindVelocity):
if self.turbulenceIntensity != None:
return meanWindVelocity.length() * self.turbulenceIntensity
else:
return self.velStdDev
class PinkNoise1D(_PinkNoiseTurbModel):
def __init__(self, turbulenceIntensity, velocityStdDev, randomSeed):
super().__init__(turbulenceIntensity, velocityStdDev, randomSeed)
def getTurbVelocity(self, altitude, meanWindVelocity, time):
self.velStdDev = super()._getDesiredTurbulenceStdDev(altitude, meanWindVelocity)
newTurbMagnitude = self.png1.getValue(time) * self.velStdDev
return meanWindVelocity.normalize() * newTurbMagnitude
class PinkNoise2D(_PinkNoiseTurbModel):
def __init__(self, turbulenceIntensity, velocityStdDev, randomSeed, randomSeed2):
super().__init__(turbulenceIntensity, velocityStdDev, randomSeed)
self.png2 = PinkNoiseGenerator(seed=randomSeed2)
def getTurbVelocity(self, altitude, meanWindVelocity, time):
velStdDev = self._getDesiredTurbulenceStdDev(altitude, meanWindVelocity)
xTurbVel = self.png1.getValue(time) * velStdDev
yTurbVel = self.png2.getValue(time) * velStdDev
return Vector(xTurbVel, yTurbVel, 0)
class PinkNoise3D(_PinkNoiseTurbModel):
def __init__(self, turbulenceIntensity, velocityStdDev, randomSeed, randomSeed2, randomSeed3):
super().__init__(turbulenceIntensity, velocityStdDev, randomSeed)
self.png2 = PinkNoiseGenerator(seed=randomSeed2)
self.png3 = PinkNoiseGenerator(seed=randomSeed3)
def getTurbVelocity(self, altitude, meanWindVelocity, time):
velStdDev = self._getDesiredTurbulenceStdDev(altitude, meanWindVelocity)
newXTurbVel = self.png1.getValue(time) * velStdDev
newYTurbVel = self.png2.getValue(time) * velStdDev
newZTurbVel = self.png2.getValue(time) * velStdDev
return Vector(newXTurbVel, newYTurbVel, newZTurbVel)
class CustomSineGust(TurbulenceModel):
def __init__(self, GustStartAltitude, GustSineBlendDistance, GustLayerThickness, GustMagnitude, GustDirection):
self.GustStartAltitude = GustStartAltitude
self.GustSineBlendDistance = GustSineBlendDistance
self.GustMagnitude = GustMagnitude
self.GustDirection = GustDirection
self.GustZoneBoundaries = [
GustStartAltitude,
GustStartAltitude + GustSineBlendDistance, # End of lower blend zone
GustStartAltitude + GustSineBlendDistance + GustLayerThickness, # Start of upper blending zone
GustStartAltitude + GustSineBlendDistance + GustLayerThickness + GustSineBlendDistance # End of upper blending zone
]
def getTurbVelocity(self, altitude, meanWindVelocity, time):
''' # From NASA HDBK-1001, pg. 2-84, equation 2.69 '''
# Determine whether the current altitude is in the blending region or plateau region
if altitude > self.GustStartAltitude:
if altitude < self.GustZoneBoundaries[1]:
# In the lower blending region
gustVel = self.GustMagnitude/2 * ( 1 - cos(pi*(altitude - self.GustStartAltitude)/self.GustSineBlendDistance))
elif altitude < self.GustZoneBoundaries[2]:
# In the plateau (gust) region
gustVel = self.GustMagnitude
elif altitude < self.GustZoneBoundaries[3]:
# In the upper blending region
gustThickness = self.GustZoneBoundaries[-1] - self.GustZoneBoundaries[0]
gustVel = self.GustMagnitude/2 * ( 1 - cos(pi*(altitude - self.GustStartAltitude - gustThickness)/self.GustSineBlendDistance))
else:
return Vector(0,0,0)
return self.GustDirection * gustVel
else:
return Vector(0,0,0)
class PinkNoiseGenerator():
'''
Inputs:
* alpha - pink noise generated has a power spectrum which has a log-log slope of -alpha.
default value of 5/3 matches power spectrum slope of turbulent integral scales
* seed - pink noise generated based on applying a filter to gaussian white noise specifying
the seed for the input random noise allows for repeatable sequences of values
* nPoles - more poles include more low frequency content in the spectrum
parameter could be interpreted as similar to the turbulence length scale
can be varied in more sophisticated models
* simulatedSamplingFreqHz - controls "sampling frequency" that the pink noise is interpreted as
also modulated the effective length scale of turbulence, together with nPoles
Methods:
* .getValue(time)
If the value is requested at time values that fall between sampling intervals, linearly
interpolated values (from adjacent samples) are returned.
If no time is provided, the sampling frequency is ignored and a new value is computed
'''
def __init__(self, alpha=5/3, seed=None, nPoles=2, simulatedSamplingFreqHz=20):
self.alpha = alpha
if seed != None:
self.seed = seed
else:
self.seed = random.randrange(1000000)
self.rng = random.Random(self.seed)
# self.rng.seed(self.seed)
self.nPoles = nPoles
# Calculate coefficient values - openrocket documentation pg. 59/60 (Section 4.1.2 Wind Modeling)
# Based on method from Kasdin (1995) "Discrete Simulation of Colored Noise and Stochastic Processes and 1/f^{alpha} Power Law Noise Generation"
self.coeffs = [ 1, ]
for i in range(nPoles):
self.coeffs.append((i - alpha/2) * self.coeffs[i]/(i+1))
self.coeffs.pop(0)
# Overwriting will wrap around the array to avoid constantly shifting values
self.nextIndexToOverwrite = 0
# Fixed sampling frequency allows for achieving "mesh convergence" as the timestep is decreased
self.lastValTime = 0
self.timeStep = 1 / simulatedSamplingFreqHz # seconds
# Fill lastValues
self.lastValues = [ self.rng.gauss(0, 1) for i in range(nPoles) ]
for i in range(10*nPoles):
self.getValue()
def getValue(self, time=None):
'''
Calculate coefficient values - openrocket documentation pg. 59/60 (Section 4.1.2 Wind Modeling)
Based on method from Kasdin (1995) "Discrete Simulation of Colored Noise and Stochastic Processes and 1/f^{alpha} Power Law Noise Generation"
'''
# Compute any required new values
while time == None or time > self.lastValTime:
whiteNoise = self.rng.gauss(0, 1)
for i in range(self.nPoles):
whiteNoise -= self.coeffs[i]*self.lastValues[(self.nextIndexToOverwrite - i - 1) % self.nPoles]
pinkNoiseSample = whiteNoise
self.lastValues[self.nextIndexToOverwrite] = pinkNoiseSample
self.nextIndexToOverwrite = (self.nextIndexToOverwrite + 1) % self.nPoles
if time == None:
return pinkNoiseSample
self.lastValTime += self.timeStep
if self.lastValTime == time:
return pinkNoiseSample
# Linearly interpolate value at desired time
lastVal = self.lastValues[self.nextIndexToOverwrite - 1]
secondLastVal = self.lastValues[self.nextIndexToOverwrite - 2]
secondLastValTime = self.lastValTime - self.timeStep
dValdt = (lastVal - secondLastVal) / self.timeStep
return dValdt*(time-secondLastValTime) + secondLastVal
Functions
def turbulenceModelFactory(envReader, silent=False)
-
Reads data from simDefinition, initializes and returns the appropriate TurbulenceModel
Expand source code
def turbulenceModelFactory(envReader, silent=False): ''' Reads data from simDefinition, initializes and returns the appropriate TurbulenceModel ''' turbModelType = envReader.getString("Environment.TurbulenceModel") if not silent: print("Turbulence/Gust Model: {}".format(turbModelType)) if turbModelType== "None": turbulenceModel = NoTurb() elif "PinkNoise" in turbModelType: measuredPinkNoiseStdDev = 2.26 # For a 2-pole PinkNoiseGenerator - re-measure if the number of poles is changed turbulenceIntensity = envReader.tryGetInt("PinkNoiseModel.turbulenceIntensity") if turbulenceIntensity != None: turbulenceIntensity /= (measuredPinkNoiseStdDev * 100) velocityStDev = envReader.tryGetInt("PinkNoiseModel.velocityStdDeviation") if velocityStDev != None: velocityStDev /= measuredPinkNoiseStdDev pinkNoiseRandomSeed1 = envReader.tryGetInt("PinkNoiseModel.randomSeed1") pinkNoiseRandomSeed2 = envReader.tryGetInt("PinkNoiseModel.randomSeed2") pinkNoiseRandomSeed3 = envReader.tryGetInt("PinkNoiseModel.randomSeed3") if turbModelType == "PinkNoise1D": turbulenceModel = PinkNoise1D(turbulenceIntensity, velocityStDev, pinkNoiseRandomSeed1) if not silent: print("Random seed 1: {}".format(turbulenceModel.png1.seed)) elif turbModelType == "PinkNoise2D": turbulenceModel = PinkNoise2D(turbulenceIntensity, velocityStDev, pinkNoiseRandomSeed1, pinkNoiseRandomSeed2) if not silent: print("Random seed 1: {}".format(turbulenceModel.png1.seed)) print("Random seed 2: {}".format(turbulenceModel.png2.seed)) elif turbModelType == "PinkNoise3D": turbulenceModel = PinkNoise3D(turbulenceIntensity, velocityStDev, pinkNoiseRandomSeed1, pinkNoiseRandomSeed2, pinkNoiseRandomSeed3) if not silent: print("Random seed 1: {}".format(turbulenceModel.png1.seed)) print("Random seed 2: {}".format(turbulenceModel.png2.seed)) print("Random seed 2: {}".format(turbulenceModel.png3.seed)) elif turbModelType == "customSineGust": GustStartAltitude = envReader.getFloat("CustomSineGust.startAltitude") GustMagnitude = envReader.getFloat("CustomSineGust.magnitude") GustSineBlendDistance = envReader.getFloat("CustomSineGust.sineBlendDistance") GustLayerThickness = envReader.getFloat("CustomSineGust.thickness") GustDirection = envReader.getVector("CustomSineGust.direction").normalize() turbulenceModel = CustomSineGust(GustStartAltitude, GustSineBlendDistance, GustLayerThickness, GustMagnitude, GustDirection) if not silent: print("Gust Altitude: {}".format(GustStartAltitude)) print("Gust Vector: {}".format(GustMagnitude * GustDirection)) else: raise ValueError("Turbulence Model {} not found. Please choose from: None, PinkNoise1/2/3D or customSineGust") return turbulenceModel
Classes
class PinkNoiseGenerator (alpha=1.6666666666666667, seed=None, nPoles=2, simulatedSamplingFreqHz=20)
-
Inputs
- alpha - pink noise generated has a power spectrum which has a log-log slope of -alpha.
default value of 5/3 matches power spectrum slope of turbulent integral scales - seed - pink noise generated based on applying a filter to gaussian white noise specifying
the seed for the input random noise allows for repeatable sequences of values - nPoles - more poles include more low frequency content in the spectrum
parameter could be interpreted as similar to the turbulence length scale
can be varied in more sophisticated models - simulatedSamplingFreqHz - controls "sampling frequency" that the pink noise is interpreted as
also modulated the effective length scale of turbulence, together with nPoles
Methods
- .getValue(time)
If the value is requested at time values that fall between sampling intervals, linearly interpolated values (from adjacent samples) are returned. If no time is provided, the sampling frequency is ignored and a new value is computed
Expand source code
class PinkNoiseGenerator(): ''' Inputs: * alpha - pink noise generated has a power spectrum which has a log-log slope of -alpha. default value of 5/3 matches power spectrum slope of turbulent integral scales * seed - pink noise generated based on applying a filter to gaussian white noise specifying the seed for the input random noise allows for repeatable sequences of values * nPoles - more poles include more low frequency content in the spectrum parameter could be interpreted as similar to the turbulence length scale can be varied in more sophisticated models * simulatedSamplingFreqHz - controls "sampling frequency" that the pink noise is interpreted as also modulated the effective length scale of turbulence, together with nPoles Methods: * .getValue(time) If the value is requested at time values that fall between sampling intervals, linearly interpolated values (from adjacent samples) are returned. If no time is provided, the sampling frequency is ignored and a new value is computed ''' def __init__(self, alpha=5/3, seed=None, nPoles=2, simulatedSamplingFreqHz=20): self.alpha = alpha if seed != None: self.seed = seed else: self.seed = random.randrange(1000000) self.rng = random.Random(self.seed) # self.rng.seed(self.seed) self.nPoles = nPoles # Calculate coefficient values - openrocket documentation pg. 59/60 (Section 4.1.2 Wind Modeling) # Based on method from Kasdin (1995) "Discrete Simulation of Colored Noise and Stochastic Processes and 1/f^{alpha} Power Law Noise Generation" self.coeffs = [ 1, ] for i in range(nPoles): self.coeffs.append((i - alpha/2) * self.coeffs[i]/(i+1)) self.coeffs.pop(0) # Overwriting will wrap around the array to avoid constantly shifting values self.nextIndexToOverwrite = 0 # Fixed sampling frequency allows for achieving "mesh convergence" as the timestep is decreased self.lastValTime = 0 self.timeStep = 1 / simulatedSamplingFreqHz # seconds # Fill lastValues self.lastValues = [ self.rng.gauss(0, 1) for i in range(nPoles) ] for i in range(10*nPoles): self.getValue() def getValue(self, time=None): ''' Calculate coefficient values - openrocket documentation pg. 59/60 (Section 4.1.2 Wind Modeling) Based on method from Kasdin (1995) "Discrete Simulation of Colored Noise and Stochastic Processes and 1/f^{alpha} Power Law Noise Generation" ''' # Compute any required new values while time == None or time > self.lastValTime: whiteNoise = self.rng.gauss(0, 1) for i in range(self.nPoles): whiteNoise -= self.coeffs[i]*self.lastValues[(self.nextIndexToOverwrite - i - 1) % self.nPoles] pinkNoiseSample = whiteNoise self.lastValues[self.nextIndexToOverwrite] = pinkNoiseSample self.nextIndexToOverwrite = (self.nextIndexToOverwrite + 1) % self.nPoles if time == None: return pinkNoiseSample self.lastValTime += self.timeStep if self.lastValTime == time: return pinkNoiseSample # Linearly interpolate value at desired time lastVal = self.lastValues[self.nextIndexToOverwrite - 1] secondLastVal = self.lastValues[self.nextIndexToOverwrite - 2] secondLastValTime = self.lastValTime - self.timeStep dValdt = (lastVal - secondLastVal) / self.timeStep return dValdt*(time-secondLastValTime) + secondLastVal
Methods
def getValue(self, time=None)
-
Calculate coefficient values - openrocket documentation pg. 59/60 (Section 4.1.2 Wind Modeling) Based on method from Kasdin (1995) "Discrete Simulation of Colored Noise and Stochastic Processes and 1/f^{alpha} Power Law Noise Generation"
Expand source code
def getValue(self, time=None): ''' Calculate coefficient values - openrocket documentation pg. 59/60 (Section 4.1.2 Wind Modeling) Based on method from Kasdin (1995) "Discrete Simulation of Colored Noise and Stochastic Processes and 1/f^{alpha} Power Law Noise Generation" ''' # Compute any required new values while time == None or time > self.lastValTime: whiteNoise = self.rng.gauss(0, 1) for i in range(self.nPoles): whiteNoise -= self.coeffs[i]*self.lastValues[(self.nextIndexToOverwrite - i - 1) % self.nPoles] pinkNoiseSample = whiteNoise self.lastValues[self.nextIndexToOverwrite] = pinkNoiseSample self.nextIndexToOverwrite = (self.nextIndexToOverwrite + 1) % self.nPoles if time == None: return pinkNoiseSample self.lastValTime += self.timeStep if self.lastValTime == time: return pinkNoiseSample # Linearly interpolate value at desired time lastVal = self.lastValues[self.nextIndexToOverwrite - 1] secondLastVal = self.lastValues[self.nextIndexToOverwrite - 2] secondLastValTime = self.lastValTime - self.timeStep dValdt = (lastVal - secondLastVal) / self.timeStep return dValdt*(time-secondLastValTime) + secondLastVal
- alpha - pink noise generated has a power spectrum which has a log-log slope of -alpha.