Skip to content

Commit

Permalink
Merge pull request #3 from ANDREWNGT/Add_time_utils
Browse files Browse the repository at this point in the history
Add time utils
  • Loading branch information
Sceki authored Apr 4, 2022
2 parents 1fbdf54 + 5e060d4 commit 3e062a5
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 2 deletions.
7 changes: 5 additions & 2 deletions kessler/cdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import datetime
import copy
import pandas as pd

from . import util


Expand Down Expand Up @@ -191,11 +190,15 @@ def __eq__(self, other):
if isinstance(other, ConjunctionDataMessage):
return hash(self) == hash(other)
return False

def set_header(self, key, value):
if key in self._keys_header:
if key in self._keys_with_dates:
# We have a field with a date string as the value. Check if the string is in the format needed by the CCSDS 508.0-B-1 standard
time_format = util.get_ccsds_time_format(value)
idx = time_format.find('DDD')
if idx!=-1:
value = util.doy_2_date(value, value[idx:idx+3], value[0:4], idx)
try:
_ = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
except Exception as e:
Expand Down
90 changes: 90 additions & 0 deletions kessler/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,93 @@ def progress_bar_end(message=None):
print()
if message is not None:
print(message)

def get_ccsds_time_format(time_string):
'''
Adapted by Andrew Ng, 18/3/2022.
Original MATLAB source code found at: https://github.com/nasa/CARA_Analysis_Tools/blob/master/two-dimension_Pc/Main/TransformationCode/TimeTransformations/getCcsdsTimeFormat.m
get_ccsds_time_format - process and outputs the format of the time string extracted from the CDM.
The CCSDS time format is required to be of the general form
yyyy-[mm-dd|ddd]THH:MM:SS[.F*][Z]
(1) The date and time fields are separated by a "T".
(2) The date field has a four digit year followed by either a two digit
month and two digit day, or a three digit day-of-year.
(3) The year, month, day, and day-of-year fields are separated by a dash.
(4) The hours, minutes and seconds fields are each two digits separated
by colons.
(5) The fraction of seconds is optional and can have any number of
digits.
(6) If a fraction of seconds is provided, it is separated from the two
digit seconds by a period.
(7) The time string can end with an optional "Z" time zone indicator
Args:
- time_string(``str``): Original time string stored in CDM.
Returns:
- time_format(``str``): Outputs the format of the time string. It must be of the form yyyy-[mm-dd|ddd]THH:MM:SS[.F*][Z], otherwise it is invalid and a RuntimeError is raised.
'''
time_format = []
numT = time_string.count('T')
if numT == -1:
# Case when this is 'T' does not exist in time_string
raise RuntimeError(f"*** Error -- Invalid CCSDS time string: {time_string}\nNo 'T' separator found between date and time portions of the string")
elif numT > 1:
raise RuntimeError(f"*** Error -- Invalid CCSDS time string: {time_string} \nMore than one 'T' separator found between date and time portions of the string")
idx_T = time_string.find('T')
if idx_T ==10:
time_format = "yyyy-mm-ddTHH:MM:SS"
elif idx_T ==8:
time_format = "yyyy-DDDTHH:MM:SS"
else:
raise RuntimeError(f"*** Error -- Invalid CCSDS time string: {time_string} \nDate format not one of yyyy-mm-dd or yyyy-DDD.\n")
# % Check if 'Z' time zone indicator appended to the string
if time_string[-1]=='Z':
z_opt = True
else:
z_opt = False
# % Find location of the fraction of seconds decimal separator
num_decimal = time_string.count('.')
if num_decimal > 1:
#time_format = []
raise RuntimeError(f"*** Error -- Invalid CCSDS time string: {time_string}\nMore than one fraction of seconds decimal separator ('.') found.\n")
idx_decimal = time_string.find('.')
nfrac = 0
if num_decimal != 0:
if z_opt:
nfrac = len(time_string) - 1 - idx_decimal -1
else:
nfrac = len(time_string) - 1 - idx_decimal
if nfrac > 0:
frac_str = '.' + ('F'*nfrac)
else:
frac_str = ""
if z_opt:
frac_str = frac_str+'Z'
time_format = time_format + frac_str
return time_format

def doy_2_date(value, doy, year, idx):
'''
Written by Andrew Ng, 18/03/2022,
Based on source code @ https://github.com/nasa/CARA_Analysis_Tools/blob/master/two-dimension_Pc/Main/TransformationCode/TimeTransformations/DOY2Date.m
Use the datetime python package.
doy_2_date - Converts Day of Year (DOY) date format to date format.
Args:
- value(``str``): Original date time string with day of year format "YYYY-DDDTHH:MM:SS.ff"
- doy (``str``): The day of year in the DOY format.
- year (``str``): The year.
- idx (``int``): Index of the start of the original "value" string at which characters 'DDD' are found.
Returns:
-value (``str``): Transformed date in traditional date format. i.e.: "YYYY-mm-ddTHH:MM:SS.ff"
'''
# Calculate datetime format
date_num = datetime.datetime(int(year), 1, 1) + datetime.timedelta(int(doy) - 1)

# Split datetime object into a date list
date_vec = [date_num.year, date_num.month, date_num.day, date_num.hour, date_num.minute]
# Extract final date string. Use zfill() to pad year, month and day fields with zeroes if not filling up sufficient spaces.
value = str(date_vec[0]).zfill(4) +'-' + str(date_vec[1]).zfill(2) + '-' + str(date_vec[2]).zfill(2) + 'T' + value[idx+4:-1]
return value
44 changes: 44 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,47 @@ def test_from_cartesian_to_rtn_2(self):
state_rtn, _ = kessler.util.from_cartesian_to_rtn(state_xyz)
self.assertAlmostEqual(np.linalg.norm(state_xyz[0]), np.linalg.norm(state_rtn[0]), places=1)
self.assertAlmostEqual(np.linalg.norm(state_xyz[1]), np.linalg.norm(state_rtn[1]), places=1)

def test_get_ccsds_time_format(self):
# This test is written by Andrew Ng, 19/03/22. It makes use of example CDMs provided by the NASA CARA
# analysis repo at https://github.com/nasa/CARA_Analysis_Tools/tree/master/two-dimension_Pc/UnitTest/InputFiles.
test_case1 = "2000-01-01T00:00:00.000" #From AlfanoTestCase11.cdm
test_case2 = "2018-229T13:56:33.000" # From DensityDecorrelationTestCaseCDM.txt
test_case1_correct = "yyyy-mm-ddTHH:MM:SS.FFF"
test_case2_correct = "yyyy-DDDTHH:MM:SS.FFF"

self.assertEqual(kessler.util.get_ccsds_time_format(test_case1), test_case1_correct)
self.assertEqual(kessler.util.get_ccsds_time_format(test_case2), test_case2_correct)

def test_doy_2_date(self):
# This test is written by Andrew Ng, 19/03/22. It makes use of example CDMs provided by the NASA CARA
# analysis repo at https://github.com/nasa/CARA_Analysis_Tools/tree/master/two-dimension_Pc/UnitTest/InputFiles.
example1 = "2010-202T12:25:19.000" # From SingleCovTestCase1-4.cdm
example2 = "2018-229T13:56:33.000" # From DensityDecorrelationTestCaseCDM.txt
example3 = "2010-365T00:00:00.000" # Check that works at the final day of a non leap year
example4 = "2010-001T00:00:00.000" # Check that works at the first day of a year
example5 = "2012-366T00:00:00.000" # Check that works at the final day of a leap year

doy_1 = example1[5:5+3]
year_1= example1[0:4]
doy_2 = example2[5:5+3]
year_2= example2[0:4]
doy_3 = example3[5:5+3]
year_3= example3[0:4]
doy_4 = example4[5:5+3]
year_4= example4[0:4]
doy_5 = example5[5:5+3]
year_5= example5[0:4]

test_case1_correct = "2010-07-21T12:25:19.00"
test_case2_correct = "2018-08-17T13:56:33.00"
test_case3_correct = "2010-12-31T00:00:00.00"
test_case4_correct = "2010-01-01T00:00:00.00"
test_case5_correct = "2012-12-31T00:00:00.00"

self.assertEqual(kessler.util.doy_2_date(example1, doy_1, year_1, 5), test_case1_correct)
self.assertEqual(kessler.util.doy_2_date(example2, doy_2, year_2, 5), test_case2_correct)
self.assertEqual(kessler.util.doy_2_date(example3, doy_3, year_3, 5), test_case3_correct)
self.assertEqual(kessler.util.doy_2_date(example4, doy_4, year_4, 5), test_case4_correct)
self.assertEqual(kessler.util.doy_2_date(example5, doy_5, year_5, 5), test_case5_correct)

0 comments on commit 3e062a5

Please sign in to comment.