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

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

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

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

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

    return ForceMomentSystem(totalForce, CPLocation, moments)
Пример #2
0
 def getAppliedForce(self, rocketState, time, environment, CG):
     '''
         Calculates force/moment applied by the recovery system using a simple drag coefficient + area model
     '''
     #### Calculate Aero Force ####
     if self.currentStage == 0:
         # No recovery system deployed yet
         return ForceMomentSystem(Vector(0,0,0))
     else:
         # 3DoF force-only aero
         airVel = AeroParameters.getAirVelRelativeToVehicle(rocketState, environment)
         dragForceMagnitude = self.chuteCds[self.currentStage] * self.chuteAreas[self.currentStage] * AeroParameters.getDynamicPressure(rocketState, environment)
         totalForce = airVel.normalize() * dragForceMagnitude
         return ForceMomentSystem(totalForce)
Пример #3
0
    def getAppliedForce(self, rocketState, time, environment, CG):
        #### If control system exists, use actuator deflections 1:1 to set fin angles ####
        if self.controlSystem != None:
            # Update fin angles
            for i in range(self.numFins):
                self.finList[i].finAngle = self.actuatorList[i].getDeflection(
                    time)

        #### Pre-calculate common properties for all child Fins ####
        precomputedData = self._getPreComputedFinAeroData(
            rocketState, environment, CG)

        #### Add up forces from all child Fins ####
        aeroForce = ForceMomentSystem(Vector(0, 0, 0), self.position)
        for fin in self.finList:
            aeroForce += fin.getAppliedForce(rocketState, time, environment,
                                             CG, precomputedData)

        # TODO: Correct for sub/transonic rolling moment fin-fin interference from a high number of fins

        # Apply fin-body interference factor to total forces in the normal (X/Y) directions and moments
        totalInterferenceFactor = self.bodyOnFinInterferenceFactor * self.finNumberInterferenceFactor
        aeroForce.force.X *= totalInterferenceFactor
        aeroForce.force.Y *= totalInterferenceFactor
        aeroForce.moment.X *= totalInterferenceFactor
        aeroForce.moment.Y *= totalInterferenceFactor

        return aeroForce
Пример #4
0
def forceFromCdCN(rocketState,
                  environment,
                  Cd,
                  CN,
                  CPLocation,
                  refArea,
                  moment=None):
    ''' 
        Convenience function for Barrowman Aero methods
        Initialize ForceMomentSystem from aerodynamic coefficients Cd and CN
        Cd should NOT already be adjusted for angle of attack 
        Moment should be a dimensional moment vector
    '''
    angleOfAttack = AeroParameters.getTotalAOA(rocketState, environment)
    q = AeroParameters.getDynamicPressure(rocketState, environment)

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

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

    totalForce = (axialForce + normalForce) * refArea * q
    return ForceMomentSystem(totalForce, CPLocation, moment)
Пример #5
0
    def __init__(self, componentDictReader, rocket, stage):
        self.rocket = rocket
        self.stage = stage
        self.componentDictReader = componentDictReader
        self.name = componentDictReader.getDictName()

        mass = componentDictReader.getFloat("mass")

        # Position in simulation definition is relative to stage position
        self.position = componentDictReader.getVector(
            "position"
        ) + stage.position  # Store position relative to nosecone here
        # CG in simulation definition is relative to component position
        cg = componentDictReader.getVector(
            "cg"
        ) + self.position  # Store cg location relative to nosecone here

        try:
            MOI = componentDictReader.getVector("MOI")
        except:
            MOI = Vector(mass * 0.01, mass * 0.01,
                         mass * 0.01)  # Avoid having zero moments of inertia

        self.inertia = Inertia(MOI, cg, mass)
        self.zeroForce = ForceMomentSystem(Vector(0, 0, 0))
Пример #6
0
    def uniformlyAcceleratingRotationTest(self, integrationMethod):
        constantZMoment = ForceMomentSystem(self.zeroVec,
                                            moment=Vector(0, 0, 1))
        restingState = RigidBodyState(self.zeroVec, self.zeroVec,
                                      self.zeroQuat, self.zeroAngVel)

        def constZMomentFunc(*allArgs):
            return constantZMoment

        def constInertiaFunc(*allArgs):
            return self.constInertia

        acceleratingZRotBody = RigidBody(restingState,
                                         constZMomentFunc,
                                         constInertiaFunc,
                                         integrationMethod=integrationMethod,
                                         simDefinition=self.simDefinition)
        acceleratingZRotBody.timeStep(1)

        newOrientation = acceleratingZRotBody.state.orientation
        newAngVel = acceleratingZRotBody.state.angularVelocity

        #print("{}: {}".format(integrationMethod, newOrientation))

        assertAngVelAlmostEqual(
            self, newAngVel,
            AngularVelocity(axisOfRotation=Vector(0, 0, 1), angularVel=1))
        if integrationMethod == "Euler":
            assertQuaternionsAlmostEqual(
                self, newOrientation,
                Quaternion(axisOfRotation=Vector(0, 0, 1), angle=0))
        else:
            assertQuaternionsAlmostEqual(
                self, newOrientation,
                Quaternion(axisOfRotation=Vector(0, 0, 1), angle=0.5))
Пример #7
0
    def uniformXAccelerationTest(self, integrationMethod):
        constantXForce = ForceMomentSystem(Vector(1, 0, 0))
        movingXState = RigidBodyState(self.zeroVec, self.zeroVec,
                                      self.zeroQuat, self.zeroAngVel)

        def constInertiaFunc(*allArgs):
            return self.constInertia

        def constXForceFunc(*allArgs):
            return constantXForce

        movingXBody = RigidBody(movingXState,
                                constXForceFunc,
                                constInertiaFunc,
                                integrationMethod=integrationMethod,
                                simDefinition=self.simDefinition)
        movingXBody.timeStep(1)

        newPos = movingXBody.state.position
        newVel = movingXBody.state.velocity
        assertVectorsAlmostEqual(self, newVel, Vector(1, 0, 0))
        if integrationMethod == "Euler":
            assertVectorsAlmostEqual(self, newPos, Vector(0, 0, 0))
        else:
            assertVectorsAlmostEqual(self, newPos, Vector(0.5, 0, 0))
Пример #8
0
    def setUp(self):
        self.appliedForce1 = ForceMomentSystem(Vector(0, 0,
                                                      10), Vector(1, 0, 0),
                                               Vector(0, 0, 0))
        self.appliedForce2 = ForceMomentSystem(Vector(0, 0,
                                                      10), Vector(2, 0, 0),
                                               Vector(0, 0, 0))
        self.appliedForce3 = ForceMomentSystem(Vector(10, 0,
                                                      0), Vector(0, 1, 0),
                                               Vector(0, 0, 0))

        self.correctForce1 = ForceMomentSystem(Vector(0, 0, 20),
                                               Vector(0, 0, 0),
                                               Vector(0, -30, 0))
        self.correctForce2 = ForceMomentSystem(Vector(10, 0, 10),
                                               Vector(0, 0, 0),
                                               Vector(0, -10, -10))
Пример #9
0
    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)
Пример #10
0
    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)
Пример #11
0
    def __init__(self, componentDictReader, rocket, stage):
        self.rocket = rocket
        self.stage = stage
        self.componentDictReader = componentDictReader
        self.name = componentDictReader.getDictName()

        self.zeroForce = ForceMomentSystem(Vector(0, 0, 0))

        inertiaTableFilePath = componentDictReader.getString("filePath")
        self._parseInertiaTable(inertiaTableFilePath)
Пример #12
0
    def test_zeroSag(self):
        unadjustedForce = ForceMomentSystem(Vector(10, 9, -10),
                                            moment=Vector(10, 11, 12))
        adjustedForce = self.rail.applyLaunchTowerForce(
            self.initState, 0, unadjustedForce)

        # Check that force is zero (don't let object sag/fall)
        assertVectorsAlmostEqual(self, adjustedForce.force, Vector(0, 0, 0))
        # Check moments are zero
        assertVectorsAlmostEqual(self, adjustedForce.moment, Vector(0, 0, 0))
Пример #13
0
    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)
Пример #14
0
    def getAppliedForce(self, rocketState, time, envConditions, rocketCG):
        mag = -2000 * self.getTankLevelDerivative(
            time, rocketState
        )  # Force magnitude proportional to flow rate out of the tank
        forceVector = Vector(0, 0, mag)

        self.rocket.appendToForceLogLine(
            " {:>6.4f}".format(mag)
        )  # This will end up in the log file, in the SampleZForce column

        return ForceMomentSystem(forceVector)
Пример #15
0
    def test_LeaveRail(self):
        unadjustedForce = ForceMomentSystem(Vector(10, 9, 11),
                                            moment=Vector(10, 11, 12))
        self.initState.position = Vector(4, 0, 4)  # Greater than 5m away
        adjustedForce = self.rail.applyLaunchTowerForce(
            self.initState, 0, unadjustedForce)

        # Check that forces/moments are unaffected
        assertVectorsAlmostEqual(self, adjustedForce.force, Vector(10, 9, 11))
        assertVectorsAlmostEqual(self, adjustedForce.moment,
                                 Vector(10, 11, 12))
Пример #16
0
    def test_alignForceWithRail(self):
        unadjustedForce = ForceMomentSystem(Vector(10, 9, 11),
                                            moment=Vector(10, 11, 12))
        adjustedForce = self.rail.applyLaunchTowerForce(
            self.initState, 0, unadjustedForce)

        # Check that force direction is now aligned with rail
        forceDirection = adjustedForce.force.normalize()
        assertVectorsAlmostEqual(self, self.rail.initialDirection,
                                 forceDirection)
        # Check moments are zero
        assertVectorsAlmostEqual(self, adjustedForce.moment, Vector(0, 0, 0))
Пример #17
0
    def getAppliedForce(self, rocketState, time, environmentalConditions,
                        rocketCG):
        # Only apply damping force if current stage's engine is firing
        # (Other stage's motors will have different exit planes)
        if time > self.stage.motor.ignitionTime and time < self.stage.engineShutOffTime:
            currentRocketInertia = self.rocket.getInertia(time, rocketState)

            # Differentiate rate of MOI change
            dt = 0.001
            nextRocketInertia = self.rocket.getInertia(time + dt, rocketState)
            MOIChangeRate = (currentRocketInertia.MOI.X -
                             nextRocketInertia.MOI.X) / dt

            dampingFactor = MOIChangeRate * self.dampingFraction

            angVel = rocketState.angularVelocity
            dampingMoment = Vector(-angVel.X * dampingFactor,
                                   -angVel.Y * dampingFactor, 0)

            return ForceMomentSystem(Vector(0, 0, 0), moment=dampingMoment)
        else:
            return ForceMomentSystem(Vector(0, 0, 0))
Пример #18
0
    def getAppliedForce(self, state, time, environment, CG):
        #TODO: Model "thrust damping" - where gases moving quickly in the engine act to damp out rotation about the x and y axes
        #TODO: Thrust vs altitude compensation
        timeSinceIgnition = max(0, time - self.ignitionTime)

        # Determine thrust magnitude
        if timeSinceIgnition < 0 or timeSinceIgnition > self.times[-1]:
            thrustMagnitude = 0
        else:
            thrustMagnitude = linInterp(self.times, self.thrustLevels,
                                        timeSinceIgnition)

        # Create Vector
        thrust = Vector(0, 0, thrustMagnitude)
        return ForceMomentSystem(thrust)
Пример #19
0
    def __init__(self, componentDictReader, rocket, stage):
        ''' A Zero-inertia component that applies a constant ForceMomentSystem to the rocket '''
        self.componentDictReader = componentDictReader
        self.rocket = rocket
        self.stage = stage
        self.name = componentDictReader.getDictName()

        # Object is just a force, inertia is zero
        self.inertia = Inertia(Vector(0, 0, 0), Vector(0, 0, 0), 0)

        force = componentDictReader.getVector("force")
        forceLocation = componentDictReader.getVector("position")
        moment = componentDictReader.getVector("moment")

        self.force = ForceMomentSystem(force, forceLocation, moment)
Пример #20
0
    def test_combineForceMomentSystems_2(self):
        # Example Question 16 - http://www.ce.siue.edu/examples/Worked_examples_Internet_text-only/Data_files-Worked_Exs-Word_&_pdf/Equivalent_forces.pdf
        # Define Force-Moment 1
        force1 = Vector(0, -3.464, -2)
        m1 = Vector(0, -51.962, -30)
        location1 = Vector(4, 1.5, 4.402)
        fms1 = ForceMomentSystem(force1, location1, m1)

        # Define Force-Moment 2
        force2 = Vector(-6, 0, 0)
        m2 = Vector(-80, 0, 0)
        location2 = Vector(8, 1.5, 1)
        fms2 = ForceMomentSystem(force2, location2, m2)

        # Combine
        combinedForce = fms1 + fms2
        combinedForceAtOrigin = combinedForce.getAt(Vector(0, 0, 0))

        # Define correct/expected result
        expectedResultantForce = Vector(-6, -3.464, -2)
        expectedResultantMoment = Vector(
            12.249, 2, -4.856
        )  # Only includes moments generated by forces, not the moments applied
        resultantLocation = Vector(0, 0, 0)
        expectedResult = ForceMomentSystem(expectedResultantForce,
                                           resultantLocation,
                                           expectedResultantMoment)

        # Compare
        from test.testUtilities import assertVectorsAlmostEqual
        assertVectorsAlmostEqual(self, combinedForceAtOrigin.force,
                                 expectedResult.force)
        assertVectorsAlmostEqual(self, combinedForceAtOrigin.location,
                                 expectedResult.location)
        assertVectorsAlmostEqual(self, combinedForceAtOrigin.moment - m1 - m2,
                                 expectedResult.moment, 3)
Пример #21
0
    def setUp(self):
        self.zeroVec = Vector(0, 0, 0)
        self.zeroQuat = Quaternion(axisOfRotation=Vector(1, 0, 0), angle=0)
        self.zeroAngVel = AngularVelocity(axisOfRotation=Vector(1, 0, 0),
                                          angularVel=0)
        self.zeroForce = ForceMomentSystem(self.zeroVec)
        self.oneVec = Vector(1, 1, 1)

        self.simDefinition.setValue(
            "SimControl.TimeStepAdaptation.minTimeStep", "1")

        #TODO: Get some tests for the higher order adaptive methods in here
        self.integrationMethods = [
            "Euler", "RK2Heun", "RK2Midpoint", "RK4", "RK12Adaptive"
        ]
        self.constInertia = Inertia(self.oneVec, self.zeroVec, 1)
Пример #22
0
    def getGravityForce(self, inertia, state) -> ForceMomentSystem:
        '''
            Inputs:
                inertia: (`MAPLEAF.Motion.Inertia`)
                state:   (`MAPLEAF.Motion.RigidBodyState`/`MAPLEAF.Motion.RigidBodyState_3DoF`)

            Returns:
                gravityForce: (ForceMomentSystem) defined in the rocket's local frame
        '''
        # Get gravity force in the global frame
        gravityForce = self.earthModel.getGravityForce(inertia, state)
        
        try:
            # Convert to local frame if in 6DoF simulation
            gravityForce = state.orientation.conjugate().rotate(gravityForce) # rotate into local frame when in 6DoF mode
        except AttributeError:
            pass # Don't do anything in 3DoF mode (No local frame exists)

        return ForceMomentSystem(gravityForce, inertia.CG)
Пример #23
0
    def getAppliedForce(self, state, time, environmentalConditions, rocketCG):
        '''
            Computes the aerodynamic force experienced by the stage.
            Does not include gravitational force - gravity is added at the rocket level.
        '''
        # Add up forces from all subcomponents
        totalAppliedComponentForce = ForceMomentSystem(Vector(0, 0, 0),
                                                       rocketCG)
        for component in self.components:
            componentForce = component.getAppliedForce(
                state, time, environmentalConditions, rocketCG)

            # Log applied forces and moments if desired
            if hasattr(component, "forcesLog"):
                component.forcesLog.append(componentForce.force)
                component.momentsLog.append(componentForce.moment)

            totalAppliedComponentForce += componentForce

        return totalAppliedComponentForce
Пример #24
0
    def applyLaunchTowerForce(self, state, time, unadjustedForce):
        '''
            If on launch tower, projects forces experienced onto the launch tower directions and sets Moments = 0
            Returns two values: onTower(Bool), adjustedForce(Motion.ForceMomentSystem)
        '''
        distanceTravelled = (state.position - self.getPosition(time)).length()
        if distanceTravelled < self.length:
            # Vehicle still on launch rail

            # Project total force onto the launch rail direction (dot product)
            adjustedForceMagnitude = unadjustedForce.force * self.getDirection(
                time)
            adjustedForceVector = self.initialDirection * adjustedForceMagnitude

            # No resultant moments while on the launch rail
            adjustedForce = ForceMomentSystem(adjustedForceVector)

            return adjustedForce
        else:
            # Vehicle has left the rail
            return unadjustedForce
Пример #25
0
    def test_nonPrincipalAxisRotation(self):
        initAngVel = AngularVelocity(rotationVector=Vector(1, 0, 1))
        rotatingZState = RigidBodyState(self.zeroVec, self.zeroVec,
                                        self.zeroQuat, initAngVel)

        constForce = ForceMomentSystem(self.zeroVec)

        def constForceFunc(*allArgs):
            return constForce

        constInertia = Inertia(Vector(2, 2, 8), self.zeroVec, 1)

        def constInertiaFunc(*allArgs):
            return constInertia

        rotatingZBody = RigidBody(rotatingZState,
                                  constForceFunc,
                                  constInertiaFunc,
                                  integrationMethod="RK4",
                                  simDefinition=self.simDefinition)

        dt = 0.01
        nTimeSteps = 10
        totalTime = dt * nTimeSteps

        for i in range(nTimeSteps):
            rotatingZBody.timeStep(dt)

        finalAngVel = rotatingZBody.state.angularVelocity

        omega = (constInertia.MOI.Z - constInertia.MOI.X) / constInertia.MOI.X
        expectedAngVel = AngularVelocity(
            rotationVector=Vector(cos(omega * totalTime), sin(omega *
                                                              totalTime), 1))

        assertAngVelAlmostEqual(self, finalAngVel, expectedAngVel)
Пример #26
0
 def step1MomentFunc(*allArgs):
     return ForceMomentSystem(
         self.zeroVec, moment=Vector(0, 0.5, 0)
     )  #Acceleration and deceleration steps to give 90 degree rotations in each axis
Пример #27
0
 def step2MomentFunc(*allArgs):
     return ForceMomentSystem(self.zeroVec, moment=Vector(
         0, -0.5, 0))  #using a time step of pi
Пример #28
0
 def step3MomentFunc(*allArgs):
     return ForceMomentSystem(
         self.zeroVec, moment=Vector(0, 0, 0.5)
     )  #There is one acceleration and one deceleration for each rotation to make it static
Пример #29
0
 def step4MomentFunc(*allArgs):
     return ForceMomentSystem(self.zeroVec, moment=Vector(0, 0, -0.5))
Пример #30
0
    def _barrowmanAeroFunc(self,
                           rocketState,
                           time,
                           environment,
                           precomputedData,
                           CG=Vector(0, 0, 0),
                           finDeflectionAngle=None):
        '''
            Precomputed Data is a named tuple (PreComputedFinAeroData) which contains data/results from the parts of the fin aerodynamics calculation
                that are common to all fins in a FinSet (drag calculations mostly). These calculations are performed at the FinSet level.
            Only the parts of the Fin Aero Computation that change from fin to fin (normal force mostly, since different fins can have different angles of attack) are computed here
        '''
        Mach = AeroParameters.getMachNumber(rocketState, environment)
        dynamicPressure = AeroParameters.getDynamicPressure(
            rocketState, environment)

        if finDeflectionAngle == None:
            finDeflectionAngle = self.finAngle  # Adjusted by parent finset during each timestep, when the FinSet is controlled

        # Unpack precomputedData
        airVelRelativeToFin, CPXPos, totalDragCoefficient = precomputedData

        #### Compute normal force-----------------------------------------------------------------------------------------------------------------

        # Find fin normal vector after fin deflection
        finDeflectionRotation = Quaternion(
            axisOfRotation=self.spanwiseDirection,
            angle=radians(finDeflectionAngle))
        finNormal = finDeflectionRotation.rotate(self.undeflectedFinNormal)
        # Get the tangential velocity component vector, per unit radial distance from the rocket centerline
        rollAngVel = AngularVelocity(0, 0, rocketState.angularVelocity.Z)
        unitSpanTangentialAirVelocity = rollAngVel.crossProduct(
            self.spanwiseDirection) * (-1)

        def subsonicNormalForce(Mach):  # Subsonic linear method
            tempBeta = AeroParameters.getBeta(Mach)
            CnAlpha = getFinCnAlpha_Subsonic_Barrowman(self.span,
                                                       self.planformArea,
                                                       tempBeta,
                                                       self.midChordSweep)
            return getSubsonicFinNormalForce(airVelRelativeToFin,
                                             unitSpanTangentialAirVelocity,
                                             finNormal, self.spanwiseDirection,
                                             self.CPSpanWisePosition.length(),
                                             CnAlpha, self)

        def supersonicNormalForce(Mach):  # Supersonic Busemann method
            gamma = AeroFunctions.getGamma()
            tempBeta = AeroParameters.getBeta(Mach)
            K1, K2, K3, Kstar = getBusemannCoefficients(Mach, tempBeta, gamma)

            # Mach Cone coords
            machAngle = asin(1 / Mach)
            machCone_negZPerRadius = 1 / tan(machAngle)
            machConeEdgeZPos = []
            outerRadius = self.spanSliceRadii[-1]
            for i in range(len(self.spanSliceRadii)):
                machConeAtCurrentRadius = (
                    outerRadius - self.spanSliceRadii[i]
                ) * machCone_negZPerRadius + self.tipPosition
                machConeEdgeZPos.append(machConeAtCurrentRadius)

            return getSupersonicFinNormalForce(
                airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal,
                machConeEdgeZPos, self.spanwiseDirection,
                self.CPSpanWisePosition.length(), K1, K2, K3, Kstar, self)

        if Mach <= 0.8:
            normalForceMagnitude, finMoment = subsonicNormalForce(Mach)

        elif Mach < 1.4:
            # Interpolate in transonic region
            # TODO: Do this with less function evaluations? Perhaps precompute AOA and Mach combinations and simply interpolate? Lazy precompute? Cython?
            x1, x2 = 0.8, 1.4  # Start, end pts of interpolated region
            dx = 0.001

            # Find normal force and derivative at start of interpolation interval
            f_x1, m_x1 = subsonicNormalForce(x1)
            f_x12, m_x12 = subsonicNormalForce(x1 + dx)

            # Find normal force and derivative at end of interpolation interval
            f_x2, m_x2 = supersonicNormalForce(x2)
            f_x22, m_x22 = supersonicNormalForce(x2 + dx)

            normalForceMagnitude = Interpolation.cubicInterp(
                Mach, x1, x2, f_x1, f_x2, f_x12, f_x22, dx)
            finMoment = Interpolation.cubicInterp(Mach, x1, x2, m_x1, m_x2,
                                                  m_x12, m_x22, dx)
        else:
            normalForceMagnitude, finMoment = supersonicNormalForce(Mach)

        # Complete redimensionalization of normal force coefficients by multiplying by dynamic pressure
        # Direct the normal force along the fin's normal direction
        normalForce = normalForceMagnitude * dynamicPressure * finNormal
        finMoment *= dynamicPressure

        #### Get axial force-----------------------------------------------------------------------------------------------------------------------

        avgAOA = getFinSliceAngleOfAttack(
            self.spanSliceRadii[round(len(self.spanSliceAreas) / 2)],
            airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal,
            self.spanwiseDirection,
            self.stallAngle)  # Approximate average fin AOA
        totalAxialForceCoefficient = AeroFunctions.getDragToAxialForceFactor(
            avgAOA) * totalDragCoefficient
        axialForceMagnitude = totalAxialForceCoefficient * self.rocket.Aref * dynamicPressure
        axialForceDirection = self.spanwiseDirection.crossProduct(finNormal)
        axialForce = axialForceDirection * axialForceMagnitude

        #### Get CP Location ----------------------------------------------------------------------------------------------------------------------

        CPChordWisePosition = self.position - Vector(
            0, 0, CPXPos
        )  # Ignoring the change in CP position due to fin deflection for now
        globalCP = self.CPSpanWisePosition + CPChordWisePosition

        #### Assemble total force moment system objects--------------------------------------------------------------------------------------------

        totalForce = normalForce + axialForce
        return ForceMomentSystem(totalForce,
                                 globalCP,
                                 moment=Vector(0, 0, finMoment)), globalCP