Module MAPLEAF.ENV.EarthModelling
These classes model the earth's gravity and perform some coordinate transformations between the global inertial frame and the ENU/launch tower frame
Expand source code
''' These classes model the earth's gravity and perform some coordinate transformations between the global inertial frame and the ENU/launch tower frame '''
from abc import ABC, abstractmethod
from math import atan2, cos, degrees, pi, radians, sin, sqrt
from typing import Union
import numpy as np
from MAPLEAF.IO import getAbsoluteFilePath
from MAPLEAF.Motion import (Inertia, Quaternion, RigidBodyState,
RigidBodyState_3DoF, Vector)
__all__ = [ "earthModelFactory", "NoEarth", "FlatEarth", "SphericalEarth", "WGS84" ]
class EarthModel(ABC):
''' Interface for all earth models '''
# All earth models will have these default values
rotationRate = 0
@abstractmethod
def getGravityForce(self, inertia: Inertia, state: Union[RigidBodyState, RigidBodyState_3DoF]) -> Vector:
''' Return gravity force vector in the global inertial frame '''
return
@abstractmethod
def getInertialToENUFrameRotation(self, x: float, y: float, z: float) -> Quaternion:
'''
Return Quaternion defining the rotation from the global inertial frame
to the North-East-Up frame defined on the surface under the air vehicle's position
'''
return
def getInertialToNEDFrameRotation(self, x, y, z):
inertialToENURotation = self.getInertialToENUFrameRotation(x, y, z)
ENUToNED = Quaternion(axisOfRotation=Vector(1,1,0), angle=pi)
return inertialToENURotation * ENUToNED
@abstractmethod
def getAltitude(self, x: float, y: float, z: float) -> float:
''' Given the aircraft coordinates in the global inertial frame, should return the altitude above SEA level (ASL) - to be used in wind calculations '''
return
def convertIntoGlobalFrame(self,
launchTowerFrameState: Union[RigidBodyState, RigidBodyState_3DoF],
latitude: float,
longitude: float,
) -> Union[RigidBodyState, RigidBodyState_3DoF]:
'''
Should take a RigidBodyState defined in the launch tower frame (fixed to the earth's surface), where
position.Z has been redefined relative to sea level (instead of ground-level), and convert it into
the global inertial frame.
Exception is the Angular Velocity, since it is defined in the vehicle's local frame. If the earth model is
rotating, add the earth's angular velocity to it (after conversion into the local frame)
'''
return
def earthModelFactory(envDictReader=None) -> EarthModel:
''' Provide an envDictReader (`MAPLEAF.IO.SubDictReader`). If none is provided, returns a FlatEarth model '''
if envDictReader == None:
return FlatEarth()
earthModel = envDictReader.getString("EarthModel")
if earthModel == "Flat":
return FlatEarth()
elif earthModel == "Round":
return SphericalEarth()
elif earthModel == "WGS84":
return WGS84()
elif earthModel == "None":
return NoEarth()
else:
raise NotImplementedError("Earth model: {} not found. Try 'Flat', 'Round' or 'WGS84'".format(earthModel))
class NoEarth(EarthModel):
rotationRate = 0 # rad/s
noRotation = Quaternion(1,0,0,0)
noForce = Vector(0,0,0)
def getGravityForce(self, inertia, state):
return self.noForce
def getInertialToENUFrameRotation(self, x, y, z):
return self.noRotation
def getAltitude(self, x, y, z):
return z
def convertIntoGlobalFrame(self, launchTowerFrameState, _, __):
''' Launch tower frame is global inertial frame '''
return launchTowerFrameState
class FlatEarth(NoEarth):
# GM = 398600.5 # km^3/s^2 Geocentric gravitational constant (Mass of atmosphere included)
GM = 398600.5e9 # m^3/s^2
def getGravityForce(self, inertia, state):
''' Returns a gravity force vector in the global frame '''
gravityDirection = Vector(0, 0, -1) # Down in the launch tower frame
distanceFromCenterOfEarth = state.position.Z + 6371000 # Adding earth's average radius
gravityMagnitude = inertia.mass * self.GM / distanceFromCenterOfEarth**2 # Checked that this equation gives results similar to the
# US Standard Atmosphere - offset a bit depending on what earth radius is used. Current 6371 km gives slightly lower values that USTDA.
return gravityDirection * gravityMagnitude
class SphericalEarth(EarthModel):
''' Models a non-rotating, uniform, spherical earth '''
radius = 6371007.1809 # m - from NESC-RP-12-007770, Volume II
rotationRate = 7.292115e-5 # rad/s angular velocity - from WGS84 model. Defined WRT stars (not our sun)
# GM = 398600.5 # km^3/s^2 Geocentric gravitational constant (Mass of atmosphere included)
GM = 3.98600436e14 # m^3/s^2- from NESC-RP-12-007770, Volume II
def geodeticToCartesian(self, lat, lon, height, timeOffset=0):
'''
Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere.
Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and
the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system
Inputs:
lat: (float) latitude - degrees
lon: (float) longitude - degrees
height: (float) ASL (usually m)
Returns:
x: (float) meters, relative to center of sphere
y: ' '
z: ' '
'''
lat = radians(lat)
lon = radians(lon)
# Adjust for rotation rate
lon += self.rotationRate * timeOffset
radius = self.radius + height
x = radius * cos(lat) * cos(lon)
y = radius * cos(lat) * sin(lon)
z = radius * sin(lat)
return x, y, z
def cartesianToGeodetic(self, x, y, z, timeOffset=0):
''' Convert from cartesian to lat/lon/height coordinates, where height is ASL '''
p = sqrt(x*x + y*y)
lon = atan2(y, x)
# Adjust for rotation rate
lon -= self.rotationRate * timeOffset
lat = atan2(z, p)
height = sqrt(x*x + y*y + z*z) - self.radius
return degrees(lat), degrees(lon), height
def getGravityForce(self, inertia, state):
''' Returns a gravity force vector in the global frame '''
gravityDirection = -(state.position.normalize()) # Gravity pulls toward the earth's center of mass, which is the origin
distanceFromCenterOfEarth = state.position.length()
gravityMagnitude = inertia.mass * self.GM / distanceFromCenterOfEarth**2 # Checked that this equation gives results similar to the
# US Standard Atmosphere - offset a bit depending on what earth radius is used. Current 6371 km gives slightly lower values that USTDA.
return gravityDirection * gravityMagnitude
def getInertialToENUFrameRotation(self, x, y, z):
''' Returns a Quaternion that defines the rotation b/w the global inertia frame and the local, surface-normal North-East-Up (y-x-z) frame '''
# Time offset not necessary, since the x, y, z coordinates are inertial, and we are not
# interested in finding a particular location on the surface of the earth,
# we are just interested in finding the surface normal under a x/y/z location.
# The result is independent of the earth's rotation.
lat, lon, height = self.cartesianToGeodetic(x, y, z)
# Rotation between inertial frame and ENU frame will be composed in two steps
# Step 1: Rotate about inertial/initial Z-axis to Y-axis in such a way, that after rotation 2,
# it will be pointing North
rot1Angle = radians(lon + 90)
rot1 = Quaternion(axisOfRotation=Vector(0,0,1), angle=rot1Angle)
# Step 2: Rotate about the local x-axis to match the latitude
rot2Angle = radians(90 - lat)
rot2 = Quaternion(axisOfRotation=Vector(1,0,0), angle=rot2Angle)
# Sequentially combine rotations by multiplying them
return rot1 * rot2
def getAltitude(self, x, y, z):
# Height is independent of the earth's rotation
_, __, height = self.cartesianToGeodetic(x, y, z)
return height
def convertIntoGlobalFrame(self, launchTowerFrameState, lat, lon):
### Position ###
height = launchTowerFrameState.position.Z # ASL altitude
globalFramePosition = Vector(*self.geodeticToCartesian(lat, lon, height))
### Velocity ###
# Find orientation of launch tower frame relative to global frame
launchTowerToGlobalFrame = self.getInertialToENUFrameRotation(*globalFramePosition)
# Rotate velocity accordingly
rotatedVelocity = launchTowerToGlobalFrame.rotate(launchTowerFrameState.velocity)
# Add earth's surface velocity
earthAngVel = Vector(0, 0, self.rotationRate)
velocityDueToEarthRotation = earthAngVel.crossProduct(globalFramePosition)
globalFrameVelocity = rotatedVelocity + velocityDueToEarthRotation
try: # 6DoF Case
### Orientation ###
globalFrameOrientation = launchTowerToGlobalFrame * launchTowerFrameState.orientation
### Angular Velocity ###
# Angular velocity is defined in the vehicle's local frame, so the conversion needs to go the other way
earthAngVel_RocketFrame = globalFrameOrientation.conjugate().rotate(earthAngVel)
localFrame_adjustedAngVel = launchTowerFrameState.angularVelocity + earthAngVel_RocketFrame
return RigidBodyState(
globalFramePosition,
globalFrameVelocity,
globalFrameOrientation,
localFrame_adjustedAngVel
)
except AttributeError: # 3DoF Case
return RigidBodyState_3DoF(globalFramePosition, globalFrameVelocity)
class WGS84(SphericalEarth):
'''
Models a rotating, ellipsoid earth, with non-uniform gravity
Inherits the getInertialToENUFrameRotation function from SphericalEarth, otherwise overrides everything
'''
# Set up/calculate the WGS84 Ellipsoid parameters and derived values
a = 6378137 # m, semi-major axis
f = 1/298.257223563 # Flattening (derived from C_20)
b = a * (1-f) # m, semi-minor axis = 6 356 752.314 140
e2 = f*(2-f) # First eccentricity squared (e^2) = 6.694 379 990 14 e-3
eP2 = e2 / ((1-f)**2) # Second eccentricity squared (e'^2) = 6.739 496 742 28 e-3s
eP = sqrt(eP2) # Second eccentricity (e')
def __init__(self):
# Read gravity coefficients from table
# Columns are: Degree Order C S (Where C and S are the coefficients)
coeffPath = getAbsoluteFilePath('./MAPLEAF/ENV/sphericalHarmonicGravityCoeffs.txt')
gravityCoeffs = np.loadtxt(coeffPath, skiprows=2)
# Convert that table into nested dictionary form, to make C and S coefficients easily accessible by degree and order
Ccoeffs = {}
Scoeffs = {}
for row in gravityCoeffs:
degree, order, C, S, = row
if degree not in Ccoeffs:
# Create subdictionary for each degree if this is the first time we're encountering that degree
Ccoeffs[degree] = {}
Scoeffs[degree] = {}
# Save coefficient from this row
Ccoeffs[degree][order] = C
Scoeffs[degree][order] = S
# Save results
# Now C_{2,3} is accessible as self.C[2][3], same for self.S
self.C = Ccoeffs
self.S = Scoeffs
def geodeticToCartesian(self, lat, lon, height, timeOffset=0):
'''
Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere.
Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and
the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system
Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates
Note: In other references, it is common to set \phi = lat, \lamda = lon
Inputs:
lat: (float) latitude - degrees
lon: (float) longitude - degrees
height: (float) ASL - m
timeOffset (float) Offset added to results based on earth's rotation rate.
At timeOffset=0, this inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame
Returns:
x: (float) meters, relative to center of ellipsoid
y: ' '
z: ' '
'''
lat = radians(lat)
lon = radians(lon)
# Adjust for earth's rate of rotation
lon += self.rotationRate * timeOffset
N = self.a / sqrt(1 - self.e2*sin(lat)**2)
x = (N + height)*cos(lat)*cos(lon)
y = (N + height)*cos(lat)*sin(lon)
z = ((self.b**2 / self.a**2)*N + height) * sin(lat)
return x, y, z
def cartesianToGeodetic(self, x, y, z, timeOffset=0):
'''
Inputs in m, sec
Convert from earth-centered inertial coordinates to WGS84 geodetic coordinates
Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates
At timeOffset=0, the inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame
Returns latitude, longitude, height in degrees, m
'''
lon = atan2(y, x) # Longitude is easy to find
# Adjust longitude for earth's rotation
lon -= self.rotationRate * timeOffset
### Compute latitude and height normal to surface w/ Newton-Raphson method ###
p = sqrt(x*x + y*y)
Kold = 1/(1 - self.e2)
Korig = Kold
while True:
ci = ((p*p + (1-self.e2)*z*z * Kold**2)**(1.5)) / (self.a * self.e2)
Knew = 1 + (p*p + (1 - self.e2)*z*z * Kold**3) / (ci - p*p)
# End iterations once delta per iteration < 1e-10
if abs(Knew - Kold) < 1e-10:
break
Kold = Knew
k = Knew
lat = atan2(k*z, p)
h = (1/self.e2)*(1/k - 1/Korig)*sqrt(p*p + z*z*k*k)
return degrees(lat), degrees(lon), h
# Re-use conversions to/from spherical coordinates from SphericalEarth
# Useful for Spherical Harmonic Gravity model calculations, which are performed in spherical coordinates
cartesianToSpherical = SphericalEarth.cartesianToGeodetic
sphericalToCartesian = SphericalEarth.geodeticToCartesian
def getGravityForce(self, inertia, state):
'''
Get gravity using the J2 model (just a single spherical harmonic coefficient accounting for the earth's oblateness)
Method from NESC-RP-12-00770, Volume II, pg.51, Eqn 29-31
'''
# J2 Model just uses the first spherical harmonic coefficient
J2 = 0.00108262982
re = self.a # Equatorial radius (Semi-major axis of ellipsoid)
x, y, z = state.position[0], state.position[1], state.position[2]
r = state.position.length()
mu = self.GM
# Precompute common parts of equation
frac = (3*J2 * re*re) / (2 * r**4)
multiplier = -mu*inertia.mass / r**3
xForce = x * multiplier * (1 - frac*(5*z*z - r*r))
yForce = y * multiplier * (1 - frac*(5*z*z - r*r))
zForce = z * multiplier * (1 - frac*(5*z*z - 3*r*r))
return Vector(xForce, yForce, zForce)
Functions
def earthModelFactory(envDictReader=None) ‑> MAPLEAF.ENV.EarthModelling.EarthModel
-
Provide an envDictReader (
SubDictReader
). If none is provided, returns a FlatEarth modelExpand source code
def earthModelFactory(envDictReader=None) -> EarthModel: ''' Provide an envDictReader (`MAPLEAF.IO.SubDictReader`). If none is provided, returns a FlatEarth model ''' if envDictReader == None: return FlatEarth() earthModel = envDictReader.getString("EarthModel") if earthModel == "Flat": return FlatEarth() elif earthModel == "Round": return SphericalEarth() elif earthModel == "WGS84": return WGS84() elif earthModel == "None": return NoEarth() else: raise NotImplementedError("Earth model: {} not found. Try 'Flat', 'Round' or 'WGS84'".format(earthModel))
Classes
class FlatEarth
-
Interface for all earth models
Expand source code
class FlatEarth(NoEarth): # GM = 398600.5 # km^3/s^2 Geocentric gravitational constant (Mass of atmosphere included) GM = 398600.5e9 # m^3/s^2 def getGravityForce(self, inertia, state): ''' Returns a gravity force vector in the global frame ''' gravityDirection = Vector(0, 0, -1) # Down in the launch tower frame distanceFromCenterOfEarth = state.position.Z + 6371000 # Adding earth's average radius gravityMagnitude = inertia.mass * self.GM / distanceFromCenterOfEarth**2 # Checked that this equation gives results similar to the # US Standard Atmosphere - offset a bit depending on what earth radius is used. Current 6371 km gives slightly lower values that USTDA. return gravityDirection * gravityMagnitude
Ancestors
- NoEarth
- MAPLEAF.ENV.EarthModelling.EarthModel
- abc.ABC
Class variables
var GM
Methods
def getGravityForce(self, inertia, state)
-
Returns a gravity force vector in the global frame
Expand source code
def getGravityForce(self, inertia, state): ''' Returns a gravity force vector in the global frame ''' gravityDirection = Vector(0, 0, -1) # Down in the launch tower frame distanceFromCenterOfEarth = state.position.Z + 6371000 # Adding earth's average radius gravityMagnitude = inertia.mass * self.GM / distanceFromCenterOfEarth**2 # Checked that this equation gives results similar to the # US Standard Atmosphere - offset a bit depending on what earth radius is used. Current 6371 km gives slightly lower values that USTDA. return gravityDirection * gravityMagnitude
Inherited members
class NoEarth
-
Interface for all earth models
Expand source code
class NoEarth(EarthModel): rotationRate = 0 # rad/s noRotation = Quaternion(1,0,0,0) noForce = Vector(0,0,0) def getGravityForce(self, inertia, state): return self.noForce def getInertialToENUFrameRotation(self, x, y, z): return self.noRotation def getAltitude(self, x, y, z): return z def convertIntoGlobalFrame(self, launchTowerFrameState, _, __): ''' Launch tower frame is global inertial frame ''' return launchTowerFrameState
Ancestors
- MAPLEAF.ENV.EarthModelling.EarthModel
- abc.ABC
Subclasses
Class variables
var noForce
var noRotation
var rotationRate
Methods
def convertIntoGlobalFrame(self, launchTowerFrameState, _, __)
-
Launch tower frame is global inertial frame
Expand source code
def convertIntoGlobalFrame(self, launchTowerFrameState, _, __): ''' Launch tower frame is global inertial frame ''' return launchTowerFrameState
def getAltitude(self, x, y, z)
-
Given the aircraft coordinates in the global inertial frame, should return the altitude above SEA level (ASL) - to be used in wind calculations
Expand source code
def getAltitude(self, x, y, z): return z
def getGravityForce(self, inertia, state)
-
Return gravity force vector in the global inertial frame
Expand source code
def getGravityForce(self, inertia, state): return self.noForce
def getInertialToENUFrameRotation(self, x, y, z)
-
Return Quaternion defining the rotation from the global inertial frame to the North-East-Up frame defined on the surface under the air vehicle's position
Expand source code
def getInertialToENUFrameRotation(self, x, y, z): return self.noRotation
class SphericalEarth
-
Models a non-rotating, uniform, spherical earth
Expand source code
class SphericalEarth(EarthModel): ''' Models a non-rotating, uniform, spherical earth ''' radius = 6371007.1809 # m - from NESC-RP-12-007770, Volume II rotationRate = 7.292115e-5 # rad/s angular velocity - from WGS84 model. Defined WRT stars (not our sun) # GM = 398600.5 # km^3/s^2 Geocentric gravitational constant (Mass of atmosphere included) GM = 3.98600436e14 # m^3/s^2- from NESC-RP-12-007770, Volume II def geodeticToCartesian(self, lat, lon, height, timeOffset=0): ''' Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere. Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system Inputs: lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL (usually m) Returns: x: (float) meters, relative to center of sphere y: ' ' z: ' ' ''' lat = radians(lat) lon = radians(lon) # Adjust for rotation rate lon += self.rotationRate * timeOffset radius = self.radius + height x = radius * cos(lat) * cos(lon) y = radius * cos(lat) * sin(lon) z = radius * sin(lat) return x, y, z def cartesianToGeodetic(self, x, y, z, timeOffset=0): ''' Convert from cartesian to lat/lon/height coordinates, where height is ASL ''' p = sqrt(x*x + y*y) lon = atan2(y, x) # Adjust for rotation rate lon -= self.rotationRate * timeOffset lat = atan2(z, p) height = sqrt(x*x + y*y + z*z) - self.radius return degrees(lat), degrees(lon), height def getGravityForce(self, inertia, state): ''' Returns a gravity force vector in the global frame ''' gravityDirection = -(state.position.normalize()) # Gravity pulls toward the earth's center of mass, which is the origin distanceFromCenterOfEarth = state.position.length() gravityMagnitude = inertia.mass * self.GM / distanceFromCenterOfEarth**2 # Checked that this equation gives results similar to the # US Standard Atmosphere - offset a bit depending on what earth radius is used. Current 6371 km gives slightly lower values that USTDA. return gravityDirection * gravityMagnitude def getInertialToENUFrameRotation(self, x, y, z): ''' Returns a Quaternion that defines the rotation b/w the global inertia frame and the local, surface-normal North-East-Up (y-x-z) frame ''' # Time offset not necessary, since the x, y, z coordinates are inertial, and we are not # interested in finding a particular location on the surface of the earth, # we are just interested in finding the surface normal under a x/y/z location. # The result is independent of the earth's rotation. lat, lon, height = self.cartesianToGeodetic(x, y, z) # Rotation between inertial frame and ENU frame will be composed in two steps # Step 1: Rotate about inertial/initial Z-axis to Y-axis in such a way, that after rotation 2, # it will be pointing North rot1Angle = radians(lon + 90) rot1 = Quaternion(axisOfRotation=Vector(0,0,1), angle=rot1Angle) # Step 2: Rotate about the local x-axis to match the latitude rot2Angle = radians(90 - lat) rot2 = Quaternion(axisOfRotation=Vector(1,0,0), angle=rot2Angle) # Sequentially combine rotations by multiplying them return rot1 * rot2 def getAltitude(self, x, y, z): # Height is independent of the earth's rotation _, __, height = self.cartesianToGeodetic(x, y, z) return height def convertIntoGlobalFrame(self, launchTowerFrameState, lat, lon): ### Position ### height = launchTowerFrameState.position.Z # ASL altitude globalFramePosition = Vector(*self.geodeticToCartesian(lat, lon, height)) ### Velocity ### # Find orientation of launch tower frame relative to global frame launchTowerToGlobalFrame = self.getInertialToENUFrameRotation(*globalFramePosition) # Rotate velocity accordingly rotatedVelocity = launchTowerToGlobalFrame.rotate(launchTowerFrameState.velocity) # Add earth's surface velocity earthAngVel = Vector(0, 0, self.rotationRate) velocityDueToEarthRotation = earthAngVel.crossProduct(globalFramePosition) globalFrameVelocity = rotatedVelocity + velocityDueToEarthRotation try: # 6DoF Case ### Orientation ### globalFrameOrientation = launchTowerToGlobalFrame * launchTowerFrameState.orientation ### Angular Velocity ### # Angular velocity is defined in the vehicle's local frame, so the conversion needs to go the other way earthAngVel_RocketFrame = globalFrameOrientation.conjugate().rotate(earthAngVel) localFrame_adjustedAngVel = launchTowerFrameState.angularVelocity + earthAngVel_RocketFrame return RigidBodyState( globalFramePosition, globalFrameVelocity, globalFrameOrientation, localFrame_adjustedAngVel ) except AttributeError: # 3DoF Case return RigidBodyState_3DoF(globalFramePosition, globalFrameVelocity)
Ancestors
- MAPLEAF.ENV.EarthModelling.EarthModel
- abc.ABC
Subclasses
Class variables
var GM
var radius
var rotationRate
Methods
def cartesianToGeodetic(self, x, y, z, timeOffset=0)
-
Convert from cartesian to lat/lon/height coordinates, where height is ASL
Expand source code
def cartesianToGeodetic(self, x, y, z, timeOffset=0): ''' Convert from cartesian to lat/lon/height coordinates, where height is ASL ''' p = sqrt(x*x + y*y) lon = atan2(y, x) # Adjust for rotation rate lon -= self.rotationRate * timeOffset lat = atan2(z, p) height = sqrt(x*x + y*y + z*z) - self.radius return degrees(lat), degrees(lon), height
def convertIntoGlobalFrame(self, launchTowerFrameState, lat, lon)
-
Should take a RigidBodyState defined in the launch tower frame (fixed to the earth's surface), where position.Z has been redefined relative to sea level (instead of ground-level), and convert it into the global inertial frame. Exception is the Angular Velocity, since it is defined in the vehicle's local frame. If the earth model is rotating, add the earth's angular velocity to it (after conversion into the local frame)
Expand source code
def convertIntoGlobalFrame(self, launchTowerFrameState, lat, lon): ### Position ### height = launchTowerFrameState.position.Z # ASL altitude globalFramePosition = Vector(*self.geodeticToCartesian(lat, lon, height)) ### Velocity ### # Find orientation of launch tower frame relative to global frame launchTowerToGlobalFrame = self.getInertialToENUFrameRotation(*globalFramePosition) # Rotate velocity accordingly rotatedVelocity = launchTowerToGlobalFrame.rotate(launchTowerFrameState.velocity) # Add earth's surface velocity earthAngVel = Vector(0, 0, self.rotationRate) velocityDueToEarthRotation = earthAngVel.crossProduct(globalFramePosition) globalFrameVelocity = rotatedVelocity + velocityDueToEarthRotation try: # 6DoF Case ### Orientation ### globalFrameOrientation = launchTowerToGlobalFrame * launchTowerFrameState.orientation ### Angular Velocity ### # Angular velocity is defined in the vehicle's local frame, so the conversion needs to go the other way earthAngVel_RocketFrame = globalFrameOrientation.conjugate().rotate(earthAngVel) localFrame_adjustedAngVel = launchTowerFrameState.angularVelocity + earthAngVel_RocketFrame return RigidBodyState( globalFramePosition, globalFrameVelocity, globalFrameOrientation, localFrame_adjustedAngVel ) except AttributeError: # 3DoF Case return RigidBodyState_3DoF(globalFramePosition, globalFrameVelocity)
def geodeticToCartesian(self, lat, lon, height, timeOffset=0)
-
Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere.
Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system
Inputs
lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL (usually m)
Returns
x
-
(float) meters, relative to center of sphere
y
-
' '
z
-
' '
Expand source code
def geodeticToCartesian(self, lat, lon, height, timeOffset=0): ''' Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere. Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system Inputs: lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL (usually m) Returns: x: (float) meters, relative to center of sphere y: ' ' z: ' ' ''' lat = radians(lat) lon = radians(lon) # Adjust for rotation rate lon += self.rotationRate * timeOffset radius = self.radius + height x = radius * cos(lat) * cos(lon) y = radius * cos(lat) * sin(lon) z = radius * sin(lat) return x, y, z
def getAltitude(self, x, y, z)
-
Given the aircraft coordinates in the global inertial frame, should return the altitude above SEA level (ASL) - to be used in wind calculations
Expand source code
def getAltitude(self, x, y, z): # Height is independent of the earth's rotation _, __, height = self.cartesianToGeodetic(x, y, z) return height
def getGravityForce(self, inertia, state)
-
Returns a gravity force vector in the global frame
Expand source code
def getGravityForce(self, inertia, state): ''' Returns a gravity force vector in the global frame ''' gravityDirection = -(state.position.normalize()) # Gravity pulls toward the earth's center of mass, which is the origin distanceFromCenterOfEarth = state.position.length() gravityMagnitude = inertia.mass * self.GM / distanceFromCenterOfEarth**2 # Checked that this equation gives results similar to the # US Standard Atmosphere - offset a bit depending on what earth radius is used. Current 6371 km gives slightly lower values that USTDA. return gravityDirection * gravityMagnitude
def getInertialToENUFrameRotation(self, x, y, z)
-
Returns a Quaternion that defines the rotation b/w the global inertia frame and the local, surface-normal North-East-Up (y-x-z) frame
Expand source code
def getInertialToENUFrameRotation(self, x, y, z): ''' Returns a Quaternion that defines the rotation b/w the global inertia frame and the local, surface-normal North-East-Up (y-x-z) frame ''' # Time offset not necessary, since the x, y, z coordinates are inertial, and we are not # interested in finding a particular location on the surface of the earth, # we are just interested in finding the surface normal under a x/y/z location. # The result is independent of the earth's rotation. lat, lon, height = self.cartesianToGeodetic(x, y, z) # Rotation between inertial frame and ENU frame will be composed in two steps # Step 1: Rotate about inertial/initial Z-axis to Y-axis in such a way, that after rotation 2, # it will be pointing North rot1Angle = radians(lon + 90) rot1 = Quaternion(axisOfRotation=Vector(0,0,1), angle=rot1Angle) # Step 2: Rotate about the local x-axis to match the latitude rot2Angle = radians(90 - lat) rot2 = Quaternion(axisOfRotation=Vector(1,0,0), angle=rot2Angle) # Sequentially combine rotations by multiplying them return rot1 * rot2
class WGS84
-
Models a rotating, ellipsoid earth, with non-uniform gravity Inherits the getInertialToENUFrameRotation function from SphericalEarth, otherwise overrides everything
Expand source code
class WGS84(SphericalEarth): ''' Models a rotating, ellipsoid earth, with non-uniform gravity Inherits the getInertialToENUFrameRotation function from SphericalEarth, otherwise overrides everything ''' # Set up/calculate the WGS84 Ellipsoid parameters and derived values a = 6378137 # m, semi-major axis f = 1/298.257223563 # Flattening (derived from C_20) b = a * (1-f) # m, semi-minor axis = 6 356 752.314 140 e2 = f*(2-f) # First eccentricity squared (e^2) = 6.694 379 990 14 e-3 eP2 = e2 / ((1-f)**2) # Second eccentricity squared (e'^2) = 6.739 496 742 28 e-3s eP = sqrt(eP2) # Second eccentricity (e') def __init__(self): # Read gravity coefficients from table # Columns are: Degree Order C S (Where C and S are the coefficients) coeffPath = getAbsoluteFilePath('./MAPLEAF/ENV/sphericalHarmonicGravityCoeffs.txt') gravityCoeffs = np.loadtxt(coeffPath, skiprows=2) # Convert that table into nested dictionary form, to make C and S coefficients easily accessible by degree and order Ccoeffs = {} Scoeffs = {} for row in gravityCoeffs: degree, order, C, S, = row if degree not in Ccoeffs: # Create subdictionary for each degree if this is the first time we're encountering that degree Ccoeffs[degree] = {} Scoeffs[degree] = {} # Save coefficient from this row Ccoeffs[degree][order] = C Scoeffs[degree][order] = S # Save results # Now C_{2,3} is accessible as self.C[2][3], same for self.S self.C = Ccoeffs self.S = Scoeffs def geodeticToCartesian(self, lat, lon, height, timeOffset=0): ''' Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere. Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates Note: In other references, it is common to set \phi = lat, \lamda = lon Inputs: lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL - m timeOffset (float) Offset added to results based on earth's rotation rate. At timeOffset=0, this inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame Returns: x: (float) meters, relative to center of ellipsoid y: ' ' z: ' ' ''' lat = radians(lat) lon = radians(lon) # Adjust for earth's rate of rotation lon += self.rotationRate * timeOffset N = self.a / sqrt(1 - self.e2*sin(lat)**2) x = (N + height)*cos(lat)*cos(lon) y = (N + height)*cos(lat)*sin(lon) z = ((self.b**2 / self.a**2)*N + height) * sin(lat) return x, y, z def cartesianToGeodetic(self, x, y, z, timeOffset=0): ''' Inputs in m, sec Convert from earth-centered inertial coordinates to WGS84 geodetic coordinates Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates At timeOffset=0, the inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame Returns latitude, longitude, height in degrees, m ''' lon = atan2(y, x) # Longitude is easy to find # Adjust longitude for earth's rotation lon -= self.rotationRate * timeOffset ### Compute latitude and height normal to surface w/ Newton-Raphson method ### p = sqrt(x*x + y*y) Kold = 1/(1 - self.e2) Korig = Kold while True: ci = ((p*p + (1-self.e2)*z*z * Kold**2)**(1.5)) / (self.a * self.e2) Knew = 1 + (p*p + (1 - self.e2)*z*z * Kold**3) / (ci - p*p) # End iterations once delta per iteration < 1e-10 if abs(Knew - Kold) < 1e-10: break Kold = Knew k = Knew lat = atan2(k*z, p) h = (1/self.e2)*(1/k - 1/Korig)*sqrt(p*p + z*z*k*k) return degrees(lat), degrees(lon), h # Re-use conversions to/from spherical coordinates from SphericalEarth # Useful for Spherical Harmonic Gravity model calculations, which are performed in spherical coordinates cartesianToSpherical = SphericalEarth.cartesianToGeodetic sphericalToCartesian = SphericalEarth.geodeticToCartesian def getGravityForce(self, inertia, state): ''' Get gravity using the J2 model (just a single spherical harmonic coefficient accounting for the earth's oblateness) Method from NESC-RP-12-00770, Volume II, pg.51, Eqn 29-31 ''' # J2 Model just uses the first spherical harmonic coefficient J2 = 0.00108262982 re = self.a # Equatorial radius (Semi-major axis of ellipsoid) x, y, z = state.position[0], state.position[1], state.position[2] r = state.position.length() mu = self.GM # Precompute common parts of equation frac = (3*J2 * re*re) / (2 * r**4) multiplier = -mu*inertia.mass / r**3 xForce = x * multiplier * (1 - frac*(5*z*z - r*r)) yForce = y * multiplier * (1 - frac*(5*z*z - r*r)) zForce = z * multiplier * (1 - frac*(5*z*z - 3*r*r)) return Vector(xForce, yForce, zForce)
Ancestors
- SphericalEarth
- MAPLEAF.ENV.EarthModelling.EarthModel
- abc.ABC
Class variables
var a
var b
var e2
var eP
var eP2
var f
Methods
def cartesianToGeodetic(self, x, y, z, timeOffset=0)
-
Inputs in m, sec Convert from earth-centered inertial coordinates to WGS84 geodetic coordinates Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates
At timeOffset=0, the inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame
Returns latitude, longitude, height in degrees, m
Expand source code
def cartesianToGeodetic(self, x, y, z, timeOffset=0): ''' Inputs in m, sec Convert from earth-centered inertial coordinates to WGS84 geodetic coordinates Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates At timeOffset=0, the inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame Returns latitude, longitude, height in degrees, m ''' lon = atan2(y, x) # Longitude is easy to find # Adjust longitude for earth's rotation lon -= self.rotationRate * timeOffset ### Compute latitude and height normal to surface w/ Newton-Raphson method ### p = sqrt(x*x + y*y) Kold = 1/(1 - self.e2) Korig = Kold while True: ci = ((p*p + (1-self.e2)*z*z * Kold**2)**(1.5)) / (self.a * self.e2) Knew = 1 + (p*p + (1 - self.e2)*z*z * Kold**3) / (ci - p*p) # End iterations once delta per iteration < 1e-10 if abs(Knew - Kold) < 1e-10: break Kold = Knew k = Knew lat = atan2(k*z, p) h = (1/self.e2)*(1/k - 1/Korig)*sqrt(p*p + z*z*k*k) return degrees(lat), degrees(lon), h # Re-use conversions to/from spherical coordinates from SphericalEarth # Useful for Spherical Harmonic Gravity model calculations, which are performed in spherical coordinates
def cartesianToSpherical(self, x, y, z, timeOffset=0)
-
Convert from cartesian to lat/lon/height coordinates, where height is ASL
Expand source code
def cartesianToGeodetic(self, x, y, z, timeOffset=0): ''' Convert from cartesian to lat/lon/height coordinates, where height is ASL ''' p = sqrt(x*x + y*y) lon = atan2(y, x) # Adjust for rotation rate lon -= self.rotationRate * timeOffset lat = atan2(z, p) height = sqrt(x*x + y*y + z*z) - self.radius return degrees(lat), degrees(lon), height
def geodeticToCartesian(self, lat, lon, height, timeOffset=0)
-
Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere.
Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system
Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates
Note: In other references, it is common to set \phi = lat, \lamda = lon
Inputs
lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL - m timeOffset (float) Offset added to results based on earth's rotation rate. At timeOffset=0, this inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame
Returns
x
-
(float) meters, relative to center of ellipsoid
y
-
' '
z
-
' '
Expand source code
def geodeticToCartesian(self, lat, lon, height, timeOffset=0): ''' Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere. Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system Method adapted from: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates Note: In other references, it is common to set \phi = lat, \lamda = lon Inputs: lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL - m timeOffset (float) Offset added to results based on earth's rotation rate. At timeOffset=0, this inertial frame is aligned with the standard earth-centered-earth-fixed (ECEF) frame Returns: x: (float) meters, relative to center of ellipsoid y: ' ' z: ' ' ''' lat = radians(lat) lon = radians(lon) # Adjust for earth's rate of rotation lon += self.rotationRate * timeOffset N = self.a / sqrt(1 - self.e2*sin(lat)**2) x = (N + height)*cos(lat)*cos(lon) y = (N + height)*cos(lat)*sin(lon) z = ((self.b**2 / self.a**2)*N + height) * sin(lat) return x, y, z
def getGravityForce(self, inertia, state)
-
Get gravity using the J2 model (just a single spherical harmonic coefficient accounting for the earth's oblateness) Method from NESC-RP-12-00770, Volume II, pg.51, Eqn 29-31
Expand source code
def getGravityForce(self, inertia, state): ''' Get gravity using the J2 model (just a single spherical harmonic coefficient accounting for the earth's oblateness) Method from NESC-RP-12-00770, Volume II, pg.51, Eqn 29-31 ''' # J2 Model just uses the first spherical harmonic coefficient J2 = 0.00108262982 re = self.a # Equatorial radius (Semi-major axis of ellipsoid) x, y, z = state.position[0], state.position[1], state.position[2] r = state.position.length() mu = self.GM # Precompute common parts of equation frac = (3*J2 * re*re) / (2 * r**4) multiplier = -mu*inertia.mass / r**3 xForce = x * multiplier * (1 - frac*(5*z*z - r*r)) yForce = y * multiplier * (1 - frac*(5*z*z - r*r)) zForce = z * multiplier * (1 - frac*(5*z*z - 3*r*r)) return Vector(xForce, yForce, zForce)
def sphericalToCartesian(self, lat, lon, height, timeOffset=0)
-
Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere.
Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system
Inputs
lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL (usually m)
Returns
x
-
(float) meters, relative to center of sphere
y
-
' '
z
-
' '
Expand source code
def geodeticToCartesian(self, lat, lon, height, timeOffset=0): ''' Converts from geodetic coordinates to cartesian coordinates, assuming the surface is a sphere. Resulting Z axis goes through north pole, X axis goes through prime meridian at the equator, and the Y axis is perpendicular to both in such a way as to form a right-handed coordinate system Inputs: lat: (float) latitude - degrees lon: (float) longitude - degrees height: (float) ASL (usually m) Returns: x: (float) meters, relative to center of sphere y: ' ' z: ' ' ''' lat = radians(lat) lon = radians(lon) # Adjust for rotation rate lon += self.rotationRate * timeOffset radius = self.radius + height x = radius * cos(lat) * cos(lon) y = radius * cos(lat) * sin(lon) z = radius * sin(lat) return x, y, z
Inherited members