Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
    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))
Ejemplo n.º 5
0
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))
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
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)