Module MAPLEAF.Rocket.AeroFunctions

Functions to calculate parameters relevant to aerodynamic calculations - Mach/Reynolds numbers, AOA, local frame air velocity etc…

Used throughout the aeordynamics functions

Expand source code
'''
Functions to calculate parameters relevant to aerodynamic calculations - 
    Mach/Reynolds numbers, AOA, local frame air velocity etc...

Used throughout the aeordynamics functions
'''

import math

from MAPLEAF.Motion import (AeroParameters, ForceMomentSystem, Quaternion,
                            Vector)
from MAPLEAF.Utilities import cacheLastResult


def _getCPZ(componentForces) -> int:
    normalForce = Vector(componentForces.force.X, componentForces.force.Y, 0)
    normalForceMagnitude = normalForce.length()

    if normalForceMagnitude == 0.0:
        # Avoid division by zero, just return CG location
        return componentForces.location.Z

    normalForceDirection = normalForce.normalize()

    momentAxis = normalForceDirection.crossProduct(Vector(0,0,1))
    momentComponent = componentForces.moment * momentAxis

    return componentForces.location.Z - momentComponent/normalForceMagnitude


#### Constants ####
def getGamma():
    return 1.4

def getGasConstant():
    return 0.287



#### Skin Friction ####
def _subSonicSkinFrictionCompressiblityCorrectionFactor(Mach, smoothSurface):
    '''
        Inputs:
            *skinFrictionCoefficient: (float) - skin friction coefficient that has not been adjusted for compressiblity
            *MachNumber: (float)
            *smooth: (boolean) - True if smooth wall, false if rough wall
    '''
    # Subsonic correlation: Eqns 4-9, 4-10 (Barrowman)
    if smoothSurface:
        return 1 - 0.10 * Mach**2
    else:
        return 1 - 0.12 * Mach**2

def _supersonicSkinFrictionCompressiblityCorrectionFactor(Mach, smoothSurface):
    '''
    Inputs:
        *skinFrictionCoefficient: (float) - skin friction coefficient that has not been adjusted for compressiblity
        *MachNumber: (float)
        *smooth: (boolean) - True if smooth wall, false if rough wall
    '''
    # Eqns 4-12, 4-13 (Barrowman)
    if smoothSurface:
        # Barrowman 4-12, for an adiabatic wall. The 0.15 coefficient would be 0.0512 for a cooled wall
        return  1 / ((1 + 0.15*Mach**2)**(0.58))
    else:
        smoothTurbFactor =  1 / ((1 + 0.15*Mach**2)**0.58)
        roughTurbFactor =   1 /  (1 + 0.18*Mach**2) #TODO: Seems like this one is always smaller than smoothTurbFactor??? - Perhaps there's a better eqn out there
        return max(smoothTurbFactor, roughTurbFactor)

@cacheLastResult
def getSkinFrictionCoefficient(state, environment, length, Mach, surfaceRoughness, assumeFullyTurbulent=True):
    ''' 
        Calculates the skin friction drag coefficient (normalized by wetted area, not cross-sectional area).  
        Uses the methods described in (Barrowman, 1967, pg. 43-48) and in (Niskanen, 2009, pg. 43-46) 

        Used for skin friction calculations for all external rocket components

        Inputs:
          * state: (RigidBodyState)
          * environment: (EnvironmentalConditions)
          * length: (float) - reference length for the current rocket component, used for Reynolds number calculation
          * Mach: (float) - current Mach number
          * surfaceRoughness (float) - micrometers

        Outputs:
          * Returns the skin friction coefficient normalized by **wetted area**, must be converted to be normalized by cross-sectional area before summing with other coefficients.
    '''
    Re = AeroParameters.getReynoldsNumber(state, environment, length)
    smoothSurface = (surfaceRoughness == 0) # True or False

    if smoothSurface:
        # Very large (unreachable) number
        criticalRe = 1e100
    else:
        criticalRe = 51*(surfaceRoughness / length)**(-1.039) # Eqn 4-7 (Barrowman)

    # Get skin friction factor (Mach number independent)
    if Re > criticalRe:
        # Re is above Critical value
        # Friction determined by roughness
        skinFrictionCoefficient = 0.032 * (surfaceRoughness / length)**0.2
    elif assumeFullyTurbulent:
        if Re < 1e4:
            # Limit for low Reynolds numbers
            skinFrictionCoefficient = 0.0148 # Eqn 3.81 (Niskanen) - match up with turbulent flow correlation
        else:
            # Eqn from Niskanen
            skinFrictionCoefficient = 1 / ((1.5 * math.log(Re) - 5.6)**2)
    else:
        # Laminar / Transitional flow
        if Re < 1e4:
            # Limit for low Reynolds numbers
            skinFrictionCoefficient = 0.01328 # Match up with transitional flow eqn
        elif Re < 5e5:
            # Laminar flow
            skinFrictionCoefficient = 1.328 / math.sqrt(Re) # Eqn 4-2 (Barrowman)
        else:
            # Transitional Flow
            skinFrictionCoefficient = 1 / ((1.5 * math.log(Re) - 5.6)**2) - 1700/Re # Eqn 4-6 (Barrowman), subtraction term from from Prandtl
    
    # Calculate compressibility correction
    if Mach < 0.9: 
        # Subsonic
        compressibilityCorrectionFactor = _subSonicSkinFrictionCompressiblityCorrectionFactor(Mach, smoothSurface)
    elif Mach < 1.1:
        # Transonic
        subsonicFactor = _subSonicSkinFrictionCompressiblityCorrectionFactor(Mach, smoothSurface)
        supersonicFactor = _supersonicSkinFrictionCompressiblityCorrectionFactor(Mach, smoothSurface)
        # Linearly interpolate in the Mach 0.9-1.1 region - same method as that used in OpenRocket v15.03
        compressibilityCorrectionFactor = subsonicFactor + ((supersonicFactor - subsonicFactor)*(Mach - 0.9) / 0.2)
    else:
        # Supersonic
        compressibilityCorrectionFactor = _supersonicSkinFrictionCompressiblityCorrectionFactor(Mach, smoothSurface)

    # Return corrected factor
    return skinFrictionCoefficient*compressibilityCorrectionFactor

def getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(state, environment, length, Mach, surfaceRoughness, wettedArea, Aref, finenessRatio, assumeFullyTurbulent=True):
    ''' Equations from Barrowman section 4.0, Niskanen section 3.4 '''
    # Get a compressiblity-corrected flat plate skin friction drag coefficient, normalized by wetted area
    skinFrictionDragCoefficient = getSkinFrictionCoefficient(state, environment, length, Mach, surfaceRoughness, assumeFullyTurbulent)
    # Account for the fact that the rocket is cylindrical, and not a flat plate (Barrowman Eqn 4-16)
    skinFrictionDragCoefficient *= (1 + (0.5 / finenessRatio))
    # Rebase coefficient to the reference area
    skinFrictionDragCoefficient *= (wettedArea / Aref)

    # Approximate avg radius as the radius of a cylinder which would have the same wetted area + length 
    avgRadius = (wettedArea / length) / (2*math.pi)
    rollingSurfaceVel = avgRadius * state.angularVelocity.Z
    # Assume velocities are perpendicular
    airVel = AeroParameters.getLocalFrameAirVel(state, environment).length()
    # Use total velocity to redimensionalize coefficient
    totalVelSquared = airVel**2 + rollingSurfaceVel**2

    if totalVelSquared == 0:
        return skinFrictionDragCoefficient, Vector(0,0,0)
    else:
        totalSurfaceForce = skinFrictionDragCoefficient * 0.5 * totalVelSquared * environment.Density * Aref
        # Calculate roll damping component of total friction force using similar triangles
        rollDampingForce = totalSurfaceForce * (rollingSurfaceVel/math.sqrt(totalVelSquared))
        # Calculate resulting moment
        rollDampingMoment = Vector(0,0, -rollDampingForce*avgRadius)
    
    return skinFrictionDragCoefficient, rollDampingMoment



#### Leading edge and base drag ####
@cacheLastResult
def getBaseDragCoefficient(Mach):
    if Mach < 1:
        return 0.12 + 0.13*Mach*Mach
    else:
        return 0.25/Mach

@cacheLastResult
def getCylinderCrossFlowCd_ZeroBaseDrag(Mach):
    ''' 
        Calculate drag coefficient of a cylinder, omitting base drag 
        Method from Barrowman Eqns 4-17 to 4-19
        Similar method in Niskanen Eqn. 3.89
    '''
    if Mach < 0.9:
        return (1 - Mach*Mach)**-0.417 - 1
    elif Mach <= 1:
        return 1 - 1.5*(Mach - 0.9)
    else:
        return 1.214 - 0.502/Mach**2 + 0.1095/Mach**4 + .0231/Mach**6

@cacheLastResult
def getBluntBodyCd_ZeroBaseDrag(Mach):
    # Originally from Hoerner (pg. 15-3), same as Niskanen Eqns (In Appendix) B.1 and B.2
    if Mach < 1:
        qStageOverQ = 1 + (Mach**2/4) + (Mach**4/40)
    else:
        qStageOverQ = 1.84 - (0.76/(Mach**2)) + (0.166/(Mach**4)) + (0.035/(Mach**6))

    return 0.85 * qStageOverQ



#### Other ####
@cacheLastResult
def getDragToAxialForceFactor(AOA):
    ''' Expected input is radians angle of attack - converts dragCoefficient to axialForceCoefficient: openRocket documentation pg. 53 '''
    # Angle of attack will not be negative if obtained from getTotalAOA in AeroParameters
        # But can be negative when fins are calculating their angles of attack, hence the abs()
    AOA = math.degrees(abs(AOA))

    # Derived to match Niskanen pg. 53
    if AOA <= 17:
        return -0.000122124*AOA**3 + 0.003114164*AOA**2 + 1
    else:
        return 0.000006684*AOA**3 - 0.0010727204*AOA**2 + 0.030677323*AOA + 1.055660807


#### Creating ForceMomentSystems from Coefficients ####
def forceFromCdCN(rocketState, environment, Cd, CN, CPLocation, refArea, moment=None):
    ''' 
        Convenience function for Barrowman Aero methods
        Initialize ForceMomentSystem from aerodynamic coefficients Cd and CN
        Cd should NOT already be adjusted for angle of attack 
        Moment should be a dimensional moment vector
    '''
    angleOfAttack = AeroParameters.getTotalAOA(rocketState, environment)
    q = AeroParameters.getDynamicPressure(rocketState, environment)

    #### Axial Force ####
    CA = getDragToAxialForceFactor(angleOfAttack) * Cd
    axialForce = Vector(0, 0, -CA) #Axial force is in the negative Z-direction 

    #### Normal Force ####
    normalForce = AeroParameters.getNormalAeroForceDirection(rocketState, environment) * CN

    totalForce = (axialForce + normalForce) * refArea * q
    return ForceMomentSystem(totalForce, CPLocation, moment)

def forceFromCoefficients(rocketState, environment, Cd, Cl, CMx, CMy, CMz, CPLocation, refArea, refLength):
    ''' Initialize ForceMomentSystem from all aerodynamic coefficients '''
    q = AeroParameters.getDynamicPressure(rocketState, environment)
    if q == 0.0:
        # No force without dynamic pressure
        return ForceMomentSystem(Vector(0,0,0))
    nonDimConstant = q * refArea

    #### Drag ####
    localFrameAirVel = AeroParameters.getLocalFrameAirVel(rocketState, environment)
    dragDirection = localFrameAirVel.normalize()
    dragForce = dragDirection * Cd

    #### Lift ####
    # Find the lift force direction
        # Lift force will be in the same plane as the normalForceDirection & rocketFrameAirVel vectors
        # But, the lift force will be perpendicular to rocketFraeAirVel, whereas the normalForceDirection might form an acute angle with it
        # Rotate the normal force direction vector until it's perpendicular with rocketFrameAirVel
    normalForceDir = AeroParameters.getNormalAeroForceDirection(rocketState, environment)
    rotAxis = localFrameAirVel.crossProduct(normalForceDir)
    angle = math.pi/2 - localFrameAirVel.angle(normalForceDir)
    rotatorQuat = Quaternion(rotAxis, angle)
    liftDirection = rotatorQuat.rotate(normalForceDir)
    liftForce = liftDirection * Cl

    # Combine and redimensionalize
    totalForce = (dragForce + liftForce) * nonDimConstant

    #### Moments ####
    moments = Vector(CMx, CMy, CMz) * nonDimConstant * refLength

    return ForceMomentSystem(totalForce, CPLocation, moments)


#### Barrowman ####
def Barrowman_GetCPLocation(length, crossSectionalArea_top, crossSectionalArea_bottom, volume, topLocation=Vector(0,0,0)):
    ''' Get Cp location for an axisymmetric body component - Niskanen Eqn 3.28, expanded from Barrowman Eqn 3-88 '''
    if crossSectionalArea_top == crossSectionalArea_bottom:
        # Body tubes
        CpDistanceFromTop = length / 2
    else:
        # Nosecones and boat tails / transitions
        CpDistanceFromTop = (length * crossSectionalArea_bottom - volume) / (crossSectionalArea_bottom - crossSectionalArea_top)
    
    return Vector(topLocation.X, topLocation.Y, topLocation.Z - CpDistanceFromTop)  # For body tubes

def Barrowman_GetCN(AOA, Aref, crossSectionalArea_top, crossSectionalArea_bottom):
    ''' 
        Get CN for an axisymmetric body component - Niskanen Eqn 3.18, expanded from Barrowman Eqn 3-63 
        AOA expected in radians
    '''
    return 2 * math.sin(AOA) * (crossSectionalArea_bottom - crossSectionalArea_top) / Aref

Functions

def Barrowman_GetCN(AOA, Aref, crossSectionalArea_top, crossSectionalArea_bottom)

Get CN for an axisymmetric body component - Niskanen Eqn 3.18, expanded from Barrowman Eqn 3-63 AOA expected in radians

Expand source code
def Barrowman_GetCN(AOA, Aref, crossSectionalArea_top, crossSectionalArea_bottom):
    ''' 
        Get CN for an axisymmetric body component - Niskanen Eqn 3.18, expanded from Barrowman Eqn 3-63 
        AOA expected in radians
    '''
    return 2 * math.sin(AOA) * (crossSectionalArea_bottom - crossSectionalArea_top) / Aref
def Barrowman_GetCPLocation(length, crossSectionalArea_top, crossSectionalArea_bottom, volume, topLocation=<MAPLEAF.Motion.CythonVector.Vector object>)

Get Cp location for an axisymmetric body component - Niskanen Eqn 3.28, expanded from Barrowman Eqn 3-88

Expand source code
def Barrowman_GetCPLocation(length, crossSectionalArea_top, crossSectionalArea_bottom, volume, topLocation=Vector(0,0,0)):
    ''' Get Cp location for an axisymmetric body component - Niskanen Eqn 3.28, expanded from Barrowman Eqn 3-88 '''
    if crossSectionalArea_top == crossSectionalArea_bottom:
        # Body tubes
        CpDistanceFromTop = length / 2
    else:
        # Nosecones and boat tails / transitions
        CpDistanceFromTop = (length * crossSectionalArea_bottom - volume) / (crossSectionalArea_bottom - crossSectionalArea_top)
    
    return Vector(topLocation.X, topLocation.Y, topLocation.Z - CpDistanceFromTop)  # For body tubes
def forceFromCdCN(rocketState, environment, Cd, CN, CPLocation, refArea, moment=None)

Convenience function for Barrowman Aero methods Initialize ForceMomentSystem from aerodynamic coefficients Cd and CN Cd should NOT already be adjusted for angle of attack Moment should be a dimensional moment vector

Expand source code
def forceFromCdCN(rocketState, environment, Cd, CN, CPLocation, refArea, moment=None):
    ''' 
        Convenience function for Barrowman Aero methods
        Initialize ForceMomentSystem from aerodynamic coefficients Cd and CN
        Cd should NOT already be adjusted for angle of attack 
        Moment should be a dimensional moment vector
    '''
    angleOfAttack = AeroParameters.getTotalAOA(rocketState, environment)
    q = AeroParameters.getDynamicPressure(rocketState, environment)

    #### Axial Force ####
    CA = getDragToAxialForceFactor(angleOfAttack) * Cd
    axialForce = Vector(0, 0, -CA) #Axial force is in the negative Z-direction 

    #### Normal Force ####
    normalForce = AeroParameters.getNormalAeroForceDirection(rocketState, environment) * CN

    totalForce = (axialForce + normalForce) * refArea * q
    return ForceMomentSystem(totalForce, CPLocation, moment)
def forceFromCoefficients(rocketState, environment, Cd, Cl, CMx, CMy, CMz, CPLocation, refArea, refLength)

Initialize ForceMomentSystem from all aerodynamic coefficients

Expand source code
def forceFromCoefficients(rocketState, environment, Cd, Cl, CMx, CMy, CMz, CPLocation, refArea, refLength):
    ''' Initialize ForceMomentSystem from all aerodynamic coefficients '''
    q = AeroParameters.getDynamicPressure(rocketState, environment)
    if q == 0.0:
        # No force without dynamic pressure
        return ForceMomentSystem(Vector(0,0,0))
    nonDimConstant = q * refArea

    #### Drag ####
    localFrameAirVel = AeroParameters.getLocalFrameAirVel(rocketState, environment)
    dragDirection = localFrameAirVel.normalize()
    dragForce = dragDirection * Cd

    #### Lift ####
    # Find the lift force direction
        # Lift force will be in the same plane as the normalForceDirection & rocketFrameAirVel vectors
        # But, the lift force will be perpendicular to rocketFraeAirVel, whereas the normalForceDirection might form an acute angle with it
        # Rotate the normal force direction vector until it's perpendicular with rocketFrameAirVel
    normalForceDir = AeroParameters.getNormalAeroForceDirection(rocketState, environment)
    rotAxis = localFrameAirVel.crossProduct(normalForceDir)
    angle = math.pi/2 - localFrameAirVel.angle(normalForceDir)
    rotatorQuat = Quaternion(rotAxis, angle)
    liftDirection = rotatorQuat.rotate(normalForceDir)
    liftForce = liftDirection * Cl

    # Combine and redimensionalize
    totalForce = (dragForce + liftForce) * nonDimConstant

    #### Moments ####
    moments = Vector(CMx, CMy, CMz) * nonDimConstant * refLength

    return ForceMomentSystem(totalForce, CPLocation, moments)
def getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(state, environment, length, Mach, surfaceRoughness, wettedArea, Aref, finenessRatio, assumeFullyTurbulent=True)

Equations from Barrowman section 4.0, Niskanen section 3.4

Expand source code
def getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(state, environment, length, Mach, surfaceRoughness, wettedArea, Aref, finenessRatio, assumeFullyTurbulent=True):
    ''' Equations from Barrowman section 4.0, Niskanen section 3.4 '''
    # Get a compressiblity-corrected flat plate skin friction drag coefficient, normalized by wetted area
    skinFrictionDragCoefficient = getSkinFrictionCoefficient(state, environment, length, Mach, surfaceRoughness, assumeFullyTurbulent)
    # Account for the fact that the rocket is cylindrical, and not a flat plate (Barrowman Eqn 4-16)
    skinFrictionDragCoefficient *= (1 + (0.5 / finenessRatio))
    # Rebase coefficient to the reference area
    skinFrictionDragCoefficient *= (wettedArea / Aref)

    # Approximate avg radius as the radius of a cylinder which would have the same wetted area + length 
    avgRadius = (wettedArea / length) / (2*math.pi)
    rollingSurfaceVel = avgRadius * state.angularVelocity.Z
    # Assume velocities are perpendicular
    airVel = AeroParameters.getLocalFrameAirVel(state, environment).length()
    # Use total velocity to redimensionalize coefficient
    totalVelSquared = airVel**2 + rollingSurfaceVel**2

    if totalVelSquared == 0:
        return skinFrictionDragCoefficient, Vector(0,0,0)
    else:
        totalSurfaceForce = skinFrictionDragCoefficient * 0.5 * totalVelSquared * environment.Density * Aref
        # Calculate roll damping component of total friction force using similar triangles
        rollDampingForce = totalSurfaceForce * (rollingSurfaceVel/math.sqrt(totalVelSquared))
        # Calculate resulting moment
        rollDampingMoment = Vector(0,0, -rollDampingForce*avgRadius)
    
    return skinFrictionDragCoefficient, rollDampingMoment
def getGamma()
Expand source code
def getGamma():
    return 1.4
def getGasConstant()
Expand source code
def getGasConstant():
    return 0.287