def test_getAirProperties_TabulatedAtm(self): self.simDefinition.setValue("Environment.LaunchSite.elevation", "0") self.simDefinition.setValue("Environment.AtmosphericPropertiesModel", "TabulatedAtmosphere") self.simDefinition.setValue("Environment.TabulatedAtmosphere.filePath", "MAPLEAF/ENV/US_STANDARD_ATMOSPHERE.txt") stdAtm = Environment(self.simDefinition, silent=True) # Sea level calculatedSeaLevelProperties = stdAtm.getAirProperties(Vector(0, 0, 0)) expectedSeaLevelProperties = EnvironmentalConditions( 0.0, 288.15, 101300, 1.225, 1.789e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculatedSeaLevelProperties, expectedSeaLevelProperties) # 1000 m ASL calculated1kProperties = stdAtm.getAirProperties(Vector(0, 0, 1000)) expected1kProperties = EnvironmentalConditions(1000.0, 8.5 + 273.15, 89880, 1.112, 1.758e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculated1kProperties, expected1kProperties) # 3750 m ASL calculated3750Properties = stdAtm.getAirProperties(Vector(0, 0, 3750)) expected3570Properties = EnvironmentalConditions( 3750.0, -9.3575 + 273.15, 63775, 0.841875, 1.66925e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculated3750Properties, expected3570Properties)
def test_constAtmosphere(self): # Set constant properties in SimDefinition self.simDefinition.setValue("Environment.AtmosphericPropertiesModel", "Constant") self.simDefinition.setValue( "Environment.ConstantAtmosphere.temp", "20") # This should be converted to K -> 293.15 expected self.simDefinition.setValue("Environment.ConstantAtmosphere.pressure", "101325") self.simDefinition.setValue("Environment.ConstantAtmosphere.density", "1.225") self.simDefinition.setValue("Environment.ConstantAtmosphere.viscosity", "1.789e-05") env = Environment(self.simDefinition, silent=True) constAirProps1 = EnvironmentalConditions(100.0, 293.15, 101325, 1.225, 1.789e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) props1 = env.getAirProperties(Vector(0, 0, 100)) constAirProps2 = EnvironmentalConditions(1000.0, 293.15, 101325, 1.225, 1.789e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) props2 = env.getAirProperties(Vector(0, 0, 1000)) constAirProps3 = EnvironmentalConditions(10000.0, 293.15, 101325, 1.225, 1.789e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) props3 = env.getAirProperties(Vector(0, 0, 10000)) self.assertEqual(constAirProps1, props1) self.assertEqual(constAirProps2, props2) self.assertEqual(constAirProps3, props3)
class TestNosecone(unittest.TestCase): def setUp(self): simDef = SimDefinition("MAPLEAF/Examples/Simulations/test3.mapleaf") rocketDictReader = SubDictReader("Rocket", simDef) self.rocket = Rocket(rocketDictReader) self.environment = Environment(silent=True) self.currentConditions = self.environment.getAirProperties( Vector(0, 0, 200)) # m self.rocketState1 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 200), Quaternion(Vector(0, 0, 1), 0), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState2 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 200), Quaternion(Vector(1, 0, 0), math.radians(2)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState3 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 500), Quaternion(Vector(1, 0, 0), math.radians(2)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState4 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, -200), Quaternion(Vector(1, 0, 0), math.radians(180)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState5 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, -200), Quaternion(Vector(1, 0, 0), math.radians(178)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState6 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, -200), Quaternion(Vector(1, 0, 0), math.radians(182)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState7 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, -200), Quaternion(Vector(1, 0, 0), math.radians(90)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState8 = RigidBodyState( Vector(0, 0, 200), Vector(20.04, -0.12, -52.78), Quaternion(Vector(0, 1, 0), math.radians(90)), AngularVelocity(rotationVector=Vector(0, 0, 0))) def test_noseconeGetPlanformArea(self): nosecone = self.rocket.stages[0].getComponentsOfType(NoseCone)[0] self.assertAlmostEqual(nosecone._getPlanformArea(), 0.077573818, 3) #From solidworks
def test_getAirProperties_USSTDA(self): self.simDefinition.setValue("Environment.LaunchSite.elevation", "0") self.simDefinition.setValue("Environment.AtmosphericPropertiesModel", "USStandardAtmosphere") stdAtm = Environment(self.simDefinition, silent=True) # Sea level calculatedSeaLevelProperties = stdAtm.getAirProperties(Vector(0, 0, 0)) expectedSeaLevelProperties = EnvironmentalConditions( 0.0, 288.15, 101325, 1.225, 1.789e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculatedSeaLevelProperties, expectedSeaLevelProperties, 5) # 1000 m ASL calculated1kProperties = stdAtm.getAirProperties(Vector(0, 0, 1000)) expected1kProperties = EnvironmentalConditions(1000.0, 8.501 + 273.15, 89876, 1.117, 1.758e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculated1kProperties, expected1kProperties, 0) # 3750 m ASL calculated3750Properties = stdAtm.getAirProperties(Vector(0, 0, 3750)) expected3570Properties = EnvironmentalConditions( 3750.0, -9.361 + 273.15, 63693, 0.84115, 1.66925e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculated3750Properties, expected3570Properties, 0) # 10,550 m ASL calculatedProperties = stdAtm.getAirProperties(Vector(0, 0, 10550)) expectedProperties = EnvironmentalConditions(10550, 219.575, 24284, 0.38529, 1.457e-6, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) # assertIterablesAlmostEqual(self, calculatedProperties, expectedProperties, 0) # 85,500 m ASL calculatedProperties = stdAtm.getAirProperties(Vector(0, 0, 85500)) expectedProperties = EnvironmentalConditions(85500, 187.920, 0.40802, 0.0000075641, 1.25816e-5, Vector(0, 0, 0), Vector(0, 0, 0), Vector(0, 0, 0)) assertIterablesAlmostEqual(self, calculatedProperties, expectedProperties, 1) # Check that above 86 km the density is zero calculatedProperties = stdAtm.getAirProperties(Vector(0, 0, 85500))
class TestAeroFunctions(unittest.TestCase): def setUp(self): removeLogger() self.dummyVelocity1 = Vector(0, 0, 50) self.dummyVelocity2 = Vector(1, 0, 20) self.dummyVelocity3 = Vector(0, 0, -100) self.dummyOrientation1 = Quaternion(Vector(1, 0, 0), math.radians(2)) self.dummyOrientation2 = Quaternion(Vector(1, 0, 0), math.radians(-2)) self.dummyOrientation3 = Quaternion(Vector(0, 1, 0), math.radians(2)) self.dummyOrientation4 = Quaternion(Vector(1, 0, 0), 0) self.dummyOrientation5 = Quaternion(Vector(1, 1, 0), math.radians(2)) self.dummyOrientation6 = Quaternion(Vector(1, 0, 0), math.radians(90)) self.environment = Environment(silent=True) self.currentConditions = self.environment.getAirProperties( Vector(0, 0, 200)) self.rocketState1 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 200), Quaternion(Vector(0, 0, 1), 0), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState3 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 500), Quaternion(Vector(1, 0, 0), math.radians(2)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState4 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, -200), Quaternion(Vector(1, 0, 0), math.radians(180)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState8 = RigidBodyState( Vector(0, 0, 200), Vector(20.04, -0.12, -52.78), Quaternion(Vector(0, 1, 0), math.radians(90)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.correctDynamicPressure1 = self.currentConditions.Density * self.rocketState1.velocity.length( )**2 / 2 self.correctDynamicPressure2 = self.currentConditions.Density * self.rocketState3.velocity.length( )**2 / 2 def test_BarrowmanCPLocation(self): # Check that for a cone, XCP = 0.666 Length length = 1 baseArea = 1 tipArea = 0 volume = baseArea * length / 3 XCP_cone = AeroFunctions.Barrowman_GetCPLocation( length, tipArea, baseArea, volume) self.assertAlmostEqual(XCP_cone.Z, -0.666666666) # Check that for a tangent ogive nosecone, XCP = 0.466 Length SimRunner = Simulation("MAPLEAF/Examples/Simulations/test3.mapleaf", silent=True) rocket = SimRunner.createRocket() rocketNosecone = rocket.stages[0].getComponentsOfType(NoseCone)[0] noseconeLength = rocketNosecone.length expectedCp = rocketNosecone.position + Vector( 0, 0, -0.46666 * noseconeLength) actualCp = rocketNosecone.CPLocation assertVectorsAlmostEqual(self, expectedCp, actualCp, 2) def test_CPZ(self): force = Vector(0, 1, 0) location = Vector(0, 0, 0) moment = Vector(1, 0, 0) testForce = ForceMomentSystem(force, location, moment) expectedCPZ = -1 calculatedCPZ = AeroFunctions._getCPZ(testForce) self.assertAlmostEqual(expectedCPZ, calculatedCPZ) def test_getDragToAxialForceFactor(self): # Should be 0 at 0, 1.3 at 17, and 0 at 90 AOAsToTest = [0, 17, 90] AOAsToTest = [math.radians(AOA) for AOA in AOAsToTest] results = [ AeroFunctions.getDragToAxialForceFactor(AOA) for AOA in AOAsToTest ] expectedResults = [1, 1.3, 0] for i in range(3): self.assertAlmostEqual(results[i], expectedResults[i], 3) def test_getSkinFrictionCoeff(self): env = self.currentConditions length = 1 def checkSkinFriction(Re, roughness, Mach, expectedCf, fullyTurbulent=True): state = createStateWithRe(Re, env, length) coeff = AeroFunctions.getSkinFrictionCoefficient( state, env, length, Mach, roughness, fullyTurbulent) self.assertAlmostEqual(coeff, expectedCf) # Min Re checkSkinFriction(0, 0, 0, 0.0148) checkSkinFriction(9999, 0, 0, 0.0148) # Fully Turbulent Flow checkSkinFriction(10000, 0, 0, 0.014815997) checkSkinFriction(100000, 0, 0, 0.007343512274) # Transitional Flow checkSkinFriction(10000, 0, 0, 0.01328, False) checkSkinFriction(500001, 0, 0, 0.001641693326, False) checkSkinFriction(5000000, 0, 0, 0.002911385467, False) # Flow with roughness roughness = 0.00002 Recrit = 3888660.338 checkSkinFriction(3888500, 0.00002, 0, 0.002958676686, False) # Check transitional flow again checkSkinFriction(3888500, 0.00002, 0, 0.003395863262, True) # Check turbulent flow again checkSkinFriction(3888700, 0.00002, 0, 0.003675834736, False) # Check roughness-determined friction checkSkinFriction(3888700, 0.00002, 0, 0.003675834736, True) # Check roughness-determined friction def test_getSubsonicCompressibilityCorrectionFactor(self): smooth = True Mach = 0.5 expected = 0.975 self.assertAlmostEqual( AeroFunctions._subSonicSkinFrictionCompressiblityCorrectionFactor( Mach, smooth), expected) smooth = False Mach = 0.5 expected = 0.97 self.assertAlmostEqual( AeroFunctions._subSonicSkinFrictionCompressiblityCorrectionFactor( Mach, smooth), expected) def test_supersonicSkinFrictionCompressibilityCorrection(self): smooth = True Mach = 1.5 expected = 0.844791628 self.assertAlmostEqual( AeroFunctions. _supersonicSkinFrictionCompressiblityCorrectionFactor( Mach, smooth), expected) def test_getCylindricalSkinFriction(self): env = self.currentConditions length = 1 Mach = 0 rough = 0 Area = 1 finenessRatio = 2 turbulent = True def checkSkinFriction(Re, roughness, Mach, finenessRatio, expectedCf, fullyTurbulent=True): state = createStateWithRe(Re, env, length) coeff, rollDamping = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment( state, env, length, Mach, roughness, 1, 1, finenessRatio, fullyTurbulent) self.assertAlmostEqual(coeff, expectedCf) self.assertAlmostEqual(rollDamping.length(), 0) checkSkinFriction(9999, rough, Mach, finenessRatio, 0.0185) def test_BarrowmanGetCN(self): AOA = 0.1 #rad Aref = 1 xArea_top = 1 xArea_bottom = 2 expected = 0.199666833 self.assertAlmostEqual( AeroFunctions.Barrowman_GetCN(AOA, Aref, xArea_top, xArea_bottom), expected) def test_BarrowmanCPLocation(self): length = 1 xArea_top = 1 xArea_bottom = 2 vol = 1.5 expected = Vector(0, 0, -0.5) assertVectorsAlmostEqual( self, expected, AeroFunctions.Barrowman_GetCPLocation(length, xArea_top, xArea_bottom, vol)) xArea_top = 1 xArea_bottom = 1 expected = Vector(0, 0, -0.5) assertVectorsAlmostEqual( self, expected, AeroFunctions.Barrowman_GetCPLocation(length, xArea_top, xArea_bottom, vol))
class Rocket(CompositeObject): ''' Class used to represent a single flying rigid body composed of `MAPLEAF.Rocket.Stage` objects. New instances of this class are also created to model the flight of dropped stages. ''' #### Initialization #### def __init__(self, rocketDictReader, silent=False, stageToInitialize=None, simRunner=None, environment=None): ''' Initialization of Rocket(s) is most easily completed through an instance of Simulation To get a single Rocket object, initialize a Simulation and call `MAPLEAF.SimulationRunners.Simulation.createRocket()`. This will return a Rocket initialized on the pad with all its stages, ready for flight. If initializing manually, can either provide fileName or simDefinition. If a simDefinition is provided, it will be used and fileName will be ignored. Inputs: * rocketDictReader: (`MAPLEAF.IO.SubDictReader`) SubDictReader pointed at the "Rocket" dictionary of the desired simulation definition file. * silent: (bool) controls console output * stageToInitialize: (int or None) controls whether to initialize a complete Rocket or a single (usually dropped) stage. None = initialize complete rocket. n = initialize only stage n, where n >= 1. * simRunner: (`MAPLEAF.SimulationRunners.Simulation`) reference to the current simulation driver/runner * environment: (`MAPLEAF.ENV.Environment`) environment model from which the rocket will retrieve atmospheric properties and wind speeds ''' self.rocketDictReader = rocketDictReader self.simDefinition = rocketDictReader.simDefinition self.simRunner = simRunner ''' Parent instance of `MAPLEAF.SimulationRunners.Simulation` (or derivative sim runner). This is usually the object that has created the current instance of Rocket. ''' self.environment = environment ''' Instance of `MAPLEAF.ENV.Environment` ''' if self.environment == None: # If no environment is passed in, create one self.environment = Environment(self.simDefinition, silent=silent) self.name = rocketDictReader.getString("name") self.silent = silent ''' Controls output to console ''' self.stage = stageToInitialize ''' If controls whether the whole rocket is initialized (if == None), or a single stage is initialized (Integer stage number) ''' self.stages = [] ''' A list of `MAPLEAF.Rocket.Stage` objects that make up the rocket, ordered from top to bottom. Populated by `_initializeStages`. ''' self.recoverySystem = None ''' Reference to the current Rocket's (which can represent a dropped stage) recovery system. Only a single recovery system is allowed per stage. Set in `MAPLEAF.Rocket.RecoverySystem.__init__` ''' self.rigidBody = None ''' (`MAPLEAF.Motion.RigidBody` or `MAPLEAF.Motion.RigidBody_3DoF`) Responsible for motion integration. Set in `_initializeRigidBody()`. ''' self.isUnderChute = False ''' (bool) Controlled by `MAPLEAF.Rocket.Recovery.RecoverySystem._deployNextStage()` ''' self.mainChuteDeployTime = None ''' (float) Filled in during flight by `MAPLEAF.Rocket.Recovery.RecoverySystem._deployNextStage()` ''' self.engineShutOffTime = None ''' (float) Filled in by `MAPLEAF.Rocket.Propulsion.TabulatedMotor.__init__()` upon initialization ''' self.turbulenceOffWhenUnderChute = rocketDictReader.getBool("Environment.turbulenceOffWhenUnderChute") ''' (bool) ''' self.maxDiameter = self._getMaxBodyTubeDiameter() ''' (float) Holds maximum constant-size body tube diameter, from bodytube components in stages ''' self.Aref = math.pi * self.maxDiameter**2 / 4 ''' Reference area for force and moment coefficients. Maximum rocket cross-sectional area. Remains constant during flight to retain a 1:1 relationship b/w coefficients in different parts of flight. Always based on the maximum body tube diameter in the fully-assembled rocket. ''' # TODO: Remove self.targetLocation = None self.simEventDetector = SimEventDetector(self) ''' (`MAPLEAF.Rocket.SimEventDetector`) Used to trigger things like recovery systems and staging ''' self.eventTimeStep = rocketDictReader.getFloat("SimControl.TimeStepAdaptation.eventTimingAccuracy") ''' If using an adaptive time stepping method, the time step will be overridden near non-time-deterministic discrete events, possibly all the way down to this minimum value ''' self.addZeroLengthBoatTailsToAccountForBaseDrag = rocketDictReader.getBool("Aero.addZeroLengthBoatTailsToAccountForBaseDrag") ''' Controls whether zero-length boat tails are automatically added to the bottom of rockets without them, to make sure base drag is accounted for ''' self.fullyTurbulentBL = rocketDictReader.getBool("Aero.fullyTurbulentBL") ''' Controls whether skin friction is solved assuming a fully turbulent BL or using laminar/transitional flow at lower Reynolds numbers ''' self.surfaceRoughness = rocketDictReader.getFloat("Aero.surfaceRoughness") ''' Default surface roughness for all rocket components ''' self.finenessRatio = None ''' Used in some aerodynamic functions. Updated after initializing subcomponents, and throughout the flight. None if no BodyComponent(s) are present in the rocket ''' self.engineShutOff = False '''Used to shut off engines in MAPLEAF.Rocket.Propulsion.DefinedMotor class. Currently set in MAPLE_AF.GNC.Navigation''' #### Init Hardware in the loop #### subDicts = rocketDictReader.getImmediateSubDicts() if "Rocket.HIL" in subDicts: self.hardwareInTheLoopControl = "yes" quatUpdateRate = rocketDictReader.getInt("HIL.quatUpdateRate") posUpdateRate = rocketDictReader.getInt("HIL.posUpdateRate") velUpdateRate = rocketDictReader.getInt("HIL.velUpdateRate") teensyComPort = rocketDictReader.getString("HIL.teensyComPort") imuComPort = rocketDictReader.getString("HIL.imuComPort") teensyBaudrate = rocketDictReader.getInt("HIL.teensyBaudrate") imuBaudrate = rocketDictReader.getInt("HIL.imuBaudrate") self.hilInterface = HILInterface(quatUpdateRate,posUpdateRate,velUpdateRate, teensyComPort, imuComPort, teensyBaudrate, imuBaudrate) else: self.hardwareInTheLoopControl = "no" #### Initialize Logging #### self.timeStepLog = None ''' Log containing one entry per time step, logs rocket state. None if logging level == 0 ''' self.derivativeEvaluationLog = None ''' Log containing one entry per rocket motion derivative evaluation, contains component forces. None if logging level < 2 ''' loggingLevel = int(self.simDefinition.getValue("SimControl.loggingLevel")) self.loggingLevel = loggingLevel if loggingLevel > 0: # Create the time step log and add columns to track the rocket state between each time step self.timeStepLog = TimeStepLog() zeroVector = Vector(0,0,0) self.timeStepLog.addColumn("Position(m)", zeroVector) self.timeStepLog.addColumn("Velocity(m/s)", zeroVector) self.timeStepLog.addColumn("OrientationQuaternion", Quaternion(0,0,0,0)) self.timeStepLog.addColumn("EulerAngle(rad)", zeroVector) self.timeStepLog.addColumn("AngularVelocity(rad/s)", zeroVector) if "Adapt" in self.simDefinition.getValue("SimControl.timeDiscretization"): self.timeStepLog.addColumn("EstimatedIntegrationError", 0) if loggingLevel > 1: self.derivativeEvaluationLog = Log() zeroVector = Vector(0,0,0) self.derivativeEvaluationLog.addColumn("Position(m)", zeroVector) self.derivativeEvaluationLog.addColumn("Velocity(m/s)", zeroVector) self.derivativeEvaluationLog.addColumn("OrientationQuaternion", Quaternion(0,0,0,0)) self.derivativeEvaluationLog.addColumn("AngularVelocity(rad/s)", zeroVector) self.derivativeEvaluationLog.addColumn("CG(m)", zeroVector) self.derivativeEvaluationLog.addColumn("MOI(kg*m^2)", zeroVector) self.derivativeEvaluationLog.addColumn("Mass(kg)", 0) self.derivativeEvaluationLog.addColumn("Wind(m/s)", zeroVector) self.derivativeEvaluationLog.addColumn("AirDensity(kg/m^3)", 0) self.derivativeEvaluationLog.addColumn("Mach", 0) self.derivativeEvaluationLog.addColumn("UnitRe", 0) self.derivativeEvaluationLog.addColumn("AOA(deg)", 0) self.derivativeEvaluationLog.addColumn("RollAngle(deg)", 0) self.derivativeEvaluationLog.addColumn("CPZ(m)", 0) self.derivativeEvaluationLog.addColumn("AeroF(N)", zeroVector) self.derivativeEvaluationLog.addColumn("AeroM(Nm)", zeroVector) self.derivativeEvaluationLog.addColumn("GravityF(N)", zeroVector) self.derivativeEvaluationLog.addColumn("TotalF(N)", zeroVector) #### Init Components #### self._createStages() self._initializeRigidBody() self._switchToStatefulRigidBodyIfRequired() self._sortStagesAndComponents() self._initializeStagingTriggers() self._precomputeComponentProperties() #### Init Parent classes #### CompositeObject.__init__(self, self.stages) self._updateFinenessRatio() # Adjust rigid body state position to correspond to the rocket's CG instead of nose cone tip position self._moveStatePositionToCG() #### Init Guidance/Navigation/Control System (if required) #### self.controlSystem = None ''' None for uncontrolled rockets. `MAPLEAF.GNC.ControlSystems.RocketControlSystem` for controlled rockets ''' if ( rocketDictReader.tryGetString("ControlSystem.controlledSystem") != None or rocketDictReader.tryGetString("ControlSystem.MomentController.Type") == "IdealMomentController") and stageToInitialize == None: # Only create a control system if this is NOT a dropped stage ControlSystemDictReader = SubDictReader("Rocket.ControlSystem", simDefinition=self.simDefinition) controlSystemLogging = loggingLevel > 3 self.controlSystem = RocketControlSystem(ControlSystemDictReader, self, log=controlSystemLogging, silent=silent) def _getMaxBodyTubeDiameter(self): ''' Gets max body tube diameter directly from config file ''' stageDicts = self._getStageSubDicts() maxDiameter = 0 for stageDict in stageDicts: componentDicts = self.rocketDictReader.getImmediateSubDicts(stageDict) for componentDict in componentDicts: className = self.rocketDictReader.getString(componentDict + ".class") if className == "Bodytube": diameter = self.rocketDictReader.getFloat(componentDict + ".outerDiameter") maxDiameter = max(maxDiameter, diameter) return maxDiameter def _getStageSubDicts(self): # Get all immediate subdictionaries of 'Rocket' stageDicts = self.rocketDictReader.getImmediateSubDicts() # Assume all subdictionaries represent rocket stages except for these exceptions nonStageSubDicts = [ "Rocket.ControlSystem", "Rocket.HIL", "Rocket.Aero" ] # Remove them from the list if they're in it for dictName in nonStageSubDicts: if dictName in stageDicts: stageDicts.remove(dictName) return stageDicts def _initializeRigidBody(self): #### Get initial kinematic state (in launch tower frame) #### initPos = self.rocketDictReader.getVector("position") initVel = self.rocketDictReader.getVector("velocity") # Check whether precise initial orientation has been specified rotationAxis = self.rocketDictReader.tryGetVector("rotationAxis", defaultValue=None) if rotationAxis != None: rotationAngle = math.radians(self.rocketDictReader.getFloat("rotationAngle")) initOrientation = Quaternion(rotationAxis, rotationAngle) else: # Calculate initial orientation quaternion in launch tower frame initialDirection = self.rocketDictReader.getVector("initialDirection").normalize() angleFromVertical = Vector(0,0,1).angle(initialDirection) rotationAxis = Vector(0,0,1).crossProduct(initialDirection) initOrientation = Quaternion(rotationAxis, angleFromVertical) initAngVel = AngularVelocity(rotationVector=self.rocketDictReader.getVector("angularVelocity")) initState_launchTowerFrame = RigidBodyState(initPos, initVel, initOrientation, initAngVel) # Convert to the global inertial frame initState_globalInertialFrame = self.environment.convertInitialStateToGlobalFrame(initState_launchTowerFrame) # Get desired time discretization method timeDisc = self.rocketDictReader.getString("SimControl.timeDiscretization") #TODO: Check for additional parameters to integrate - if they exist create a StatefulRigidBody + RocketState instead! # Ask each rocket component whether it would like to add parameters to integrate after all the components have been initialized discardDtCallback = None if (self.simRunner == None) else self.simRunner.discardForceLogsForPreviousTimeStep #### Initialize the rigid body #### self.rigidBody = RigidBody( initState_globalInertialFrame, self._getAppliedForce, self.getInertia, integrationMethod=timeDisc, discardedTimeStepCallback=discardDtCallback, simDefinition=self.simDefinition ) def _createStages(self): ''' Initialize each of the stages and all of their subcomponents. ''' stageDicts = self._getStageSubDicts() # If we're initializing a dropped stage, figure out which one if self.stage != None: stageNumbers = [] stageNumberSet = set() for stageDict in stageDicts: stageNumber = self.rocketDictReader.getFloat(stageDict + ".stageNumber") stageNumbers.append(stageNumber) stageNumberSet.add(stageNumber) if len(stageNumbers) != len(stageNumberSet): raise ValueError("For multi-stage rockets, each stage must have a unique stage number. Set the Rocket.StageName.stageNumber value for each stage. 0 for first stage, 1 for second, etc...") stageNumbers.sort() stageToInitialize = stageNumbers[self.stage] # Initialize Stage(s) initializingAllStages = (self.stage == None) for stageDictionary in stageDicts: stageDictReader = SubDictReader(stageDictionary, self.simDefinition) stageNumber = stageDictReader.getFloat("stageNumber") if initializingAllStages or stageNumber == stageToInitialize: newStage = Stage(stageDictReader, self) self.stages.append(newStage) if newStage.name not in self.__dict__: # Avoid clobbering existing info setattr(self, newStage.name, newStage) # Make stage available as rocket.stageName def _sortStagesAndComponents(self): # Create Planar Interfaces b/w components inside stages for stage in self.stages: stage.components = PlanarInterface.sortByZLocation(stage.components, self.rigidBody.state) stage.componentInterfaces = PlanarInterface.createPlanarComponentInterfaces(stage.components) # Create planar interfaces b/w stages self.stages = PlanarInterface.sortByZLocation(self.stages, self.rigidBody.state) self.stageInterfaces = PlanarInterface.createPlanarComponentInterfaces(self.stages) if self.maxDiameter > 0: # Only run this if we're running a real rocket with body tubes self._ensureBaseDragIsAccountedFor() def _initializeStagingTriggers(self): ''' Set up trigger conditions for staging ''' # Self.stage is not passed in if the current instance represents a rocket ready to launch - then we have to set up staging events if self.stage == None: for stageIndex in range(len(self.stages)): stage = self.stages[stageIndex] if stage.separationConditionType != 'None': self.simEventDetector.subscribeToEvent( stage.separationConditionType, self._stageSeparation, stage.separationConditionValue, triggerDelay=stage.separationDelay ) if stageIndex != len(self.stages)-1: # Set all upper stage motors to ignite a very long time in the future stage.motor.updateIgnitionTime(1000000000, fakeValue=True) else: # Otherwise this rocket object is representing a single dropped stage, and no stage separations are necessary # Motor is burned out self.stages[0].motor.updateIgnitionTime(-1000000000, fakeValue=True) def _switchToStatefulRigidBodyIfRequired(self): ''' Query all of the rocket components to see if any of them are stateful by attempting to call their getExtraParametersToIntegrate function If the rocket contains stateful components, the rocket state is converted to a StateList and all of these state variables requested by components are added to it Otherwise, nothing changes and the rocket state remains a RigidBodyState ''' varNames = [] initVals = [] derivativeFuncs = [] # Query all of the components for stage in self.stages: for component in stage.components: try: paramNames, initValues, derivativeFunctions = component.getExtraParametersToIntegrate() varNames += paramNames initVals += initValues derivativeFuncs += derivativeFunctions except AttributeError: pass # No extra parameters to integrate # If any of the components are stateful, keep track of their states in the main rocket state if len(varNames) > 0: if len(varNames) != len(initVals) or len(initVals) != len(derivativeFuncs): raise ValueError("ERROR: Mismatch in number of extra parameters names ({}), init values({}), and derivative functions({}) to integrate".format(len(varNames), len(initVals), len(derivativeFuncs))) # Switch to a stateful rigid body initState = self.rigidBody.state forceFunc = self.rigidBody.forceFunc inertiaFunc = self.rigidBody.inertiaFunc integrationMethod = self.rocketDictReader.getString("SimControl.timeDiscretization") discardDtCallback = None if (self.simRunner == None) else self.simRunner.discardForceLogsForPreviousTimeStep self.rigidBody = StatefulRigidBody( initState, forceFunc, inertiaFunc, integrationMethod=integrationMethod, discardedTimeStepCallback=discardDtCallback, simDefinition=self.simDefinition ) # Add the additional state variables for i in range(len(varNames)): self.rigidBody.addStateVariable(varNames[i], initVals[i], derivativeFuncs[i]) def _precomputeComponentProperties(self): for stage in self.stages: for component in stage.components: try: component.precomputeProperties() except AttributeError: pass def _moveStatePositionToCG(self): ''' Moves self.rigidBody.state.position to have it represent the rocket's initial CG position, not the initial nose cone position ''' initInertia = self.getInertia(0, self.rigidBody.state) CGPosition_wrtNoseCone_localFrame = initInertia.CG CGPosition__wrtNoseCone_globalFrame = self.rigidBody.state.orientation.rotate(CGPosition_wrtNoseCone_localFrame) CGPosition_globalFrame = self.rigidBody.state.position + CGPosition__wrtNoseCone_globalFrame self.rigidBody.state.position = CGPosition_globalFrame def getLength(self): totalLength = 0 for stage in self.stages: try: totalLength += stage.getLength() except TypeError: pass # Stage Length was None - no body components in stage return totalLength def plotShape(self): plt.figure() plt.gca().set_aspect('equal') rocketInertia = self.getInertia(0, self.rigidBody.state) TotalCG = rocketInertia.CG.Z TotalMass = rocketInertia.mass TotalCGplt = plt.plot(TotalCG, 0, color='b', marker='d', label='Total CG', linestyle='None') CGsubZ = [] CGsubY = [] plt.title('Total Rocket CG: %10.4f m \n Total Rocket Mass: %10.4f Kg' % (TotalCG,TotalMass) ) for stage in self.stages: zCGs, yCGs = stage.plotShape() # Add subcomponents CGs to arrays CGsubZ += zCGs CGsubY += yCGs SubCGplt = plt.plot(CGsubZ, CGsubY, color='g', marker='.', label='Subcomponent CG', linestyle='None') legendHeight = self.maxDiameter plt.legend(loc='upper center', bbox_to_anchor = (0.5,-1.05)) plt.show() #### Stage Separation #### def _stageSeparation(self): print("Stage {} Separation".format(self.simRunner.stagingIndex + 1)) # Initialize dropped stage as a new rocket self.simRunner.createNewDetachedStage() # Drop stage from current rocket self._dropStage() # Ignite next motor (set ignition time to the time of stage separation) currentTime = self.rigidBody.time self.stages[0].motor.updateIgnitionTime(currentTime) self._ensureBaseDragIsAccountedFor() self._updateFinenessRatio() def _dropStage(self, stageIndex=-1): droppedStage = self.stages.pop(stageIndex) delattr(self, droppedStage.name) self.recomputeFixedMassInertia() if self.controlSystem != None: # Check whether the controlled system was part of the dropped stage if self.controlSystem.controlledSystem in droppedStage.components: # If so delete the rocket's control system and remove any control system-induced time stepping modifications print("Rocket's controlled system was on the dropped stage. Deactivating control system.") self.controlSystem.restoreOriginalTimeStepping() self.controlSystem = None def _ensureBaseDragIsAccountedFor(self): ''' If no BoatTail exists at the bottom of the rocket, adds a zero-length boat tail. This is necessary b/c Boat Tail aero-functions are the ones that account for base drag ''' boatTailComponentAtBottomOfRocket = False bottomStage = self.stages[-1] for comp in reversed(bottomStage.components): if isinstance(comp, BodyComponent): if isinstance(comp, BoatTail): boatTailComponentAtBottomOfRocket = True break if not boatTailComponentAtBottomOfRocket and self.addZeroLengthBoatTailsToAccountForBaseDrag: if not self.silent: print("Adding zero-length BoatTail to the bottom of current bottom stage ({}) to account for base drag".format(bottomStage.name)) # Create a zero-length, zero-mass boat tail to account for base drag zeroInertia = Inertia(Vector(0,0,0), Vector(0,0,0), 0) diameter = self.maxDiameter # TODO: Get the actual bottom-body-tube diameter from a future Stage.getRadius function length = 0 position = bottomStage.getBottomInterfaceLocation() boatTail = BoatTail( diameter, diameter, length, position, zeroInertia, self, bottomStage, "Auto-AddedZeroLengthBoatTail", self.surfaceRoughness ) initializeForceLogging(boatTail, "FakeRocketName.Auto-AddedZeroLengthBoatTail", self) bottomStage.components.append(boatTail) def _updateFinenessRatio(self): ''' Updates self.finenessRatio based on current BodyComponents in rocket stages ''' length = self.getLength() maxDiameter = max([ stage.getMaxDiameter() for stage in self.stages ]) if maxDiameter == 0: self.finenessRatio = None else: self.finenessRatio = length/maxDiameter #### Component-buildup method for Force #### def _getEnvironmentalConditions(self, time, state): env = self.environment.getAirProperties(state.position, time) # Neglect turbulent component of wind if required if self.isUnderChute and self.turbulenceOffWhenUnderChute: env = EnvironmentalConditions( env.ASLAltitude, env.Temp, env.Pressure, env.Density, env.DynamicViscosity, env.MeanWind, env.MeanWind, Vector(0, 0, 0), ) return env 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 #### Driving / Controlling Simulation #### def _logState(self): ''' Logs the initial state of the rocket to the time step log ''' state = self.rigidBody.state time = self.rigidBody.time if self.timeStepLog is not None: self.timeStepLog.newLogRow(time) self.timeStepLog.logValue("Position(m)", state.position) self.timeStepLog.logValue("Velocity(m/s)", state.velocity) try: # 6DoF Mode self.timeStepLog.logValue("OrientationQuaternion", state.orientation) self.timeStepLog.logValue("AngularVelocity(rad/s)", state.angularVelocity) # Also log NED Tait-Bryan 3-2-1 z-y-x Euler Angles if in 6DoF mode globalOrientation = state.orientation orientationOfNEDFrameInGlobalFrame = self.environment.earthModel.getInertialToNEDFrameRotation(*state.position) orientationRelativeToNEDFrame = orientationOfNEDFrameInGlobalFrame.conjugate() * globalOrientation eulerAngles = orientationRelativeToNEDFrame.toEulerAngles() self.timeStepLog.logValue("EulerAngle(rad)", eulerAngles) except AttributeError: pass # 3DoF mode # Print the current time and altitude to the console altitude = self.environment.earthModel.getAltitude(*state.position) consoleOutput = "{:<8.4f} {:>6.5f}".format(time, altitude) print(consoleOutput) def _runControlSystem(self): ''' Attempts to run the rocket control system (only runs if it's time to run again, based on its updated rate) (updating target positions for actuators) ''' if self.controlSystem != None and not self.isUnderChute: state = self.rigidBody.state time = self.rigidBody.time environment = self._getEnvironmentalConditions(time, state) ### Run Control Loop ### self.controlSystem.runControlLoopIfRequired(time, state, environment) def timeStep(self, dt: float): ''' Tells the simulation to take a time step of size dt. Usually called by functions like `MAPLEAF.SimulationRunners.Simulation.run()` Returns: * timeStepAdaptationFactor: (float) indicates the factor by which the adaptive time stepping method would like to change the timestep for the next timestep (1.0 for non-adaptive methods) * dt: (float) actual size of time step taken. Adaptive methods will override the dt asked for if the predicted error for a time step is over their error threshold. ''' # Stop the rocket from sliding off the bottom of the launch rail self.rigidBody.state = self.environment.applyLaunchRailMotionConstraints(self.rigidBody.state, self.rigidBody.time) # Trigger any events that occurred during the last time step estimatedTimeToNextEvent, accuratePrediction = self.simEventDetector.triggerEvents() # If required, override time step to accurately resolve upcoming discrete events if "Adapt" in self.rigidBody.integrate.method and estimatedTimeToNextEvent < dt: if accuratePrediction: # For time-deterministic events, just set the time step to ever so slightly past the event newDt = estimatedTimeToNextEvent + 1e-5 print("Rocket + SimEventDetector overriding time step from {} to {} to accurately trigger resolve time-deterministic event.".format(dt, newDt)) dt = newDt else: # For time-nondeterministic events, slowly approach the event newDt = max(estimatedTimeToNextEvent/1.5, self.eventTimeStep) estimatedOccurenceTime = self.rigidBody.time + estimatedTimeToNextEvent print("Rocket + SimEventDetector overriding time step from {} to {} to accurately resolve upcoming event. Estimated occurence at: {}".format(dt, newDt, estimatedOccurenceTime)) dt = newDt self._runControlSystem() self._logState() # Take timestep integrationResult = self.rigidBody.timeStep(dt) # If required, log estimated integration error if "Adapt" in self.rigidBody.integrate.method and self.timeStepLog is not None: self.timeStepLog.logValue("EstimatedIntegrationError", integrationResult.errorMagEstimate) return integrationResult def _switchTo3DoF(self): ''' Switch to 3DoF simulation after recovery system is deployed ''' print("Switching to 3DoF simulation") new3DoFState = RigidBodyState_3DoF(self.rigidBody.state.position, self.rigidBody.state.velocity) # Re-read time discretization in case an adaptive method has been selected while using a fixed-update rate control system - in that case, want to switch back to adaptive time stepping for the recovery (uncontrolled) portion of the flight originalTimeDiscretization = self.rocketDictReader.getString("SimControl.timeDiscretization") if self.simRunner != None: self.rigidBody = RigidBody_3DoF( new3DoFState, self._getAppliedForce, self.getMass, startTime=self.rigidBody.time, integrationMethod=originalTimeDiscretization, discardedTimeStepCallback=self.simRunner.discardForceLogsForPreviousTimeStep, simDefinition=self.simDefinition ) else: self.rigidBody = RigidBody_3DoF( new3DoFState, self._getAppliedForce, self.getMass, startTime=self.rigidBody.time, integrationMethod=originalTimeDiscretization, simDefinition=self.simDefinition ) #### After Simulation #### def writeLogsToFile(self, directory="."): logfilePaths = [] if self.timeStepLog is not None: rocketName = self.components[0].name # Rocket is named after its top stage path = os.path.join(directory, "{}_timeStepLog.csv".format(rocketName)) # Time step log if self.timeStepLog.writeToCSV(path): logfilePaths.append(path) # Derivative evaluation log if self.derivativeEvaluationLog is not None: path = os.path.join(directory, "{}_derivativeEvaluationLog.csv".format(rocketName)) if self.derivativeEvaluationLog.writeToCSV(path): logfilePaths.append(path) # Calculate aerodynamic coefficients if desired if self.loggingLevel > 2: expandedLogPath = Logging.postProcessForceEvalLog(path, refArea=self.Aref, refLength=self.maxDiameter) logfilePaths.append(expandedLogPath) # Control system log if self.controlSystem != None: if self.controlSystem.log != None: controlSystemLogPath = self.controlSystem.writeLogsToFile(directory) logfilePaths.append(controlSystemLogPath) return logfilePaths
class TestAeroParameters(unittest.TestCase): def setUp(self): removeLogger() self.dummyVelocity1 = Vector(0, 0, 50) self.dummyVelocity2 = Vector(1, 0, 20) self.dummyVelocity3 = Vector(0, 0, -100) self.dummyOrientation1 = Quaternion(Vector(1, 0, 0), math.radians(2)) self.dummyOrientation2 = Quaternion(Vector(1, 0, 0), math.radians(-2)) self.dummyOrientation3 = Quaternion(Vector(0, 1, 0), math.radians(2)) self.dummyOrientation4 = Quaternion(Vector(1, 0, 0), 0) self.dummyOrientation5 = Quaternion(Vector(1, 1, 0), math.radians(2)) self.dummyOrientation6 = Quaternion(Vector(1,0,0), math.radians(90)) self.environment = Environment(silent=True) self.currentConditions = self.environment.getAirProperties(Vector(0,0,200)) self.rocketState1 = RigidBodyState(Vector(0, 0, 200), Vector(0, 0, 200), Quaternion(Vector(0, 0, 1), 0), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState3 = RigidBodyState(Vector(0, 0, 200), Vector(0, 0, 500), Quaternion(Vector(1, 0, 0), math.radians(2)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState4 = RigidBodyState(Vector(0, 0, 200), Vector(0, 0, -200), Quaternion(Vector(1, 0, 0), math.radians(180)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState8 = RigidBodyState(Vector(0, 0, 200), Vector(20.04, -0.12, -52.78), Quaternion(Vector(0, 1, 0), math.radians(90)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.correctDynamicPressure1 = self.currentConditions.Density * self.rocketState1.velocity.length()**2 / 2 self.correctDynamicPressure2 = self.currentConditions.Density * self.rocketState3.velocity.length()**2 / 2 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 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 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_dynamicPressure(self): self.assertAlmostEqual(AeroParameters.getDynamicPressure(self.rocketState1,self.currentConditions), self.correctDynamicPressure1) self.assertAlmostEqual(AeroParameters.getDynamicPressure(self.rocketState3,self.currentConditions), self.correctDynamicPressure2) 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_getReynoldsNumber(self): environment = self.environment.getAirProperties(Vector(0,0,0)) self.assertAlmostEqual(AeroParameters.getReynoldsNumber(self.rocketState1, environment, 1), 13701279.02972, 2) def test_getBeta(self): self.assertAlmostEqual(AeroParameters.getBeta(0.5), 0.86602540) self.assertAlmostEqual(AeroParameters.getBeta(1.5), 1.118033989) def test_getAOA(self): test1State = RigidBodyState(velocity=Vector(1, 0, 1)) expectedAOA = math.radians(-45) computedAOA = AeroParameters.getAOA(test1State, self.currentConditions) self.assertAlmostEqual(expectedAOA, computedAOA) 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)
class TestRocketComponents(unittest.TestCase): def setUp(self): simDef = SimDefinition("MAPLEAF/Examples/Simulations/test3.mapleaf", silent=True) rocketDictReader = SubDictReader("Rocket", simDef) self.rocket = Rocket(rocketDictReader, silent=True) self.nosecone = self.rocket.stages[0].getComponentsOfType(NoseCone)[0] self.bodytube = self.rocket.stages[0].getComponentsOfType(BodyTube)[0] # Find the Fixed Mass component that's actually just a Fixed Mass, not a child class (Nosecone, Bodytube) fixedMassComponents = self.rocket.stages[0].getComponentsOfType(FixedMass) for comp in fixedMassComponents: if type(comp) == FixedMass: self.fixedMass = comp self.environment = Environment(silent=True) self.currentConditions = self.environment.getAirProperties(Vector(0,0,200)) # m #### FixedMass #### def test_getInertia(self): noseconeInertia = Inertia(Vector(0.001,0.001,0.001), Vector(0,0,-0.2), mass=5) bodytubeInertia = Inertia(Vector(0.001,0.001,0.001), Vector(0,0,-1), mass=50) generalInertia = Inertia(Vector(0.001,0.001,0.001), Vector(0,0,0.0), mass=100) state = self.rocket.rigidBody.state self.almostEqualVectors(self.nosecone.getInertia(0, state).CG, noseconeInertia.CG) self.assertEqual(self.nosecone.getInertia(0, state).mass, noseconeInertia.mass) self.almostEqualVectors(self.bodytube.getInertia(0, state).CG, bodytubeInertia.CG) self.assertEqual(self.bodytube.getInertia(0, state).mass, bodytubeInertia.mass) self.almostEqualVectors(self.fixedMass.getInertia(0, state).CG, generalInertia.CG) self.assertEqual(self.fixedMass.getInertia(0, state).mass, generalInertia.mass) ### FixedForce ### def test_fixedForceInit(self): simDef = SimDefinition("MAPLEAF/Examples/Simulations/FixedForce.mapleaf", silent=True) rocketDictReader = SubDictReader("Rocket", simDef) rocket = Rocket(rocketDictReader, silent=True) fixedForce = rocket.stages[0].getComponentsOfType(FixedForce)[0] expectedFMS = ForceMomentSystem(Vector(0,0,0), Vector(0,0,0), Vector(0,1,0)) assertForceMomentSystemsAlmostEqual(self, fixedForce.force, expectedFMS) def test_getFixedForceInertia(self): simDef = SimDefinition("MAPLEAF/Examples/Simulations/FixedForce.mapleaf", silent=True) rocketDictReader = SubDictReader("Rocket", simDef) rocket = Rocket(rocketDictReader, silent=True) fixedForce = rocket.stages[0].getComponentsOfType(FixedForce)[0] inertia = fixedForce.getInertia(0, "fakeState") expectedInertia = Inertia(Vector(0,0,0), Vector(0,0,0), 0) self.assertEqual(inertia, expectedInertia) def test_getFixedForce(self): simDef = SimDefinition("MAPLEAF/Examples/Simulations/FixedForce.mapleaf", silent=True) rocketDictReader = SubDictReader("Rocket", simDef) rocket = Rocket(rocketDictReader, silent=True) fixedForce = rocket.stages[0].getComponentsOfType(FixedForce)[0] force = fixedForce.getAppliedForce("fakeState", 0, "fakeEnv", Vector(0,0,0)) expectedForce = ForceMomentSystem(Vector(0,0,0), moment=Vector(0,1,0)) self.assertEqual(force.force, expectedForce.force) self.assertEqual(force.moment, expectedForce.moment) ### TabulatedAeroForce ### def test_TabulatedAeroForce(self): # Init rocket that uses tabulated aero data simDef = SimDefinition("MAPLEAF/Examples/Simulations/NASATwoStageOrbitalRocket.mapleaf", silent=True) rocketDictReader = SubDictReader("Rocket", simDef) rocket = Rocket(rocketDictReader, silent=True) comp1, comp2, comp3 = rocket.stages[0].getComponentsOfType(TabulatedAeroForce) if comp1.Lref == 3.0: tabMoment = comp1 tabFOrce = comp2 else: tabMoment = comp2 tabForce = comp1 zeroAOAState = RigidBodyState(Vector(0,0,0), Vector(0,0,100), Quaternion(1,0,0,0), Vector(0,0,0)) # CD, CL, CMx, CMy, CMz expectedAero = [ 0.21, 0, 0, 0, 0 ] calculatedAero = tabForce._getAeroCoefficients(zeroAOAState, self.currentConditions) assertIterablesAlmostEqual(self, expectedAero, calculatedAero) expectedAero = [ 0, 0, 0, 0, 0 ] calculatedAero = tabMoment._getAeroCoefficients(zeroAOAState, self.currentConditions) assertIterablesAlmostEqual(self, expectedAero, calculatedAero) #### Utilities #### def almostEqualVectors(self, Vector1, Vector2, n=7): self.assertAlmostEqual(Vector1.X, Vector2.X, n) self.assertAlmostEqual(Vector1.Y, Vector2.Y, n) self.assertAlmostEqual(Vector1.Z, Vector2.Z, n)
class TestBodyTube(unittest.TestCase): def setUp(self): simDef = SimDefinition("MAPLEAF/Examples/Simulations/test3.mapleaf") rocketDictReader = SubDictReader("Rocket", simDef) self.rocket = Rocket(rocketDictReader) self.environment = Environment(silent=True) self.currentConditions = self.environment.getAirProperties( Vector(0, 0, 200)) # m self.rocketState1 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 200), Quaternion(Vector(0, 0, 1), 0), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState2 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 200), Quaternion(Vector(1, 0, 0), math.radians(2)), AngularVelocity(rotationVector=Vector(0, 0, 0))) self.rocketState3 = RigidBodyState( Vector(0, 0, 200), Vector(0, 0, 500), Quaternion(Vector(1, 0, 0), math.radians(2)), AngularVelocity(rotationVector=Vector(0, 0, 0))) # def test_bodytubeOpenRocketAeroCoefficients(self): # time = 1 # bodyTube = self.rocket.stages[0].getComponentsOfType(Bodytube)[0] # aeroForce = bodyTube.getAppliedForce(self.rocketState1, time, self.currentConditions, self.rocket.getCG(0, self.rocketState1)) # normalForceDirection = AeroParameters.getNormalAeroForceDirection(self.rocketState1, self.currentConditions) # axialForceDirection = Vector(0, 0, -1) #By definition of axial force # normalForceHandCalc = 0 # axialForceHandCalc = 86.86005 # CpWRTNoseconeTip = Vector(0, 0, -0.762 - 1) #Planform centroid # normalForce = normalForceDirection.__mul__(normalForceHandCalc) # axialForce = axialForceDirection.__mul__(axialForceHandCalc) # appliedNormalForce = ForceMomentSystem(normalForce, CpWRTNoseconeTip) # appliedAxialForce = ForceMomentSystem(axialForce, CpWRTNoseconeTip) # correctAeroForce = appliedNormalForce + appliedAxialForce # assertVectorsAlmostEqual(self, bodyTube.CPLocation, Vector(0,0,-1.762)) # assertForceMomentSystemsAlmostEqual(self, aeroForce, correctAeroForce, 4) # aeroForce = bodyTube.getAppliedForce(self.rocketState2, time, self.currentConditions, self.rocket.getCG(0, self.rocketState2)) # normalForceDirection = AeroParameters.getNormalAeroForceDirection(self.rocketState2, self.currentConditions) # axialForceDirection = Vector(0, 0, -1) #By definition of axial force # normalForceHandCalc = 9.820304 # axialForceHandCalc = 87.857221 # CpWRTNoseconeTip = Vector(0, 0, -1.762) #Planform centroid # normalForce = normalForceDirection.__mul__(normalForceHandCalc) # axialForce = axialForceDirection.__mul__(axialForceHandCalc) # appliedNormalForce = ForceMomentSystem(normalForce, CpWRTNoseconeTip) # appliedAxialForce = ForceMomentSystem(axialForce, CpWRTNoseconeTip) # correctAeroForce = appliedNormalForce + appliedAxialForce # assertVectorsAlmostEqual(self, bodyTube.CPLocation, Vector(0,0,-1.762)) # assertForceMomentSystemsAlmostEqual(self, aeroForce, correctAeroForce, 6) # aeroForce = bodyTube.getAppliedForce(self.rocketState3, time, self.currentConditions, self.rocket.getCG(0, self.rocketState3)) # normalForceDirection = AeroParameters.getNormalAeroForceDirection(self.rocketState3, self.currentConditions) # axialForceDirection = Vector(0, 0, -1) #By definition of axial force # normalForceHandCalc = 61.3769010 # axialForceHandCalc = 483.10726 # CpWRTNoseconeTip = Vector(0, 0, -0.762 - 1) #Planform centroid # normalForce = normalForceDirection.__mul__(normalForceHandCalc) # axialForce = axialForceDirection.__mul__(axialForceHandCalc) # appliedNormalForce = ForceMomentSystem(normalForce, CpWRTNoseconeTip) # appliedAxialForce = ForceMomentSystem(axialForce, CpWRTNoseconeTip) # correctAeroForce = appliedNormalForce + appliedAxialForce # assertVectorsAlmostEqual(self, bodyTube.CPLocation, Vector(0,0,-1.762)) # assertForceMomentSystemsAlmostEqual(self, aeroForce, correctAeroForce, 5) def test_bodyTubeDampingMoment(self): simRunner = Simulation("MAPLEAF/Examples/Simulations/test3.mapleaf", silent=True) rocket = simRunner.createRocket() bodyTube = rocket.stages[0].getComponentsOfType(BodyTube)[0] # Create a rigid body state rotating at 2 rad/s about the x-axis pos = Vector(0, 0, 0) vel = Vector(0, 0, 0) orientation = Quaternion(1, 0, 0, 0) angVel = AngularVelocity(2, 0, 0) xRotatingState = RigidBodyState(pos, vel, orientation, angVel) envConditions = rocket.environment.getAirProperties(Vector(0, 0, 0)) #### CoR = Middle of the body tube #### fakeRocketCG = Vector(0, 0, -1.762) # Get computed (numerically-integrated) result calculatedDampingMoment = bodyTube._computeLongitudinalDampingMoments( xRotatingState, envConditions, fakeRocketCG, nSegments=100) # Compute analytical result (Niskanen Eqn 3.58) * 2 for both halves of the body tube (above and below center of rotation) expectedXDampingMoment = 2 * 0.275 * envConditions.Density * ( bodyTube.outerDiameter / 2) * 4 expectedTotalDampingMoment = Vector(-expectedXDampingMoment, 0, 0) # Compare assertVectorsAlmostEqual(self, calculatedDampingMoment, expectedTotalDampingMoment, 4) #### Case 2: CoR at End of Body Tube #### fakeRocketCG = Vector(0, 0, -2.762) # Get computed (numerically-integrated) result calculatedDampingMoment = bodyTube._computeLongitudinalDampingMoments( xRotatingState, envConditions, fakeRocketCG, nSegments=150) # Factor of 16 from moving to a tube length of two (2^4 = 16), but dividing by two (one half instead of two) expectedTotalDampingMoment *= 8 # Compare assertVectorsAlmostEqual(self, calculatedDampingMoment, expectedTotalDampingMoment, 4)