Source code for mica.archive.aca_hdr3

"""
Experimental/alpha code to work with ACA L0 Header 3 data
"""
import re
import numpy as np
import numpy.ma as ma
import collections
from scipy.interpolate import interp1d
from Chandra.Time import DateTime
from Ska.Numpy import search_both_sorted

from mica.archive import aca_l0
from mica.common import MissingDataError


#In case it isn't obvious, for an MSID of HD3TLM<I><W> in ACA image data
#for slot <S>, that maps to table 11.1 like:
# Image No.   = <S>
# Image type  = <I>
# Hdr 3 Word  = <W>


def two_byte_sum(byte_msids, scale=1):
    def func(slot_data):
        return ((slot_data[byte_msids[0]].astype('int') >> 7) * (-1 * 65535)
                + (slot_data[byte_msids[0]].astype('int') << 8)
                + (slot_data[byte_msids[1]].astype('int'))) * scale
    return func


# 8x8 header values (largest possible ACA0 header set)
ACA_DTYPE = [('TIME', '>f8'), ('QUALITY', '>i4'), ('IMGSIZE', '>i4'),
             ('HD3TLM62', '|u1'),
             ('HD3TLM63', '|u1'), ('HD3TLM64', '|u1'), ('HD3TLM65', '|u1'),
             ('HD3TLM66', '|u1'), ('HD3TLM67', '|u1'), ('HD3TLM72', '|u1'),
             ('HD3TLM73', '|u1'), ('HD3TLM74', '|u1'), ('HD3TLM75', '|u1'),
             ('HD3TLM76', '|u1'), ('HD3TLM77', '|u1'), ('FILENAME', '<U128')
             ]

ACA_DTYPE_NAMES = [k[0] for k in ACA_DTYPE]


a_to_d = [
('3D70', -40),
('3C9E', -35),
('3B98', -30),
('3A55', -25),
('38CD', -20),
('36FD', -15),
('34E1', -10),
('3279',  -5),
('2FCB',   0),
('2CDF',   5),
('29C5',  10),
('2688',  15),
('2340',  20),
('2000',  25),
('1CD3',  30),
('19CC',  35),
('16F4',  40),
('1454',  45),
('11EF',  50),
('0FC5',  55),
('0DD8',  60),
('0C22',  65),
('0A9F',  70),
('094D',  75),
('0825',  80)]

# reverse this end up with increasing hex values
a_to_d = a_to_d[::-1]
a_to_d = np.rec.fromrecords(a_to_d, names=['hex', 'tempC'])
x = np.array([int(a, 16) for a in a_to_d['hex']])
ad_func = interp1d(x, a_to_d['tempC'], kind='cubic', bounds_error=False)


def ad_temp(msids):

    def func(slot_data):
        sum = two_byte_sum(msids)(slot_data)
        # As of scipy 0.17 cannot interpolate a masked array.  In this
        # case we can temporarily fill with some value that will always
        # be in the range, then re-mask afterward.
        masked = isinstance(sum, np.ma.MaskedArray)
        if masked:
            mask = sum.mask
            sum = sum.filled(16000)
        out = ad_func(sum)
        if masked:
            out = np.ma.MaskedArray(out, mask=mask)
        return out

    return func

# dictionary that defines the header 3 'MSID's.
# Also includes a value key that describes how to determine the value
# of the MSID

HDR3_DEF = {
    '062': {'desc': 'AC status word',
            'msid': 'ac_status_word',
            'longdesc': """
AC status word.  A status word read from the AC.  The bits in the word are
defined as follows:

xxxx xxrr rrtt eccp

X = spare bits
R = CCD readout mode
   1 => S/H input is grounded during pixel readout
   2 => CCD reset is pulsed during column flush
   4 => CCD reset/sw is pulsed during row shifts
   8 => MUX is switched to ground when A/D not in use
T = Test signal select (test signals not available in flight)
E = Cmd error.  AC cmd input buffer was overwritten
C = Clock period for parallel shifts
   0 => 6 microsec
   1 => 12 microsec
   2 => 24 microsec
   3 => 48 microsec
P = PromWrite? flag: true when AC EEPROM is in the write mode.
"""},

    '064': {'desc': 'Misc status bits',
            'msid': 'misc_status_bits',
            'longdesc': """
Miscellaneous status bits showing the status of the following 16 flag variables
starting with the LSB and ending with the MSB:

bit 0 (LSB): AcSendTimeOut?
bit 1: AcIdleTimeOut?
bit 2: TecActive?
bit 3: TecHeat?
bit 4: DecAcTable?
bit 5: AcTableCkSumOK?
bit 6: StackError?
bit 7: WarmBoot?
bit 8: IdleCode LSB
bit 9: CalMode?
bit 10: CalModePending?
bit 11: IuData?
bit 12: IuDataPending?
bit 13: DsnFixed?
bit 14: InitialCalFillOK?
bit 15 (MSB): IoUpdTimeout?
"""},

    '066': {'desc': 'A/D CCD molyb therm 1',
            'msid': 'ccd_molyb_therm_1',
            'value': ad_temp(['HD3TLM66', 'HD3TLM67']),
            'longdesc': """
A/D converter reading for the CCD moly base thermistor number 1
"""},

    '072': {'desc': 'A/D CCD molyb therm 2',
            'msid': 'ccd_molyb_therm_2',
            'value': ad_temp(['HD3TLM72', 'HD3TLM73']),
            'longdesc': """
A/D converter reading for the CCD moly base thermistor number 2
"""},

    '074': {'desc': 'A/D CCD detector therm',
            'msid': 'ccd_det_therm',
            'value': ad_temp(['HD3TLM74', 'HD3TLM75']),
            'longdesc': """
A/D converter reading for the CCD detector thermistor
"""},

    '076': {'desc': 'A/D +5 volt PS',
            'msid': 'ad_5v_ps',
            'value': two_byte_sum(['HD3TLM76', 'HD3TLM77'], scale=0.20518),
            'longdesc': """
A/D converter reading for the +5 volt power supply; 1 LSB=0.30518 mv
"""},

    '162': {'desc': 'A/D +15 volt PS',
            'msid': 'ad_15v_ps',
            'value': two_byte_sum(['HD3TLM62', 'HD3TLM63'], scale=0.61035),
            'longdesc': """
A/D converter reading for the +15 volt power supply; 1 LSB=0.61035 mv
"""},

    '164': {'desc': 'A/D -15 volt PS',
            'msid': 'ad_m15v_ps',
            'value': two_byte_sum(['HD3TLM64', 'HD3TLM65'], scale=0.61035),
            'longdesc': """
A/D converter reading for the -15 volt power supply; 1 LSB=0.61035 mv
"""},

    '166': {'desc': 'A/D +27 volt PS',
            'msid': 'ad_27v_ps',
            'value': two_byte_sum(['HD3TLM66', 'HD3TLM67'], scale=1.04597),
            'longdesc': """
A/D converter reading for the +27 volt power supply; 1 LSB=1.04597 mv
"""},

    '172': {'desc': 'A/D analog ground',
            'msid': 'ad_analog_gnd',
            'value': ad_temp(['HD3TLM72', 'HD3TLM73']),
            'longdesc': """
A/D converter reading for analog ground; 1 LSB=0.30518 mv
"""},

    '174': {'desc': 'A/D for A/D convertor therm',
            'msid': 'ad_converter_therm',
            'value': ad_temp(['HD3TLM74', 'HD3TLM75']),
            'longdesc': """
A/D converter reading for the A/D converter thermistor.
"""},

    '176': {'desc': 'A/D secondary mirror therm. HRMA side',
            'msid': 'ad_smhs_therm',
            'value': ad_temp(['HD3TLM76', 'HD3TLM77']),
            'longdesc': """
A/D converter reading for the secondary mirror thermistor, HRMA side
"""},

    '262': {'desc': 'A/D secondary mirror therm. Opp HRMA side',
            'msid': 'ad_smohs_therm',
            'value': ad_temp(['HD3TLM62', 'HD3TLM63']),
            'longdesc': """
A/D converter reading for the secondary mirror thermistor, Opposite from the
HRMA side
"""},

    '264': {'desc': 'A/D primary mirror therm. HRMA side',
            'msid': 'ad_pmhs_therm',
            'value': ad_temp(['HD3TLM64', 'HD3TLM65']),
            'longdesc': """
A/D converter reading for the primary mirror thermistor, HRMA side
"""},

    '266': {'desc': 'A/D primary mirror therm. Opp HRMA side',
            'msid': 'ad_pmohs_therm',
            'value': ad_temp(['HD3TLM66', 'HD3TLM67']),
            'longdesc': """
A/D converter reading for the primary mirror thermistor, opposite from the
HRMA side
"""},

    '272': {'desc': 'A/D AC housing therm.  HRMA side',
            'msid': 'ad_achhs_therm',
            'value': ad_temp(['HD3TLM72', 'HD3TLM73']),
            'longdesc': """
A/D converter reading for the AC housing thermistor, HRMA side
"""},

    '274': {'desc': 'A/D AC housing therm.  Opp HRMA side',
            'msid': 'ad_achohs_therm',
            'value': ad_temp(['HD3TLM74', 'HD3TLM75']),
            'longdesc': """
A/D converter reading for the AC housing thermistor, opposite HRMA side
"""},

    '276': {'desc': 'A/D lens cell therm.',
            'msid': 'ad_lc_therm',
            'value': ad_temp(['HD3TLM76', 'HD3TLM77']),
            'longdesc': """
A/D converter reading for the lens cell thermistor
"""},

    '362': {'desc': 'Processor stack pointer and telem update counter',
            'msid': 'proc_stack_telem_ctr',
            'longdesc': """
A word containing the processor data stack pointer in the high byte, and
an update counter in the low byte that increments once for every 1.025
second telemetry update.
"""},

    '364': {'desc': 'Science header pulse period',
            'msid': 'sci_hdr_pulse_period',
            'longdesc': """
The science header pulse period, as measured by the PEA; 1 LSB = 2 microseconds
""",
            'nbytes': 4},

    '372': {'desc': '16-bit zero offset for pixels from CCD quad A',
            'msid': 'zero_off16_quad_a',
            'longdesc': """
A 16-bit zero offset for pixels read from CCD quadrant A; 1 LSB = 1 A/D
converter count (nominally 5 electrons)
"""},

    '374': {'desc': '16-bit zero offset for pixels from CCD quad B',
            'msid': 'zero_off16_quad_b',
            'longdesc': """
A 16-bit zero offset for pixels read from CCD quadrant B; 1 LSB = 1 A/D
converter count (nominally 5 electrons)
"""},

    '376': {'desc': '16-bit zero offset for pixels from CCD quad C',
            'msid': 'zero_off16_quad_c',
            'longdesc': """
A 16-bit zero offset for pixels read from CCD quadrant C; 1 LSB = 1 A/D
converter count (nominally 5 electrons)
"""},

    '462': {'desc': '16-bit zero offset for pixels from CCD quad D',
            'msid': 'zero_off16_quad_d',
            'longdesc': """
A 16-bit zero offset for pixels read from CCD quadrant D; 1 LSB = 1 A/D
converter count (nominally 5 electrons)
"""},

    '464': {'desc': '32-bit zero offset for pixels from CCD quad A',
            'msid': 'zero_off32_quad_a',
            'longdesc': """
A 32-bit zero offset for pixels read from CCD quadrant A; 1 LSB = 2^-16
A/D converter counts
""",
            'nbytes': 4},

    '472': {'desc': '32-bit zero offset for pixels from CCD quad B',
            'msid': 'zero_off32_quad_b',
            'longdesc': """
A 32-bit zero offset for pixels read from CCD quadrant B; 1 LSB = 2^-16
A/D converter counts
""",
            'nbytes': 4},

    '476': {'desc': '32-bit zero offset for pixels from CCD quad C',
            'msid': 'zero_off32_quad_c',
            'longdesc': """
A 32-bit zero offset for pixels read from CCD quadrant C; 1 LSB = 2^-16
A/D converter counts
""",
            'nbytes': 4},

    '564': {'desc': '32-bit zero offset for pixels from CCD quad D',
            'msid': 'zero_off32_quad_d',
            'longdesc': """
A 32-bit zero offset for pixels read from CCD quadrant D; 1 LSB = 2^-16
A/D converter counts
""",
            'nbytes': 4},

    '572': {'desc': 'last CCD flush duration',
            'msid': 'ccd_flush_dur',
            'longdesc': """
The time required for the most recent flush of the CCD; 1 LSB=2 microseconds
"""},

    '574': {'desc': 'CCD row shift clock period',
            'msid': 'ccd_row_shift_period',
            'longdesc': """
The CCD row shift clock period currently in effect; 1 LSB = 1 microsecond
"""},

    '576': {'desc': 'average backround reading',
            'msid': 'avg_bkg',
            'longdesc': """
An overall average background reading derived from the most recent CCD readout.
This is an average from all tracked images and from all search readout
segments. One LSB = 1 A/D converter count (nominally 5 electrons).
""",
            'value': two_byte_sum(['HD3TLM76', 'HD3TLM77'], scale=5)},

    '662': {'desc': 'Header 1 for DSN records',
            'msid': 'dsn_hdr1',
            'longdesc': """
Header 1 for Deep Space Network record.
""",
            'nbytes': 6},

    '672': {'desc': 'record counter and Header 2 for DSN',
            'msid': 'dsn_hdr2',
            'longdesc': """
The record counter and Header 2 for Deep Space Network records.
The record counter occupies the three high order bytes, and Header 2
occupies the low order byte.
""",
            'nbytes': 4},

    '676': {'desc': 'CCD temperature',
            'msid': 'ccd_temp',
            'longdesc': """
CCD temperature.  1 LSB=0.01/(2^16) degrees C.  The high order 16 bits give the
CCD temperature in units of 1 LSB = 0.01 degrees C.
""",
            'nbytes': 4,
            'value': two_byte_sum(['HD3TLM76', 'HD3TLM77'], scale=0.01)},

    '764': {'desc': 'CCD setpoint',
            'msid': 'ccd_setpoint',
            'longdesc': """
The CCD temperature control setpoint; 1 LSB=0.01 degrees C
""",
            'value': two_byte_sum(['HD3TLM64', 'HD3TLM65'])},

    '766': {'desc': 'temperature for position/angle cal',
            'msid': 'aca_temp',
            'longdesc': """
The temperature used in the angle calibration equations that convert star
positions from CCD row and column coordinates to Y and Z angles for OBC
telemetry; 1 LSB = 1/256 degrees C.
""",
            'nbytes': 4,
            'value': two_byte_sum(['HD3TLM72', 'HD3TLM73'],
                                  scale=1 / 256.)},
    '774': {'desc': 'RAM address of last write-read test failure',
            'msid': 'last_ram_fail_addr',
            'longdesc': """
The address in RAM of the failure most recently detected by the RAM
write-and-read test
"""},

    '776': {'desc': 'TEC DAC number',
            'msid': 'dac',
            'value': two_byte_sum(['HD3TLM76', 'HD3TLM77']),
            'longdesc': """
The number most recently written to the TEC power control DAC.
"""}}

MSID_ALIASES = {HDR3_DEF[key]['msid']: key for key in HDR3_DEF}


[docs]class MSID(object): """ ACA header 3 data object to work with header 3 data from available 8x8 ACA L0 telemetry:: >>> from mica.archive import aca_hdr3 >>> ccd_temp = aca_hdr3.MSID('ccd_temp', '2012:001', '2012:020') >>> type(ccd_temp.vals) 'numpy.ma.core.MaskedArray' When given an ``msid`` and ``start`` and ``stop`` range, the object will query the ACA L0 archive to populate the object, which includes the MSID values (``vals``) at the given times (``times``). The parameter ``msid_data`` is used to create an MSID object from the data of another MSID object. When ``filter_bad`` is supplied then only valid data values are stored and the ``vals`` and ``times`` attributes are `np.ndarray` instead of `ma.MaskedArray`. :param msid: MSID name :param start: Chandra.Time compatible start time :param stop: Chandra.Time compatible stop time :param msid_data: data dictionary or object from another MSID object :param filter_bad: remove missing values """ def __init__(self, msid, start, stop, msid_data=None, filter_bad=False): if msid_data is None: msid_data = MSIDset([msid], start, stop)[msid] self.msid = msid self.tstart = DateTime(start).secs self.tstop = DateTime(stop).secs self.datestart = DateTime(self.tstart).date self.datestop = DateTime(self.tstop).date # msid_data may be dictionary or object with these # attributes for attr in ('hdr3_msid', 'vals', 'times', 'desc', 'longdesc'): if hasattr(msid_data, attr): setattr(self, attr, getattr(msid_data, attr)) else: setattr(self, attr, msid_data.get(attr)) # If requested filter out bad values and set self.bad = None if filter_bad: self.filter_bad() def copy(self): from copy import deepcopy return deepcopy(self)
[docs] def filter_bad(self, copy=False): """Filter out any missing values. After applying this method the ``vals`` attributes will be a plain np.ndarray object instead of a masked array. :param copy: return a copy of MSID object with bad values filtered """ obj = self.copy() if copy else self if isinstance(obj.vals, ma.MaskedArray): obj.times = obj.times[~obj.vals.mask] obj.vals = obj.vals.compressed() if copy: return obj
class Msid(MSID): """ ACA header 3 data object to work with header 3 data from available 8x8 ACA L0 telemetry. >>> from mica.archive import aca_hdr3 >>> ccd_temp = aca_hdr3.Msid('ccd_temp', '2012:001', '2012:020') >>> type(ccd_temp.vals) 'numpy.ndarray' When given an ``msid`` and ``start`` and ``stop`` range, the object will query the ACA L0 archive to populate the object, which includes the MSID values (``vals``) at the given times (``times``). Only valid data values are returned. :param msid: MSID :param start: Chandra.Time compatible start time :param stop: Chandra.Time compatible stop time """ def __init__(self, msid, start, stop): super(Msid, self).__init__(msid, start, stop, filter_bad=True) def confirm_msid(req_msid): """ Check to see if the 'MSID' is an alias or is in the HDR3_DEF dictionary. If in the aliases, return the unaliased value. :param req_msid: requested msid :return: hdr3_def MSID name """ if req_msid in MSID_ALIASES: return MSID_ALIASES[req_msid] else: if req_msid not in HDR3_DEF: raise MissingDataError("msid %s not found" % req_msid) else: return req_msid def slot_for_msid(msid): """ For a given 'MSID' return the slot number that contains those data. """ mmatch = re.match('(\d)\d\d', msid) slot = int(mmatch.group(1)) return slot
[docs]class MSIDset(collections.OrderedDict): """ ACA header 3 data object to work with header 3 data from available 8x8 ACA L0 telemetry. An MSIDset works with multiple MSIDs simultaneously. >>> from mica.archive import aca_hdr3 >>> perigee_data = aca_hdr3.MSIDset(['ccd_temp', 'aca_temp', 'dac'], ... '2012:001', '2012:030') :param msids: list of MSIDs :param start: Chandra.Time compatible start time :param stop: Chandra.Time compatible stop time """ def __init__(self, msids, start, stop): super(MSIDset, self).__init__() self.tstart = DateTime(start).secs self.tstop = DateTime(stop).secs self.datestart = DateTime(self.tstart).date self.datestop = DateTime(self.tstop).date slot_datas = {} slots = set(slot_for_msid(confirm_msid(msid)) for msid in msids) for slot in slots: # get the 8x8 data tstop = self.tstop + 33.0 # Major frame of padding slot_data = aca_l0.get_slot_data( self.tstart, tstop, slot, imgsize=[8], columns=ACA_DTYPE_NAMES) # Find samples where the time stamp changes by a value other than 4.1 secs # (which is the value for 8x8 readouts). In that case there must have been a # break in L0 decom, typically due to a change to 4x4 or 6x6 data. # t[0] = 1.0 # t[1] = 5.1 <= This record could be bad, as indicated by the gap afterward # t[2, 3] = 17.4, 21.5 # To form the time diffs first add `tstop` to the end so that if 8x8 data # does not extend through `tstop` then the last record gets chopped. dt = np.diff(np.concatenate([slot_data['TIME'], [tstop]])) bad = np.abs(dt - 4.1) > 1e-3 slot_data[bad] = ma.masked # Chop off the padding i_stop = np.searchsorted(slot_data['TIME'], self.tstop, side='right') slot_data = slot_data[:i_stop] # explicitly unmask useful columns slot_data['TIME'].mask = ma.nomask slot_data['IMGSIZE'].mask = ma.nomask slot_data['FILENAME'].mask = ma.nomask slot_datas[slot] = slot_data # make a shared time ndarray that is the union of the time sets in the # slots. The ACA L0 telemetry has the same timestamps across slots, # so the only differences here are caused by different times in # non-TRAK across the slots (usually SRCH differences at the beginning # of the observation) shared_time = np.unique(np.concatenate([ slot_datas[slot]['TIME'].data for slot in slots])) for msid in msids: hdr3_msid = confirm_msid(msid) slot = slot_for_msid(hdr3_msid) full_data = ma.zeros(len(shared_time), dtype=slot_datas[slot].dtype) full_data.mask = ma.masked fd_idx = search_both_sorted(shared_time, slot_datas[slot]['TIME']) full_data[fd_idx] = slot_datas[slot] # make a data dictionary to feed to the MSID constructor slot_data = {'vals': HDR3_DEF[hdr3_msid]['value'](full_data), 'desc': HDR3_DEF[hdr3_msid]['desc'], 'longdesc': HDR3_DEF[hdr3_msid]['longdesc'], 'times': shared_time, 'hdr3_msid': hdr3_msid} self[msid] = MSID(msid, start, stop, slot_data)