Module MAPLEAF.Rocket.RocketComponents
Contains interface definitions (RocketComponent
/BodyComponent
) and Base classes for all RocketComponents (FixedMass
),
as well as some simple rocket component classes (FixedForce
,AeroForce
,AeroDamping
etc.)
Expand source code
# -*- coding: utf-8 -*-
"""
Contains interface definitions (`RocketComponent`/`BodyComponent`) and Base classes for all RocketComponents (`FixedMass`),
as well as some simple rocket component classes (`FixedForce`,`AeroForce`,`AeroDamping` etc.)
"""
from abc import ABC, abstractmethod
from typing import List, Union
import numpy as np
from MAPLEAF.Motion import (AeroParameters, ForceMomentSystem, Inertia,
NoNaNLinearNDInterpolator, RigidBodyState, Vector,
linInterp)
from . import AeroFunctions
__all__ = [ "RocketComponent", "BodyComponent", "PlanarInterface", "FixedMass", "FixedForce", "AeroForce", "AeroDamping", "TabulatedAeroForce", "TabulatedInertia", "FractionalJetDamping" ]
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
'''
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
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
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
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
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)
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)
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)
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
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
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 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 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 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 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 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])