Module ephemeris_library.common

Expand source code
import ephemeris_library.database as database

from typing import Union
import datetime
import warnings
import numpy
import sys
from numpy.lib import recfunctions

OVERRIDE_VERSION_CHECK = False

MAJOR_VERSION = '0'
MINOR_VERSION = 'a'
VERSION = '.'.join([MAJOR_VERSION, MINOR_VERSION])

CASSIOPE_LAUNCH_MET = 1431216000
EARTH_RADIUS_KM = 6371.2

# Attitude quality and integer lookups:
QUALITY_FLAG_INT        = [0,1,2,3,4]
QUALITY_FLAG_STR_SHORT  = ['dropout', 'rough', 'coarse', 'moderate', 'fine']
QUALITY_FLAG_STR_DETAIL = ["No Solution", "Coarse Sun Sensors", "Coarsely-splined star sensors",
                           "Moderately-splined star sensors", "Finely-splined star sensors"]

# Attitude source value from database to string conversion
ATTITUDE_DATA_SOURCE_STRING = {0: "No Data", 1: "MGF+CSS No Reference", 2: "MGF+CSS", 3: "Onboard SSA+SSB", 4: "SSA", 5: "SSB", 6: "SSA+SSB"}


def add_utc_column( data: numpy.array, met_column_name: str = 'met', utc_column_name: str = 'utc'):
  '''
  Add UTC column to numpy structured array.

  Arguments:
    met_column_name: Source column of met times to convert to UTC
    data: Structured array to add column name to.
    utc_column_name: Optional. Defaults to 'utc'. Name of new UTC column.

  Return:
    Updated data structure with new colunn appended at the end.
  '''
  if not met_column_name in data.dtype.names:
    raise ValueError(met_column_name + " not found for conversion to UTC")

  return recfunctions.append_fields(data,
                                    utc_column_name,
                                    [met_to_utc(met).isoformat() for met in data[met_column_name]],
                                    dtypes=['datetime64[ms]'], usemask=False)

def add_string_column( data: numpy.array, index_column_name: str, output_column_name: str, index_lookup_table: dict ):
  '''
  Add string-reference data to the numpy structured array.

  Arguments:
    data: Structured array that is to be adjusted. Must contain the column given in the 'index_column_name' var.
    input_column_name: The name of the column that is to be used as the index-refernece.
    output_column_name: The name of the column to be added to the structured array.
    index_lookup_table: The list/dictionary as to translate the integer-indecies to strings.

  Return:
    Updated data structure with new colunn appended at the end.
  '''  
  if index_column_name not in data.dtype.names:
    raise ValueError("Could not find '{:s}' within the given structured array!".format( index_column_name ))
  
  # For sanity, do things linearly. A full day (86400 @ 1hz) on a string-lookup takes milliseconds.
  output_strings = [ index_lookup_table[Index] for Index in data[ index_column_name ]]
  
  # Remove the previous instantiation of the output_column_name if needed
  if output_column_name in data.dtype.names:
    data = data[[ column for column in data.dtype.names if column != output_column_name ]]
  # And then add it (back) in!
  data = recfunctions.append_fields( data, output_column_name, output_strings, dtypes=['U32'], usemask=False)

  # And then make sure to return a nominal copy, rather than a view.
  if data.base is not None:
    data = data.copy()

  return data


def _verify_time(time_value: Union[datetime.datetime, str, int, float]) -> float:
  '''
  Verify time value.

  Verify that time is after CASSIOPE start of mission.
  Convert to MET.

  Argument:
    time_value: Time to verify
      If input is ISO formatted datetime string: convert to met.
      If input is datetime: convert to met.
      If input is int or float: Assume met time.

  Return:
    float representing met time
    Raises TypeError on inability to translate,
    Raises ValueError if time was prior to CASSIOPE launch.
  '''

  met = None
  if isinstance(time_value, datetime.datetime):
    met = utc_to_met(time_value)

  elif isinstance(time_value, int):
    met = float(time_value)

  elif isinstance(time_value, float):
    met = float(time_value)

  elif isinstance(time_value, str):
    try:
      met = float(time_value)
    except:
      pass

    if met is None:
      try:
        utc = datetime.datetime.fromisoformat(time_value)
        met = utc_to_met(utc)
      except:
        pass

  if met is None:
    raise TypeError('Could not convert: ', time_value, ' to MET. (type: ', type(time_value), ')')

  if met < CASSIOPE_LAUNCH_MET:
    raise ValueError('MET time: ', time_value, ' prior to CASSIOPE launch.')

  return met

def _verify_quality( quality_value: Union[str, int] ) -> int:
  """
  Given an integer or string that matches to any within the above QUALITY_FLAG_* local variables,
  Return the relevant index that it maps to.
  If given None, returns 0.
  
  Args:
    quality_value: int or string.
  Returns:
    int: Quality integer that maps to the given Quality-flag.    
  """
  # If it's not a string, then cast to integer.
  # Type-safe for all reasonable python & numpy types.
  if quality_value is None:
    return 0
    
  elif not isinstance( quality_value, str ):
    try:
      quality_value = int( quality_value )
      return quality_value
    except Exception as E:
      pass # Exception thrown below. Also catches string-parsing errors.
      
  else: # Is a string:
    for i in range( len( QUALITY_FLAG_INT )):
      if quality_value.lower() == QUALITY_FLAG_STR_SHORT[i].lower():
        return i
      elif quality_value.lower() == QUALITY_FLAG_STR_DETAIL[i].lower():
        return i
  
  # Could not find a lookup.
  raise ValueError("Could not interpret "+quality_value+" given to as a minimum quality! Must be an integer or within the common.QUALITY_FLAG_STR_* vars!") from E


def get_version():
  '''
  Returns a string of the current major/minor revision.
  Pulls from ephemeris_library.common.VERSION
  '''
  return VERSION

def _check_version():
  '''
  Verify that the major version of the ephemeris library matches the major revision
  Provide information if there is a minor version mismatch.
  
  Can override this automatic check by setting ephemeris_library.common.OVERRIDE_VERSION_CHECK to True.
  '''
  if OVERRIDE_VERSION_CHECK:
    print('Version check disabled.')
    return

  # connect to database and fetch data
  statement = ('SELECT `version` FROM `cassiope_ephemeris`.`library_version`')

  connection = database._connect_to_database()
  cursor = connection.cursor()
  cursor.execute(statement)
  data = cursor.fetchall()
  cursor.close()
  connection.close()

  db_major_version, db_minor_version = data[0][0].split('.')
  if db_major_version != MAJOR_VERSION:
    raise ImportError('Major version change for ephemeris_library. Update to latest version: v.' + str(data[0][0]))

  if db_minor_version != MINOR_VERSION:
    warnings.warn('A minor update is available for ephemeris_library. v.' + str(data[0][0]))


def get_wgs84_earth_radius_meters(x: float, y: float, z: float) -> float:
  '''
    Given geographic cartesian coordinates, calculate the Earth's radius (WGS84) at the surface above/below that point.
    This is used to convert spacecraft altitude to altitude above the earth.
    Valid with either singleton values or np.arrays of values.
    If given np.arrays of floats, returns an np.array of floats.
    
    Note: Accuracy is best given ITRF/GEO data, though due to the small shift of the Z-Axis between
      the frames, giving it ICRF/J2K data is also sufficient.
    
    Args:
      x, y, z: Cartesion earth centered coordintes for which we calculate the Earth's Radius at point.
               
    Returns:
      Earth's Radius along the given cartesian vector, in meters.
    '''
  # Elipsoid:    x^2/a^2 + y^2/b^2 = 1
  # Flatten:     x^2*b^2 + y^2*a^2 = (ab)^2
  # Substitute:  x= r*cos(theta), y= r*sin(theta)
  #   r^2*b^2*cos(theta)^2 + r^2*a^2*sin(theta)^2 = (a*b)^2
  # Solve for r:
  #   r = a*b / sqrt( b^2*cos(theta)^2 + a^2*sin(theta)^2 )

  a = 6378137.0
  b = 6356752.314245

  theta = numpy.arctan(z / (x**2 + y**2))  # = Geocentric Latittude

  WGS84_earth_radius = a * b / (b**2 * numpy.cos(theta)**2 + a**2 * numpy.sin(theta)**2)**0.5

  return WGS84_earth_radius


def utc2met(date_time: Union[datetime.datetime, str]) -> float:
  '''
  **Deprecated**
    Use 'utc_to_met()' instead. 

  Convert a given timestamp to CASSIOPE MET. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    date_time: Either a datetime instance or an ISO datetime string. 
      Note: Strings are read using datetime.fromisoformat()

  Return:
    Floating point MET value.
  '''

  import warnings
  warnings.warn('Deprecated. Use uct_to_met().', DeprecationWarning)
  return utc_to_met(date_time)


def utc_to_met(date_time: Union[datetime.datetime, str]) -> float:
  '''
  Convert a given timestamp to CASSIOPE MET. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    date_time: Either a datetime instance or an ISO datetime string. 
      Note: Strings are read using datetime.fromisoformat()

  Return:
    Floating point MET value.
    None if string date_time is not an ISO string or datetime object
  '''
  try:
    if isinstance(date_time, str):
      utc = datetime.datetime.fromisoformat(date_time)

    elif isinstance(date_time, datetime.datetime):
      utc = date_time.replace(tzinfo=None)

    else:
      raise TypeError('ERROR: Expected date_time to be a datetime object or ISO date format string')

  except ValueError:
    print('ERROR: Could not interpret date_time string as an ISO date format string', file=sys.stderr)
    raise

  met = (utc - datetime.datetime(year=1968, month=5, day=24, hour=0, minute=0, second=0)).total_seconds()
  return met


def met2utc(met: float) -> datetime.datetime:
  '''
  **Deprecated**
    Use 'met_to_utc()' instead. 
  Convert a given CASSIOPE MET to datetime. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    met: Floating point number representing MET. 

  Return:
    datetime object representing datetime at MET.
  '''
  import warnings
  warnings.warn('Deprecated. Use met_to_utc().', DeprecationWarning)
  return met_to_utc(met)


def met_to_utc(met: float) -> datetime.datetime:
  '''
  Convert a given CASSIOPE MET to datetime. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    met: Floating point number representing MET. 

  Return:
    datetime object representing datetime at MET.
  '''
  return(datetime.datetime.strptime("1968-05-24 00:00:00", '%Y-%m-%d %H:%M:%S') + datetime.timedelta(seconds=met))

Functions

def add_string_column(data: , index_column_name: str, output_column_name: str, index_lookup_table: dict)

Add string-reference data to the numpy structured array.

Arguments

data: Structured array that is to be adjusted. Must contain the column given in the 'index_column_name' var. input_column_name: The name of the column that is to be used as the index-refernece. output_column_name: The name of the column to be added to the structured array. index_lookup_table: The list/dictionary as to translate the integer-indecies to strings.

Return

Updated data structure with new colunn appended at the end.

Expand source code
def add_string_column( data: numpy.array, index_column_name: str, output_column_name: str, index_lookup_table: dict ):
  '''
  Add string-reference data to the numpy structured array.

  Arguments:
    data: Structured array that is to be adjusted. Must contain the column given in the 'index_column_name' var.
    input_column_name: The name of the column that is to be used as the index-refernece.
    output_column_name: The name of the column to be added to the structured array.
    index_lookup_table: The list/dictionary as to translate the integer-indecies to strings.

  Return:
    Updated data structure with new colunn appended at the end.
  '''  
  if index_column_name not in data.dtype.names:
    raise ValueError("Could not find '{:s}' within the given structured array!".format( index_column_name ))
  
  # For sanity, do things linearly. A full day (86400 @ 1hz) on a string-lookup takes milliseconds.
  output_strings = [ index_lookup_table[Index] for Index in data[ index_column_name ]]
  
  # Remove the previous instantiation of the output_column_name if needed
  if output_column_name in data.dtype.names:
    data = data[[ column for column in data.dtype.names if column != output_column_name ]]
  # And then add it (back) in!
  data = recfunctions.append_fields( data, output_column_name, output_strings, dtypes=['U32'], usemask=False)

  # And then make sure to return a nominal copy, rather than a view.
  if data.base is not None:
    data = data.copy()

  return data
def add_utc_column(data: , met_column_name: str = 'met', utc_column_name: str = 'utc')

Add UTC column to numpy structured array.

Arguments

met_column_name: Source column of met times to convert to UTC data: Structured array to add column name to. utc_column_name: Optional. Defaults to 'utc'. Name of new UTC column.

Return

Updated data structure with new colunn appended at the end.

Expand source code
def add_utc_column( data: numpy.array, met_column_name: str = 'met', utc_column_name: str = 'utc'):
  '''
  Add UTC column to numpy structured array.

  Arguments:
    met_column_name: Source column of met times to convert to UTC
    data: Structured array to add column name to.
    utc_column_name: Optional. Defaults to 'utc'. Name of new UTC column.

  Return:
    Updated data structure with new colunn appended at the end.
  '''
  if not met_column_name in data.dtype.names:
    raise ValueError(met_column_name + " not found for conversion to UTC")

  return recfunctions.append_fields(data,
                                    utc_column_name,
                                    [met_to_utc(met).isoformat() for met in data[met_column_name]],
                                    dtypes=['datetime64[ms]'], usemask=False)
def get_version()

Returns a string of the current major/minor revision. Pulls from ephemeris_library.common.VERSION

Expand source code
def get_version():
  '''
  Returns a string of the current major/minor revision.
  Pulls from ephemeris_library.common.VERSION
  '''
  return VERSION
def get_wgs84_earth_radius_meters(x: float, y: float, z: float) ‑> float

Given geographic cartesian coordinates, calculate the Earth's radius (WGS84) at the surface above/below that point. This is used to convert spacecraft altitude to altitude above the earth. Valid with either singleton values or np.arrays of values. If given np.arrays of floats, returns an np.array of floats.

Note: Accuracy is best given ITRF/GEO data, though due to the small shift of the Z-Axis between the frames, giving it ICRF/J2K data is also sufficient.

Args

x, y, z: Cartesion earth centered coordintes for which we calculate the Earth's Radius at point.

Returns

Earth's Radius along the given cartesian vector, in meters.

Expand source code
def get_wgs84_earth_radius_meters(x: float, y: float, z: float) -> float:
  '''
    Given geographic cartesian coordinates, calculate the Earth's radius (WGS84) at the surface above/below that point.
    This is used to convert spacecraft altitude to altitude above the earth.
    Valid with either singleton values or np.arrays of values.
    If given np.arrays of floats, returns an np.array of floats.
    
    Note: Accuracy is best given ITRF/GEO data, though due to the small shift of the Z-Axis between
      the frames, giving it ICRF/J2K data is also sufficient.
    
    Args:
      x, y, z: Cartesion earth centered coordintes for which we calculate the Earth's Radius at point.
               
    Returns:
      Earth's Radius along the given cartesian vector, in meters.
    '''
  # Elipsoid:    x^2/a^2 + y^2/b^2 = 1
  # Flatten:     x^2*b^2 + y^2*a^2 = (ab)^2
  # Substitute:  x= r*cos(theta), y= r*sin(theta)
  #   r^2*b^2*cos(theta)^2 + r^2*a^2*sin(theta)^2 = (a*b)^2
  # Solve for r:
  #   r = a*b / sqrt( b^2*cos(theta)^2 + a^2*sin(theta)^2 )

  a = 6378137.0
  b = 6356752.314245

  theta = numpy.arctan(z / (x**2 + y**2))  # = Geocentric Latittude

  WGS84_earth_radius = a * b / (b**2 * numpy.cos(theta)**2 + a**2 * numpy.sin(theta)**2)**0.5

  return WGS84_earth_radius
def met2utc(met: float) ‑> datetime.datetime

Deprecated Use 'met_to_utc()' instead. Convert a given CASSIOPE MET to datetime. (CASSIOPE Epoch is 1968-05-24:00:00:00)

Arguments

met: Floating point number representing MET.

Return

datetime object representing datetime at MET.

Expand source code
def met2utc(met: float) -> datetime.datetime:
  '''
  **Deprecated**
    Use 'met_to_utc()' instead. 
  Convert a given CASSIOPE MET to datetime. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    met: Floating point number representing MET. 

  Return:
    datetime object representing datetime at MET.
  '''
  import warnings
  warnings.warn('Deprecated. Use met_to_utc().', DeprecationWarning)
  return met_to_utc(met)
def met_to_utc(met: float) ‑> datetime.datetime

Convert a given CASSIOPE MET to datetime. (CASSIOPE Epoch is 1968-05-24:00:00:00)

Arguments

met: Floating point number representing MET.

Return

datetime object representing datetime at MET.

Expand source code
def met_to_utc(met: float) -> datetime.datetime:
  '''
  Convert a given CASSIOPE MET to datetime. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    met: Floating point number representing MET. 

  Return:
    datetime object representing datetime at MET.
  '''
  return(datetime.datetime.strptime("1968-05-24 00:00:00", '%Y-%m-%d %H:%M:%S') + datetime.timedelta(seconds=met))
def utc2met(date_time: Union[datetime.datetime, str]) ‑> float

Deprecated Use 'utc_to_met()' instead.

Convert a given timestamp to CASSIOPE MET. (CASSIOPE Epoch is 1968-05-24:00:00:00)

Arguments

date_time: Either a datetime instance or an ISO datetime string. Note: Strings are read using datetime.fromisoformat()

Return

Floating point MET value.

Expand source code
def utc2met(date_time: Union[datetime.datetime, str]) -> float:
  '''
  **Deprecated**
    Use 'utc_to_met()' instead. 

  Convert a given timestamp to CASSIOPE MET. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    date_time: Either a datetime instance or an ISO datetime string. 
      Note: Strings are read using datetime.fromisoformat()

  Return:
    Floating point MET value.
  '''

  import warnings
  warnings.warn('Deprecated. Use uct_to_met().', DeprecationWarning)
  return utc_to_met(date_time)
def utc_to_met(date_time: Union[datetime.datetime, str]) ‑> float

Convert a given timestamp to CASSIOPE MET. (CASSIOPE Epoch is 1968-05-24:00:00:00)

Arguments

date_time: Either a datetime instance or an ISO datetime string. Note: Strings are read using datetime.fromisoformat()

Return

Floating point MET value. None if string date_time is not an ISO string or datetime object

Expand source code
def utc_to_met(date_time: Union[datetime.datetime, str]) -> float:
  '''
  Convert a given timestamp to CASSIOPE MET. (CASSIOPE Epoch is 1968-05-24:00:00:00)

  Arguments:
    date_time: Either a datetime instance or an ISO datetime string. 
      Note: Strings are read using datetime.fromisoformat()

  Return:
    Floating point MET value.
    None if string date_time is not an ISO string or datetime object
  '''
  try:
    if isinstance(date_time, str):
      utc = datetime.datetime.fromisoformat(date_time)

    elif isinstance(date_time, datetime.datetime):
      utc = date_time.replace(tzinfo=None)

    else:
      raise TypeError('ERROR: Expected date_time to be a datetime object or ISO date format string')

  except ValueError:
    print('ERROR: Could not interpret date_time string as an ISO date format string', file=sys.stderr)
    raise

  met = (utc - datetime.datetime(year=1968, month=5, day=24, hour=0, minute=0, second=0)).total_seconds()
  return met