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 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 getAppliedForce(self, rocketState, time, environment, CG): Aref = self.rocket.Aref Mach = AeroParameters.getMachNumber(rocketState, environment) # Normal Force -------------------------------------------------------------------------------- # TODO: Account for rate of pitch/yaw rotation in AOA calculation? Or do separate Pitch/Yaw damping moments? AOA = AeroParameters.getTotalAOA(rocketState, environment) normalForceCoefficient = AeroFunctions.Barrowman_GetCN( AOA, Aref, 0, self.baseArea) # Drag Force --------------------------------------------------------------------------------------------- #### Skin Friction #### skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, self.length, Mach, \ self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Pressure #### pressureDragCoefficient = self._getCd_Pressure(Mach) totalDragCoefficient = pressureDragCoefficient + skinFrictionDragCoefficient # Damping moments -------------------------------------------------------------------------------------- # Roll damping due to skin friction dampingMoments = rollDampingMoment # Combine forces and return total ------------------------------------------------------------------------------------- return AeroFunctions.forceFromCdCN(rocketState, environment, totalDragCoefficient, normalForceCoefficient, self.CPLocation, Aref, moment=dampingMoments)
def getAppliedForce(self, rocketState, time, environment, CG): Aref = self.rocket.Aref # Normal Force ---------------------------------------------------------------------------------------- AOA = AeroParameters.getTotalAOA(rocketState, environment) # Niskanen Eqn 3.26 - originally from Galejs normalForceCoefficient = 1.1 * (math.sin(AOA))**2 normalForceCoefficient *= self.planformArea / Aref # Drag ----------------------------------------------------------------------------------------------- # Skin Friction Mach = AeroParameters.getMachNumber(rocketState, environment) skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, self.length, Mach, self.surfaceRoughness, \ self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #There is no pressure drag associated with bodytubes - skin friction drag is total drag # Damping moments -------------------------------------------------------------------------------------- dampingMoments = self._computeLongitudinalDampingMoments(rocketState, environment, CG) # Roll damping due to skin friction dampingMoments += rollDampingMoment # Compute & dimensionalize total------------------------------------------------------------------------- return AeroFunctions.forceFromCdCN(rocketState, environment, skinFrictionDragCoefficient, normalForceCoefficient, self.CPLocation, Aref, moment=dampingMoments)
def getAppliedForce(self, rocketState, time, environment, CG) -> ForceMomentSystem: Mach = AeroParameters.getMachNumber(rocketState, environment) Aref = self.rocket.Aref #### Normal Force #### AOA = AeroParameters.getTotalAOA(rocketState, environment) CN = AeroFunctions.Barrowman_GetCN(AOA, Aref, self.topArea, self.bottomArea) #### Pressure Drag #### if self.startDiameter > self.endDiameter: # Pressure base drag Cd_base = AeroFunctions.getBaseDragCoefficient(Mach) Cd_pressure = Cd_base * self.CdAdjustmentFactor else: # Pressure drag calculated like a nose cone if Mach < 1: # Niskanen pg. 48 eq. 3.87 - Power law interpolation Cd_pressure = self.SubsonicCdPolyCoeffs[ 0] * Mach**self.SubsonicCdPolyCoeffs[1] elif Mach > 1 and Mach < 1.3: # Interpolate in transonic region - derived from Niskanen Appendix B, Eqns B.3 - B.6 Cd_pressure = self.TransonicCdPolyCoeffs[0] + self.TransonicCdPolyCoeffs[1]*Mach + \ self.TransonicCdPolyCoeffs[2]*Mach**2 + self.TransonicCdPolyCoeffs[3]*Mach**3 else: Cd_pressure = getSupersonicPressureDragCoeff_Hoerner( self.coneHalfAngle, Mach) # Make reference are the rocket's, not this objects Cd_pressure *= self.frontalArea / Aref #### Skin Friction Drag #### if self.wettedArea == 0: skinFrictionDragCoefficient = 0 rollDampingMoment = Vector(0, 0, 0) else: skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, \ self.length, Mach, self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Total Drag #### Cd = Cd_pressure + skinFrictionDragCoefficient #### Assemble & return final force object #### return AeroFunctions.forceFromCdCN(rocketState, environment, Cd, CN, self.CPLocation, Aref, moment=rollDampingMoment)
def _getCPXPos(self, Mach): ''' Finds the chordwise position of the center of pressure, relative to the tip of the root chord (self.position) Methods from Tactical missile design by Fleeman through Niskanen ''' if Mach < 0.5: XF = self.XMACLeadingEdge + 0.25 * self.MACLength #As per open rocket documentation elif Mach >= 2.0: beta = AeroParameters.getBeta(Mach) CPCalc = lambda AR, B: (AR * B - 0.67) / ( 2 * AR * B - 1 ) #Eq 3.35 of open rocket documentation (greater than M = 2) XF = self.MACLength * CPCalc( self.aspectRatio, beta) + self.XMACLeadingEdge #As per open rocket documentation else: #Between the two extremes is a polynomial curve fit #Fifth order polynomial fit as per Niskanen Eqn 3.36 (Originally from Fleeman) # Old code: XF = np.polyval(self.x, Mach)*self.MACLength + self.XMACLeadingEdge # Evaluate polynomial manually for maximum speed (8.5x improvement for 6 coeffs) # Speed test in test/speedTests/PolyvalSpeed.py polyval = 0 nTerms = len(self.x) for i in range(nTerms): polyval += self.x[i] * Mach**(nTerms - 1 - i) XF = polyval * self.MACLength + self.XMACLeadingEdge # Polyval defines the fraction of the MAC that the Cp is behind the leading edge return XF
def subsonicNormalForce(Mach): # Subsonic linear method tempBeta = AeroParameters.getBeta(Mach) CnAlpha = getFinCnAlpha_Subsonic_Barrowman(self.span, self.planformArea, tempBeta, self.midChordSweep) return getSubsonicFinNormalForce(airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, self.spanwiseDirection, self.CPSpanWisePosition.length(), CnAlpha, self)
def test_getNormalAeroForceDirection(self): zeroAngVel = AngularVelocity(0,0,0) zeroPos = Vector(0,0,0) state1 = RigidBodyState(zeroPos, self.dummyVelocity1, self.dummyOrientation1, zeroAngVel) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state1, self.currentConditions), Vector(0, -1, 0)) state2 = RigidBodyState(zeroPos, self.dummyVelocity1, self.dummyOrientation2, zeroAngVel) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state2, self.currentConditions), Vector(0, 1, 0)) state3 = RigidBodyState(zeroPos, self.dummyVelocity1, self.dummyOrientation3, zeroAngVel) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state3, self.currentConditions), Vector(1, 0, 0)) state4 = RigidBodyState(zeroPos, self.dummyVelocity1, self.dummyOrientation4, zeroAngVel) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state4, self.currentConditions), Vector(1, 0, 0)) state5 = RigidBodyState(zeroPos, self.dummyVelocity1, self.dummyOrientation5, zeroAngVel) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state5, self.currentConditions), Vector(0.707, -0.707, 0),3) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(self.rocketState8, self.currentConditions), Vector(-0.99999, 0.0022736, 0),3) # Prep rocket by calling getLocalFrameAirVel first to cache AOA result, then get cached result from getTotalAOA def testLocalFrameAirVelNormalForceDir(vel, orientation, expectedResult, precision=7): state = RigidBodyState(Vector(0,0,0), vel, orientation, AngularVelocity(0,0,0)) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state, self.currentConditions), expectedResult, precision) testLocalFrameAirVelNormalForceDir(self.dummyVelocity1, self.dummyOrientation1, Vector(0, -1, 0)) testLocalFrameAirVelNormalForceDir(self.dummyVelocity1, self.dummyOrientation2, Vector(0, 1, 0)) testLocalFrameAirVelNormalForceDir(self.dummyVelocity1, self.dummyOrientation3, Vector(1, 0, 0)) testLocalFrameAirVelNormalForceDir(self.dummyVelocity1, self.dummyOrientation4, Vector(1, 0, 0)) testLocalFrameAirVelNormalForceDir(self.dummyVelocity1, self.dummyOrientation5, Vector(0.707, -0.707, 0), 3) testLocalFrameAirVelNormalForceDir(self.rocketState8.velocity, self.rocketState8.orientation, Vector(-0.99999, 0.0022736, 0),3)
def test_getTotalAOA(self): def getDummyState(vel, orientation): zeroVec = Vector(0,0,0) zeroAngVel = AngularVelocity(rotationVector=zeroVec) return RigidBodyState(zeroVec, vel, orientation, zeroAngVel) state1 = getDummyState(self.dummyVelocity1, self.dummyOrientation1) self.assertAlmostEqual(AeroParameters.getTotalAOA(state1, self.currentConditions), math.radians(2)) state2 = getDummyState(self.dummyVelocity1, self.dummyOrientation2) self.assertAlmostEqual(AeroParameters.getTotalAOA(state2, self.currentConditions), math.radians(2)) state3 = getDummyState(self.dummyVelocity2, self.dummyOrientation3) self.assertAlmostEqual(AeroParameters.getTotalAOA(state3, self.currentConditions), math.radians(0.862405226)) state4 = getDummyState(self.dummyVelocity2, self.dummyOrientation4) self.assertAlmostEqual(AeroParameters.getTotalAOA(state4, self.currentConditions), math.radians(2.862405226)) self.assertAlmostEqual(AeroParameters.getTotalAOA(self.rocketState4, self.currentConditions), 0) state5 = getDummyState(self.dummyVelocity3, self.dummyOrientation6) self.assertAlmostEqual(AeroParameters.getTotalAOA(state5, self.currentConditions), math.radians(90)) # Prep rocket by calling getLocalFrameAirVel first to cache AOA result, then get cached result from getTotalAOA def testLocalFrameAirVelAOA(vel, orientation, expectedResult): state = RigidBodyState(Vector(0,0,0), vel, orientation, AngularVelocity(rotationVector=Vector(0,0,0))) self.assertAlmostEqual(AeroParameters.getTotalAOA(state, self.currentConditions), math.radians(expectedResult)) testLocalFrameAirVelAOA(self.dummyVelocity1, self.dummyOrientation1, 2) testLocalFrameAirVelAOA(self.dummyVelocity1, self.dummyOrientation2, 2) testLocalFrameAirVelAOA(self.dummyVelocity2, self.dummyOrientation3, 0.862405226) testLocalFrameAirVelAOA(self.dummyVelocity2, self.dummyOrientation4, 2.862405226) testLocalFrameAirVelAOA(self.rocketState4.velocity, self.rocketState4.orientation, 0) testLocalFrameAirVelAOA(self.dummyVelocity3, self.dummyOrientation6, 90)
def getAppliedForce(self, rocketState, time, environment, CG) -> ForceMomentSystem: Mach = AeroParameters.getMachNumber(rocketState, environment) Aref = self.rocket.Aref #### Normal Force #### AOA = AeroParameters.getTotalAOA(rocketState, environment) CN = AeroFunctions.Barrowman_GetCN(AOA, Aref, self.topArea, self.bottomArea) #### Pressure Drag #### Cd_base = AeroFunctions.getBaseDragCoefficient(Mach) Cd_pressure = Cd_base * self.CdAdjustmentFactor Cd_pressure *= self.frontalArea / self.rocket.Aref noEngine = (self.stage.engineShutOffTime == None) if noEngine or time > self.stage.engineShutOffTime: # Add base drag if engine is off Cd_pressure += Cd_base * self.bottomArea / Aref #### Skin Friction Drag #### if self.wettedArea == 0: skinFrictionDragCoefficient = 0 rollDampingMoment = Vector(0, 0, 0) else: skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, \ self.length, Mach, self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Total Drag #### Cd = Cd_pressure + skinFrictionDragCoefficient #### Assemble & return final force object #### return AeroFunctions.forceFromCdCN(rocketState, environment, Cd, CN, self.CPLocation, Aref, moment=rollDampingMoment)
def getAppliedForce(self, rocketState, time, environment, CG): ''' Calculates force/moment applied by the recovery system using a simple drag coefficient + area model ''' #### Calculate Aero Force #### if self.currentStage == 0: # No recovery system deployed yet return ForceMomentSystem(Vector(0,0,0)) else: # 3DoF force-only aero airVel = AeroParameters.getAirVelRelativeToVehicle(rocketState, environment) dragForceMagnitude = self.chuteCds[self.currentStage] * self.chuteAreas[self.currentStage] * AeroParameters.getDynamicPressure(rocketState, environment) totalForce = airVel.normalize() * dragForceMagnitude return ForceMomentSystem(totalForce)
def getDesiredMoments(self, rocketState, environment, targetOrientation, time, dt): ''' Inputs: gainKeyList: iterable of length nKeyColumns, containing the data required to interpolate the PIDxy and PIDz coefficients in the gain table rocketState: must have .orientation attribute (Quaternion) targetOrientation: (Quaternion) _: currently unused (time argument) dt: (numeric) time since last execution of the control system ''' orientationError = self._getOrientationError(rocketState, targetOrientation) gainKeyList = AeroParameters.getAeroPropertiesList( self.keyFunctionList, rocketState, environment) self.updateCoefficientsFromGainTable(gainKeyList) return self.getNewSetPoint(orientationError, dt)
def getAppliedForce(self, state, time, environment, rocketCG): airspeed = max( AeroParameters.getLocalFrameAirVel(state, environment).length(), 0.0000001) redimConst = self.Lref / (2 * airspeed) # Calculate moment coefficients from damping coefficients localFrameAngularVelocity = Vector(*state.angularVelocity) zMomentCoeff = self.zDampingCoeffs * localFrameAngularVelocity * redimConst yMomentCoeff = self.yDampingCoeffs * localFrameAngularVelocity * redimConst xMomentCoeff = self.xDampingCoeffs * localFrameAngularVelocity * redimConst momentCoeffs = [xMomentCoeff, yMomentCoeff, zMomentCoeff] return AeroFunctions.forceFromCoefficients(state, environment, 0, 0, *momentCoeffs, self.position, self.Aref, self.Lref)
def _getAeroCoefficients(self, state, environment): keys = AeroParameters.getAeroPropertiesList(self.parameterFunctions, state, environment) if len(keys) > 1: # Multi-dimensional linear interpolation interpolatedCoefficients = self._interpAeroCoefficients(keys)[0] else: # 1D linear interpolation interpolatedCoefficients = linInterp(self.keys, self.values, keys[0]) aeroCoefficients = [0.0] * 5 for i in range(len(interpolatedCoefficients)): indexInCoeffArray = self.aeroCoeffIndices[i] aeroCoefficients[indexInCoeffArray] = interpolatedCoefficients[i] return aeroCoefficients
def supersonicNormalForce(Mach): # Supersonic Busemann method gamma = AeroFunctions.getGamma() tempBeta = AeroParameters.getBeta(Mach) K1, K2, K3, Kstar = getBusemannCoefficients(Mach, tempBeta, gamma) # Mach Cone coords machAngle = asin(1 / Mach) machCone_negZPerRadius = 1 / tan(machAngle) machConeEdgeZPos = [] outerRadius = self.spanSliceRadii[-1] for i in range(len(self.spanSliceRadii)): machConeAtCurrentRadius = ( outerRadius - self.spanSliceRadii[i] ) * machCone_negZPerRadius + self.tipPosition machConeEdgeZPos.append(machConeAtCurrentRadius) return getSupersonicFinNormalForce( airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, machConeEdgeZPos, self.spanwiseDirection, self.CPSpanWisePosition.length(), K1, K2, K3, Kstar, self)
def setTargetActuatorDeflections(self, desiredMoments, state, environment, time): ''' Inputs: desiredMoments: (3-length iterable) contains desired moments about the x,y,and z axes time: time at which the moments are requested (time of current control loop execution) ''' # Construct key vector, starting with non-moment components: keyVector = AeroParameters.getAeroPropertiesList( self.keyFunctionList, state, environment) for desiredMoment in desiredMoments: keyVector.append(desiredMoment) newActuatorPositionTargets = self._getPositionTargets(*keyVector) for i in range(len(self.actuatorList)): self.actuatorList[i].setTargetDeflection( newActuatorPositionTargets[i], time) return list(newActuatorPositionTargets)
def getFinSliceArgs(self, vel, orientation, angVel, fin1, finSlicePosition=0): rocketState = RigidBodyState(Vector(0,0,0), vel, orientation, angVel) CG = self.rocket2.getCG(0, rocketState) rocketVelocity = AeroParameters.getLocalFrameAirVel(rocketState, self.currentConditions) # Add fin velocities due to motion of the rocket velocityDueToRocketPitchYaw = rocketState.angularVelocity.crossProduct(fin1.position - CG)*(-1) #The negative puts it in the wind frame #We need to find the angle between the rocket velocity vector and the plane described by the fin deflection angle #and the shaft normal finNormal = fin1.spanwiseDirection.crossProduct(Vector(0, 0, 1)) finDeflectionAngle = fin1.finAngle # Adjusted by parent finset during each timestep if(finDeflectionAngle != 0): rotation = Quaternion(axisOfRotation = fin1.spanwiseDirection, angle=math.radians(finDeflectionAngle)) finNormal = rotation.rotate(finNormal) finUnitSpanTangentialVelocity = rocketState.angularVelocity.crossProduct(fin1.spanwiseDirection)*(-1) finVelWithoutRoll = rocketVelocity + velocityDueToRocketPitchYaw return finSlicePosition, finVelWithoutRoll, finUnitSpanTangentialVelocity, finNormal, fin1.spanwiseDirection, fin1.stallAngle
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 test_getRocketRollAngle(self): def getDummyState(vel, orientation): zeroVec = Vector(0,0,0) zeroAngVel = AngularVelocity(rotationVector=zeroVec) return RigidBodyState(zeroVec, vel, orientation, zeroAngVel) state1 = getDummyState(self.dummyVelocity1, self.dummyOrientation4) rollAngle1 = AeroParameters.getRollAngle(state1, self.currentConditions) state2 = getDummyState(self.dummyVelocity1, self.dummyOrientation1) rollAngle2 = AeroParameters.getRollAngle(state2, self.currentConditions) state3 = getDummyState(self.dummyVelocity1, self.dummyOrientation2) rollAngle3 = AeroParameters.getRollAngle(state3, self.currentConditions) state4 = getDummyState(self.dummyVelocity1, self.dummyOrientation5) rollAngle4 = AeroParameters.getRollAngle(state4, self.currentConditions) state5 = getDummyState(self.dummyVelocity2, self.dummyOrientation4) rollAngle5 = AeroParameters.getRollAngle(state5, self.currentConditions) state6 = getDummyState(self.dummyVelocity1, self.dummyOrientation3) rollAngle6 = AeroParameters.getRollAngle(state6, self.currentConditions) self.assertAlmostEqual(rollAngle1, 180) self.assertAlmostEqual(rollAngle2, 90) self.assertAlmostEqual(rollAngle3, 270) self.assertAlmostEqual(rollAngle4, 135) self.assertAlmostEqual(rollAngle5, 0) self.assertAlmostEqual(rollAngle6, 180) # Prep rocket by calling getLocalFrameAirVel first to cache AOA result, then get cached result from getTotalAOA def testLocalFrameAirVelRollAngle(vel, orientation, expectedResult): state = RigidBodyState(Vector(0,0,0), vel, orientation, AngularVelocity(rotationVector=Vector(0,0,0))) self.assertAlmostEqual(AeroParameters.getRollAngle(state, self.currentConditions), expectedResult) testLocalFrameAirVelRollAngle(self.dummyVelocity1, self.dummyOrientation4, 180) testLocalFrameAirVelRollAngle(self.dummyVelocity1, self.dummyOrientation1, 90) testLocalFrameAirVelRollAngle(self.dummyVelocity1, self.dummyOrientation2, 270) testLocalFrameAirVelRollAngle(self.dummyVelocity1, self.dummyOrientation5, 135) testLocalFrameAirVelRollAngle(self.dummyVelocity2, self.dummyOrientation4, 0) testLocalFrameAirVelRollAngle(self.dummyVelocity1, self.dummyOrientation3, 180)
def testLocalFrameAirVelNormalForceDir(vel, orientation, expectedResult, precision=7): state = RigidBodyState(Vector(0,0,0), vel, orientation, AngularVelocity(0,0,0)) assertVectorsAlmostEqual(self, AeroParameters.getNormalAeroForceDirection(state, self.currentConditions), expectedResult, precision)
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 _getPreComputedFinAeroData(self, rocketState, environment, CG): #General Info --------------------------------------------------------------------------------------------------------------------- Aref = self.rocket.Aref Mach = AeroParameters.getMachNumber(rocketState, environment) dynamicPressure = AeroParameters.getDynamicPressure( rocketState, environment) # Skin Friction Drag ------------------------------------------------------------------------------------------------------------- skinFrictionCoefficient = AeroFunctions.getSkinFrictionCoefficient( rocketState, environment, self.MACLength, Mach, self.surfaceRoughness, self.rocket.fullyTurbulentBL) # Adjust to the rocket reference area (skinFrictionCoefficient is based on wetted area) skinFrictionDragCoefficient = skinFrictionCoefficient * ( self.wettedArea / Aref) # Correct for additional surface area due to fin thickness - Niskanen Eqn 3.85 skinFrictionDragCoefficient *= (1 + 2 * self.thickness / self.MACLength) # Pressure Drag ------------------------------------------------------------------------------------------------------------------ # Leading edge drag coefficient if self.leadingEdgeShape == "Round": leadingEdgeCd = AeroFunctions.getCylinderCrossFlowCd_ZeroBaseDrag( Mach) LEthickness = self.leadingEdgeRadius * 2 elif self.leadingEdgeShape == "Blunt": leadingEdgeCd = AeroFunctions.getBluntBodyCd_ZeroBaseDrag(Mach) LEthickness = self.leadingEdgeThickness # Adjust for leading edge angle and convert reference area to rocket reference area - Barrowman Eqn 4-22 leadingEdgeCdAdjustmentFactor = LEthickness * self.span * cos( self.sweepAngle)**2 / Aref leadingEdgeCd *= leadingEdgeCdAdjustmentFactor # Trailing edge drag coefficient - simpler method from Niskanen section 3.4.4. Corrected to use only the trailing edge area, not the full fin frontal area # more intricate method available in Barrowman baseDragCd = AeroFunctions.getBaseDragCoefficient(Mach) if self.trailingEdgeShape == "Tapered": TEthickness = 0 # Zero base drag elif self.trailingEdgeShape == "Round": TEthickness = self.trailingEdgeRadius # 0.5 base drag elif self.trailingEdgeShape == "Blunt": TEthickness = self.trailingEdgeThickness # Full base drag # Convert to standard rocket reference Area trailingEdgeCd = baseDragCd * self.span * TEthickness / Aref # Thickness / Wave Drag #TODO: This section doesn't seem to be working quite right if Mach <= 1: # Thickness drag, subsonic thicknessDrag = 4*skinFrictionCoefficient*((self.thickness/self.rootChord)*cos(self.midChordSweep) + \ (30 * (self.thickness/self.rootChord)**4 * cos(self.midChordSweep)**2) / \ (self.subsonicFinThicknessK - Mach**2 * cos(self.midChordSweep)**2)**(3/2)) else: # Supersonic wave drag # Using simplistic method from Hoerner - assumes diamond profile (pg 17-12, Eqn 29) # TODO: Implement method from Barrowman's FIN program thicknessDrag = 2.3 * self.aspectRatio * (self.thickness / self.MACLength)**2 thicknessDrag *= self.planformArea / Aref pressureDragCoefficient = leadingEdgeCd + trailingEdgeCd + thicknessDrag # Total Drag -------------------------------------------------------------------------------------------------------------------- totalDragCoefficient = pressureDragCoefficient + skinFrictionDragCoefficient localFrameRocketVelocity = AeroParameters.getLocalFrameAirVel( rocketState, environment) axialPositionRelCG = self.position - CG finVelocityDueToRocketPitchYaw = rocketState.angularVelocity.crossProduct( axialPositionRelCG) airVelRelativeToFin = localFrameRocketVelocity - finVelocityDueToRocketPitchYaw # The negative puts it in the wind frame CPXPos = self._getCPXPos(Mach) #### Transfer info to fins #### return PreComputedFinAeroData(airVelRelativeToFin, CPXPos, totalDragCoefficient)
def _getAppliedForce(self, time, state): ''' Get the total force currently being experienced by the rocket, used by self.rigidBody to calculate the rocket's acceleration ''' ### Precomputations and Logging ### environment = self._getEnvironmentalConditions(time, state) # Get and log current air/wind properties rocketInertia = self.getInertia(time, state) # Get and log current rocket inertia if self.derivativeEvaluationLog is not None: self.derivativeEvaluationLog.newLogRow(time) self.derivativeEvaluationLog.logValue("Position(m)", state.position) self.derivativeEvaluationLog.logValue("Velocity(m/s)", state.velocity) ### Component Forces ### if not self.isUnderChute: # Precompute and log Mach = AeroParameters.getMachNumber(state, environment) unitRe = AeroParameters.getReynoldsNumber(state, environment, 1.0) AOA = AeroParameters.getTotalAOA(state, environment) rollAngle = AeroParameters.getRollAngle(state, environment) if self.derivativeEvaluationLog is not None: self.derivativeEvaluationLog.logValue("Mach", Mach) self.derivativeEvaluationLog.logValue("UnitRe", unitRe) self.derivativeEvaluationLog.logValue("AOA(deg)", math.degrees(AOA)) self.derivativeEvaluationLog.logValue("RollAngle(deg)", rollAngle) self.derivativeEvaluationLog.logValue("OrientationQuaternion", state.orientation) self.derivativeEvaluationLog.logValue("AngularVelocity(rad/s)", state.angularVelocity) # This function will be the inherited function CompositeObject.getAppliedForce componentForces = self.getAppliedForce(state, time, environment, rocketInertia.CG) else: # When under chute, neglect forces from other components componentForces = self.recoverySystem.getAppliedForce(state, time, environment, Vector(0,0,-1)) # Log the recovery system's applied force (Normally handled in CompositeObject.getAppliedForce) if hasattr(self.recoverySystem, "forcesLog"): self.recoverySystem.forcesLog.append(componentForces.force) self.recoverySystem.momentsLog.append(componentForces.moment) # Move Force-Moment system to rocket CG componentForces = componentForces.getAt(rocketInertia.CG) ### Gravity ### gravityForce = self.environment.getGravityForce(rocketInertia, state) totalForce = componentForces + gravityForce ### Launch Rail ### totalForce = self.environment.applyLaunchTowerForce(state, time, totalForce) if self.derivativeEvaluationLog is not None: self.derivativeEvaluationLog.logValue("Wind(m/s)", environment.Wind) self.derivativeEvaluationLog.logValue("AirDensity(kg/m^3)", environment.Density) self.derivativeEvaluationLog.logValue("CG(m)", rocketInertia.CG) self.derivativeEvaluationLog.logValue("MOI(kg*m^2)", rocketInertia.MOI) self.derivativeEvaluationLog.logValue("Mass(kg)", rocketInertia.mass) CPZ = AeroFunctions._getCPZ(componentForces) self.derivativeEvaluationLog.logValue("CPZ(m)", CPZ) self.derivativeEvaluationLog.logValue("AeroF(N)", componentForces.force) self.derivativeEvaluationLog.logValue("AeroM(Nm)", componentForces.moment) self.derivativeEvaluationLog.logValue("GravityF(N)", gravityForce.force) self.derivativeEvaluationLog.logValue("TotalF(N)", totalForce.force) return totalForce
def _barrowmanAeroFunc(self, rocketState, time, environment, precomputedData, CG=Vector(0, 0, 0), finDeflectionAngle=None): ''' Precomputed Data is a named tuple (PreComputedFinAeroData) which contains data/results from the parts of the fin aerodynamics calculation that are common to all fins in a FinSet (drag calculations mostly). These calculations are performed at the FinSet level. Only the parts of the Fin Aero Computation that change from fin to fin (normal force mostly, since different fins can have different angles of attack) are computed here ''' Mach = AeroParameters.getMachNumber(rocketState, environment) dynamicPressure = AeroParameters.getDynamicPressure( rocketState, environment) if finDeflectionAngle == None: finDeflectionAngle = self.finAngle # Adjusted by parent finset during each timestep, when the FinSet is controlled # Unpack precomputedData airVelRelativeToFin, CPXPos, totalDragCoefficient = precomputedData #### Compute normal force----------------------------------------------------------------------------------------------------------------- # Find fin normal vector after fin deflection finDeflectionRotation = Quaternion( axisOfRotation=self.spanwiseDirection, angle=radians(finDeflectionAngle)) finNormal = finDeflectionRotation.rotate(self.undeflectedFinNormal) # Get the tangential velocity component vector, per unit radial distance from the rocket centerline rollAngVel = AngularVelocity(0, 0, rocketState.angularVelocity.Z) unitSpanTangentialAirVelocity = rollAngVel.crossProduct( self.spanwiseDirection) * (-1) def subsonicNormalForce(Mach): # Subsonic linear method tempBeta = AeroParameters.getBeta(Mach) CnAlpha = getFinCnAlpha_Subsonic_Barrowman(self.span, self.planformArea, tempBeta, self.midChordSweep) return getSubsonicFinNormalForce(airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, self.spanwiseDirection, self.CPSpanWisePosition.length(), CnAlpha, self) def supersonicNormalForce(Mach): # Supersonic Busemann method gamma = AeroFunctions.getGamma() tempBeta = AeroParameters.getBeta(Mach) K1, K2, K3, Kstar = getBusemannCoefficients(Mach, tempBeta, gamma) # Mach Cone coords machAngle = asin(1 / Mach) machCone_negZPerRadius = 1 / tan(machAngle) machConeEdgeZPos = [] outerRadius = self.spanSliceRadii[-1] for i in range(len(self.spanSliceRadii)): machConeAtCurrentRadius = ( outerRadius - self.spanSliceRadii[i] ) * machCone_negZPerRadius + self.tipPosition machConeEdgeZPos.append(machConeAtCurrentRadius) return getSupersonicFinNormalForce( airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, machConeEdgeZPos, self.spanwiseDirection, self.CPSpanWisePosition.length(), K1, K2, K3, Kstar, self) if Mach <= 0.8: normalForceMagnitude, finMoment = subsonicNormalForce(Mach) elif Mach < 1.4: # Interpolate in transonic region # TODO: Do this with less function evaluations? Perhaps precompute AOA and Mach combinations and simply interpolate? Lazy precompute? Cython? x1, x2 = 0.8, 1.4 # Start, end pts of interpolated region dx = 0.001 # Find normal force and derivative at start of interpolation interval f_x1, m_x1 = subsonicNormalForce(x1) f_x12, m_x12 = subsonicNormalForce(x1 + dx) # Find normal force and derivative at end of interpolation interval f_x2, m_x2 = supersonicNormalForce(x2) f_x22, m_x22 = supersonicNormalForce(x2 + dx) normalForceMagnitude = Interpolation.cubicInterp( Mach, x1, x2, f_x1, f_x2, f_x12, f_x22, dx) finMoment = Interpolation.cubicInterp(Mach, x1, x2, m_x1, m_x2, m_x12, m_x22, dx) else: normalForceMagnitude, finMoment = supersonicNormalForce(Mach) # Complete redimensionalization of normal force coefficients by multiplying by dynamic pressure # Direct the normal force along the fin's normal direction normalForce = normalForceMagnitude * dynamicPressure * finNormal finMoment *= dynamicPressure #### Get axial force----------------------------------------------------------------------------------------------------------------------- avgAOA = getFinSliceAngleOfAttack( self.spanSliceRadii[round(len(self.spanSliceAreas) / 2)], airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, self.spanwiseDirection, self.stallAngle) # Approximate average fin AOA totalAxialForceCoefficient = AeroFunctions.getDragToAxialForceFactor( avgAOA) * totalDragCoefficient axialForceMagnitude = totalAxialForceCoefficient * self.rocket.Aref * dynamicPressure axialForceDirection = self.spanwiseDirection.crossProduct(finNormal) axialForce = axialForceDirection * axialForceMagnitude #### Get CP Location ---------------------------------------------------------------------------------------------------------------------- CPChordWisePosition = self.position - Vector( 0, 0, CPXPos ) # Ignoring the change in CP position due to fin deflection for now globalCP = self.CPSpanWisePosition + CPChordWisePosition #### Assemble total force moment system objects-------------------------------------------------------------------------------------------- totalForce = normalForce + axialForce return ForceMomentSystem(totalForce, globalCP, moment=Vector(0, 0, finMoment)), globalCP
def testLocalFrameAirVelAOA(vel, orientation, expectedResult): state = RigidBodyState(Vector(0,0,0), vel, orientation, AngularVelocity(rotationVector=Vector(0,0,0))) self.assertAlmostEqual(AeroParameters.getTotalAOA(state, self.currentConditions), math.radians(expectedResult))
def test_getAOSS(self): test1State = RigidBodyState(velocity=Vector(0, 1, 1)) expectedAOA = math.radians(45) computedAOA = AeroParameters.getAOSS(test1State, self.currentConditions) self.assertAlmostEqual(expectedAOA, computedAOA)
def test_getBeta(self): self.assertAlmostEqual(AeroParameters.getBeta(0.5), 0.86602540) self.assertAlmostEqual(AeroParameters.getBeta(1.5), 1.118033989)
def test_getReynoldsNumber(self): environment = self.environment.getAirProperties(Vector(0,0,0)) self.assertAlmostEqual(AeroParameters.getReynoldsNumber(self.rocketState1, environment, 1), 13701279.02972, 2)
def test_getMachNumber(self): environment = self.environment.getAirProperties(Vector(0,0,0)) # Assumes gamma=1.4, R=287 self.assertAlmostEqual(AeroParameters.getMachNumber(self.rocketState1, environment), 0.587781235)
def test_dynamicPressure(self): self.assertAlmostEqual(AeroParameters.getDynamicPressure(self.rocketState1,self.currentConditions), self.correctDynamicPressure1) self.assertAlmostEqual(AeroParameters.getDynamicPressure(self.rocketState3,self.currentConditions), self.correctDynamicPressure2)