Module MAPLEAF.Rocket
Code specific to modelling Rockets.
Main class is Rocket
.
Other files define models for specific rocket components or stages.
In general the responsibility of rocket component classes is to model:
- The forces/moments (aerodynamic or otherwise (excluding gravitational forces)) applied to the rocket by that rocket component
- The inertia of that rocket component
The interface all rocket components are expected to implement is defined by the abstract base class RocketComponent
. All rocket components inherit from this base class.
MAPLEAF.Rocket relies on (in order of coupling) MAPLEAF.Motion
, MAPLEAF.ENV
, MAPLEAF.IO
, and MAPLEAF.GNC
Approximate Material Roughnesses
Material | Roughness ( \mu m ) |
---|---|
Mirror: | 0 |
Glass: | 0.1 |
Finished/Polished surface: | 0.5 |
Aircraft-type sheet-metal surface: | 2 |
Optimum paint-sprayed surfaces: | 5 |
Planed wooden boards: | 15 |
Paint in aircraft mass production: | 20 |
Steel plating: bare: | 50 |
Smooth cement: | 50 |
Surface with asphalt-type coating: | 100 |
Dip-galvanized metal surface: | 150 |
Incorrectly sprayed paint: | 200 |
Natural cast-iron surface: | 250 |
Raw wooden boards: | 500 |
Average concrete: | 1000 |
(From Barrowman, 1967, Table 4-1)
Expand source code
r'''
Code specific to modelling Rockets.
Main class is `Rocket`.
Other files define models for specific rocket components or stages.
In general the responsibility of rocket component classes is to model:
1. The forces/moments (aerodynamic or otherwise (excluding gravitational forces)) applied to the rocket by that rocket component
2. The inertia of that rocket component
The interface all rocket components are expected to implement is defined by the abstract base class `MAPLEAF.Rocket.RocketComponent`. All rocket components inherit from this base class.
.. image:: https://airandspace.si.edu/sites/default/files/images/NASAJSC2002-01598h.jpg
MAPLEAF.Rocket relies on (in order of coupling) `MAPLEAF.Motion`, `MAPLEAF.ENV`, `MAPLEAF.IO`, and `MAPLEAF.GNC`
### Approximate Material Roughnesses
| Material | Roughness ( \(\mu m\) ) |
|------------------------------------|-------------------------------------------------------|
| Mirror: | 0 |
| Glass: | 0.1 |
| Finished/Polished surface: | 0.5 |
| Aircraft-type sheet-metal surface: | 2 |
| Optimum paint-sprayed surfaces: | 5 |
| Planed wooden boards: | 15 |
| Paint in aircraft mass production: | 20 |
| Steel plating: bare: | 50 |
| Smooth cement: | 50 |
| Surface with asphalt-type coating: | 100 |
| Dip-galvanized metal surface: | 150 |
| Incorrectly sprayed paint: | 200 |
| Natural cast-iron surface: | 250 |
| Raw wooden boards: | 500 |
| Average concrete: | 1000 |
(From Barrowman, 1967, Table 4-1)
'''
# Make the classes in all submodules importable directly from MAPLEAF.Rocket
from .RocketComponents import *
from .simEventDetector import *
from .boatTail import *
from .bodyTube import *
from .Fins import *
from .Propulsion import *
from .sampleStatefulRocketComponent import *
from .Recovery import *
from .noseCone import *
from .RocketComponentFactory import * # Must be imported after the components above, the factory relies on them
from .stage import *
from .rocket import *
subModules = [ RocketComponents, simEventDetector, boatTail, bodyTube, Fins, Propulsion, sampleStatefulRocketComponent, Recovery, noseCone, stage, rocket, RocketComponentFactory ]
__all__ = [ ]
for subModule in subModules:
__all__ += subModule.__all__
Sub-modules
MAPLEAF.Rocket.AeroFunctions
-
Functions to calculate parameters relevant to aerodynamic calculations - Mach/Reynolds numbers, AOA, local frame air velocity etc …
MAPLEAF.Rocket.CompositeObject
-
Both
Stage
andRocket
objects inherit fromMAPLEAF.Rocket.CompositeObject
. It implements functionality to add forces and inertias … MAPLEAF.Rocket.CythonFinFunctions
-
Cython functions to speed up the
FinSet
aerodynamic model MAPLEAF.Rocket.Fins
MAPLEAF.Rocket.Propulsion
MAPLEAF.Rocket.Recovery
MAPLEAF.Rocket.RocketComponentFactory
-
In charge of initializing rocket components. Add new components to
stringNameToClassMap
to make them instantiate themselves when included in a Rocket MAPLEAF.Rocket.RocketComponents
-
Contains interface definitions (
RocketComponent
/BodyComponent
) and Base classes for all RocketComponents (FixedMass
),
as well as some simple … MAPLEAF.Rocket.boatTail
MAPLEAF.Rocket.bodyTube
MAPLEAF.Rocket.noseCone
MAPLEAF.Rocket.rocket
-
Rocket ties together the code in
MAPLEAF.GNC,
MAPLEAF.Rocket,
MAPLEAF.Motion, and
MAPLEAF.ENV` to simulate the flight of a single rocket or … MAPLEAF.Rocket.sampleStatefulRocketComponent
MAPLEAF.Rocket.simEventDetector
-
Generalized event detector (Apogee, motor burnout etc…) for a single vehicle.
Used to trigger stage separations and recovery system deployments. MAPLEAF.Rocket.stage
Functions
def initializeForceLogging(component, subDictPath, rocket)
-
Expand source code
def initializeForceLogging(component, subDictPath, rocket): if rocket.derivativeEvaluationLog is not None: componentName = subDictPath[subDictPath.index(".")+1:] zeroVector = Vector(0,0,0) component.forcesLog = rocket.derivativeEvaluationLog.addColumn(componentName + "F(N)", zeroVector) component.momentsLog = rocket.derivativeEvaluationLog.addColumn(componentName + "M(Nm)", zeroVector)
def rocketComponentFactory(subDictPath, rocket, stage)
-
Initializes a rocket component based on the stringNameToClassMap
Inputs
subDictPath: (string) Path to subDict in simulation definition, like "Rocket.Stage1.Nosecone" rocket: (Rocket) that the component is a part of stage: (Stage) That the component is a part of Also uses the stringNameToClassMap dictionary
Expand source code
def rocketComponentFactory(subDictPath, rocket, stage): """ Initializes a rocket component based on the stringNameToClassMap Inputs: subDictPath: (string) Path to subDict in simulation definition, like "Rocket.Stage1.Nosecone" rocket: (Rocket) that the component is a part of stage: (Stage) That the component is a part of Also uses the stringNameToClassMap dictionary """ # Create SubDictReader for the rocket component's dictionary componentDictReader = SubDictReader(subDictPath, rocket.simDefinition) # Figure out which class to initialize className = componentDictReader.getString("class") referencedClass = stringNameToClassMap[className] # Initialize the rocket component newComponent = referencedClass(componentDictReader, rocket, stage) # Initialize logging component forces (if desired) initializeForceLogging(newComponent, subDictPath, rocket) return newComponent
Classes
class AeroDamping (componentDictReader, rocket, stage)
-
A zero-inertia component with constant aerodynamic damping coefficients
Expand source code
class AeroDamping(AeroForce): ''' A zero-inertia component with constant aerodynamic damping coefficients ''' position = Vector(0,0,0) def __init__(self, componentDictReader, rocket, stage): self.componentDictReader = componentDictReader self.rocket = rocket self.stage = stage self.name = componentDictReader.getDictName() self.Aref = componentDictReader.getFloat("Aref") self.Lref = componentDictReader.getFloat("Lref") self.zDampingCoeffs = componentDictReader.getVector("zDampingCoeffs") self.yDampingCoeffs = componentDictReader.getVector("yDampingCoeffs") self.xDampingCoeffs = componentDictReader.getVector("xDampingCoeffs") def getAppliedForce(self, state, time, environment, rocketCG): airspeed = max(AeroParameters.getLocalFrameAirVel(state, environment).length(), 0.0000001) redimConst = self.Lref / (2*airspeed) # Calculate moment coefficients from damping coefficients localFrameAngularVelocity = Vector(*state.angularVelocity) zMomentCoeff = self.zDampingCoeffs * localFrameAngularVelocity * redimConst yMomentCoeff = self.yDampingCoeffs * localFrameAngularVelocity * redimConst xMomentCoeff = self.xDampingCoeffs * localFrameAngularVelocity * redimConst momentCoeffs = [ xMomentCoeff, yMomentCoeff, zMomentCoeff ] return AeroFunctions.forceFromCoefficients(state, environment, 0, 0, *momentCoeffs, self.position, self.Aref, self.Lref)
Ancestors
- AeroForce
- RocketComponent
- abc.ABC
Class variables
var position
Methods
def getAppliedForce(self, state, time, environment, rocketCG)
-
Expand source code
def getAppliedForce(self, state, time, environment, rocketCG): airspeed = max(AeroParameters.getLocalFrameAirVel(state, environment).length(), 0.0000001) redimConst = self.Lref / (2*airspeed) # Calculate moment coefficients from damping coefficients localFrameAngularVelocity = Vector(*state.angularVelocity) zMomentCoeff = self.zDampingCoeffs * localFrameAngularVelocity * redimConst yMomentCoeff = self.yDampingCoeffs * localFrameAngularVelocity * redimConst xMomentCoeff = self.xDampingCoeffs * localFrameAngularVelocity * redimConst momentCoeffs = [ xMomentCoeff, yMomentCoeff, zMomentCoeff ] return AeroFunctions.forceFromCoefficients(state, environment, 0, 0, *momentCoeffs, self.position, self.Aref, self.Lref)
class AeroForce (componentDictReader, rocket, stage)
-
A zero-Inertia component with constant aerodynamic coefficients
Expand source code
class AeroForce(RocketComponent): ''' A zero-Inertia component with constant aerodynamic coefficients ''' # Object is just a force, inertia is zero inertia = Inertia(Vector(0,0,0), Vector(0,0,0), 0) def __init__(self, componentDictReader, rocket, stage): self.componentDictReader = componentDictReader self.rocket = rocket self.stage = stage self.name = componentDictReader.getDictName() self.position = componentDictReader.getVector("position") self.Aref = componentDictReader.getFloat("Aref") self.Lref = componentDictReader.getFloat("Lref") Cd = componentDictReader.getFloat("Cd") Cl = componentDictReader.getFloat("Cl") momentCoeffs = componentDictReader.getVector("momentCoeffs") self.aeroCoeffs = [ Cd, Cl, *momentCoeffs ] def getInertia(self, time, state): return self.inertia def getAppliedForce(self, state, time, environment, rocketCG): return AeroFunctions.forceFromCoefficients(state, environment, *self.aeroCoeffs, self.position, self.Aref, self.Lref)
Ancestors
- RocketComponent
- abc.ABC
Subclasses
Class variables
var inertia
Methods
def getAppliedForce(self, state, time, environment, rocketCG)
-
Expand source code
def getAppliedForce(self, state, time, environment, rocketCG): return AeroFunctions.forceFromCoefficients(state, environment, *self.aeroCoeffs, self.position, self.Aref, self.Lref)
def getInertia(self, time, state)
-
Expand source code
def getInertia(self, time, state): return self.inertia
class BoatTail (*args)
-
Defines a conical boattail (aerodynamic properties quite similar to curved boattails, especially in supersonic flight) Always assumes it's at the bottom of a rocket. Modelled like a Transition object, but accounts for base drag.
Two possible sets of inputs:
1. Initialization as a regular, dictionary-defined rocket component:
* args = (componentDictReader, rocket, stage)
* Expected classes: (SubDictReader
,Rocket
,Stage
)
2. Manual initialization:
* args = (startDiameter, endDiameter, length, position, inertia, rocket, stage, name, surfaceRoughness)
* Expected classes: (float, float, float,Vector
,Inertia
,Rocket
,Stage
, str, float)Expand source code
class BoatTail(Transition): ''' Defines a conical boattail (aerodynamic properties quite similar to curved boattails, especially in supersonic flight) Always assumes it's at the bottom of a rocket. Modelled like a Transition object, but accounts for base drag. ''' canConnectToComponentBelow = False ''' Overrides attribute inherited from BodyComponent (through Transition), to indicate that this component must exist at the very bottom of a rocket ''' def getAppliedForce(self, rocketState, time, environment, CG) -> ForceMomentSystem: Mach = AeroParameters.getMachNumber(rocketState, environment) Aref = self.rocket.Aref #### Normal Force #### AOA = AeroParameters.getTotalAOA(rocketState, environment) CN = AeroFunctions.Barrowman_GetCN(AOA, Aref, self.topArea, self.bottomArea) #### Pressure Drag #### Cd_base = AeroFunctions.getBaseDragCoefficient(Mach) Cd_pressure = Cd_base * self.CdAdjustmentFactor Cd_pressure *= self.frontalArea / self.rocket.Aref noEngine = (self.stage.engineShutOffTime == None) if noEngine or time > self.stage.engineShutOffTime: # Add base drag if engine is off Cd_pressure += Cd_base * self.bottomArea / Aref #### Skin Friction Drag #### if self.wettedArea == 0: skinFrictionDragCoefficient = 0 rollDampingMoment = Vector(0,0,0) else: skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, \ self.length, Mach, self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Total Drag #### Cd = Cd_pressure + skinFrictionDragCoefficient #### Assemble & return final force object #### return AeroFunctions.forceFromCdCN(rocketState, environment, Cd, CN, self.CPLocation, Aref, moment=rollDampingMoment)
Ancestors
Class variables
var canConnectToComponentBelow
-
Overrides attribute inherited from BodyComponent (through Transition), to indicate that this component must exist at the very bottom of a rocket
Methods
def getAppliedForce(self, rocketState, time, environment, CG) ‑> ForceMomentSystem
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG) -> ForceMomentSystem: Mach = AeroParameters.getMachNumber(rocketState, environment) Aref = self.rocket.Aref #### Normal Force #### AOA = AeroParameters.getTotalAOA(rocketState, environment) CN = AeroFunctions.Barrowman_GetCN(AOA, Aref, self.topArea, self.bottomArea) #### Pressure Drag #### Cd_base = AeroFunctions.getBaseDragCoefficient(Mach) Cd_pressure = Cd_base * self.CdAdjustmentFactor Cd_pressure *= self.frontalArea / self.rocket.Aref noEngine = (self.stage.engineShutOffTime == None) if noEngine or time > self.stage.engineShutOffTime: # Add base drag if engine is off Cd_pressure += Cd_base * self.bottomArea / Aref #### Skin Friction Drag #### if self.wettedArea == 0: skinFrictionDragCoefficient = 0 rollDampingMoment = Vector(0,0,0) else: skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, \ self.length, Mach, self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Total Drag #### Cd = Cd_pressure + skinFrictionDragCoefficient #### Assemble & return final force object #### return AeroFunctions.forceFromCdCN(rocketState, environment, Cd, CN, self.CPLocation, Aref, moment=rollDampingMoment)
Inherited members
class BodyComponent
-
Class that defines interface for axisymmetric body components. Contains logic for detecting adjacent body components & defining interfaces with them Examples:
NoseCone
,Stage
Expand source code
class BodyComponent(ABC): ''' Class that defines interface for axisymmetric body components. Contains logic for detecting adjacent body components & defining interfaces with them Examples: `MAPLEAF.Rocket.NoseCone`, `MAPLEAF.Rocket.Stage` ''' # Override these attributes in child classes to change whether they can connect to components above/below them canConnectToComponentAbove = True canConnectToComponentBelow = True def getTopInterfaceLocation(self) -> Union[None, Vector]: ''' For planar cylindrical interfaces, returns the location of the center of the cylindrical interface ''' if self.canConnectToComponentAbove: return self.position else: return None def getBottomInterfaceLocation(self) -> Union[None, Vector]: ''' For planar cylindrical interfaces, returns the location of the center of the cylindrical interface ''' if self.canConnectToComponentBelow: baseZCoord = self.position.Z-self.length return Vector(self.position.X, self.position.Y, baseZCoord) else: return None def _getCenterOfPressure(self, *args) -> Vector: return self.CPLocation @abstractmethod def getMaxDiameter(self): ''' These functions used for determining rocket's current max diameter ''' return @abstractmethod def getRadius(self, distanceFromTop: float) -> float: ''' Should return body component radius as a function of distance from the top of the component ''' return
Ancestors
- abc.ABC
Subclasses
Class variables
var canConnectToComponentAbove
var canConnectToComponentBelow
Methods
def getBottomInterfaceLocation(self) ‑> Optional[None]
-
For planar cylindrical interfaces, returns the location of the center of the cylindrical interface
Expand source code
def getBottomInterfaceLocation(self) -> Union[None, Vector]: ''' For planar cylindrical interfaces, returns the location of the center of the cylindrical interface ''' if self.canConnectToComponentBelow: baseZCoord = self.position.Z-self.length return Vector(self.position.X, self.position.Y, baseZCoord) else: return None
def getMaxDiameter(self)
-
These functions used for determining rocket's current max diameter
Expand source code
@abstractmethod def getMaxDiameter(self): ''' These functions used for determining rocket's current max diameter ''' return
def getRadius(self, distanceFromTop: float) ‑> float
-
Should return body component radius as a function of distance from the top of the component
Expand source code
@abstractmethod def getRadius(self, distanceFromTop: float) -> float: ''' Should return body component radius as a function of distance from the top of the component ''' return
def getTopInterfaceLocation(self) ‑> Optional[None]
-
For planar cylindrical interfaces, returns the location of the center of the cylindrical interface
Expand source code
def getTopInterfaceLocation(self) -> Union[None, Vector]: ''' For planar cylindrical interfaces, returns the location of the center of the cylindrical interface ''' if self.canConnectToComponentAbove: return self.position else: return None
class BodyTube (componentDictReader, rocket, stage)
-
Represent a cylindrical body tube
Expand source code
class BodyTube(FixedMass, BodyComponent): ''' Represent a cylindrical body tube ''' #### Init Functions #### def __init__(self, componentDictReader, rocket, stage): FixedMass.__init__(self, componentDictReader, rocket, stage) self.length = componentDictReader.getFloat("length") self.outerDiameter = componentDictReader.getFloat("outerDiameter") self.surfaceRoughness = componentDictReader.tryGetFloat("surfaceRoughness", defaultValue=self.rocket.surfaceRoughness) # Tell the rocket/stage what its diameter is self.rocket.bodyTubeDiameter = self.outerDiameter self.stage.bodyTubeDiameter = self.outerDiameter self._precomputeGeometry() def _precomputeGeometry(self): self.volume = math.pi * self.outerDiameter**2 / 4 * self.length self.planformArea = self.outerDiameter * self.length self.wettedArea = math.pi * self.outerDiameter * self.length self.CPLocation = self.position - Vector(0,0,self.length/2) #### Operational Functions #### def getAppliedForce(self, rocketState, time, environment, CG): Aref = self.rocket.Aref # Normal Force ---------------------------------------------------------------------------------------- AOA = AeroParameters.getTotalAOA(rocketState, environment) # Niskanen Eqn 3.26 - originally from Galejs normalForceCoefficient = 1.1 * (math.sin(AOA))**2 normalForceCoefficient *= self.planformArea / Aref # Drag ----------------------------------------------------------------------------------------------- # Skin Friction Mach = AeroParameters.getMachNumber(rocketState, environment) skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, self.length, Mach, self.surfaceRoughness, \ self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #There is no pressure drag associated with bodytubes - skin friction drag is total drag # Damping moments -------------------------------------------------------------------------------------- dampingMoments = self._computeLongitudinalDampingMoments(rocketState, environment, CG) # Roll damping due to skin friction dampingMoments += rollDampingMoment # Compute & dimensionalize total------------------------------------------------------------------------- return AeroFunctions.forceFromCdCN(rocketState, environment, skinFrictionDragCoefficient, normalForceCoefficient, self.CPLocation, Aref, moment=dampingMoments) def _computeLongitudinalDampingMoments(self, rocketState, environment, CoR, nSegments=25): ''' CoR = Center of Rotation (usually the rocket's CG) ''' # TODO: Check if there's an nice analytical solution to this integral # Eqn (3.57) from Niskanen, converted to vector form and integrated numerically # Numerical integration: Divide body tube into segments # Works for body tubes in any position, rotating on any axis dL = self.length / nSegments dA = dL * self.outerDiameter totalDampingMoment = Vector(0,0,0) for i in range(nSegments): # Center of current segment of body tube segmentCentroid = self.position + Vector(0,0, -dL*i - dL/2) # Vector from center of rotation to segment centroid psi = segmentCentroid - CoR # Velocity due to rotation is angular velocity cross distance (from center of rotation) vector segmentVelocity = rocketState.angularVelocity.crossProduct(psi) # Re-dimensionalizing coefficient required squared velocity - while preserving sign sqVel = Vector( segmentVelocity.X*abs(segmentVelocity.X), segmentVelocity.Y*abs(segmentVelocity.Y), \ segmentVelocity.Z*abs(segmentVelocity.Z) ) # Moment is distance vector cross force vector dM = -psi.crossProduct(sqVel) totalDampingMoment += dM # All terms of the integral summation must be multiplied by this constant: # 1.1 is drag coefficient of a cylinder in crossflow # 1/2 and density are from re-dimensionalizing the coefficient # Outerdiameter is part of area calculation integrationConstantMultiple = (1.1/2)*environment.Density*dA totalDampingMoment *= integrationConstantMultiple return totalDampingMoment def getMaxDiameter(self): return self.outerDiameter def getRadius(self, _): return self.outerDiameter/2 def plotShape(self): import matplotlib.pyplot as plt # Assume body tube is on the Z-axis tipZ = self.position.Z tailZ = tipZ - self.length radius = self.outerDiameter/2 Xvals = [] Yvals = [] Xvals.append(tipZ) Yvals.append(radius) # top right Xvals.append(tipZ) Yvals.append(-radius) # bottom right Xvals.append(tailZ) Yvals.append(-radius) # bottom left Xvals.append(tailZ) Yvals.append(radius) # top left Xvals.append(tipZ) Yvals.append(radius) # close the square plt.plot(Xvals, Yvals, color = 'k')
Ancestors
- FixedMass
- RocketComponent
- BodyComponent
- abc.ABC
Methods
def getAppliedForce(self, rocketState, time, environment, CG)
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG): Aref = self.rocket.Aref # Normal Force ---------------------------------------------------------------------------------------- AOA = AeroParameters.getTotalAOA(rocketState, environment) # Niskanen Eqn 3.26 - originally from Galejs normalForceCoefficient = 1.1 * (math.sin(AOA))**2 normalForceCoefficient *= self.planformArea / Aref # Drag ----------------------------------------------------------------------------------------------- # Skin Friction Mach = AeroParameters.getMachNumber(rocketState, environment) skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, self.length, Mach, self.surfaceRoughness, \ self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #There is no pressure drag associated with bodytubes - skin friction drag is total drag # Damping moments -------------------------------------------------------------------------------------- dampingMoments = self._computeLongitudinalDampingMoments(rocketState, environment, CG) # Roll damping due to skin friction dampingMoments += rollDampingMoment # Compute & dimensionalize total------------------------------------------------------------------------- return AeroFunctions.forceFromCdCN(rocketState, environment, skinFrictionDragCoefficient, normalForceCoefficient, self.CPLocation, Aref, moment=dampingMoments)
def plotShape(self)
-
Expand source code
def plotShape(self): import matplotlib.pyplot as plt # Assume body tube is on the Z-axis tipZ = self.position.Z tailZ = tipZ - self.length radius = self.outerDiameter/2 Xvals = [] Yvals = [] Xvals.append(tipZ) Yvals.append(radius) # top right Xvals.append(tipZ) Yvals.append(-radius) # bottom right Xvals.append(tailZ) Yvals.append(-radius) # bottom left Xvals.append(tailZ) Yvals.append(radius) # top left Xvals.append(tipZ) Yvals.append(radius) # close the square plt.plot(Xvals, Yvals, color = 'k')
Inherited members
class EventTypes (value, names=None, *, module=None, qualname=None, type=None, start=1)
-
An enumeration.
Expand source code
class EventTypes(Enum): Apogee = "apogee" AscendingThroughAltitude = "ascendingThroughAltitude" DescendingThroughAltitude = "descendingThroughAltitude" MotorBurnout = "motorBurnout" TimeReached = "timeReached"
Ancestors
- enum.Enum
Class variables
var Apogee
var AscendingThroughAltitude
var DescendingThroughAltitude
var MotorBurnout
var TimeReached
class Fin (componentDictReader, parentFinSet, spanwiseDirection, rocket, stage)
-
Represents a single fin in a FinSet
Expand source code
class Fin(FixedMass): ''' Represents a single fin in a FinSet ''' def __init__(self, componentDictReader, parentFinSet, spanwiseDirection, rocket, stage): FixedMass.__init__(self, componentDictReader, rocket, stage) self.finSet = parentFinSet # type: FinSet ''' Reference to the parent fin set ''' self.spanSliceAreas = self.finSet.spanSliceAreas self.spanSliceRadii = self.finSet.spanSliceRadii self.sliceLEPositions = self.finSet.sliceLEPositions self.sliceLengths = self.finSet.sliceLengths self.finAngle = self.finSet.finAngle self.stallAngle = self.finSet.stallAngle self.span = self.finSet.span self.planformArea = self.finSet.planformArea self.midChordSweep = self.finSet.midChordSweep # Unit vector parallel to the fin's shaft and/or fin's spanwise direction, normal to the rocket's body tube self.spanwiseDirection = spanwiseDirection # Vector normal to the fin surface, in the X/Y Plane, when self.finAngle == 0 self.undeflectedFinNormal = Vector(self.spanwiseDirection[1], -self.spanwiseDirection[0], 0) self.tipPosition = self.finSet.dZ_dSpan_LE_neg * self.span # Spanwise component of Center of Pressure (CP) location self.CPSpanWisePosition = self.spanwiseDirection*(self.finSet.MACYPos + self.finSet.bodyRadius) def _barrowmanAeroFunc(self, rocketState, time, environment, precomputedData, CG = Vector(0,0,0), finDeflectionAngle = None): ''' Precomputed Data is a named tuple (PreComputedFinAeroData) which contains data/results from the parts of the fin aerodynamics calculation that are common to all fins in a FinSet (drag calculations mostly). These calculations are performed at the FinSet level. Only the parts of the Fin Aero Computation that change from fin to fin (normal force mostly, since different fins can have different angles of attack) are computed here ''' Mach = AeroParameters.getMachNumber(rocketState, environment) dynamicPressure = AeroParameters.getDynamicPressure(rocketState, environment) if finDeflectionAngle == None: finDeflectionAngle = self.finAngle # Adjusted by parent finset during each timestep, when the FinSet is controlled # Unpack precomputedData airVelRelativeToFin, CPXPos, totalDragCoefficient = precomputedData #### Compute normal force----------------------------------------------------------------------------------------------------------------- # Find fin normal vector after fin deflection finDeflectionRotation = Quaternion(axisOfRotation=self.spanwiseDirection, angle=radians(finDeflectionAngle)) finNormal = finDeflectionRotation.rotate(self.undeflectedFinNormal) # Get the tangential velocity component vector, per unit radial distance from the rocket centerline rollAngVel = AngularVelocity(0, 0, rocketState.angularVelocity.Z) unitSpanTangentialAirVelocity = rollAngVel.crossProduct(self.spanwiseDirection)*(-1) def subsonicNormalForce(Mach): # Subsonic linear method tempBeta = AeroParameters.getBeta(Mach) CnAlpha = getFinCnAlpha_Subsonic_Barrowman(self.span, self.planformArea, tempBeta, self.midChordSweep) return getSubsonicFinNormalForce(airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, self.spanwiseDirection, self.CPSpanWisePosition.length(), CnAlpha, self) def supersonicNormalForce(Mach): # Supersonic Busemann method gamma = AeroFunctions.getGamma() tempBeta = AeroParameters.getBeta(Mach) K1, K2, K3, Kstar = getBusemannCoefficients(Mach, tempBeta, gamma) # Mach Cone coords machAngle = asin(1/Mach) machCone_negZPerRadius = 1 / tan(machAngle) machConeEdgeZPos = [] outerRadius = self.spanSliceRadii[-1] for i in range(len(self.spanSliceRadii)): machConeAtCurrentRadius = (outerRadius - self.spanSliceRadii[i])*machCone_negZPerRadius + self.tipPosition machConeEdgeZPos.append(machConeAtCurrentRadius) return getSupersonicFinNormalForce(airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, machConeEdgeZPos, self.spanwiseDirection, self.CPSpanWisePosition.length(), K1, K2, K3, Kstar, self) if Mach <= 0.8: normalForceMagnitude, finMoment = subsonicNormalForce(Mach) elif Mach < 1.4: # Interpolate in transonic region # TODO: Do this with less function evaluations? Perhaps precompute AOA and Mach combinations and simply interpolate? Lazy precompute? Cython? x1, x2 = 0.8, 1.4 # Start, end pts of interpolated region dx = 0.001 # Find normal force and derivative at start of interpolation interval f_x1, m_x1 = subsonicNormalForce(x1) f_x12, m_x12 = subsonicNormalForce(x1+dx) # Find normal force and derivative at end of interpolation interval f_x2, m_x2 = supersonicNormalForce(x2) f_x22, m_x22 = supersonicNormalForce(x2+dx) normalForceMagnitude = Interpolation.cubicInterp(Mach, x1, x2, f_x1, f_x2, f_x12, f_x22, dx) finMoment = Interpolation.cubicInterp(Mach, x1, x2, m_x1, m_x2, m_x12, m_x22, dx) else: normalForceMagnitude, finMoment = supersonicNormalForce(Mach) # Complete redimensionalization of normal force coefficients by multiplying by dynamic pressure # Direct the normal force along the fin's normal direction normalForce = normalForceMagnitude * dynamicPressure * finNormal finMoment *= dynamicPressure #### Get axial force----------------------------------------------------------------------------------------------------------------------- avgAOA = getFinSliceAngleOfAttack(self.spanSliceRadii[round(len(self.spanSliceAreas)/2)], airVelRelativeToFin, unitSpanTangentialAirVelocity, finNormal, self.spanwiseDirection, self.stallAngle) # Approximate average fin AOA totalAxialForceCoefficient = AeroFunctions.getDragToAxialForceFactor(avgAOA) * totalDragCoefficient axialForceMagnitude = totalAxialForceCoefficient * self.rocket.Aref * dynamicPressure axialForceDirection = self.spanwiseDirection.crossProduct(finNormal) axialForce = axialForceDirection*axialForceMagnitude #### Get CP Location ---------------------------------------------------------------------------------------------------------------------- CPChordWisePosition = self.position - Vector(0, 0, CPXPos) # Ignoring the change in CP position due to fin deflection for now globalCP = self.CPSpanWisePosition + CPChordWisePosition #### Assemble total force moment system objects-------------------------------------------------------------------------------------------- totalForce = normalForce + axialForce return ForceMomentSystem(totalForce, globalCP, moment=Vector(0, 0, finMoment)), globalCP def getAppliedForce(self, rocketState, time, environment, CG, precomputedData): [ aeroForce, CP ] = self._barrowmanAeroFunc(rocketState, time, environment, precomputedData, CG) return aeroForce
Ancestors
- FixedMass
- RocketComponent
- abc.ABC
Instance variables
var finSet
-
Reference to the parent fin set
Methods
def getAppliedForce(self, rocketState, time, environment, CG, precomputedData)
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG, precomputedData): [ aeroForce, CP ] = self._barrowmanAeroFunc(rocketState, time, environment, precomputedData, CG) return aeroForce
class FinSet (componentDictReader, rocket, stage)
-
Class represents a set of n identical fins, all at the same longitudinal location, arranged axisymmetrically. Fin orientations can be controlled by a
ControlSystem
Expand source code
class FinSet(FixedMass, ActuatedSystem): ''' Class represents a set of n identical fins, all at the same longitudinal location, arranged axisymmetrically. Fin orientations can be controlled by a `MAPLEAF.GNC.ControlSystems.ControlSystem` ''' #### Initialization Functions #### def __init__(self, componentDictReader, rocket, stage): FixedMass.__init__(self, componentDictReader, rocket, stage) self.controlSystem = None # Set by self.initializeActuators, if it is called by a control system being initialized ex: RocketControlSystem.__init__() self.actuatorList = None # Set by self.initializeActuators, if it is called by a control system being initialized ex: RocketControlSystem.__init__() self.numFins = componentDictReader.getInt("numFins") self.firstFinAngle = componentDictReader.getFloat("firstFinAngle") # Set fin properties self.finAngle = componentDictReader.getFloat("finCantAngle") self.sweepAngle = math.radians(componentDictReader.getFloat("sweepAngle")) self.rootChord = componentDictReader.getFloat("rootChord") self.tipChord = componentDictReader.getFloat("tipChord") self.span = componentDictReader.getFloat("span") self.surfaceRoughness = componentDictReader.tryGetFloat("surfaceRoughness", defaultValue=self.rocket.surfaceRoughness) self.thickness = componentDictReader.getFloat("thickness") self.numFinSpanSteps = componentDictReader.getInt("numFinSpanSlicesForIntegration") # Leading Edge properties self.leadingEdgeShape = componentDictReader.getString("LeadingEdge.shape") if self.leadingEdgeShape == "Round": self.leadingEdgeRadius = componentDictReader.tryGetFloat("LeadingEdge.radius", defaultValue=self.thickness/100) elif self.leadingEdgeShape == "Blunt": self.leadingEdgeThickness = componentDictReader.tryGetFloat("LeadingEdge.thickness", defaultValue=self.thickness) else: raise ValueError("ERROR: Leading edge shape: {} not implemented. Use 'Round' or 'Blunt'".format(self.leadingEdgeShape)) # Trailing Edge properties self.trailingEdgeShape = componentDictReader.getString("TrailingEdge.shape") if self.trailingEdgeShape == "Round": self.trailingEdgeRadius = componentDictReader.getFloat("TrailingEdge.radius") elif self.trailingEdgeShape == "Blunt": self.trailingEdgeThickness = componentDictReader.getFloat("TrailingEdge.thickness") elif self.trailingEdgeShape != "Tapered": raise ValueError("ERROR: Trailing edge shape: {} not implemented. Use 'Round', 'Blunt', or 'Tapered'".format(self.trailingEdgeShape)) def initializeActuators(self, controlSystem): self.controlSystem = controlSystem # Initialize an actuator model for each fin ActuatedSystem.__init__(self, self.numFins) def precomputeProperties(self): self._calculateSweepAngles() ### Compute Other Simple Properties #### self.planformArea = self.span * (self.tipChord + self.rootChord) / 2 self.wettedArea = 2*self.planformArea self.aspectRatio = (2*self.span)**2/self.planformArea # Aspect ratio of the wing that would be created by reflecting the fin about its root chord (Fleeman/Niskanen) self.stallAngle = 15 * (1 + 1.1/self.aspectRatio) # Compute stall angle from Hoerner, 'Fluid Dynamic Lift' for low aspect ratio wings # Get the body radius where the fin is mounted stageTopZ = self.stage.position.Z distanceFromTopOfStageToTopOfThisFinSet = stageTopZ - self.position.Z self.bodyRadius = self.stage.getRadius(distanceFromTopOfStageToTopOfThisFinSet) self._calculateInterferenceFactors() self._calculateMACProperties() self._splitFinsIntoSlices() self._precomputeSubsonicFinThicknessDrag() self._precomputeCPInterpolationPolynomial() # Initialize all the child fins in the self.finList list self._initChildFins(self.componentDictReader, self.rocket, self.stage) def _calculateSweepAngles(self): ''' Compute Trailing Edge (TE) and Mid Chord sweep angles ''' LEtipChordBehindRootChord = self.span * math.tan(self.sweepAngle) # Z-distance the front of the tip chord is behind the front of the root chord TETipChordBehindRootChord = self.tipChord + LEtipChordBehindRootChord - self.rootChord # Z-distance the back of the tip chord is behind the back of the root chord if(TETipChordBehindRootChord == 0): self.trailingEdgeSweep = 0.0 elif(TETipChordBehindRootChord < 0): # Negative trailing edge sweep is a forward-swept rear self.trailingEdgeSweep = -1*math.atan2(abs(TETipChordBehindRootChord),self.span) else: self.trailingEdgeSweep = math.atan2(abs(TETipChordBehindRootChord), self.span) self.midChordSweep = (self.sweepAngle + self.trailingEdgeSweep) / 2 self.dZ_dSpan_LE_neg = math.tan(self.sweepAngle) # -dZ/dSpan (leading edge slope) self.dZ_dSpan_TE_neg = math.tan(self.trailingEdgeSweep) # dZ/dSpan (trailing edge slope) def _calculateInterferenceFactors(self): # Interference w/ body tube # Accounts for additional normal forces generated by the fins, due to the presence of the rocket body # Niskanen Eqn 3.56, originally from Barrowman self.bodyOnFinInterferenceFactor = 1 + self.bodyRadius / (self.span + self.bodyRadius) # Interference b/w the fins # Niskanen Eqn 3.54, from MIL-HDBK-762 # Accounts for reduced normal force generated by large numbers of fins in a group # Not expected to work well for more than 8 fins if self.numFins <= 4: self.finNumberInterferenceFactor = 1.0 elif self.numFins <= 8: numberToFactorMap = { 5: 0.948, 6: 0.913, 7: 0.854, 8: 0.810, } self.finNumberInterferenceFactor = numberToFactorMap[self.finNumber] else: self.finNumberInterferenceFactor = 0.75 def _calculateMACProperties(self): ''' Calculates MAC Length, MACY, and MACX - Niskanen (Eqn. 3.30-3.32) ''' def LEdgeAtY(y): # Leading Edge at y - where y is the spanwise coordinate return self.dZ_dSpan_LE_neg*y numSpanSteps = round(self.span / 0.0001) # One step per 0.1 mm finSpanStep = self.span / numSpanSteps MACIntegralSum = 0 MACYPosIntegralSum = 0 XMACLeadingEdgeIntegralSum = 0 for i in range(numSpanSteps): y = i * finSpanStep + finSpanStep/2 MACIntegralSum += ((self.getChord(y)**2)*finSpanStep) # Eqn 3.30, integrated numerically MACYPosIntegralSum += y * (self.getChord(y) * finSpanStep) # Eqn 3.31, integrated numerically XMACLeadingEdgeIntegralSum += LEdgeAtY(y) * self.getChord(y)*finSpanStep # Eqn 3.32, integrated numerically # Save results self.MACLength = MACIntegralSum/self.planformArea self.MACYPos = MACYPosIntegralSum/self.planformArea self.XMACLeadingEdge = XMACLeadingEdgeIntegralSum/self.planformArea def _splitFinsIntoSlices(self): ''' Precomputes fin area slices for normal force integration ''' stepSize = self.span / self.numFinSpanSteps bodyRadius = self.bodyRadius self.spanSliceAreas = [] # Area of each slice, m^2 self.spanSliceRadii = [] # Radius of the center of each slice, from the rocket centerline, m self.sliceLEPositions = [] # Z - Position of the LE at the center of the slice, from the tip of root chord self.sliceLengths = [] for i in range(self.numFinSpanSteps): y = i*stepSize + stepSize*0.5 self.spanSliceRadii.append(y + bodyRadius) self.spanSliceAreas.append(stepSize*self.getChord(y)) self.sliceLEPositions.append(y * self.dZ_dSpan_LE_neg) self.sliceLengths.append(self.getChord(y)) def _precomputeSubsonicFinThicknessDrag(self): ''' Precompute the subsonic thickness drag coefficient (Barrowman Eqn 4-36) ''' sigma = (self.aspectRatio/2) * (self.thickness / self.rootChord)**(1/3) # Barrowman Eqn 4-35 b CD_TT_star = 1.15 * (self.thickness/self.rootChord)**(5/3) * (1.61 + sigma - sqrt((sigma - 1.43)**2 + 0.578)) # Get skin friction coefficient at Mach 1 mach1State = copy.deepcopy(self.rocket.rigidBody.state) mach1State.velocity = Vector(0,0,340.3) # Get speed up around Mach 1 environment = self.rocket.environment.getAirProperties(self.rocket.rigidBody.state.position, 0) CD_f_star = AeroFunctions.getSkinFrictionCoefficient(mach1State, environment, self.MACLength, 1.0, self.surfaceRoughness, self.rocket.fullyTurbulentBL) # Final result self.subsonicFinThicknessK = cos(self.midChordSweep)**2 + (((CD_TT_star/CD_f_star - 4*cos(self.midChordSweep)*(self.thickness/self.rootChord)) / (120 * cos(self.midChordSweep)**2 * (self.thickness/self.rootChord)**4))**2)**(1/3) def _precomputeCPInterpolationPolynomial(self): ''' Precompute Interpolation polynomial coefficients for getCPXLocation function ''' AR = self.aspectRatio f_2 = (AR * (3)**0.5 - 0.67) / (2 * (3)**0.5 * AR - 1) f_prime_2_t1 = (2 * (3)**-0.5 * AR) * (2 * AR * (3)**0.5 - 1) f_prime_2_t2 = ((3)**0.5 * AR - 0.67) * (AR * 4 * (3)**-0.5) f_prime_2_denom = (2 * AR * (3)**0.5 -1)**2 f_prime_2 = (f_prime_2_t1 - f_prime_2_t2) / f_prime_2_denom a_mat = [ [0.5**5, 0.5**4, 0.5**3, 0.5**2, 0.5, 1], [5*(0.5)**4, 4*(0.5)**3, 3*(0.5)**2, 2*(0.5), 1, 0], [2**5, 2**4, 2**3, 2**2, 2, 1], [5*(2)**4, 4*(2)**3, 3*(2)**2, 2*2, 1, 0], [20*2**3, 12*2**2, 6*2, 2, 0, 0], [60*2**2, 24*2, 6, 0, 0, 0], ] b_vec = [0.25, 0, f_2, f_prime_2, 0, 0] # Save results self.x = np.linalg.solve(a_mat, b_vec) def _initChildFins(self, componentDictReader, rocket, stage): self.finList = [] finSeparationAngle = 360/self.numFins firstFinAngle = self.firstFinAngle # Create child fins for i in range(self.numFins): # Figure out what direction the fin points in spanwiseUnitX = math.cos(math.radians(firstFinAngle + i*finSeparationAngle)) spanwiseUnitY = math.sin(math.radians(firstFinAngle + i*finSeparationAngle)) spanWiseDirection = Vector(spanwiseUnitX, spanwiseUnitY, 0) #Initialize each fin separately and keep in a list self.finList.append(Fin(componentDictReader, self, spanWiseDirection, rocket, stage)) # TODO: Replace this with control system log # TODO: Remove all getLogHeader functions def getLogHeader(self): header = " {}FX(N) {}FY(N) {}FZ(N) {}MX(Nm) {}MY(Nm) {}MZ(Nm)".format(*[self.name]*6) if self.controlSystem != None: for finNumber in range(1, self.numFins+1): header += " {}Fin{}Angle(deg)".format(self.name, finNumber) return header ### Functions used during simulation ### 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 def _getPreComputedFinAeroData(self, rocketState, environment, CG): #General Info --------------------------------------------------------------------------------------------------------------------- Aref = self.rocket.Aref Mach = AeroParameters.getMachNumber(rocketState, environment) dynamicPressure = AeroParameters.getDynamicPressure(rocketState, environment) # Skin Friction Drag ------------------------------------------------------------------------------------------------------------- skinFrictionCoefficient = AeroFunctions.getSkinFrictionCoefficient(rocketState, environment, self.MACLength, Mach, self.surfaceRoughness, self.rocket.fullyTurbulentBL) # Adjust to the rocket reference area (skinFrictionCoefficient is based on wetted area) skinFrictionDragCoefficient = skinFrictionCoefficient*(self.wettedArea / Aref) # Correct for additional surface area due to fin thickness - Niskanen Eqn 3.85 skinFrictionDragCoefficient *= (1 + 2*self.thickness/self.MACLength) # Pressure Drag ------------------------------------------------------------------------------------------------------------------ # Leading edge drag coefficient if self.leadingEdgeShape == "Round": leadingEdgeCd = AeroFunctions.getCylinderCrossFlowCd_ZeroBaseDrag(Mach) LEthickness = self.leadingEdgeRadius*2 elif self.leadingEdgeShape == "Blunt": leadingEdgeCd = AeroFunctions.getBluntBodyCd_ZeroBaseDrag(Mach) LEthickness = self.leadingEdgeThickness # Adjust for leading edge angle and convert reference area to rocket reference area - Barrowman Eqn 4-22 leadingEdgeCdAdjustmentFactor = LEthickness * self.span * cos(self.sweepAngle)**2 / Aref leadingEdgeCd *= leadingEdgeCdAdjustmentFactor # Trailing edge drag coefficient - simpler method from Niskanen section 3.4.4. Corrected to use only the trailing edge area, not the full fin frontal area # more intricate method available in Barrowman baseDragCd = AeroFunctions.getBaseDragCoefficient(Mach) if self.trailingEdgeShape == "Tapered": TEthickness = 0 # Zero base drag elif self.trailingEdgeShape == "Round": TEthickness = self.trailingEdgeRadius # 0.5 base drag elif self.trailingEdgeShape == "Blunt": TEthickness = self.trailingEdgeThickness # Full base drag # Convert to standard rocket reference Area trailingEdgeCd = baseDragCd * self.span*TEthickness/Aref # Thickness / Wave Drag #TODO: This section doesn't seem to be working quite right if Mach <= 1: # Thickness drag, subsonic thicknessDrag = 4*skinFrictionCoefficient*((self.thickness/self.rootChord)*cos(self.midChordSweep) + \ (30 * (self.thickness/self.rootChord)**4 * cos(self.midChordSweep)**2) / \ (self.subsonicFinThicknessK - Mach**2 * cos(self.midChordSweep)**2)**(3/2)) else: # Supersonic wave drag # Using simplistic method from Hoerner - assumes diamond profile (pg 17-12, Eqn 29) # TODO: Implement method from Barrowman's FIN program thicknessDrag = 2.3*self.aspectRatio * (self.thickness / self.MACLength)**2 thicknessDrag *= self.planformArea/Aref pressureDragCoefficient = leadingEdgeCd + trailingEdgeCd + thicknessDrag # Total Drag -------------------------------------------------------------------------------------------------------------------- totalDragCoefficient = pressureDragCoefficient + skinFrictionDragCoefficient localFrameRocketVelocity = AeroParameters.getLocalFrameAirVel(rocketState, environment) axialPositionRelCG = self.position - CG finVelocityDueToRocketPitchYaw = rocketState.angularVelocity.crossProduct(axialPositionRelCG) airVelRelativeToFin = localFrameRocketVelocity - finVelocityDueToRocketPitchYaw # The negative puts it in the wind frame CPXPos = self._getCPXPos(Mach) #### Transfer info to fins #### return PreComputedFinAeroData(airVelRelativeToFin, CPXPos, totalDragCoefficient) def _getCPXPos(self, Mach): ''' Finds the chordwise position of the center of pressure, relative to the tip of the root chord (self.position) Methods from Tactical missile design by Fleeman through Niskanen ''' if Mach < 0.5: XF = self.XMACLeadingEdge + 0.25*self.MACLength #As per open rocket documentation elif Mach >= 2.0: beta = AeroParameters.getBeta(Mach) CPCalc = lambda AR, B : (AR*B - 0.67) / (2*AR*B - 1) #Eq 3.35 of open rocket documentation (greater than M = 2) XF = self.MACLength*CPCalc(self.aspectRatio, beta) + self.XMACLeadingEdge #As per open rocket documentation else: #Between the two extremes is a polynomial curve fit #Fifth order polynomial fit as per Niskanen Eqn 3.36 (Originally from Fleeman) # Old code: XF = np.polyval(self.x, Mach)*self.MACLength + self.XMACLeadingEdge # Evaluate polynomial manually for maximum speed (8.5x improvement for 6 coeffs) # Speed test in test/speedTests/PolyvalSpeed.py polyval = 0 nTerms = len(self.x) for i in range(nTerms): polyval += self.x[i]*Mach**(nTerms-1-i) XF = polyval*self.MACLength + self.XMACLeadingEdge # Polyval defines the fraction of the MAC that the Cp is behind the leading edge return XF def getChord(self, y): ''' Returns chord at y, where y==0 is at the root chord, and y==span is at the tip chord ''' return (self.tipChord - self.rootChord) * (y/self.span) + self.rootChord def plotShape(self): import matplotlib.pyplot as plt BodyTubeDiameter = self.bodyRadius*2 SubComponentPos = self.position.Z span = self.span rootChord = self.rootChord tipChord = self.tipChord sweepAngle = self.sweepAngle Xvals = [] Yvals = [] Xvals.append(SubComponentPos) Yvals.append(BodyTubeDiameter/2) Xvals.append(Xvals[0]-span*math.tan(sweepAngle)) Yvals.append(Yvals[0]+span) Xvals.append(Xvals[1]-tipChord) Yvals.append(Yvals[1]) Xvals.append(Xvals[0]-rootChord) Yvals.append(Yvals[0]) Xvals.append(Xvals[0]) Yvals.append(Yvals[0]) plt.plot(Xvals,Yvals, color = 'k') if self.numFins == 4: Yvals = [-1*i for i in Yvals] plt.plot(Xvals,Yvals, color = 'k') Yvals = [0*i for i in Yvals] plt.plot(Xvals,Yvals, color = 'k') elif self.numFins == 3: Yvals = [] Yvals.append(-BodyTubeDiameter/2*math.sin(math.radians(30))) Yvals.append(-(BodyTubeDiameter/2 + span)*math.sin(math.radians(30))) Yvals.append(Yvals[1]) Yvals.append(Yvals[0]) Yvals.append(Yvals[0]) plt.plot (Xvals, Yvals, color = 'k')
Ancestors
- FixedMass
- RocketComponent
- ActuatedSystem
- abc.ABC
Methods
def getAppliedForce(self, rocketState, time, environment, CG)
-
Expand source code
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
def getChord(self, y)
-
Returns chord at y, where y==0 is at the root chord, and y==span is at the tip chord
Expand source code
def getChord(self, y): ''' Returns chord at y, where y==0 is at the root chord, and y==span is at the tip chord ''' return (self.tipChord - self.rootChord) * (y/self.span) + self.rootChord
def getLogHeader(self)
-
Expand source code
def getLogHeader(self): header = " {}FX(N) {}FY(N) {}FZ(N) {}MX(Nm) {}MY(Nm) {}MZ(Nm)".format(*[self.name]*6) if self.controlSystem != None: for finNumber in range(1, self.numFins+1): header += " {}Fin{}Angle(deg)".format(self.name, finNumber) return header
def plotShape(self)
-
Expand source code
def plotShape(self): import matplotlib.pyplot as plt BodyTubeDiameter = self.bodyRadius*2 SubComponentPos = self.position.Z span = self.span rootChord = self.rootChord tipChord = self.tipChord sweepAngle = self.sweepAngle Xvals = [] Yvals = [] Xvals.append(SubComponentPos) Yvals.append(BodyTubeDiameter/2) Xvals.append(Xvals[0]-span*math.tan(sweepAngle)) Yvals.append(Yvals[0]+span) Xvals.append(Xvals[1]-tipChord) Yvals.append(Yvals[1]) Xvals.append(Xvals[0]-rootChord) Yvals.append(Yvals[0]) Xvals.append(Xvals[0]) Yvals.append(Yvals[0]) plt.plot(Xvals,Yvals, color = 'k') if self.numFins == 4: Yvals = [-1*i for i in Yvals] plt.plot(Xvals,Yvals, color = 'k') Yvals = [0*i for i in Yvals] plt.plot(Xvals,Yvals, color = 'k') elif self.numFins == 3: Yvals = [] Yvals.append(-BodyTubeDiameter/2*math.sin(math.radians(30))) Yvals.append(-(BodyTubeDiameter/2 + span)*math.sin(math.radians(30))) Yvals.append(Yvals[1]) Yvals.append(Yvals[0]) Yvals.append(Yvals[0]) plt.plot (Xvals, Yvals, color = 'k')
def precomputeProperties(self)
-
Expand source code
def precomputeProperties(self): self._calculateSweepAngles() ### Compute Other Simple Properties #### self.planformArea = self.span * (self.tipChord + self.rootChord) / 2 self.wettedArea = 2*self.planformArea self.aspectRatio = (2*self.span)**2/self.planformArea # Aspect ratio of the wing that would be created by reflecting the fin about its root chord (Fleeman/Niskanen) self.stallAngle = 15 * (1 + 1.1/self.aspectRatio) # Compute stall angle from Hoerner, 'Fluid Dynamic Lift' for low aspect ratio wings # Get the body radius where the fin is mounted stageTopZ = self.stage.position.Z distanceFromTopOfStageToTopOfThisFinSet = stageTopZ - self.position.Z self.bodyRadius = self.stage.getRadius(distanceFromTopOfStageToTopOfThisFinSet) self._calculateInterferenceFactors() self._calculateMACProperties() self._splitFinsIntoSlices() self._precomputeSubsonicFinThicknessDrag() self._precomputeCPInterpolationPolynomial() # Initialize all the child fins in the self.finList list self._initChildFins(self.componentDictReader, self.rocket, self.stage)
Inherited members
class FixedForce (componentDictReader, rocket, stage)
-
Interface definition for rocket components
A Zero-inertia component that applies a constant ForceMomentSystem to the rocket
Expand source code
class FixedForce(RocketComponent): 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) def getInertia(self, time, state): return self.inertia def getAppliedForce(self, rocketState, time, environment, rocketCG): return self.force
Ancestors
- RocketComponent
- abc.ABC
Methods
def getAppliedForce(self, rocketState, time, environment, rocketCG)
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, rocketCG): return self.force
def getInertia(self, time, state)
-
Expand source code
def getInertia(self, time, state): return self.inertia
class FixedMass (componentDictReader, rocket, stage)
-
Base class for all fixed-mass rocket components Implements functionality to read/store inertia and position info from sim definition file
Expand source code
class FixedMass(RocketComponent): ''' Base class for all fixed-mass rocket components Implements functionality to read/store inertia and position info from sim definition file ''' 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)) def getInertia(self, time, state): return self.inertia def getMass(self, time): return self.inertia.mass def getCG(self, time): return self.inertia.CG def getAppliedForce(self, rocketState, time, environment, CG): return self.zeroForce
Ancestors
- RocketComponent
- abc.ABC
Subclasses
Methods
def getAppliedForce(self, rocketState, time, environment, CG)
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG): return self.zeroForce
def getCG(self, time)
-
Expand source code
def getCG(self, time): return self.inertia.CG
def getInertia(self, time, state)
-
Expand source code
def getInertia(self, time, state): return self.inertia
def getMass(self, time)
-
Expand source code
def getMass(self, time): return self.inertia.mass
class FractionalJetDamping (componentDictReader, rocket, stage)
-
A component to model Jet damping as per NASA's Two Stage to Orbit verification case
Expand source code
class FractionalJetDamping(RocketComponent): ''' A component to model Jet damping as per NASA's Two Stage to Orbit verification case ''' # Object is just a force, inertia is zero inertia = Inertia(Vector(0,0,0), Vector(0,0,0), 0) def __init__(self, componentDictReader, rocket, stage): self.rocket = rocket self.stage = stage self.componentDictReader = componentDictReader self.name = componentDictReader.getDictName() self.dampingFraction = componentDictReader.getFloat("fraction") 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)) def getInertia(self, time, state): return self.inertia
Ancestors
- RocketComponent
- abc.ABC
Class variables
var inertia
Methods
def getAppliedForce(self, rocketState, time, environmentalConditions, rocketCG)
-
Expand source code
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))
def getInertia(self, time, state)
-
Expand source code
def getInertia(self, time, state): return self.inertia
class NoseCone (componentDictReader, rocket, stage)
-
Represent a Tangent Ogive Nosecone
Expand source code
class NoseCone(FixedMass, BodyComponent): ''' Represent a Tangent Ogive Nosecone ''' canConnectToComponentAbove = False # Overrides attribute from BodyComponent -> Nosecone must be at top of rocket #### Initialization Functions #### def __init__(self, componentDictReader, rocket, stage): FixedMass.__init__(self, componentDictReader, rocket, stage) self.aspectRatio = componentDictReader.getFloat("aspectRatio") ''' Length over diameter - aka fineness ratio (of exposed surface area) ''' self.baseDiameter = componentDictReader.getFloat("baseDiameter") ''' Diameter at base of nosecone (m) ''' self.length = self.aspectRatio * self.baseDiameter ''' Length from tip to tail, of external surface (m) ''' self.shape = componentDictReader.getString("shape") ''' Description of nosecone shape - something like 'Ogive' or 'Cone' ''' self.surfaceRoughness = componentDictReader.tryGetFloat("surfaceRoughness", defaultValue=self.rocket.surfaceRoughness) ''' (m) ''' self._precomputeGeometry() def _precomputeGeometry(self): self.length = self.baseDiameter * self.aspectRatio self.volume = self._getVolume() self.planformArea = self._getPlanformArea() self.wettedArea = self._getWettedArea() # Using Barrowman's method, the CP Location is a constant self.baseArea = self.baseDiameter**2 * math.pi / 4 self.CPLocation = AeroFunctions.Barrowman_GetCPLocation(self.length, 0, self.baseArea, self.volume) # Precompute coefficients for pressure drag interpolation------------------------------------------- # Niskanen 3.4 and Appendix B self.coneHalfAngle = abs(math.atan((self.baseDiameter/2) / self.length)) self.SubsonicCdPolyCoeffs = computeSubsonicPolyCoeffs(self.coneHalfAngle) self.TransonicCdPolyCoeffs = computeTransonicPolyCoeffs(self.coneHalfAngle) def _getVolume(self): if self.shape == "tangentOgive": #https://www.rocketryforum.com/attachments/nosecone_eqn2-pdf.336812/ baseRadius = self.baseDiameter / 2 length = self.baseDiameter * self.aspectRatio noseconeRadiusOfCurvature = (baseRadius**2 + length**2) / (2*baseRadius) return math.pi * ( length * noseconeRadiusOfCurvature**2 - length**3 / 3 - (noseconeRadiusOfCurvature - baseRadius) * noseconeRadiusOfCurvature**2 * (math.asin(length / noseconeRadiusOfCurvature))) elif self.shape == "cone": return math.pi * (self.baseDiameter)**2 * self.length / 3 elif self.shape == "halfSeries": #https://www.rocketryforum.com/attachments/nosecone_eqn2-pdf.336812/ baseRadius = self.baseDiameter / 2 length = self.baseDiameter * self.aspectRatio return math.pi * baseRadius**2 * length / 2 else: raise TypeError("The nosecone shape {} is not recognized".format(self.shape)) def _getPlanformArea(self): if self.shape == "tangentOgive": baseRadius = self.baseDiameter / 2 length = self.baseDiameter * self.aspectRatio noseconeRadiusOfCurvature = (baseRadius**2 + length**2) / (2*baseRadius) arcSectorAngle = math.asin(length / noseconeRadiusOfCurvature) arcSectorArea = arcSectorAngle * noseconeRadiusOfCurvature**2 / 2 return 2 * (arcSectorArea - (length * (noseconeRadiusOfCurvature - baseRadius) / 2)) elif self.shape == "cone": return self.baseDiameter * self.length / 2 else: raise TypeError("The nosecone shape {} is not recognized".format(self.shape)) def _getWettedArea(self): if self.shape == "tangentOgive": #TODO Analytical Solution: https://www.rocketryforum.com/attachments/nosecone_eqn2-pdf.336812/ baseRadius = self.baseDiameter / 2 length = self.baseDiameter * self.aspectRatio noseconeRadiusOfCurvature = (baseRadius**2 + length**2) / (2*baseRadius) a = noseconeRadiusOfCurvature numSteps = 2000 stepSize = length / numSteps wettedArea = 0 for x in range(numSteps + 1): wettedArea += 2 * math.pi * ((math.sqrt(a**2 - (length - x*stepSize)**2)) + baseRadius - a) * stepSize return wettedArea elif self.shape == "cone": baseRad = self.baseDiameter/2 return math.pi * baseRad * math.sqrt(baseRad**2 + self.length**2) else: raise TypeError("The nosecone shape {} is not recognized".format(self.shape)) #### Operation Functions #### def getAppliedForce(self, rocketState, time, environment, CG): Aref = self.rocket.Aref Mach = AeroParameters.getMachNumber(rocketState, environment) # Normal Force -------------------------------------------------------------------------------- # TODO: Account for rate of pitch/yaw rotation in AOA calculation? Or do separate Pitch/Yaw damping moments? AOA = AeroParameters.getTotalAOA(rocketState, environment) normalForceCoefficient = AeroFunctions.Barrowman_GetCN(AOA, Aref, 0, self.baseArea) # Drag Force --------------------------------------------------------------------------------------------- #### Skin Friction #### skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, self.length, Mach, \ self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Pressure #### pressureDragCoefficient = self._getCd_Pressure(Mach) totalDragCoefficient = pressureDragCoefficient + skinFrictionDragCoefficient # Damping moments -------------------------------------------------------------------------------------- # Roll damping due to skin friction dampingMoments = rollDampingMoment # Combine forces and return total ------------------------------------------------------------------------------------- return AeroFunctions.forceFromCdCN(rocketState, environment, totalDragCoefficient, normalForceCoefficient, self.CPLocation, Aref, moment=dampingMoments) def _getCd_Pressure(self, Mach): if Mach < 1: # Niskanen pg. 48 eq. 3.87 - Power law interpolation pressureDragCoefficient = self.SubsonicCdPolyCoeffs[0] * Mach**self.SubsonicCdPolyCoeffs[1] elif Mach > 1 and Mach < 1.3: # Interpolate in transonic region - derived from Niskanen Appendix B, Eqns B.3 - B.6 pressureDragCoefficient = self.TransonicCdPolyCoeffs[0] + self.TransonicCdPolyCoeffs[1]*Mach + \ self.TransonicCdPolyCoeffs[2]*Mach**2 + self.TransonicCdPolyCoeffs[3]*Mach**3 else: pressureDragCoefficient = getSupersonicPressureDragCoeff_Hoerner(self.coneHalfAngle, Mach) return pressureDragCoefficient * self.baseArea/self.rocket.Aref def getMaxDiameter(self): return self.baseDiameter def getRadius(self, distanceFromTip: float) -> float: if self.shape == "cone": return (self.baseDiameter/2) * (distanceFromTip/self.length) elif self.shape == "tangentOgive": curveRadius = ((self.baseDiameter/2)**2 + self.length**2) / (self.baseDiameter) yComponent = math.sqrt(curveRadius**2 - (self.length - distanceFromTip)**2) return yComponent - (curveRadius - (self.baseDiameter/2)) def plotShape(self) -> None: import matplotlib.pyplot as plt SubComponentPos = self.position.Z length = self.baseDiameter * self.aspectRatio ConeRad = self.baseDiameter/2 Rho = (ConeRad**2 + length**2)/(2*ConeRad) # values for tangent ogive equation Xvals = list(np.linspace(0,length, num=100)) # create 50 element tuple from 0 to length Yvals = [] # create y list for x in Xvals: # populate y list Yvals.append(self.getRadius(x)) Xvals = [-1*i + SubComponentPos for i in Xvals] # these were positive. Need them to be negative to point correct way # top line, body tube to tip XvalsTop = list(Xvals) YvalsTop = list(Yvals) XvalsTop.reverse() YvalsTop.reverse() # bottom line, tip to body tube XvalsBot = list(Xvals) YvalsBot = list(Yvals) YvalsBot = [-1*i for i in YvalsBot] # so the yvals are negative # put it all together now Xvals = XvalsTop + XvalsBot Yvals = YvalsTop + YvalsBot plt.plot(Xvals,Yvals, color = 'k')
Ancestors
- FixedMass
- RocketComponent
- BodyComponent
- abc.ABC
Class variables
var canConnectToComponentAbove
Instance variables
var aspectRatio
-
Length over diameter - aka fineness ratio (of exposed surface area)
var baseDiameter
-
Diameter at base of nosecone (m)
var length
-
Length from tip to tail, of external surface (m)
var shape
-
Description of nosecone shape - something like 'Ogive' or 'Cone'
var surfaceRoughness
-
(m)
Methods
def getAppliedForce(self, rocketState, time, environment, CG)
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG): Aref = self.rocket.Aref Mach = AeroParameters.getMachNumber(rocketState, environment) # Normal Force -------------------------------------------------------------------------------- # TODO: Account for rate of pitch/yaw rotation in AOA calculation? Or do separate Pitch/Yaw damping moments? AOA = AeroParameters.getTotalAOA(rocketState, environment) normalForceCoefficient = AeroFunctions.Barrowman_GetCN(AOA, Aref, 0, self.baseArea) # Drag Force --------------------------------------------------------------------------------------------- #### Skin Friction #### skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, self.length, Mach, \ self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Pressure #### pressureDragCoefficient = self._getCd_Pressure(Mach) totalDragCoefficient = pressureDragCoefficient + skinFrictionDragCoefficient # Damping moments -------------------------------------------------------------------------------------- # Roll damping due to skin friction dampingMoments = rollDampingMoment # Combine forces and return total ------------------------------------------------------------------------------------- return AeroFunctions.forceFromCdCN(rocketState, environment, totalDragCoefficient, normalForceCoefficient, self.CPLocation, Aref, moment=dampingMoments)
def plotShape(self) ‑> None
-
Expand source code
def plotShape(self) -> None: import matplotlib.pyplot as plt SubComponentPos = self.position.Z length = self.baseDiameter * self.aspectRatio ConeRad = self.baseDiameter/2 Rho = (ConeRad**2 + length**2)/(2*ConeRad) # values for tangent ogive equation Xvals = list(np.linspace(0,length, num=100)) # create 50 element tuple from 0 to length Yvals = [] # create y list for x in Xvals: # populate y list Yvals.append(self.getRadius(x)) Xvals = [-1*i + SubComponentPos for i in Xvals] # these were positive. Need them to be negative to point correct way # top line, body tube to tip XvalsTop = list(Xvals) YvalsTop = list(Yvals) XvalsTop.reverse() YvalsTop.reverse() # bottom line, tip to body tube XvalsBot = list(Xvals) YvalsBot = list(Yvals) YvalsBot = [-1*i for i in YvalsBot] # so the yvals are negative # put it all together now Xvals = XvalsTop + XvalsBot Yvals = YvalsTop + YvalsBot plt.plot(Xvals,Yvals, color = 'k')
Inherited members
class PlanarInterface (location: Vector, component1: RocketComponent, component2: RocketComponent, planeNormal=<MAPLEAF.Motion.CythonVector.Vector object>)
-
Defines a planar interface between two components
In the local frame, the normalVector is expected to point across the interface from component1 to component2
In a rocket, this means that (with the default normalVector pointing in the -'ve Z direction (towards the tail)), component1 is above component2Expand source code
class PlanarInterface(): def __init__(self, location: Vector, component1: RocketComponent, component2: RocketComponent, planeNormal=Vector(0,0,-1)): ''' Defines a planar interface between two components In the local frame, the normalVector is expected to point across the interface from component1 to component2 In a rocket, this means that (with the default normalVector pointing in the -'ve Z direction (towards the tail)), component1 is above component2 ''' self.location = location self.component1 = component1 self.component2 = component2 self.normalVector = planeNormal @classmethod def createPlanarComponentInterfaces(cls, components): ''' Expects components in the list to be sorted by z location (top to bottom) Tries to construct PlanarInterface objects connecting all of the BodyComponent object from top to bottom Returns a list of PlanarInterface objects, ordered from top to bottom ''' return None # Ignore components that aren't of type 'BodyComponent' bodyComponents = [] for comp in components: if isinstance(comp, BodyComponent): bodyComponents.append(comp) # Construct interfaces between components componentInterfaces = [] interfaceLocationTolerance = 0.001 # m for i in range(len(bodyComponents)-1): topComponent = bodyComponents[i] bottomComponent = bodyComponents[i+1] topInterfaceLoc = topComponent.getBottomInterfaceLocation() bottomInterfaceLoc = bottomComponent.getTopInterfaceLocation() if (topInterfaceLoc - bottomInterfaceLoc).length() < interfaceLocationTolerance: interfaceLocation = (topInterfaceLoc + bottomInterfaceLoc) / 2 # Average location is where the interface will be componentInterfaces.append(PlanarInterface(interfaceLocation, topComponent, bottomComponent)) else: raise ValueError("Body Component Location mismatch {} ({}) bottom interface at {} vs {} ({}) top interface at {}. Current interface tolerance = 0.001m".format(\ topComponent.name, type(topComponent), topInterfaceLoc, bottomComponent.name, type(bottomComponent), bottomInterfaceLoc)) return componentInterfaces @classmethod def sortByZLocation(cls, components, state) -> List[RocketComponent]: ''' Sort the components in order from top to bottom, component.position.Z This function could be relocated somewhere more suitable, at the time of writing, it is only being used to order components before creating interfaces b/w them ''' def getZPosition(component): try: return component.position.Z except AttributeError: return component.getInertia(0, state).CG.Z components.sort(key=getZPosition, reverse=True) return components
Static methods
def createPlanarComponentInterfaces(components)
-
Expects components in the list to be sorted by z location (top to bottom) Tries to construct PlanarInterface objects connecting all of the BodyComponent object from top to bottom Returns a list of PlanarInterface objects, ordered from top to bottom
Expand source code
@classmethod def createPlanarComponentInterfaces(cls, components): ''' Expects components in the list to be sorted by z location (top to bottom) Tries to construct PlanarInterface objects connecting all of the BodyComponent object from top to bottom Returns a list of PlanarInterface objects, ordered from top to bottom ''' return None # Ignore components that aren't of type 'BodyComponent' bodyComponents = [] for comp in components: if isinstance(comp, BodyComponent): bodyComponents.append(comp) # Construct interfaces between components componentInterfaces = [] interfaceLocationTolerance = 0.001 # m for i in range(len(bodyComponents)-1): topComponent = bodyComponents[i] bottomComponent = bodyComponents[i+1] topInterfaceLoc = topComponent.getBottomInterfaceLocation() bottomInterfaceLoc = bottomComponent.getTopInterfaceLocation() if (topInterfaceLoc - bottomInterfaceLoc).length() < interfaceLocationTolerance: interfaceLocation = (topInterfaceLoc + bottomInterfaceLoc) / 2 # Average location is where the interface will be componentInterfaces.append(PlanarInterface(interfaceLocation, topComponent, bottomComponent)) else: raise ValueError("Body Component Location mismatch {} ({}) bottom interface at {} vs {} ({}) top interface at {}. Current interface tolerance = 0.001m".format(\ topComponent.name, type(topComponent), topInterfaceLoc, bottomComponent.name, type(bottomComponent), bottomInterfaceLoc)) return componentInterfaces
def sortByZLocation(components, state) ‑> List[RocketComponent]
-
Sort the components in order from top to bottom, component.position.Z This function could be relocated somewhere more suitable, at the time of writing, it is only being used to order components before creating interfaces b/w them
Expand source code
@classmethod def sortByZLocation(cls, components, state) -> List[RocketComponent]: ''' Sort the components in order from top to bottom, component.position.Z This function could be relocated somewhere more suitable, at the time of writing, it is only being used to order components before creating interfaces b/w them ''' def getZPosition(component): try: return component.position.Z except AttributeError: return component.getInertia(0, state).CG.Z components.sort(key=getZPosition, reverse=True) return components
class RecoverySystem (componentDictReader, rocket, stage)
-
Represents a recovery system with an arbitrary number of stages
Expand source code
class RecoverySystem(FixedMass): ''' Represents a recovery system with an arbitrary number of stages ''' #### Init Functions #### def __init__(self, componentDictReader, rocket, stage): FixedMass.__init__(self, componentDictReader, rocket, stage) self.numStages = componentDictReader.getInt("numStages") # Set rocket properties rocket.recoverySystem = self self._initRecoveryStages() def _initRecoveryStages(self): self.stageTriggers = [ None, ] self.stageTriggerValues = [ None, ] self.chuteAreas = [ 0, ] self.chuteCds = [ 0, ] self.delayTimes = [ 0, ] for i in range(1,self.numStages+1): self.stageTriggers.append(self.componentDictReader.getString("stage{}Trigger".format(i))) try: self.stageTriggerValues.append(self.componentDictReader.getFloat("stage{}TriggerValue".format(i))) except KeyError: self.stageTriggerValues.append(None) self.chuteAreas.append(self.componentDictReader.getFloat("stage{}ChuteArea".format(i))) self.chuteCds.append(self.componentDictReader.getFloat("stage{}Cd".format(i))) self.delayTimes.append(self.componentDictReader.getFloat("stage{}DelayTime".format(i))) self.currentStage = -1 self.nextStageDeployTime = None # Initialize by deploying stage 0 (no chutes) # Sets up trigger function for the real first chute self._deployNextStage() #### Operational Functions #### def getAppliedForce(self, rocketState, time, environment, CG): ''' Calculates force/moment applied by the recovery system using a simple drag coefficient + area model ''' #### Calculate Aero Force #### if self.currentStage == 0: # No recovery system deployed yet return ForceMomentSystem(Vector(0,0,0)) else: # 3DoF force-only aero airVel = AeroParameters.getAirVelRelativeToVehicle(rocketState, environment) dragForceMagnitude = self.chuteCds[self.currentStage] * self.chuteAreas[self.currentStage] * AeroParameters.getDynamicPressure(rocketState, environment) totalForce = airVel.normalize() * dragForceMagnitude return ForceMomentSystem(totalForce) def _deployNextStage(self): ''' Deploys the next stage of the recovery system If appropriate, will set the simulation mode to 3DoF. If appropriate, will set up the trigger to deploy the next stage of the recovery system, by calling this function again. If this is the final recovery system stage, records the main chute deployment time. ''' self.currentStage += 1 if self.currentStage > 0: print("Deployed Recovery Stage {}".format(self.currentStage)) if self.currentStage == 1: self.rocket.isUnderChute = True self.rocket._switchTo3DoF() if self.currentStage == self.numStages: self.rocket.mainChuteDeployTime = self.rocket.rigidBody.time if self.currentStage < self.numStages: nextTrigger = self.stageTriggers[self.currentStage + 1] nextTriggerValue = self.stageTriggerValues[self.currentStage + 1] nextTriggerDelay = self.delayTimes[self.currentStage + 1] # Translate trigger conditions into language simEventDetector can understand if nextTrigger == "Apogee": nextTrigger = EventTypes.Apogee if nextTrigger == "Altitude": nextTrigger = EventTypes.DescendingThroughAltitude if nextTrigger == "Time": nextTrigger = EventTypes.TimeReached # Set the trigger self.rocket.simEventDetector.subscribeToEvent(nextTrigger, self._deployNextStage, eventTriggerValue=nextTriggerValue, triggerDelay=nextTriggerDelay)
Ancestors
- FixedMass
- RocketComponent
- abc.ABC
Methods
def getAppliedForce(self, rocketState, time, environment, CG)
-
Calculates force/moment applied by the recovery system using a simple drag coefficient + area model
Expand source code
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)
class Rocket (rocketDictReader, silent=False, stageToInitialize=None, simRunner=None, environment=None)
-
Class used to represent a single flying rigid body composed of
Stage
objects. New instances of this class are also created to model the flight of dropped stages.Initialization of Rocket(s) is most easily completed through an instance of Simulation To get a single Rocket object, initialize a Simulation and call
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:
(
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:
(
Simulation
) reference to the current simulation driver/runner - environment:
(
Environment
) environment model from which the rocket will retrieve atmospheric properties and wind speeds
Expand source code
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
Ancestors
Instance variables
var Aref
-
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.
var 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
var controlSystem
-
None for uncontrolled rockets.
RocketControlSystem
for controlled rockets var derivativeEvaluationLog
-
Log containing one entry per rocket motion derivative evaluation, contains component forces. None if logging level < 2
var engineShutOff
-
Used to shut off engines in MAPLEAF.Rocket.Propulsion.DefinedMotor class. Currently set in MAPLE_AF.GNC.Navigation
var engineShutOffTime
-
(float) Filled in by
TabulatedMotor
upon initialization var environment
-
Instance of
Environment
var eventTimeStep
-
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
var finenessRatio
-
Used in some aerodynamic functions. Updated after initializing subcomponents, and throughout the flight. None if no BodyComponent(s) are present in the rocket
var fullyTurbulentBL
-
Controls whether skin friction is solved assuming a fully turbulent BL or using laminar/transitional flow at lower Reynolds numbers
var isUnderChute
-
(bool) Controlled by
RecoverySystem._deployNextStage()
var mainChuteDeployTime
-
(float) Filled in during flight by
RecoverySystem._deployNextStage()
var maxDiameter
-
(float) Holds maximum constant-size body tube diameter, from bodytube components in stages
var recoverySystem
-
Reference to the current Rocket's (which can represent a dropped stage) recovery system. Only a single recovery system is allowed per stage.
Set inRecoverySystem
var rigidBody
-
(
RigidBody
orRigidBody_3DoF
) Responsible for motion integration.
Set in_initializeRigidBody()
. var silent
-
Controls output to console
var simEventDetector
-
(
SimEventDetector
) Used to trigger things like recovery systems and staging var simRunner
-
Parent instance of
Simulation
(or derivative sim runner). This is usually the object that has created the current instance of Rocket. var stage
-
If controls whether the whole rocket is initialized (if == None), or a single stage is initialized (Integer stage number)
var stages
-
A list of
Stage
objects that make up the rocket, ordered from top to bottom.
Populated by_initializeStages
. var surfaceRoughness
-
Default surface roughness for all rocket components
var timeStepLog
-
Log containing one entry per time step, logs rocket state. None if logging level == 0
var turbulenceOffWhenUnderChute
-
(bool)
Methods
def getLength(self)
-
Expand source code
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)
-
Expand source code
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()
def timeStep(self, dt: float)
-
Tells the simulation to take a time step of size dt.
Usually called by functions like
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.
Expand source code
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 writeLogsToFile(self, directory='.')
-
Expand source code
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
Inherited members
- rocketDictReader:
(
class RocketComponent (componentDictReader, rocket, stage)
-
Interface definition for rocket components
Expand source code
class RocketComponent(ABC): ''' Interface definition for rocket components ''' @abstractmethod def __init__(self, componentDictReader, rocket, stage): return @abstractmethod def getInertia(self, time, state) -> Inertia: return @abstractmethod def getAppliedForce(self, rocketState, time, environmentalConditions, rocketCG) -> ForceMomentSystem: return #### Other optional functions #### # def getExtraParametersToIntegrate(self): ''' Override this method to make MAPLEAF integrate additional parameters (in addition to rigid body motion) Should return three lists - A list of (string) parameter names - The parameter will then be accessible under state.parameterName whenever the rocket state is available - A list of intial parameter values - Values can be scalars or vectors, or any other parameter that implements addition, subtraction and multiplication/division by scalars - A list of parameter derivative functions - Derivative functions should accept two parameters, the current time and the current state object and return the corresponding derivative of the value of that parameter '''
Ancestors
- abc.ABC
Subclasses
- TabulatedMotor
- AeroForce
- FixedForce
- FixedMass
- FractionalJetDamping
- TabulatedInertia
- SampleStatefulComponent
Methods
def getAppliedForce(self, rocketState, time, environmentalConditions, rocketCG) ‑> ForceMomentSystem
-
Expand source code
@abstractmethod def getAppliedForce(self, rocketState, time, environmentalConditions, rocketCG) -> ForceMomentSystem: return
def getInertia(self, time, state) ‑> Inertia
-
Expand source code
@abstractmethod def getInertia(self, time, state) -> Inertia: return
class SampleStatefulComponent (componentDictReader, rocket, stage)
-
Interface definition for rocket components
Expand source code
class SampleStatefulComponent(RocketComponent): def __init__(self, componentDictReader, rocket, stage): self.rocket = rocket self.stage = stage self.name = componentDictReader.getDictName() def getExtraParametersToIntegrate(self): # Examples below for a single parameter to be integrated, can put as many as required in these lists paramNames = [ "tankLevel" ] initValues = [ 1.0 ] derivativeFunctions = [ self.getTankLevelDerivative ] return paramNames, initValues, derivativeFunctions def getTankLevelDerivative(self, time, rocketState): return -2*rocketState.tankLevel # tankLevel will asymptotically approach 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) def getInertia(self, time, rocketState): mass = 5 + rocketState.tankLevel*4.56 # Fixed Mass + fluid mass MOI = Vector(mass, mass, mass*0.05) # Related to current mass CGz = -3 + rocketState.tankLevel # Moves depending on current tank level CG = Vector(0, 0, CGz) return Inertia(MOI, CG, mass)
Ancestors
- RocketComponent
- abc.ABC
Methods
def getAppliedForce(self, rocketState, time, envConditions, rocketCG)
-
Expand source code
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)
def getExtraParametersToIntegrate(self)
-
Expand source code
def getExtraParametersToIntegrate(self): # Examples below for a single parameter to be integrated, can put as many as required in these lists paramNames = [ "tankLevel" ] initValues = [ 1.0 ] derivativeFunctions = [ self.getTankLevelDerivative ] return paramNames, initValues, derivativeFunctions
def getInertia(self, time, rocketState)
-
Expand source code
def getInertia(self, time, rocketState): mass = 5 + rocketState.tankLevel*4.56 # Fixed Mass + fluid mass MOI = Vector(mass, mass, mass*0.05) # Related to current mass CGz = -3 + rocketState.tankLevel # Moves depending on current tank level CG = Vector(0, 0, CGz) return Inertia(MOI, CG, mass)
def getTankLevelDerivative(self, time, rocketState)
-
Expand source code
def getTankLevelDerivative(self, time, rocketState): return -2*rocketState.tankLevel # tankLevel will asymptotically approach 0
class SimEventDetector (rocket)
-
Each sim event detector detects events on a single rocket
Expand source code
class SimEventDetector(): ''' Each sim event detector detects events on a single rocket ''' def __init__(self, rocket): self.rocket = rocket self.callbackFunctions = [] self.conditionsEvalFunctions = [] self.conditionValues = [] self.triggerDelays = [] def subscribeToEvent(self, eventType, callbackFunction, eventTriggerValue=None, triggerDelay=0): ''' Inputs: eventType: "apogee", "ascendingThroughAltitude", "descendingThroughAltitude", 'motorBurnout" or "timeReached" Can pass in a member of the EventType Enum defined above, if desired callbackfunction: function to call when the desired event occurs. Must have no arguments eventTriggerValue: None, or a trigger parameter for the eventType - an altitude or a time triggerDelay: (numeric) the amount of time, in seconds, to wait before triggering an event after a trigger condition has been met Note: Motor Burnout always triggers at the burnout time of the LOWEST stage's motor! ''' # Define map between eventType strings and checker functions stringToEvalFnMap = { EventTypes.Apogee.value: self._isAfterApogee, EventTypes.AscendingThroughAltitude.value: self._isAboveAltitude, EventTypes.DescendingThroughAltitude.value: self._isBelowAltitude, EventTypes.MotorBurnout.value: self._isBottomStageMotorBurnedOut, EventTypes.TimeReached.value: self._timeReached } # Make sure eventType is a string if not isinstance(eventType, str): eventType = eventType.value # If for example EventType.Apogee is passed in, EventType.Apogee.value retrieves "apogee" from the EventType enum self.callbackFunctions.append(callbackFunction) self.conditionValues.append(eventTriggerValue) self.conditionsEvalFunctions.append(stringToEvalFnMap[eventType]) self.triggerDelays.append(triggerDelay) def triggerEvents(self) -> Tuple[float, bool]: ''' Checks if any of the events that this SimEventDetector is supposed to detect have happened. If so, triggers their callback functions. Returns the estimated time to the next event, and whether that event happens at a set time (time-deterministic) or not (non-time-deterministic - like an altitude condition) These return values are used to influence time step adaptation to accurately resolve discrete event timing ''' # Save indices of conditions that have been triggered, will now be removed indicesToRemove = [] estimatedTimeToNextEvent = 1e10 nextEventTimeDeterministic = False # Precomputed to pass to sub-functions (required for altitude conditions) ENUState = self.rocket.environment.convertStateToENUFrame(self.rocket.rigidBody.state) # Check each callback condition for i in range(len(self.callbackFunctions)): # If a conditions has come true eventOccurred, timeToOccurrence, timeDeterministic = self.conditionsEvalFunctions[i](self.conditionValues[i], ENUState) # Get info about the next event to occur if timeToOccurrence < estimatedTimeToNextEvent and not eventOccurred: estimatedTimeToNextEvent = timeToOccurrence nextEventTimeDeterministic = timeDeterministic if eventOccurred: if self.triggerDelays[i] == 0: # Call its function # TODO: Print message to simulation log saying that the event has been triggered self.callbackFunctions[i]() else: # Schedule the event to happen in triggerDelay seconds triggerTime = self.rocket.rigidBody.time + self.triggerDelays[i] self.subscribeToEvent(EventTypes.TimeReached, self.callbackFunctions[i], triggerTime) # And mark it for deletion indicesToRemove.append(i) # Delete all of the events that were triggered - working in reverse to ensure the indices of the items aren't changed before we delete them for i in range(len(indicesToRemove)): delI = len(indicesToRemove)-1-i indexToRemove = indicesToRemove[delI] del self.callbackFunctions[indexToRemove] del self.conditionValues[indexToRemove] del self.conditionsEvalFunctions[indexToRemove] del self.triggerDelays[indexToRemove] # Save velocity and time for next timestep (if we're trying to detect apogee) # Doing this here to avoid problems calling the apogee function multiple times in a single time step self.lastVelocity = ENUState.velocity self.lastTime = self.rocket.rigidBody.time return estimatedTimeToNextEvent, nextEventTimeDeterministic ### Event evaluation functions - each is expected to have a single parameter, return True/False ### def _isAfterApogee(self, _, ENUState): # Time > 1.0 condition here to avoid setting off events if sliding slightly down before engine lights eventOccurred = ENUState.velocity.Z <= 0 and self.rocket.rigidBody.time > 1.0 timeToOccurrence = 1e10 try: accel = (ENUState.velocity.Z - self.lastVelocity.Z) / (self.rocket.rigidBody.time - self.lastTime) if accel < 0 and ENUState.velocity.Z > 0: timeToOccurrence = -ENUState.velocity.Z / accel except (AttributeError, ZeroDivisionError): pass # Haven't saved a velocity/time yet / calling function twice in a single time step return eventOccurred, timeToOccurrence, False def _isAboveAltitude(self, altitude, ENUState): eventOccurred = ENUState.position.Z >= altitude and ENUState.velocity.Z > 0 if ENUState.velocity.Z > 0: timeToOccurrence = (altitude - ENUState.position.Z) / ENUState.velocity.Z else: timeToOccurrence = 1e10 return eventOccurred, timeToOccurrence, False def _isBelowAltitude(self, altitude, ENUState): eventOccurred = ENUState.position.Z <= altitude and ENUState.velocity.Z < 0 if ENUState.velocity.Z < 0: timeToOccurrence = (altitude - ENUState.position.Z) / ENUState.velocity.Z else: timeToOccurrence = 1e10 return eventOccurred, timeToOccurrence, False def _isBottomStageMotorBurnedOut(self, _, ENUState): eventOccurred = self.rocket.rigidBody.time >= self.rocket.stages[-1].engineShutOffTime timeToOccurrence = self.rocket.stages[-1].engineShutOffTime - self.rocket.rigidBody.time return eventOccurred, timeToOccurrence, True def _timeReached(self, time, ENUState): eventOccurred = self.rocket.rigidBody.time >= time timeToOccurrence = time - self.rocket.rigidBody.time return eventOccurred, timeToOccurrence, True
Methods
def subscribeToEvent(self, eventType, callbackFunction, eventTriggerValue=None, triggerDelay=0)
-
Inputs
eventType: "apogee", "ascendingThroughAltitude", "descendingThroughAltitude", 'motorBurnout" or "timeReached" Can pass in a member of the EventType Enum defined above, if desired callbackfunction: function to call when the desired event occurs. Must have no arguments eventTriggerValue: None, or a trigger parameter for the eventType - an altitude or a time triggerDelay: (numeric) the amount of time, in seconds, to wait before triggering an event after a trigger condition has been met
Note
Motor Burnout always triggers at the burnout time of the LOWEST stage's motor!
Expand source code
def subscribeToEvent(self, eventType, callbackFunction, eventTriggerValue=None, triggerDelay=0): ''' Inputs: eventType: "apogee", "ascendingThroughAltitude", "descendingThroughAltitude", 'motorBurnout" or "timeReached" Can pass in a member of the EventType Enum defined above, if desired callbackfunction: function to call when the desired event occurs. Must have no arguments eventTriggerValue: None, or a trigger parameter for the eventType - an altitude or a time triggerDelay: (numeric) the amount of time, in seconds, to wait before triggering an event after a trigger condition has been met Note: Motor Burnout always triggers at the burnout time of the LOWEST stage's motor! ''' # Define map between eventType strings and checker functions stringToEvalFnMap = { EventTypes.Apogee.value: self._isAfterApogee, EventTypes.AscendingThroughAltitude.value: self._isAboveAltitude, EventTypes.DescendingThroughAltitude.value: self._isBelowAltitude, EventTypes.MotorBurnout.value: self._isBottomStageMotorBurnedOut, EventTypes.TimeReached.value: self._timeReached } # Make sure eventType is a string if not isinstance(eventType, str): eventType = eventType.value # If for example EventType.Apogee is passed in, EventType.Apogee.value retrieves "apogee" from the EventType enum self.callbackFunctions.append(callbackFunction) self.conditionValues.append(eventTriggerValue) self.conditionsEvalFunctions.append(stringToEvalFnMap[eventType]) self.triggerDelays.append(triggerDelay)
def triggerEvents(self) ‑> Tuple[float, bool]
-
Checks if any of the events that this SimEventDetector is supposed to detect have happened. If so, triggers their callback functions. Returns the estimated time to the next event, and whether that event happens at a set time (time-deterministic) or not (non-time-deterministic - like an altitude condition) These return values are used to influence time step adaptation to accurately resolve discrete event timing
Expand source code
def triggerEvents(self) -> Tuple[float, bool]: ''' Checks if any of the events that this SimEventDetector is supposed to detect have happened. If so, triggers their callback functions. Returns the estimated time to the next event, and whether that event happens at a set time (time-deterministic) or not (non-time-deterministic - like an altitude condition) These return values are used to influence time step adaptation to accurately resolve discrete event timing ''' # Save indices of conditions that have been triggered, will now be removed indicesToRemove = [] estimatedTimeToNextEvent = 1e10 nextEventTimeDeterministic = False # Precomputed to pass to sub-functions (required for altitude conditions) ENUState = self.rocket.environment.convertStateToENUFrame(self.rocket.rigidBody.state) # Check each callback condition for i in range(len(self.callbackFunctions)): # If a conditions has come true eventOccurred, timeToOccurrence, timeDeterministic = self.conditionsEvalFunctions[i](self.conditionValues[i], ENUState) # Get info about the next event to occur if timeToOccurrence < estimatedTimeToNextEvent and not eventOccurred: estimatedTimeToNextEvent = timeToOccurrence nextEventTimeDeterministic = timeDeterministic if eventOccurred: if self.triggerDelays[i] == 0: # Call its function # TODO: Print message to simulation log saying that the event has been triggered self.callbackFunctions[i]() else: # Schedule the event to happen in triggerDelay seconds triggerTime = self.rocket.rigidBody.time + self.triggerDelays[i] self.subscribeToEvent(EventTypes.TimeReached, self.callbackFunctions[i], triggerTime) # And mark it for deletion indicesToRemove.append(i) # Delete all of the events that were triggered - working in reverse to ensure the indices of the items aren't changed before we delete them for i in range(len(indicesToRemove)): delI = len(indicesToRemove)-1-i indexToRemove = indicesToRemove[delI] del self.callbackFunctions[indexToRemove] del self.conditionValues[indexToRemove] del self.conditionsEvalFunctions[indexToRemove] del self.triggerDelays[indexToRemove] # Save velocity and time for next timestep (if we're trying to detect apogee) # Doing this here to avoid problems calling the apogee function multiple times in a single time step self.lastVelocity = ENUState.velocity self.lastTime = self.rocket.rigidBody.time return estimatedTimeToNextEvent, nextEventTimeDeterministic
class Stage (stageDictReader, rocket)
-
Represents a single rocket stage
Expand source code
class Stage(CompositeObject, BodyComponent): ''' Represents a single rocket stage ''' def __init__(self, stageDictReader, rocket): #TODO: This should take a boolean parameter 'dropped' which specified whether the stage being initialized is on the pad (full engine) or being dropped (empty engine) self.rocket = rocket self.stageDictReader = stageDictReader self.name = stageDictReader.getDictName() self.stageNumber = stageDictReader.getInt("stageNumber") self.position = stageDictReader.getVector("position") self.separationConditionType = stageDictReader.getString("separationTriggerType") self.separationDelay = stageDictReader.getFloat("separationDelay") self.separationConditionValue = stageDictReader.getFloat("separationTriggerValue") #### Placeholder fields, to be filled by subcomponents #### self.motor = None # Filled in by Motor.__init__ self.engineShutOffTime = None # Filled in by Motor.__init__ - used for base drag calculations self.components = [] self.initializeSubComponents() # Check if any stage-level inertia properties are being overriden CGOverride = stageDictReader.tryGetVector("constCG", defaultValue=None) if CGOverride != None: CGOverride += self.position massOverride = stageDictReader.tryGetFloat("constMass", defaultValue=None) MOIOverride = stageDictReader.tryGetVector("constMOI", defaultValue=None) # Initialize functionality inherited from CompositeObject - pass in any overrides CompositeObject.__init__(self, components=self.components, CGOverride=CGOverride, MOIOverride=MOIOverride, massOverride=massOverride) def initializeSubComponents(self): # Assume each sub dictionary represents a component subDicts = self.stageDictReader.getImmediateSubDicts() def returnZeroIfBodyComponent(componentDictPath): className = self.stageDictReader.getString(componentDictPath + ".class") componentClass = stringNameToClassMap[className] if issubclass(componentClass, BodyComponent): return 0, componentDictPath else: return 1, componentDictPath # Make sure BodyComponents are initializede before others # This allows non-body components (which are attached to body components) like the fins # to get info about the body radius where they're mounted subDicts.sort(key=returnZeroIfBodyComponent) # Initialize each component, add to componets for componentDict in subDicts: newComponent = rocketComponentFactory(componentDict, self.rocket, self) self.components.append(newComponent) try: if newComponent.name not in self.__dict__: # Avoid clobbering existing info setattr(self, newComponent.name, newComponent) # Make component available as stage.componentName except AttributeError: pass # Expect to arrive here if the component doesn't define a "name" attribute def getComponentsOfType(self, type): matchingComponents = [] for comp in self.components: if isinstance(comp, type): matchingComponents.append(comp) return matchingComponents #### Geometry / Introspection #### def _getFirstComponentOfType(self, componentList, desiredType): for comp in componentList: if isinstance(comp, desiredType): return comp return None def getTopInterfaceLocation(self) -> Union[Vector, None]: # Expects components to be sorted by z-location, top to bottom comp = self._getFirstComponentOfType(self.components, BodyComponent) try: topZ = comp.getTopInterfaceLocation().Z return Vector(self.position.X, self.position.Y, topZ) except AttributeError: return None # Occurs when comp.getTopInterfaceLocation() == None def getBottomInterfaceLocation(self) -> Union[Vector, None]: # Expects components to be sorted by z-location, top to bottom comp = self._getFirstComponentOfType(reversed(self.components), BodyComponent) try: bottomZ = comp.getBottomInterfaceLocation().Z return Vector(self.position.X, self.position.Y, bottomZ) except AttributeError: return None # Occurs when comp.getBottomInterfaceLocation() == None def getLength(self): try: topBodyComponent = self._getFirstComponentOfType(self.components, BodyComponent) topLocationZ = topBodyComponent.position.Z bottomBodyComponent = self._getFirstComponentOfType(reversed(self.components), BodyComponent) bottomLocationZ = bottomBodyComponent.position.Z - bottomBodyComponent.length return (topLocationZ - bottomLocationZ) except AttributeError: return None # No body components in current Stage def getMaxDiameter(self): maxDiameter = 0 for comp in self.components: try: maxDiameter = max(comp.getMaxDiameter(), maxDiameter) except AttributeError: pass # Component is not a BodyComponent return maxDiameter def getRadius(self, distanceFromTop): topOfStageZ = self.position.Z desiredZ = topOfStageZ - distanceFromTop for comp in self.components: try: # Check if desired location is part of this component, if so, return the component's radius compTopZ = comp.position.Z compBottomZ = compTopZ - comp.length if desiredZ <= compTopZ and desiredZ >= compBottomZ: distanceFromTopOfComponent = compTopZ - desiredZ return comp.getRadius(distanceFromTopOfComponent) except AttributeError: pass # Not a body component raise ValueError("Length {} from the top of stage '{}' does not fall between the top and bottom of any of its components".format(distanceFromTop, self.name)) def plotShape(self): ''' Calls .plotShape() on all subcomponents Returns CGsubZ, CGsubY (two lists of scalars for a 2D plot) ''' CGsubZ = [] CGsubY = [] for component in self.components: try: component.plotShape() initialComponentInertia = component.getInertia(0, self.rocket.rigidBody.state) CGsubZ.append(initialComponentInertia.CG.Z) CGsubY.append(initialComponentInertia.CG.Y) except AttributeError: pass # Plotting function not implemented return CGsubZ, CGsubY
Ancestors
- CompositeObject
- BodyComponent
- abc.ABC
Methods
def getComponentsOfType(self, type)
-
Expand source code
def getComponentsOfType(self, type): matchingComponents = [] for comp in self.components: if isinstance(comp, type): matchingComponents.append(comp) return matchingComponents
def getLength(self)
-
Expand source code
def getLength(self): try: topBodyComponent = self._getFirstComponentOfType(self.components, BodyComponent) topLocationZ = topBodyComponent.position.Z bottomBodyComponent = self._getFirstComponentOfType(reversed(self.components), BodyComponent) bottomLocationZ = bottomBodyComponent.position.Z - bottomBodyComponent.length return (topLocationZ - bottomLocationZ) except AttributeError: return None # No body components in current Stage
def initializeSubComponents(self)
-
Expand source code
def initializeSubComponents(self): # Assume each sub dictionary represents a component subDicts = self.stageDictReader.getImmediateSubDicts() def returnZeroIfBodyComponent(componentDictPath): className = self.stageDictReader.getString(componentDictPath + ".class") componentClass = stringNameToClassMap[className] if issubclass(componentClass, BodyComponent): return 0, componentDictPath else: return 1, componentDictPath # Make sure BodyComponents are initializede before others # This allows non-body components (which are attached to body components) like the fins # to get info about the body radius where they're mounted subDicts.sort(key=returnZeroIfBodyComponent) # Initialize each component, add to componets for componentDict in subDicts: newComponent = rocketComponentFactory(componentDict, self.rocket, self) self.components.append(newComponent) try: if newComponent.name not in self.__dict__: # Avoid clobbering existing info setattr(self, newComponent.name, newComponent) # Make component available as stage.componentName except AttributeError: pass # Expect to arrive here if the component doesn't define a "name" attribute
def plotShape(self)
-
Calls .plotShape() on all subcomponents Returns CGsubZ, CGsubY (two lists of scalars for a 2D plot)
Expand source code
def plotShape(self): ''' Calls .plotShape() on all subcomponents Returns CGsubZ, CGsubY (two lists of scalars for a 2D plot) ''' CGsubZ = [] CGsubY = [] for component in self.components: try: component.plotShape() initialComponentInertia = component.getInertia(0, self.rocket.rigidBody.state) CGsubZ.append(initialComponentInertia.CG.Z) CGsubY.append(initialComponentInertia.CG.Y) except AttributeError: pass # Plotting function not implemented return CGsubZ, CGsubY
Inherited members
class TabulatedAeroForce (componentDictReader, rocket, stage)
-
A zero-inertia component with aerodynamic coefficients that are tabulated according to one or more parameters (ex. AOA)
Expand source code
class TabulatedAeroForce(AeroForce): ''' A zero-inertia component with aerodynamic coefficients that are tabulated according to one or more parameters (ex. AOA) ''' def __init__(self, componentDictReader, rocket, stage): self.componentDictReader = componentDictReader self.rocket = rocket self.stage = stage self.name = componentDictReader.getDictName() self.position = componentDictReader.getVector("position") self.Aref = componentDictReader.getFloat("Aref") self.Lref = componentDictReader.getFloat("Lref") coefficientTableFilePath = componentDictReader.getString("filePath") self._loadCoefficients(coefficientTableFilePath) def _loadCoefficients(self, filePath): # Load first row to figure out what the columns mean with open(filePath) as f: columnNames = f.readline().strip().split(',') # Get functions that calculate the parameters used for interpolation # All these 'key'/parameter columns are expected to come before 'value' columns to be interpolated over self.parameterFunctions = [] i = 0 while i < len(columnNames): col = columnNames[i] if col in AeroParameters.stringToAeroFunctionMap: self.parameterFunctions.append(AeroParameters.stringToAeroFunctionMap[col]) else: break i += 1 # Continue parsing column names - aero coefficient names now # This is the ordering expected by AeroFunctions.forceFromCoefficients aeroCoeffStrings = [ "CD", "CL", "CMx", "CMy", "CMz" ] self.aeroCoeffIndices = [] # Provides mapping between value column position in interpolation table & position in output aero coefficient list (ordered like aeroCoeffStrings above) while i < len(columnNames): coeff = columnNames[i] if coeff in aeroCoeffStrings: self.aeroCoeffIndices.append(aeroCoeffStrings.index(coeff)) else: raise ValueError("ERROR: One of the following columns: {} did not match any of the expected columns names: Keys: {}, values: {}. \ Or was in the wrong order. All key columns must come BEFORE value columns.".format(columnNames, AeroParameters.stringToAeroFunctionMap.keys(), aeroCoeffStrings)) i += 1 # Load the data table to be interpolated dataTable = np.loadtxt(filePath, delimiter=',', skiprows=1) nKeyCols = len(self.parameterFunctions) keys = dataTable[:, 0:nKeyCols] aeroCoefficients = dataTable[:, nKeyCols:] if nKeyCols > 1: # Create n-dimensional interpolation function for aero coefficients self._interpAeroCoefficients = NoNaNLinearNDInterpolator(keys, aeroCoefficients, filePath) else: # Save to use with MAPLEAF.Motion.linInterp self.keys = [ key[0] for key in keys ] self.values = aeroCoefficients def _getAeroCoefficients(self, state, environment): keys = AeroParameters.getAeroPropertiesList(self.parameterFunctions, state, environment) if len(keys) > 1: # Multi-dimensional linear interpolation interpolatedCoefficients = self._interpAeroCoefficients(keys)[0] else: # 1D linear interpolation interpolatedCoefficients = linInterp(self.keys, self.values, keys[0]) aeroCoefficients = [0.0] * 5 for i in range(len(interpolatedCoefficients)): indexInCoeffArray = self.aeroCoeffIndices[i] aeroCoefficients[indexInCoeffArray] = interpolatedCoefficients[i] return aeroCoefficients def getAppliedForce(self, state, time, environment, rocketCG): aeroCoefficients = self._getAeroCoefficients(state, environment) return AeroFunctions.forceFromCoefficients(state, environment, *aeroCoefficients, self.position, self.Aref, self.Lref)
Ancestors
- AeroForce
- RocketComponent
- abc.ABC
Methods
def getAppliedForce(self, state, time, environment, rocketCG)
-
Expand source code
def getAppliedForce(self, state, time, environment, rocketCG): aeroCoefficients = self._getAeroCoefficients(state, environment) return AeroFunctions.forceFromCoefficients(state, environment, *aeroCoefficients, self.position, self.Aref, self.Lref)
class TabulatedInertia (componentDictReader, rocket, stage)
-
A zero-force component with time-varying tabulated inertia
Expand source code
class TabulatedInertia(RocketComponent): ''' A zero-force component with time-varying tabulated inertia ''' 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) def _parseInertiaTable(self, filePath): data = np.loadtxt(filePath, skiprows=1, delimiter=',') self.times = data[:, 0] self.inertiaData = data[:, 1:] # Check that the right number of columns is present if data.shape[1] != 8: raise ValueError("Wrong number of columns in inertia table: {}. Expecting 8 columns: \ Time, Mass, CGx, CGy, CGz, MOIx, MOIy, MOIz") def getInertia(self, time, state): inertiaData = linInterp(self.times, self.inertiaData, time) # MOI is last three columns, CG is the three before that, and mass is column 0 return Inertia(Vector(*inertiaData[-3:]), Vector(*inertiaData[1:4]), inertiaData[0]) def getAppliedForce(self, rocketState, time, environment, CG): return self.zeroForce
Ancestors
- RocketComponent
- abc.ABC
Methods
def getAppliedForce(self, rocketState, time, environment, CG)
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG): return self.zeroForce
def getInertia(self, time, state)
-
Expand source code
def getInertia(self, time, state): inertiaData = linInterp(self.times, self.inertiaData, time) # MOI is last three columns, CG is the three before that, and mass is column 0 return Inertia(Vector(*inertiaData[-3:]), Vector(*inertiaData[1:4]), inertiaData[0])
class TabulatedMotor (componentDictReader, rocket, stage)
-
Interface
Initialization: In rocket text file, attribute: "path", pointing at a motor definition text file Format:"test/testMotorDefintion.txt" Functions: .Thrust(time) returns current thrust level .OxWeight(time) returns current oxidizer weight .FuelWeight(time) returns current fuel weight Attributes: .initialOxidizerWeight .initialFuelWeight
All in same units as in the motor definition file (linearly interpolated) The motor is assumed to apply zero moment to the rocket, thrust is applied in z-direction
Expand source code
class TabulatedMotor(RocketComponent): ''' Interface: Initialization: In rocket text file, attribute: "path", pointing at a motor definition text file Format:"test/testMotorDefintion.txt" Functions: .Thrust(time) returns current thrust level .OxWeight(time) returns current oxidizer weight .FuelWeight(time) returns current fuel weight Attributes: .initialOxidizerWeight .initialFuelWeight All in same units as in the motor definition file (linearly interpolated) The motor is assumed to apply zero moment to the rocket, thrust is applied in z-direction ''' #### Init Functions #### def __init__(self, componentDictReader, rocket, stage): #TODO: Oxidizer and Fuel CG Locations should be defined relative to the motor location self.rocket = rocket self.stage = stage self.componentDictReader = componentDictReader self.name = componentDictReader.getDictName() stage.motor = self self.classType = componentDictReader.getString("class") self.ignitionTime = 0 # Modified by Rocket._initializeStaging and Rocket._stageSeparation # Impulse adjustments (mostly for Monte Carlo sims) self.impulseAdjustFactor = componentDictReader.getFloat("impulseAdjustFactor") self.burnTimeAdjustFactor = componentDictReader.getFloat("burnTimeAdjustFactor") motorFilePath = componentDictReader.getString("path") self._parseMotorDefinitionFile(motorFilePath) # Set the position to the initial CG location initInertia = self.getInertia(0, "fakeState") self.position = initInertia.CG #TODO: Build converter/parser for standard engine format like rasp/.eng or something like that def _parseMotorDefinitionFile(self, motorFilePath): ''' Parses a motor definition text file. See MAPLEAF/Examples/Motors for examples ''' # Get motor definition text with open(motorFilePath, "r") as motorFile: motorFileText = motorFile.read() # Remove all comment rows comment = re.compile("#.*") motorFileText = re.sub(comment, "", motorFileText) #Remove blank lines motorFileText = [line for line in motorFileText.split('\n') if line.strip() != ''] # Parse CG locations # TODO: Future motors should be able to exist off the rocket's center axis self.initOxCG_Z = float(motorFileText[0].split()[1]) + self.stage.position.Z self.finalOxCG_Z = float(motorFileText[1].split()[1]) + self.stage.position.Z self.initFuelCG_Z = float(motorFileText[2].split()[1]) + self.stage.position.Z self.finalFuelCG_Z = float(motorFileText[3].split()[1]) + self.stage.position.Z motorFileText = motorFileText[4:] # Parse data; Columns defined in MAPLEAF/Examples/Motors/test.txt # Gets defined values for: Time, thrust, oxFlowRate, fuelFlowRate, oxMOI, fuelMOI self.times = [] self.thrustLevels = [] oxFlowRate = [] fuelFlowRate = [] self.oxMOIs = [] self.fuelMOIs = [] for dataLine in motorFileText: # Splits line at each white space info = dataLine.split() self.times.append(float(info[0])) self.thrustLevels.append(float(info[1])) oxFlowRate.append(float(info[2])) fuelFlowRate.append(float(info[3])) oxVecStartIndex = dataLine.index('(') oxVecEndIndex = dataLine.index(')', oxVecStartIndex)+1 oxVecString = dataLine[oxVecStartIndex:oxVecEndIndex] oxMOIVec = Vector(oxVecString) self.oxMOIs.append(oxMOIVec) fuelVecStartIndex = dataLine.index('(', oxVecEndIndex) fuelVecEndIndex = dataLine.index(')', fuelVecStartIndex)+1 fuelVecString = dataLine[fuelVecStartIndex:fuelVecEndIndex] fuelMOIVec = Vector(fuelVecString) self.fuelMOIs.append(fuelMOIVec) # Tell the rocket and stage when their engines shut off -> used for flight animations self.stage.engineShutOffTime = self.times[-1] if self.rocket.engineShutOffTime == None: self.rocket.engineShutOffTime = self.times[-1] else: self.rocket.engineShutOffTime = max(self.rocket.engineShutOffTime, self.times[-1]) # Apply adjustment factors for monte carlo sims self.thrustLevels = [ thrust*self.impulseAdjustFactor/self.burnTimeAdjustFactor for thrust in self.thrustLevels ] self.times = [ t*self.burnTimeAdjustFactor for t in self.times ] # Calculate initial fuel and oxidizer masses through trapezoid rule # Trapezoid rule matches the linear interpolation used to find thrust values self.initialOxidizerWeight = 0 self.initialFuelWeight = 0 self.oxWeights = [ 0 ] self.fuelWeights = [ 0 ] for i in range(len(self.times)-1, 0, -1): deltaT = self.times[i] - self.times[i-1] def integrateVal(value, sum, timeSeries): sum += deltaT * (value[i-1] + value[i]) / 2 timeSeries.insert(0, sum) return sum self.initialOxidizerWeight = integrateVal(oxFlowRate, self.initialOxidizerWeight, self.oxWeights) self.initialFuelWeight = integrateVal(fuelFlowRate, self.initialFuelWeight, self.fuelWeights) #### Operational Functions #### def getInertia(self, time, state): timeSinceIgnition = max(0, time - self.ignitionTime) oxInertia = self._getOxInertia(timeSinceIgnition) fuelInertia = self._getFuelInertia(timeSinceIgnition) return oxInertia + fuelInertia 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) def updateIgnitionTime(self, ignitionTime, fakeValue=False): self.ignitionTime = ignitionTime if not fakeValue: self.rocket.engineShutOffTime = max(self.rocket.engineShutOffTime, self.ignitionTime + self.times[-1]) self.stage.engineShutOffTime = self.ignitionTime + self.times[-1] def getTotalImpulse(self): # Integrate the thrust - assume linear interpolations between points given -> midpoint rule integrates this perfectly totalImpulse = 0 for i in range(1, len(self.times)): deltaT = self.times[i] - self.times[i-1] totalImpulse += deltaT * (self.thrustLevels[i-1] + self.thrustLevels[i]) / 2 return totalImpulse def _getMass(self, timeSinceIgnition): return self.OxWeight(timeSinceIgnition) + self.FuelWeight(timeSinceIgnition) def _getOxInertia(self, timeSinceIgnition): if self.initialOxidizerWeight == 0: return Inertia(Vector(0,0,0), Vector(0,0,0), 0) oxWeight = linInterp(self.times, self.oxWeights, timeSinceIgnition) # Find fraction of oxidizer burned oxBurnedFrac = 1 - (oxWeight/self.initialOxidizerWeight) #Linearly interpolate CG location based on fraction of oxidizer burned oxCG_Z = self.initOxCG_Z*(1 - oxBurnedFrac) + self.finalOxCG_Z*oxBurnedFrac #TODO: Allow motor(s) to be defined off-axis oxCG = Vector(0,0,oxCG_Z) # Get MOI oxMOI = linInterp(self.times, self.oxMOIs, timeSinceIgnition) return Inertia(oxMOI, oxCG, oxWeight) def _getFuelInertia(self, timeSinceIgnition): if self.initialFuelWeight == 0: return Inertia(Vector(0,0,0), Vector(0,0,0), 0) #See comments in _getOxInertia() fuelWeight = linInterp(self.times, self.fuelWeights, timeSinceIgnition) fuelBurnedFrac = 1 - (fuelWeight / self.initialFuelWeight) fuelCG_Z = self.initFuelCG_Z*(1 - fuelBurnedFrac) + self.finalFuelCG_Z*fuelBurnedFrac fuelCG = Vector(0,0,fuelCG_Z) fuelMOI = linInterp(self.times, self.fuelMOIs, timeSinceIgnition) return Inertia(fuelMOI, fuelCG, fuelWeight)
Ancestors
- RocketComponent
- abc.ABC
Methods
def getAppliedForce(self, state, time, environment, CG)
-
Expand source code
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)
def getInertia(self, time, state)
-
Expand source code
def getInertia(self, time, state): timeSinceIgnition = max(0, time - self.ignitionTime) oxInertia = self._getOxInertia(timeSinceIgnition) fuelInertia = self._getFuelInertia(timeSinceIgnition) return oxInertia + fuelInertia
def getTotalImpulse(self)
-
Expand source code
def getTotalImpulse(self): # Integrate the thrust - assume linear interpolations between points given -> midpoint rule integrates this perfectly totalImpulse = 0 for i in range(1, len(self.times)): deltaT = self.times[i] - self.times[i-1] totalImpulse += deltaT * (self.thrustLevels[i-1] + self.thrustLevels[i]) / 2 return totalImpulse
def updateIgnitionTime(self, ignitionTime, fakeValue=False)
-
Expand source code
def updateIgnitionTime(self, ignitionTime, fakeValue=False): self.ignitionTime = ignitionTime if not fakeValue: self.rocket.engineShutOffTime = max(self.rocket.engineShutOffTime, self.ignitionTime + self.times[-1]) self.stage.engineShutOffTime = self.ignitionTime + self.times[-1]
class Transition (*args)
-
Models a conical diameter transition (growing or shrinking)
Two possible sets of inputs:
1. Initialization as a regular, dictionary-defined rocket component:
* args = (componentDictReader, rocket, stage)
* Expected classes: (SubDictReader
,Rocket
,Stage
)
2. Manual initialization:
* args = (startDiameter, endDiameter, length, position, inertia, rocket, stage, name, surfaceRoughness)
* Expected classes: (float, float, float,Vector
,Inertia
,Rocket
,Stage
, str, float)Expand source code
class Transition(FixedMass, BodyComponent): ''' Models a conical diameter transition (growing or shrinking) ''' def __init__(self, *args): ''' Two possible sets of inputs: 1. Initialization as a regular, dictionary-defined rocket component: * args = (componentDictReader, rocket, stage) * Expected classes: (`MAPLEAF.IO.SubDictReader`, `MAPLEAF.Rocket.Rocket`, `MAPLEAF.Rocket.Stage`) 2. Manual initialization: * args = (startDiameter, endDiameter, length, position, inertia, rocket, stage, name, surfaceRoughness) * Expected classes: (float, float, float, `MAPLEAF.Motion.Vector`, `MAPLEAF.Motion.Inertia`, `MAPLEAF.Rocket.Rocket`, `MAPLEAF.Rocket.Stage`, str, float) ''' if len(args) == 3: # Classic initialization from componentDictReader componentDictReader, rocket, stage = args FixedMass.__init__(self, componentDictReader, rocket, stage) self.length = componentDictReader.getFloat("length") self.startDiameter = componentDictReader.getFloat("startDiameter") self.endDiameter = componentDictReader.getFloat("endDiameter") self.surfaceRoughness = componentDictReader.tryGetFloat("surfaceRoughness", defaultValue=self.rocket.surfaceRoughness) else: # Initialization from parameters passed in self.startDiameter, self.endDiameter, self.length, self.position, self.inertia, self.rocket, self.stage, self.name, self.surfaceRoughness = args self._precomputeGeometry() def _precomputeGeometry(self): # Calculate basic areas self.topArea = self.startDiameter**2 * math.pi/4 self.bottomArea = self.endDiameter**2 * math.pi/4 self.frontalArea = abs(self.topArea - self.bottomArea) # Calculate surface area, volume, CP if self.length == 0: self.wettedArea = 0.0 self.volume = 0.0 self.CPLocation = self.position else: maxDiameter = max(self.startDiameter, self.endDiameter) minDiameter = min(self.startDiameter, self.endDiameter) hypotenuseSlope = ((maxDiameter - minDiameter) / self.length) # Boat tail is a truncated cone # Compute surface area of non-truncated cone starting at top of boattail baseConeHeight = maxDiameter / hypotenuseSlope baseHypotenuse = math.sqrt((maxDiameter/2)**2 + baseConeHeight**2) fullConeSurfaceArea = math.pi * (maxDiameter/2) * baseHypotenuse # Compute surface area of non-truncated cone starting at bottom of boattail tipConeHeight = minDiameter / hypotenuseSlope tipHypotenuse = math.sqrt((minDiameter/2)**2 + tipConeHeight**2) tipConeSurfaceArea = math.pi * (minDiameter/2) * tipHypotenuse # Surface area is the difference b/w the two self.wettedArea = fullConeSurfaceArea - tipConeSurfaceArea # Volume calculation uses same method as surface area calculation above fullConeVolume = self.topArea * baseConeHeight / 3 tipConeVolume = self.bottomArea * tipConeHeight / 3 self.volume = fullConeVolume - tipConeVolume # Calculate CP self.CPLocation = AeroFunctions.Barrowman_GetCPLocation(self.length, self.topArea, self.bottomArea, self.volume, self.position) # Calc aspect ratio if self.startDiameter == self.endDiameter: aspectRatio = 100 # Avoid division by zero else: aspectRatio = self.length / abs(self.startDiameter - self.endDiameter) # Precompute drag properties if self.endDiameter <= self.startDiameter: # Calculate Cd multiplier based on aspect ratio - Niskanen Eqn 3.88 if aspectRatio < 1: self.CdAdjustmentFactor = 1 elif aspectRatio < 3: self.CdAdjustmentFactor = (3 - aspectRatio) / 2 else: self.CdAdjustmentFactor = 0 else: if self.length == 0: coneHalfAngle = math.pi/2 else: coneHalfAngle = math.atan(abs(self.startDiameter - self.endDiameter)/2 / self.length) self.coneHalfAngle = coneHalfAngle self.SubsonicCdPolyCoeffs = computeSubsonicPolyCoeffs(coneHalfAngle) self.TransonicCdPolyCoeffs = computeTransonicPolyCoeffs(coneHalfAngle) def plotShape(self): import matplotlib.pyplot as plt Xvals = [] Yvals = [] forePos = self.position.Z aftPos = forePos - self.length foreRadius = self.startDiameter/2 aftRadius = self.endDiameter/2 Xvals.append(forePos) Yvals.append(foreRadius) # top right Xvals.append(aftPos) Yvals.append(aftRadius) # top left Xvals.append(aftPos) Yvals.append(-aftRadius) # bottom left Xvals.append(forePos) Yvals.append(-foreRadius) # bottom right Xvals.append(forePos) Yvals.append(foreRadius) # close in the shape plt.plot(Xvals, Yvals, color = 'k') def getAppliedForce(self, rocketState, time, environment, CG) -> ForceMomentSystem: Mach = AeroParameters.getMachNumber(rocketState, environment) Aref = self.rocket.Aref #### Normal Force #### AOA = AeroParameters.getTotalAOA(rocketState, environment) CN = AeroFunctions.Barrowman_GetCN(AOA, Aref, self.topArea, self.bottomArea) #### Pressure Drag #### if self.startDiameter > self.endDiameter: # Pressure base drag Cd_base = AeroFunctions.getBaseDragCoefficient(Mach) Cd_pressure = Cd_base * self.CdAdjustmentFactor else: # Pressure drag calculated like a nose cone if Mach < 1: # Niskanen pg. 48 eq. 3.87 - Power law interpolation Cd_pressure = self.SubsonicCdPolyCoeffs[0] * Mach**self.SubsonicCdPolyCoeffs[1] elif Mach > 1 and Mach < 1.3: # Interpolate in transonic region - derived from Niskanen Appendix B, Eqns B.3 - B.6 Cd_pressure = self.TransonicCdPolyCoeffs[0] + self.TransonicCdPolyCoeffs[1]*Mach + \ self.TransonicCdPolyCoeffs[2]*Mach**2 + self.TransonicCdPolyCoeffs[3]*Mach**3 else: Cd_pressure = getSupersonicPressureDragCoeff_Hoerner(self.coneHalfAngle, Mach) # Make reference are the rocket's, not this objects Cd_pressure *= self.frontalArea / Aref #### Skin Friction Drag #### if self.wettedArea == 0: skinFrictionDragCoefficient = 0 rollDampingMoment = Vector(0,0,0) else: skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, \ self.length, Mach, self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Total Drag #### Cd = Cd_pressure + skinFrictionDragCoefficient #### Assemble & return final force object #### return AeroFunctions.forceFromCdCN(rocketState, environment, Cd, CN, self.CPLocation, Aref, moment=rollDampingMoment) def getMaxDiameter(self): return max(self.startDiameter, self.endDiameter) def getRadius(self, distanceFromTip): return (distanceFromTip/self.length * (self.endDiameter - self.startDiameter) + self.startDiameter) / 2
Ancestors
- FixedMass
- RocketComponent
- BodyComponent
- abc.ABC
Subclasses
Methods
def getAppliedForce(self, rocketState, time, environment, CG) ‑> ForceMomentSystem
-
Expand source code
def getAppliedForce(self, rocketState, time, environment, CG) -> ForceMomentSystem: Mach = AeroParameters.getMachNumber(rocketState, environment) Aref = self.rocket.Aref #### Normal Force #### AOA = AeroParameters.getTotalAOA(rocketState, environment) CN = AeroFunctions.Barrowman_GetCN(AOA, Aref, self.topArea, self.bottomArea) #### Pressure Drag #### if self.startDiameter > self.endDiameter: # Pressure base drag Cd_base = AeroFunctions.getBaseDragCoefficient(Mach) Cd_pressure = Cd_base * self.CdAdjustmentFactor else: # Pressure drag calculated like a nose cone if Mach < 1: # Niskanen pg. 48 eq. 3.87 - Power law interpolation Cd_pressure = self.SubsonicCdPolyCoeffs[0] * Mach**self.SubsonicCdPolyCoeffs[1] elif Mach > 1 and Mach < 1.3: # Interpolate in transonic region - derived from Niskanen Appendix B, Eqns B.3 - B.6 Cd_pressure = self.TransonicCdPolyCoeffs[0] + self.TransonicCdPolyCoeffs[1]*Mach + \ self.TransonicCdPolyCoeffs[2]*Mach**2 + self.TransonicCdPolyCoeffs[3]*Mach**3 else: Cd_pressure = getSupersonicPressureDragCoeff_Hoerner(self.coneHalfAngle, Mach) # Make reference are the rocket's, not this objects Cd_pressure *= self.frontalArea / Aref #### Skin Friction Drag #### if self.wettedArea == 0: skinFrictionDragCoefficient = 0 rollDampingMoment = Vector(0,0,0) else: skinFrictionDragCoefficient, rollDampingMoment = AeroFunctions.getCylindricalSkinFrictionDragCoefficientAndRollDampingMoment(rocketState, environment, \ self.length, Mach, self.surfaceRoughness, self.wettedArea, Aref, self.rocket.finenessRatio, self.rocket.fullyTurbulentBL) #### Total Drag #### Cd = Cd_pressure + skinFrictionDragCoefficient #### Assemble & return final force object #### return AeroFunctions.forceFromCdCN(rocketState, environment, Cd, CN, self.CPLocation, Aref, moment=rollDampingMoment)
def plotShape(self)
-
Expand source code
def plotShape(self): import matplotlib.pyplot as plt Xvals = [] Yvals = [] forePos = self.position.Z aftPos = forePos - self.length foreRadius = self.startDiameter/2 aftRadius = self.endDiameter/2 Xvals.append(forePos) Yvals.append(foreRadius) # top right Xvals.append(aftPos) Yvals.append(aftRadius) # top left Xvals.append(aftPos) Yvals.append(-aftRadius) # bottom left Xvals.append(forePos) Yvals.append(-foreRadius) # bottom right Xvals.append(forePos) Yvals.append(foreRadius) # close in the shape plt.plot(Xvals, Yvals, color = 'k')
Inherited members