def get_q_atts_transforms(telems, slot, dt):
    """
    Get quaternions and associated transforms, matched to the times of yag/zag data
    in slot.  Apply a time offset ``dt`` to account for latencies in telemetry
    and ACA image readout.
    """
    logger.verbose('Interpolating quaternions for slot {}'.format(slot))
    yz_times = telems['aoacyan{}'.format(slot)].times
    q_times = telems['aoattqt1'].times
    qs = np.empty((len(yz_times), 4), dtype=np.float64)

    for ii in range(4):
        q_vals = telems['aoattqt{}'.format(ii + 1)].vals
        qs[:, ii] = Ska.Numpy.interpolate(q_vals, q_times + dt, yz_times, sorted=True)
    q_atts = quaternion.Quat(quaternion.normalize(qs))
    transforms = q_atts.transform  # N x 3 x 3
    return q_atts, transforms
Example #2
0
def consecutive(data, stepsize=1):
    return np.split(data, np.where(np.diff(data) != stepsize)[0] + 1)


ds = events.dwells.filter(start='2008:007')
#ds = events.dwells.filter(obsid=17321)
low_obsids = []
for d in ds:
    obsid = d.get_obsid()
    print "obsid {} start {}".format(obsid, d.manvr.start)
    pcad_data = get_pcad(d)
    q_atts = Quat(
        normalize(
            np.column_stack([
                pcad_data['AOATTQT1'].vals, pcad_data['AOATTQT2'].vals,
                pcad_data['AOATTQT3'].vals, pcad_data['AOATTQT4'].vals
            ])))
    try:
        cat = get_catalog(obsid, d.manvr.start)
    except (ValueError, IndexError) as e:
        print "Skipping {} {}".format(obsid, e)
        continue
    gcat = cat['cat'][(cat['cat']['type'] == 'BOT') |
                      (cat['cat']['type'] == 'GUI')]
    yag_offs = np.zeros((len(pcad_data['AOKALSTR'].vals), len(gcat)))
    zag_offs = np.zeros((len(pcad_data['AOKALSTR'].vals), len(gcat)))
    slot_ok_old = {}
    slot_ok_new = {}
    for idx, entry in enumerate(gcat):
        slot = entry['slot']
Example #3
0
def main(opt):
    opt, args = get_options()
    if not os.path.exists(opt.outdir):
        os.mkdir(opt.outdir)

    config_logging(opt.outdir, opt.verbose)

    # Store info relevant to processing for use in outputs
    proc = dict(
        run_user=os.environ['USER'],
        run_time=time.ctime(),
        errors=[],
    )
    logger.info(
        '#####################################################################'
    )
    logger.info(
        '# %s run at %s by %s' %
        (os.path.dirname(__file__), proc['run_time'], proc['run_user']))
    logger.info('# version = %s' % VERSION)
    logger.info('# characteristics version = %s' % characteristics.VERSION)
    logger.info(
        '#####################################################################\n'
    )

    logger.info('Command line options:\n%s\n' % pformat(opt.__dict__))

    # Connect to database (NEED TO USE aca_read)
    tnow = DateTime(opt.run_start_time).secs
    tstart = tnow

    # Get temperature telemetry for 3 weeks prior to min(tstart, NOW)
    tlm = get_telem_values(tstart, [
        'sim_z', 'dp_pitch', 'aoacaseq', 'aodithen', 'cacalsta', 'cobsrqid',
        'aofunlst', 'aopcadmd', '4ootgsel', '4ootgmtn', 'aocmdqt1', 'aocmdqt2',
        'aocmdqt3', '1de28avo', '1deicacu', '1dp28avo', '1dpicacu', '1dp28bvo',
        '1dpicbcu'
    ],
                           days=opt.days,
                           name_map={
                               'sim_z': 'tscpos',
                               'cobsrqid': 'obsid'
                           })

    tlm['tscpos'] = tlm['tscpos'] * -397.7225924607
    outdir = opt.outdir
    states = get_states(tlm[0].date, tlm[-1].date)
    write_states(opt, states)
    tlm = Ska.Numpy.add_column(tlm, 'power', smoothed_power(tlm))

    # Get bad time intervals
    bad_time_mask = get_bad_mask(tlm)

    # Interpolate states onto the tlm.date grid
    state_vals = cmd_states.interpolate_states(states, tlm['date'])

    # "Forgive" dither intervals with dark current replicas
    # This will also exclude dither disables that are in cmd states for standard dark cals
    dark_mask = np.zeros(len(tlm), dtype='bool')
    dark_times = []
    # Find dither "disable" states from tlm
    dith_disa_states = logical_intervals(tlm['date'],
                                         tlm['aodithen'] == 'DISA')
    for state in dith_disa_states:
        # Index back into telemetry for each of these constant dither disable states
        idx0 = np.searchsorted(tlm['date'], state['tstart'], side='left')
        idx1 = np.searchsorted(tlm['date'], state['tstop'], side='right')
        # If any samples have aca calibration flag, mark interval for exclusion.
        if np.any(tlm['cacalsta'][idx0:idx1] != 'OFF '):
            dark_mask[idx0:idx1] = True
            dark_times.append({
                'start': state['datestart'],
                'stop': state['datestop']
            })

    # Calculate the 4th term of the commanded quaternions
    cmd_q4 = np.sqrt(
        np.abs(1.0 - tlm['aocmdqt1']**2 - tlm['aocmdqt2']**2 -
               tlm['aocmdqt3']**2))
    raw_tlm_q = np.vstack(
        [tlm['aocmdqt1'], tlm['aocmdqt2'], tlm['aocmdqt3'],
         cmd_q4]).transpose()

    # Calculate angle/roll differences in state cmd vs tlm cmd quaternions
    raw_state_q = np.vstack([state_vals[n]
                             for n in ['q1', 'q2', 'q3', 'q4']]).transpose()
    tlm_q = normalize(raw_tlm_q)
    # only use values that aren't NaNs
    good = np.isnan(np.sum(tlm_q, axis=-1)) == False
    # and are in NPNT
    npnt = tlm['aopcadmd'] == 'NPNT'
    # and are in KALM after the first 2 sample of the transition
    not_kalm = tlm['aoacaseq'] != 'KALM'
    kalm = (not_kalm | np.hstack([[False, False], not_kalm[:-2]])) == False
    # and aren't during momentum unloads or in the first 2 samples after unloads
    unload = tlm['aofunlst'] != 'NONE'
    no_unload = (unload | np.hstack([[False, False], unload[:-2]])) == False
    ok = good & npnt & kalm & no_unload & ~bad_time_mask
    state_q = normalize(raw_state_q)
    dot_q = np.sum(tlm_q[ok] * state_q[ok], axis=-1)
    dot_q[dot_q > 1] = 1
    angle_diff = np.degrees(2 * np.arccos(dot_q))
    angle_diff = np.min([angle_diff, 360 - angle_diff], axis=0)
    roll_diff = Quat(tlm_q[ok]).roll - Quat(state_q[ok]).roll
    roll_diff = np.min([roll_diff, 360 - roll_diff], axis=0)

    for msid in MODE_SOURCE:
        tlm_col = np.zeros(len(tlm))
        state_col = np.zeros(len(tlm))
        for mode, idx in zip(MODE_MSIDS[msid], count()):
            tlm_col[tlm[MODE_SOURCE[msid]] == mode] = idx
            state_col[state_vals[msid] == mode] = idx
        tlm = Ska.Numpy.add_column(tlm, msid, tlm_col)
        state_vals = Ska.Numpy.add_column(state_vals, "{}_pred".format(msid),
                                          state_col)

    for msid in ['letg', 'hetg']:
        txt = np.repeat('RETR', len(tlm))
        # use a combination of the select telemetry and the insertion telem to
        # approximate the state_vals values
        txt[(tlm['4ootgsel'] == msid.upper())
            & (tlm['4ootgmtn'] == 'INSE')] = 'INSE'
        tlm_col = np.zeros(len(tlm))
        state_col = np.zeros(len(tlm))
        for mode, idx in zip(MODE_MSIDS[msid], count()):
            tlm_col[txt == mode] = idx
            state_col[state_vals[msid] == mode] = idx
        tlm = Ska.Numpy.add_column(tlm, msid, tlm_col)
        state_vals = Ska.Numpy.add_column(state_vals, "{}_pred".format(msid),
                                          state_col)

    diff_only = {
        'pointing': {
            'diff': angle_diff * 3600,
            'date': tlm['date'][ok]
        },
        'roll': {
            'diff': roll_diff * 3600,
            'date': tlm['date'][ok]
        }
    }

    pred = {
        'dp_pitch': state_vals.pitch,
        'obsid': state_vals.obsid,
        'dither': state_vals['dither_pred'],
        'pcad_mode': state_vals['pcad_mode_pred'],
        'letg': state_vals['letg_pred'],
        'hetg': state_vals['hetg_pred'],
        'tscpos': state_vals.simpos,
        'power': state_vals.power,
        'pointing': 1,
        'roll': 1
    }

    plots_validation = []
    valid_viols = []
    logger.info('Making validation plots and quantile table')
    quantiles = (1, 5, 16, 50, 84, 95, 99)
    # store lines of quantile table in a string and write out later
    quant_table = ''
    quant_head = ",".join(['MSID'] + ["quant%d" % x for x in quantiles])
    quant_table += quant_head + "\n"
    for fig_id, msid in enumerate(sorted(pred)):
        plot = dict(msid=msid.upper())
        fig = plt.figure(10 + fig_id, figsize=(7, 3.5))
        fig.clf()
        scale = SCALES.get(msid, 1.0)
        ax = None
        if msid not in diff_only:
            if msid in MODE_MSIDS:
                state_msid = np.zeros(len(tlm))
                for mode, idx in zip(MODE_MSIDS[msid], count()):
                    state_msid[state_vals[msid] == mode] = idx
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 tlm[msid],
                                                 fig=fig,
                                                 fmt='-r')
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 state_msid,
                                                 fig=fig,
                                                 fmt='-b')
                plt.yticks(range(len(MODE_MSIDS[msid])), MODE_MSIDS[msid])
            else:
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 tlm[msid] / scale,
                                                 fig=fig,
                                                 fmt='-r')
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 pred[msid] / scale,
                                                 fig=fig,
                                                 fmt='-b')
        else:
            ticklocs, fig, ax = plot_cxctime(diff_only[msid]['date'],
                                             diff_only[msid]['diff'] / scale,
                                             fig=fig,
                                             fmt='-k')
        plot['diff_only'] = msid in diff_only
        ax.set_title(TITLE[msid])
        ax.set_ylabel(LABELS[msid])
        xlims = ax.get_xlim()
        ylims = ax.get_ylim()

        bad_times = list(characteristics.bad_times)

        # Add the time intervals of dark current calibrations that have been excluded from
        # the diffs to the "bad_times" for validation so they also can be marked with grey
        # rectangles in the plot.  This is only really visible with interactive/zoomed plot.
        if msid in ['dither', 'pcad_mode']:
            bad_times.extend(dark_times)

        # Add "background" grey rectangles for excluded time regions to vs-time plot
        for bad in bad_times:
            bad_start = cxc2pd([DateTime(bad['start']).secs])[0]
            bad_stop = cxc2pd([DateTime(bad['stop']).secs])[0]
            if not ((bad_stop >= xlims[0]) & (bad_start <= xlims[1])):
                continue
            rect = matplotlib.patches.Rectangle((bad_start, ylims[0]),
                                                bad_stop - bad_start,
                                                ylims[1] - ylims[0],
                                                alpha=.2,
                                                facecolor='black',
                                                edgecolor='none')
            ax.add_patch(rect)

        filename = msid + '_valid.png'
        outfile = os.path.join(outdir, filename)
        logger.info('Writing plot file %s' % outfile)
        plt.tight_layout()
        plt.margins(0.05)
        fig.savefig(outfile)
        plot['lines'] = filename

        if msid not in diff_only:
            ok = ~bad_time_mask
            if msid in ['dither', 'pcad_mode']:
                # For these two validations also ignore intervals during a dark current calibration
                ok &= ~dark_mask
            diff = tlm[msid][ok] - pred[msid][ok]
        else:
            diff = diff_only[msid]['diff']

        # Sort the diffs in-place because we're just using them in aggregate
        diff = np.sort(diff)

        # if there are only a few residuals, don't bother with histograms
        if msid.upper() in validation_scale_count:
            plot['samples'] = len(diff)
            plot['diff_count'] = np.count_nonzero(diff)
            plot['n_changes'] = 1 + np.count_nonzero(pred[msid][1:] -
                                                     pred[msid][0:-1])
            if (plot['diff_count'] <
                (plot['n_changes'] * validation_scale_count[msid.upper()])):
                plots_validation.append(plot)
                continue
            # if the msid exceeds the diff count, add a validation violation
            else:
                viol = {
                    'msid':
                    "{}_diff_count".format(msid),
                    'value':
                    plot['diff_count'],
                    'limit':
                    plot['n_changes'] * validation_scale_count[msid.upper()],
                    'quant':
                    None,
                }
                valid_viols.append(viol)
                logger.info(
                    'WARNING: %s %d discrete diffs exceed limit of %d' %
                    (msid, plot['diff_count'],
                     plot['n_changes'] * validation_scale_count[msid.upper()]))

        # Make quantiles
        if (msid != 'obsid'):
            quant_line = "%s" % msid
            for quant in quantiles:
                quant_val = diff[(len(diff) * quant) // 100]
                plot['quant%02d' % quant] = FMTS[msid] % quant_val
                quant_line += (',' + FMTS[msid] % quant_val)
            quant_table += quant_line + "\n"

        for histscale in ('lin', 'log'):
            fig = plt.figure(20 + fig_id, figsize=(4, 3))
            fig.clf()
            ax = fig.gca()
            ax.hist(diff / scale, bins=50, log=(histscale == 'log'))
            ax.set_title(msid.upper() + ' residuals: telem - cmd states',
                         fontsize=11)
            ax.set_xlabel(LABELS[msid])
            fig.subplots_adjust(bottom=0.18)
            plt.tight_layout()
            filename = '%s_valid_hist_%s.png' % (msid, histscale)
            outfile = os.path.join(outdir, filename)
            logger.info('Writing plot file %s' % outfile)
            fig.savefig(outfile)
            plot['hist' + histscale] = filename

        plots_validation.append(plot)

    filename = os.path.join(outdir, 'validation_quant.csv')
    logger.info('Writing quantile table %s' % filename)
    f = open(filename, 'w')
    f.write(quant_table)
    f.close()

    # If run_start_time is specified this is likely for regression testing
    # or other debugging.  In this case write out the full predicted and
    # telemetered dataset as a pickle.
    if opt.run_start_time:
        filename = os.path.join(outdir, 'validation_data.pkl')
        logger.info('Writing validation data %s' % filename)
        f = open(filename, 'w')
        pickle.dump({'pred': pred, 'tlm': tlm}, f, protocol=-1)
        f.close()

    valid_viols.extend(make_validation_viols(plots_validation))
    if len(valid_viols) > 0:
        # generate daily plot url if outdir in expected year/day format
        daymatch = re.match('.*(\d{4})/(\d{3})', opt.outdir)
        if daymatch:
            url = os.path.join(URL, daymatch.group(1), daymatch.group(2))
            logger.info('validation warning(s) at %s' % url)
        else:
            logger.info('validation warning(s) in output at %s' % opt.outdir)

    write_index_rst(opt, proc, plots_validation, valid_viols)
    rst_to_html(opt, proc)
Example #4
0
def kal(dwell, telem, limit=20, catalog=None, nowflags=False):

    cat = catalog

    # Track status
    fids = np.column_stack([(telem['AOACFID{}'.format(slot)].vals == 'FID ')
                            for slot in range(0, 8)])
    trak = np.column_stack([(telem['AOACFCT{}'.format(slot)].vals == 'TRAK')
                            for slot in range(0, 8)])
    # Flags
    ir = np.column_stack([(telem['AOACIIR{}'.format(slot)].vals == 'OK ')
                          for slot in range(0, 8)])
    sp = np.column_stack([(telem['AOACISP{}'.format(slot)].vals == 'OK ')
                          for slot in range(0, 8)])
    dp_date = DateTime('2013:297:11:25:52.000').secs
    dp = np.column_stack([((telem['AOACIDP{}'.format(slot)].vals == 'OK ')
                           | (telem['AOKALSTR'].times > dp_date))
                          for slot in range(0, 8)])
    if dwell.start > '2015:251':
        # I'm not sure about the fetch grid if we use fetch interpolate, so just use
        # a sorted search to see if the MSS flag should apply
        mss = telem['AOACIMSS'].vals == 'ENAB'
        mss_times = telem['AOACIMSS'].times
        mss_at_times = mss[
            np.searchsorted(mss_times[1:-1], telem['AOACIMS0'].times) - 1]
        ms = np.column_stack([((telem['AOACIMS{}'.format(slot)].vals == 'OK ')
                               | ~mss_at_times) for slot in range(0, 8)])
    else:
        ms = np.column_stack([(telem['AOACIMS{}'.format(slot)].vals == 'OK ')
                              for slot in range(0, 8)])

    # use rolled-by-4 for ~last 4.1 sample
    last_trak = np.roll(trak, 4, axis=0)
    last_trak[0] = True

    # Calc centroid residuals using CYAN/CZAN
    q_atts = Quat(
        normalize(
            np.column_stack([
                telem['AOATTQT1'].vals, telem['AOATTQT2'].vals,
                telem['AOATTQT3'].vals, telem['AOATTQT4'].vals
            ])))

    # guide and bot slots
    gcat = cat[(cat['type'] == 'BOT') | (cat['type'] == 'GUI')]
    # make a couple of structures for the offsets
    yag_offs = np.zeros((len(telem['AOKALSTR'].vals), 8))
    zag_offs = np.zeros((len(telem['AOKALSTR'].vals), 8))
    for idx, entry in enumerate(gcat):
        slot = entry['slot']
        #ok = telem['AOACFCT{}'.format(slot)].vals == 'TRAK'
        star = agasc.get_star(entry['id'], date=dwell.manvr.start)
        eci = radec2eci(star['RA_PMCORR'], star['DEC_PMCORR'])
        d_aca = np.dot(q_atts.transform.transpose(0, 2, 1), eci)
        yag = np.degrees(np.arctan2(d_aca[:, 1], d_aca[:, 0])) * 3600
        zag = np.degrees(np.arctan2(d_aca[:, 2], d_aca[:, 0])) * 3600
        yag_offs[:, slot] = yag - telem['AOACYAN{}'.format(entry['slot'])].vals
        zag_offs[:, slot] = zag - telem['AOACZAN{}'.format(entry['slot'])].vals

    if nowflags:
        kal = (~fids & trak & last_trak & ir & sp
               & (np.abs(yag_offs) < limit) & (np.abs(zag_offs) < limit))
    else:
        kal = (~fids & trak & last_trak & ir & sp & dp & ms
               & (np.abs(yag_offs) < limit) & (np.abs(zag_offs) < limit))

    return telem['AOKALSTR'].times, kal
Example #5
0
def main(opt):
    opt, args = get_options()
    if not os.path.exists(opt.outdir):
        os.mkdir(opt.outdir)

    config_logging(opt.outdir, opt.verbose)

    # Store info relevant to processing for use in outputs
    proc = dict(run_user=os.environ['USER'],
                run_time=time.ctime(),
                errors=[],
                )
    logger.info('#####################################################################')
    logger.info('# %s run at %s by %s' % (os.path.dirname(__file__),
                                          proc['run_time'], proc['run_user']))
    logger.info('# version = %s' % VERSION)
    logger.info('# characteristics version = %s' % characteristics.VERSION)
    logger.info('#####################################################################\n')

    logger.info('Command line options:\n%s\n' % pformat(opt.__dict__))

    # Connect to database (NEED TO USE aca_read)
    tnow = DateTime(opt.run_start_time).secs
    tstart = tnow

    # Get temperature telemetry for 3 weeks prior to min(tstart, NOW)
    tlm = get_telem_values(tstart,
                           ['sim_z', 'dp_pitch', 'aoacaseq',
                            'aodithen', 'cobsrqid', 'aofunlst',
                            'aopcadmd', '4ootgsel', '4ootgmtn',
                            'aocmdqt1', 'aocmdqt2', 'aocmdqt3',
                            '1de28avo', '1deicacu',
                            '1dp28avo', '1dpicacu',
                            '1dp28bvo', '1dpicbcu'],
                           days=opt.days,
                           name_map={'sim_z': 'tscpos',
                                     'cobsrqid': 'obsid'})

    tlm['tscpos'] = tlm['tscpos'] * -397.7225924607
    outdir = opt.outdir
    states = get_states(tlm[0].date, tlm[-1].date)
    write_states(opt, states)
    tlm = Ska.Numpy.add_column(tlm, 'power', smoothed_power(tlm))

    # Get bad time intervals
    bad_time_mask = get_bad_mask(tlm)

    # Interpolate states onto the tlm.date grid
    state_vals = cmd_states.interpolate_states(states, tlm['date'])

    # Calculate the 4th term of the commanded quaternions
    cmd_q4 = np.sqrt(np.abs(1.0
                            - tlm['aocmdqt1']**2
                            - tlm['aocmdqt2']**2
                            - tlm['aocmdqt3']**2))
    raw_tlm_q = np.vstack([tlm['aocmdqt1'],
                           tlm['aocmdqt2'],
                           tlm['aocmdqt3'],
                           cmd_q4]).transpose()

    # Calculate angle/roll differences in state cmd vs tlm cmd quaternions
    raw_state_q = np.vstack([state_vals[n] for n
                             in ['q1', 'q2', 'q3', 'q4']]).transpose()
    tlm_q = normalize(raw_tlm_q)
    # only use values that aren't NaNs
    good = np.isnan(np.sum(tlm_q, axis=-1)) == False
    # and are in NPNT
    npnt = tlm['aopcadmd'] == 'NPNT'
    # and are in KALM after the first 2 sample of the transition
    not_kalm = tlm['aoacaseq'] != 'KALM'
    kalm = (not_kalm | np.hstack([[False, False], not_kalm[:-2]])) == False
    # and aren't during momentum unloads or in the first 2 samples after unloads
    unload = tlm['aofunlst'] != 'NONE'
    no_unload = (unload | np.hstack([[False, False], unload[:-2]])) == False
    ok = good & npnt & kalm & no_unload & ~bad_time_mask
    state_q = normalize(raw_state_q)
    dot_q = np.sum(tlm_q[ok] * state_q[ok], axis=-1)
    dot_q[dot_q > 1] = 1
    angle_diff = np.degrees(2 * np.arccos(dot_q))
    angle_diff = np.min([angle_diff, 360 - angle_diff], axis=0)
    roll_diff = Quat(tlm_q[ok]).roll - Quat(state_q[ok]).roll
    roll_diff = np.min([roll_diff, 360 - roll_diff], axis=0)

    for msid in MODE_SOURCE:
        tlm_col = np.zeros(len(tlm))
        state_col = np.zeros(len(tlm))
        for mode, idx in zip(MODE_MSIDS[msid], count()):
            tlm_col[tlm[MODE_SOURCE[msid]] == mode] = idx
            state_col[state_vals[msid] == mode] = idx
        tlm = Ska.Numpy.add_column(tlm, msid, tlm_col)
        state_vals = Ska.Numpy.add_column(state_vals,
                                          "{}_pred".format(msid), state_col)

    for msid in ['letg', 'hetg']:
        txt = np.repeat('RETR', len(tlm))
        # use a combination of the select telemetry and the insertion telem to
        # approximate the state_vals values
        txt[(tlm['4ootgsel'] == msid.upper())
            & (tlm['4ootgmtn'] == 'INSE')] = 'INSE'
        tlm_col = np.zeros(len(tlm))
        state_col = np.zeros(len(tlm))
        for mode, idx in zip(MODE_MSIDS[msid], count()):
            tlm_col[txt == mode] = idx
            state_col[state_vals[msid] == mode] = idx
        tlm = Ska.Numpy.add_column(tlm, msid, tlm_col)
        state_vals = Ska.Numpy.add_column(state_vals,
                                          "{}_pred".format(msid), state_col)


    diff_only = {'pointing': {'diff': angle_diff * 3600,
                              'date': tlm['date'][ok]},
                 'roll': {'diff': roll_diff * 3600,
                          'date': tlm['date'][ok]}}

    pred = {'dp_pitch': state_vals.pitch,
            'obsid': state_vals.obsid,
            'dither': state_vals['dither_pred'],
            'pcad_mode': state_vals['pcad_mode_pred'],
            'letg': state_vals['letg_pred'],
            'hetg': state_vals['hetg_pred'],
            'tscpos': state_vals.simpos,
            'power': state_vals.power,
            'pointing': 1,
            'roll': 1}

    plots_validation = []
    valid_viols = []
    logger.info('Making validation plots and quantile table')
    quantiles = (1, 5, 16, 50, 84, 95, 99)
    # store lines of quantile table in a string and write out later
    quant_table = ''
    quant_head = ",".join(['MSID'] + ["quant%d" % x for x in quantiles])
    quant_table += quant_head + "\n"
    for fig_id, msid in enumerate(sorted(pred)):
        plot = dict(msid=msid.upper())
        fig = plt.figure(10 + fig_id, figsize=(7, 3.5))
        fig.clf()
        scale = SCALES.get(msid, 1.0)
        ax = None
        if msid not in diff_only:
            if msid in MODE_MSIDS:
                state_msid = np.zeros(len(tlm))
                for mode, idx in zip(MODE_MSIDS[msid], count()):
                    state_msid[state_vals[msid] == mode] = idx
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 tlm[msid], fig=fig, fmt='-r')
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 state_msid, fig=fig, fmt='-b')
                plt.yticks(range(len(MODE_MSIDS[msid])), MODE_MSIDS[msid])
            else:
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 tlm[msid] / scale, fig=fig, fmt='-r')
                ticklocs, fig, ax = plot_cxctime(tlm['date'],
                                                 pred[msid] / scale, fig=fig, fmt='-b')
        else:
            ticklocs, fig, ax = plot_cxctime(diff_only[msid]['date'],
                                             diff_only[msid]['diff'] / scale, fig=fig, fmt='-k')
        plot['diff_only'] = msid in diff_only
        ax.set_title(TITLE[msid])
        ax.set_ylabel(LABELS[msid])
        xlims = ax.get_xlim()
        ylims = ax.get_ylim()
        for bad in characteristics.bad_times:
            bad_start = cxc2pd([DateTime(bad['start']).secs])[0]
            bad_stop = cxc2pd([DateTime(bad['stop']).secs])[0]
            if not ((bad_stop >= xlims[0]) & (bad_start <= xlims[1])):
                continue
            rect = matplotlib.patches.Rectangle((bad_start, ylims[0]),
                                                bad_stop - bad_start,
                                                ylims[1] - ylims[0],
                                                alpha=.2,
                                                facecolor='black',
                                                edgecolor='none')
            ax.add_patch(rect)

        filename = msid + '_valid.png'
        outfile = os.path.join(outdir, filename)
        logger.info('Writing plot file %s' % outfile)
        plt.tight_layout()
        fig.savefig(outfile)
        plot['lines'] = filename

        if msid not in diff_only:
            diff = tlm[msid][~bad_time_mask] - pred[msid][~bad_time_mask]
            diff = np.sort(diff)
        else:
            diff = np.sort(diff_only[msid]['diff'])

        # if there are only a few residuals, don't bother with histograms
        if msid.upper() in validation_scale_count:
            plot['samples'] = len(diff)
            plot['diff_count'] = np.count_nonzero(diff)
            plot['n_changes'] = 1 + np.count_nonzero(pred[msid][1:] - pred[msid][0:-1])
            if (plot['diff_count'] <
                (plot['n_changes'] * validation_scale_count[msid.upper()])):
                plots_validation.append(plot)
                continue
            # if the msid exceeds the diff count, add a validation violation
            else:
                viol = {'msid': "{}_diff_count".format(msid),
                        'value': plot['diff_count'],
                        'limit': plot['n_changes'] * validation_scale_count[msid.upper()],
                        'quant': None,
                        }
                valid_viols.append(viol)
                logger.info('WARNING: %s %d discrete diffs exceed limit of %d' %
                            (msid, plot['diff_count'],
                             plot['n_changes'] * validation_scale_count[msid.upper()]))

        # Make quantiles
        if (msid != 'obsid'):
            quant_line = "%s" % msid
            for quant in quantiles:
                quant_val = diff[(len(diff) * quant) // 100]
                plot['quant%02d' % quant] = FMTS[msid] % quant_val
                quant_line += (',' + FMTS[msid] % quant_val)
            quant_table += quant_line + "\n"

        for histscale in ('lin', 'log'):
            fig = plt.figure(20 + fig_id, figsize=(4, 3))
            fig.clf()
            ax = fig.gca()
            ax.hist(diff / scale, bins=50, log=(histscale == 'log'))
            ax.set_title(msid.upper() + ' residuals: telem - cmd states', fontsize=11)
            ax.set_xlabel(LABELS[msid])
            fig.subplots_adjust(bottom=0.18)
            plt.tight_layout()
            filename = '%s_valid_hist_%s.png' % (msid, histscale)
            outfile = os.path.join(outdir, filename)
            logger.info('Writing plot file %s' % outfile)
            fig.savefig(outfile)
            plot['hist' + histscale] = filename

        plots_validation.append(plot)

    filename = os.path.join(outdir, 'validation_quant.csv')
    logger.info('Writing quantile table %s' % filename)
    f = open(filename, 'w')
    f.write(quant_table)
    f.close()

    # If run_start_time is specified this is likely for regression testing
    # or other debugging.  In this case write out the full predicted and
    # telemetered dataset as a pickle.
    if opt.run_start_time:
        filename = os.path.join(outdir, 'validation_data.pkl')
        logger.info('Writing validation data %s' % filename)
        f = open(filename, 'w')
        pickle.dump({'pred': pred, 'tlm': tlm}, f, protocol=-1)
        f.close()

    valid_viols.extend(make_validation_viols(plots_validation))
    if len(valid_viols) > 0:
        # generate daily plot url if outdir in expected year/day format
        daymatch = re.match('.*(\d{4})/(\d{3})', opt.outdir)
        if daymatch:
            url = os.path.join(URL, daymatch.group(1), daymatch.group(2))
            logger.info('validation warning(s) at %s' % url)
        else:
            logger.info('validation warning(s) in output at %s' % opt.outdir)

    write_index_rst(opt, proc, plots_validation, valid_viols)
    rst_to_html(opt, proc)