diff --git a/edosl0util/__init__.py b/edosl0util/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3d096a1e02125cd6a672db736d777dd9eeccd041 100644 --- a/edosl0util/__init__.py +++ b/edosl0util/__init__.py @@ -0,0 +1,3 @@ +from pkg_resources import get_distribution + +__version__ = get_distribution(__name__).version \ No newline at end of file diff --git a/edosl0util/jpssrdr.py b/edosl0util/jpssrdr.py index beef1e2f2bd22c6106d990423725a4946cf52a92..4062c741a1b9048fa709acbb6bb8fac8b8148b36 100644 --- a/edosl0util/jpssrdr.py +++ b/edosl0util/jpssrdr.py @@ -192,7 +192,7 @@ def _rdrs_for_packet_dataset(group): if group: for name, buf in _generate_packet_datasets(group): try: - rdr = _decode_rdr_blob(buf) + rdr = decode_rdr_blob(buf) except ValueError as e: LOG.warn('{} ({})'.format(e.message, name)) continue @@ -200,7 +200,7 @@ def _rdrs_for_packet_dataset(group): return rdrs -def _decode_rdr_blob(buf): +def decode_rdr_blob(buf): if buf.shape[0] < c.sizeof(StaticHeader): raise ValueError('Not enough bytes for static header') header = StaticHeader.from_buffer(buf) diff --git a/edosl0util/rdrgen.py b/edosl0util/rdrgen.py index 73ac856be35ec8018f59ee9ac574b85535337a17..420699039fe6834319bc42564de93f49d57a743e 100644 --- a/edosl0util/rdrgen.py +++ b/edosl0util/rdrgen.py @@ -1,27 +1,114 @@ import ctypes +import itertools from collections import OrderedDict +from datetime import datetime import attr -from astropy.time import Time, TimeDelta +import h5py import numpy as np +from astropy.time import Time, TimeDelta -from edosl0util.jpssrdr import StaticHeader, Apid as ApidListItem, PacketTracker +import edosl0util +from edosl0util.jpssrdr import ( + StaticHeader, Apid as ApidListItem, PacketTracker, decode_rdr_blob) -def prototype(): - pds_file = 'P1571289CRISSCIENCEA6T17230135400001.PDS' - satellite = 'snpp' - rdr_type = 'CRIS-SCIENCE-RDR' - granule_iet = 1881755831079000 +def write_rdr(sat, blob, + distributor=None, origin=None, domain=None, orbit_number=0, + creation_time=None, gran_creation_time=None, software_ver=None): + # TODO: write out the user block too?? + distributor = distributor or origin or default_origin + origin = origin or default_origin + domain = domain or default_domain + creation_time = creation_time or datetime.now() + gran_creation_time = gran_creation_time or creation_time + software_ver = (software_ver + or edosl0util.__name__ + '-' + edosl0util.__version__) + blob_info = decode_rdr_blob(blob) + rdr_type = SpacecraftDiaryRdrType() # TODO: select via APID + gran_iet = blob_info.header.start_boundary + gran_end_iet = blob_info.header.end_boundary + gran_time = iet_to_datetime(gran_iet) + gran_end_time = iet_to_datetime(gran_end_iet) + gran_id = make_granule_id(sat, gran_iet) + gran_ver = 'A1' + format_date = lambda t: t.strftime('%Y%m%d') + format_time = lambda t: t.strftime('%H%M%S.%fZ') + with h5py.File('rdr.h5', 'w') as h5_file: + set_h5_attrs(h5_file, { + 'Distributor': distributor, + 'Mission_Name': 'S-NPP/JPSS', + 'N_Dataset_Source': origin, + 'N_HDF_Creation_Date': format_date(creation_time), + 'N_HDF_Creation_Time': format_time(creation_time), + 'Platform_Short_Name': platform_short_names[sat]}) + all_grp = h5_file.create_group('All_Data') + raw_grp = all_grp.create_group(rdr_type.short_name + '_All') + raw_ds = raw_grp.create_dataset( + 'RawApplicationPackets_0', data=blob, maxshape=[None]) + prod_grp = h5_file.create_group('Data_Products') + gran_grp = prod_grp.create_group(rdr_type.short_name) + set_h5_attrs(gran_grp, { + 'Instrument_Short_Name': instrument_short_names[rdr_type.sensor], + 'N_Collection_Short_Name': rdr_type.short_name, + 'N_Dataset_Type_Tag': 'RDR', + 'N_Processing_Domain': domain}) + gran_ds = gran_grp.create_dataset( + rdr_type.short_name + '_Gran_0', [1], h5_regionref_dtype, maxshape=[None]) + gran_ds[0] = raw_ds.regionref[:] + set_h5_attrs(gran_ds, { + 'Beginning_Date': format_date(gran_time), + 'Beginning_Time': format_time(gran_time), + 'Ending_Date': format_date(gran_end_time), + 'Ending_Time': format_time(gran_end_time), + 'N_Beginning_Orbit_Number': np.uint64(orbit_number), # FIXME: detect? + 'N_Beginning_Time_IET': np.uint64(gran_iet), + 'N_Creation_Date': format_date(gran_creation_time), + 'N_Creation_Time': format_time(gran_creation_time), + 'N_Ending_Time_IET': np.uint64(gran_end_iet), + 'N_Granule_ID': gran_id, + 'N_Granule_Status': 'N/A', + 'N_Granule_Version': gran_ver, + 'N_IDPS_Mode': domain, + 'N_JPSS_Document_Ref': rdr_type.document, + 'N_LEOA_Flag': 'Off', + 'N_Packet_Type': [a.name for a in blob_info.apids], + 'N_Packet_Type_Count': [np.uint64(a.pkts_received) for a in blob_info.apids], + 'N_Percent_Missing_Data': np.float32(calc_percent_missing(blob_info)), + 'N_Primary_Label': 'Primary', # TODO: find out what this is + 'N_Reference_ID': ':'.join([rdr_type.short_name, gran_id, gran_ver]), + 'N_Software_Version': software_ver}) + aggr_ds = gran_grp.create_dataset( + rdr_type.short_name + '_Aggr', [1], h5_ref_dtype, maxshape=[None]) + aggr_ds[0] = raw_grp.ref + set_h5_attrs(aggr_ds, { + 'AggregateBeginningDate': format_date(gran_time), + 'AggregateBeginningGranuleID': gran_id, + 'AggregateBeginningOrbitNumber': np.uint64(orbit_number), + 'AggregateBeginningTime': format_time(gran_time), + 'AggregateEndingDate': format_date(gran_end_time), + 'AggregateEndingGranuleID': gran_id, + 'AggregateEndingOrbitNumber': np.uint64(orbit_number), + 'AggregateEndingTime': format_time(gran_end_time), + 'AggregateNumberGranules': np.uint64(1)}) -class RdrBlobBuilder(object): - def __init__(self, rdr_type, sat, granule_iet): - self.sat = sat +def set_h5_attrs(h5_obj, attrs): + # for some reason all the H5 attributes are in 2-D arrays in IDPS files + for name, value in attrs.items(): + if isinstance(value, list): + value = [[x] for x in value] + else: + value = [[value]] + h5_obj.attrs[name] = value -def build_rdr_blob(pkt_stream, rdr_type, sat, granule_iet): - granule_iet_end = granule_iet + rdr_type.granularity +def build_rdr_blob(sat, pkt_stream): + first_pkt = next(pkt_stream) # FIXME: what if there are no packets? + rdr_type = SpacecraftDiaryRdrType() # TODO: select based on first packet APID + granule_iet = calc_iet_granule(satellite_base_times[sat], rdr_type.gran_len, + timecode_to_iet(first_pkt.secondary_header.timecode)) + granule_iet_end = granule_iet + rdr_type.gran_len total_pkt_size = 0 apid_info = OrderedDict() @@ -35,12 +122,13 @@ def build_rdr_blob(pkt_stream, rdr_type, sat, granule_iet): 'pkt_info': [{} for _ in range(apid.max_expected)]} total_trackers += apid.max_expected - for pkt in pkt_stream: + for pkt in itertools.chain([first_pkt], pkt_stream): if pkt.apid not in apid_info: - continue + raise ValueError( + 'APID {} not expected for {}'.format(pkt.apid, rdr_type.short_name)) pkt_iet = timecode_to_iet(pkt.secondary_header.timecode) if not granule_iet <= pkt_iet < granule_iet_end: - continue + raise ValueError('packet stream crosses granule boundary') info = apid_info[pkt.apid] pkt_info = info['pkt_info'][info['pkts_received']] pkt_info['obs_time'] = pkt_iet @@ -58,8 +146,8 @@ def build_rdr_blob(pkt_stream, rdr_type, sat, granule_iet): buf = np.zeros([buf_size], np.uint8) # zeros needed to null-pad strings header = StaticHeader.from_buffer(buf) - header.satellite = sat # XXX: mapping? - header.sensor = rdr_type.sensor + header.satellite = platform_short_names[sat] + header.sensor = instrument_short_names[rdr_type.sensor] header.type_id = rdr_type.type_id header.num_apids = len(apid_info) header.apid_list_offset = apid_list_offset @@ -140,14 +228,67 @@ class ApidSpec(object): class CrisScienceRdrType(object): short_name = 'CRIS-SCIENCE-RDR' - granularity = 31997000 - apids = get_cris_science_apids() - sensor = 'CrIS' + gran_len = 31997000 + sensor = 'cris' type_id = 'SCIENCE' + document = '474-00448-02-03_JPSS-DD-Vol-II-Part-3_0200B.pdf' + apids = get_cris_science_apids() + + +class SpacecraftDiaryRdrType(object): + short_name = 'SPACECRAFT-DIARY-RDR' + gran_len = 20000000 + sensor = None + type_id = 'DIARY' + document = '474-00448-02-08_JPSS-DD-Vol-II-Part-8_0200B.pdf' + apids = [ApidSpec(0, 'CRITICAL', max_expected=21), + ApidSpec(8, 'ADCS_HKH', max_expected=21), + ApidSpec(11, 'DIARY', max_expected=21)] def timecode_to_iet(timecode): # FIXME: move to timecode.py - ccsds_epoch = iet_epoch = Time('1958-01-01', scale='tai') + ccsds_epoch = Time('1958-01-01', scale='tai') day = Time(ccsds_epoch.jd + timecode.days, scale='utc', format='jd') - iet_epoch return int(day.sec * 1e6 + timecode.milliseconds * 1e3 + timecode.microseconds) + + +def iet_to_datetime(iet): + return (iet_epoch + TimeDelta(iet * 1e-6, format='sec')).utc.datetime + + +def calc_iet_granule(base_time, gran_len, iet): + return (iet - base_time) // gran_len * gran_len + base_time + + +def calc_iet_aggregate(base_time, gran_len, grans_per_aggr, iet): + # see "DDS Aggregation Methodology" in CDFCB vol I + Y = gran_len + N = grans_per_aggr + G_s = calc_iet_granule(base_time, Y, iet) + A_n = G_s // (N * Y) + O = G_s % Y + A_s = A_n * (Y * N) + O + return A_s + + +def make_granule_id(sat, gran_iet): + tenths = (gran_iet - satellite_base_times[sat]) // 100000 + return '{}{:012d}'.format(platform_short_names[sat], tenths) + + +def calc_percent_missing(common_rdr): + total_received = sum(a.pkts_received for a in common_rdr.apids) + total_reserved = sum(a.pkts_reserved for a in common_rdr.apids) + return 100. * (total_reserved - total_received) / total_reserved + + +iet_epoch = Time('1958-01-01', scale='tai') +satellite_base_times = {'snpp': 1698019234000000} +platform_short_names = {'snpp': 'NPP'} +instrument_short_names = {'cris': 'CrIS', None: 'SPACECRAFT'} +default_origin = 'ssec' +default_domain = 'dev' + +h5_ref_dtype = h5py.special_dtype(ref=h5py.Reference) +h5_regionref_dtype = h5py.special_dtype(ref=h5py.RegionReference)