This notebook characterizes the drift in aimpoint (aka absolute pointing) that occurs as a result of alignment changes in the ACA when temperature changes.
It develops functions and methodologies for handling this issue, and in particular
defines the first-generation transforms from aspect solution DY/DZ to CHIPX/CHIPY
for ACIS.  These are used in the aimpoint monitor code.  See the asol_to_chip_transform
notebook for the generalized version of this that is used in the
chandra_aca.drift module for operational dynamic ACA offsets.
In [1]:
    
import argparse
import re
import time
from itertools import izip, cycle
import functools
import numpy as np
import Ska.DBI
from astropy.table import Table, vstack
from astropy.time import Time
import mica.archive.obspar
from mica.archive import asp_l1
import Ska.Shell
from Ska.quatutil import yagzag2radec
from Quaternion import Quat
from Ska.quatutil import radec2yagzag
import matplotlib.pyplot as plt
%matplotlib inline
    
In [2]:
    
FIDS = {'ACIS-S': [1, 2, 3, 4, 5, 6],
        'ACIS-I': [1, 2, 3, 4, 5, 6],
        'HRC-I': [7, 8, 9, 10],
        'HRC-S': [11, 12, 13, 14],
        }
    
In [49]:
    
aimpoint_data_cache = {}
acis_pixscale = 0.492  # arcsec / pixel
def get_aimpoint_data(det):
    """
    Read aimpoint data provided by P. Zhao and define additional columns
    that make it easier to compare with fid light drift data.
    """
    if det not in aimpoint_data_cache:
        filename = '{}_ap_pos.rdb'.format(re.sub(r'-', '', det).lower())
        dat = Table.read(filename, format='ascii.rdb')
        y_off = dat['y_det_offset'] * 60 / acis_pixscale  # pix / arcmin
        z_off = dat['z_det_offset'] * 60 / acis_pixscale  # pix / arcmin
        if det == 'ACIS-S':
            dat['target_chipx'] = dat['dmt_chipx'] + y_off
            dat['target_chipy'] = dat['dmt_chipy'] + z_off
            dat['target_sim_y'] = dat['target_chipx'] * acis_pixscale
            dat['target_sim_z'] = dat['target_chipy'] * acis_pixscale
            dat['opt_axis_sim_y'] = dat['dmp_chipx'] * acis_pixscale
            dat['opt_axis_sim_z'] = dat['dmp_chipy'] * acis_pixscale
        elif det == 'ACIS-I':
            dat['target_chipx'] = dat['dmt_chipx'] + z_off
            dat['target_chipy'] = dat['dmt_chipy'] - y_off
            dat['target_sim_y'] = -dat['target_chipy'] * acis_pixscale
            dat['target_sim_z'] = dat['target_chipx'] * acis_pixscale
            dat['opt_axis_sim_y'] = -dat['dmp_chipy'] * acis_pixscale
            dat['opt_axis_sim_z'] = dat['dmp_chipx'] * acis_pixscale
        else:
            raise ValueError('Only ACIS-S and ACIS-I are supported here')
        aimpoint_data_cache[det] = dat
    return aimpoint_data_cache[det]
    
In [50]:
    
fid_stats_cache = {}
def get_fid_stats(det):
    """
    Get fid light drift data from Sybase fid_stats table.  This corrects
    the data for SIM-Z offset as well as the nominal SIM-Z for each detector.
    """
    if det not in fid_stats_cache:
        EXCLUDE_OBSIDS = '(2010, 2783, 1431, 1411)'
        
        db = Ska.DBI.DBI(dbi='sybase', server='sybase', user='aca_read')
        query = ('select obsid, id_num, id_string, tstart, ang_y_med, ang_z_med, sim_z_offset'
                 ' FROM fid_stats'
                 ' WHERE proc_status IS NULL AND id_string LIKE "{}%" '
                 ' and obsid not in {} '
                 .format(det, EXCLUDE_OBSIDS))
        vals = Table(db.fetchall(query))
        db.conn.close()
        
        vals['ang_y_norm'] = -9999.0
        vals['ang_z_norm'] = -9999.0
        
        sim_z_nom = np.median(vals['sim_z_offset'])
        vals['year'] = Time(vals['tstart'], format='cxcsec').decimalyear
        for fid in FIDS[det]:
            fid_idx = np.where(vals['id_num'] == fid)
            fs = vals[fid_idx]
            year = vals['year'][fid_idx]
            
            ang_y_corr = fs['ang_y_med']
            ang_z_corr = fs['ang_z_med'] + fs['sim_z_offset'] - sim_z_nom
            year_ok = (year > 2002) & (year < 2003)
            if np.count_nonzero(year_ok) < 3:
                print('Fid %s %d' % (det, fid))
                print('Not enough readouts for the median')
                continue
            y0 = np.median(ang_y_corr[year_ok])
            z0 = np.median(ang_z_corr[year_ok])
            vals['ang_y_norm'][fid_idx] = ang_y_corr - y0
            vals['ang_z_norm'][fid_idx] = ang_z_corr - z0
        
        fid_stats_cache[det] = vals
    
    return fid_stats_cache[det]
    
In [51]:
    
def plot_fid_drift_vs_year(detstats, det):
    """
    Plot fid drift data for specified detector.
    """
    year0 = 0.
    fidcolor = ' bgrcmkbgrcbgrc'
    plt.figure(figsize=(6, 4.5))
    plt.clf()
    plt.subplot(2, 1, 1)
    plt.ylabel('Y offset (arcsec)')
    maxyear = int(time.strftime('%Y')) + 1
    DEFAULT_MIN = -25 # arcsec
    plt.axis([1999, maxyear, DEFAULT_MIN, 10])
    plt.title(det + ' Fid Drift')
    plt.grid()
    plt.subplot(2, 1, 2)
    plt.xlabel('Year')  # MET (years)')
    plt.ylabel('Z offset (arcsec)')
    plt.axis([1999, maxyear, DEFAULT_MIN, 10])
    plt.grid()
    min_plot_y = DEFAULT_MIN
    for fid in FIDS[det]:
        fidstats = detstats[detstats['id_num'] == fid]
        year = fidstats['year']
        fid_min_y = np.min(fidstats['ang_y_norm'])
        fid_min_z = np.min(fidstats['ang_z_norm'])
        fid_min = np.min([fid_min_y, fid_min_z])
        # if the plot min value is less than we've seen for this detector
        # or less than the default, update the minimum (rounded down to the nearest 5)
        if fid_min < min_plot_y:
            min_plot_y = fid_min - (fid_min % 5)
        plt.subplot(2, 1, 1)
        plt.plot(year - year0, fidstats['ang_y_norm'],
                 '.', markersize=4, markerfacecolor=fidcolor[fid], mew=0,
                 scaley=False, scalex=False)
        plt.ylim(ymin=min_plot_y)
        plt.subplot(2, 1, 2)
        plt.plot(year - year0, fidstats['ang_z_norm'],
                 '.', markersize=4, markerfacecolor=fidcolor[fid], mew=0,
                 scaley=False, scalex=False)
        plt.ylim(ymin=min_plot_y)
    
In [6]:
    
def plot_aimpoint_vs_year(det, w=None, ref_pos='target'):
    """
    Plot aimpoint data vs time for specified detector.
    """
    dat = get_aimpoint_data(det)
    ok = (dat['year'] > 2002.0) & (dat['year'] < 2003.)
    sc_y0 = np.median(dat['{}_sim_y'.format(ref_pos)][ok])
    sc_z0 = np.median(dat['{}_sim_z'.format(ref_pos)][ok])
    y_off = (dat['{}_sim_y'.format(ref_pos)] - sc_y0)
    z_off = (dat['{}_sim_z'.format(ref_pos)] - sc_z0)
    plt.subplot(2, 1, 1)
    plt.plot(dat['year'], y_off + 20, '.', alpha=0.4)
    if w is not None:
        plt.ylim(-w, w)
    plt.subplot(2, 1, 2)
    plt.plot(dat['year'], z_off + 20, '.', alpha=0.4)
    if w is not None:
        plt.ylim(-w, w)
    
In [7]:
    
def plot_aimpoint_xy(det='ACIS-S', years=range(1999, 2020), 
                     xlim=(180, 270), ylim=(450, 540)):
    """
    Plot aimpoint chipx and chipy values for ACIS-S over the specified
    time range. Each year of values is plotted in a different color.
    """
    dat = get_aimpoint_data(det)
    colors = ('r', 'g', 'b', 'm', 'k', 'c')
    plt.figure(4, figsize=(5, 5))
    for year, color in izip(years, cycle(colors)):
        ok = (dat['year'] > year) & (dat['year'] < year + 1)
        if np.count_nonzero(ok) > 2:
            x = dat['target_chipx'][ok]
            y = dat['target_chipy'][ok]
            plt.plot(x, y, '.', color=color)
            medok = (x >= xlim[0]) & (x <= xlim[1]) & (y >= ylim[0]) & (y <= ylim[1])
            x_med = np.median(x[medok])
            y_med = np.median(y[medok])
            print('Year: {} Median chipx, chipy = {:.2f}, {:.2f}'.format(year, x_med, y_med))
    plt.xlim(*xlim)
    plt.ylim(*ylim)
    plt.grid()
    
In [52]:
    
def plot_fids_and_aimpoints(det='ACIS-S', w=30, ref_pos='target'):
    """
    Make combined plot of fid drift and aimpoint drift, offsetting
    the aimpoint drift by 20 arcsec for visual clarity.
    """
    detstats = get_fid_stats(det)
    plot_fid_drift_vs_year(detstats, det)
    plot_aimpoint_vs_year(det, w, ref_pos)
    
In [9]:
    
plot_fids_and_aimpoints('ACIS-S', ref_pos='target')
    
    
In [11]:
    
plot_aimpoint_xy(years=(2013.5,))
plt.plot([206], [480], 'xb', ms=10, mew=3);
    
    
    
In [12]:
    
plot_aimpoint_xy(years=(2014.5,))
plt.plot([200.7], [476.9], 'xb', ms=10, mew=3);
    
    
    
In [13]:
    
dat = get_aimpoint_data('ACIS-S')
    
In [14]:
    
plot_aimpoint_xy(det='ACIS-I', years=(2014.5,), xlim=(880, 980), ylim=(950, 1050))
plt.plot([930.2], [1009.6], 'xb', ms=10, mew=3);
    
    
    
In [53]:
    
def get_zero_offset_aimpoint_data(det, min_year=2010):
    """
    Return aimpoint data for observations with zero target offset.
    This simplifies the correlation of aspect solution and dmcoords
    results with the published POG plots (chapter 4) of aimpoint drift.
    """
    dat = get_aimpoint_data(det)
    ok = (dat['y_det_offset'] == 0) & (dat['z_det_offset'] == 0) & (dat['year'] > min_year)
    return dat[ok]
    
In [19]:
    
dats = get_zero_offset_aimpoint_data('ACIS-S', 2006)
dati = get_zero_offset_aimpoint_data('ACIS-I', 2006)
observations = vstack([dats, dati])
    
In [20]:
    
dats[-5:]
    
    Out[20]:
In [21]:
    
# Define nominal SIM-X and Z positions as median observed values
sim_z_nom = {'ACIS-S': np.median(dats['sim_z']),
             'ACIS-I': np.median(dati['sim_z'])}
sim_x_nom = {'ACIS-S': np.median(dats['sim_x']),
             'ACIS-I': np.median(dati['sim_x'])}
sim_z_nom
    
    Out[21]:
In [22]:
    
# Use the obspar for each obsid to fill in some additional
# columns.  Yag and Zag represent the local frame position
# (arcsec) of the target in the nominal frame.
noms = ('ra_nom', 'dec_nom', 'roll_nom')
for nom in noms:
    observations[nom] = 0.0
observations['yag'] = 0.0
observations['zag'] = 0.0
for obs in observations:
    obspar = mica.archive.obspar.get_obspar(obs['obsid'])
    for nom in noms:
        obs[nom] = obspar[nom]
    obs['ra_targ'] = obspar['ra_targ']
    obs['dec_targ'] = obspar['dec_targ']
    q_nom = Quat([obs[nom] for nom in noms])
    obs['yag'], obs['zag'] = radec2yagzag(obspar['ra_targ'], obspar['dec_targ'], q_nom)
observations['yag'] *= 3600
observations['zag'] *= 3600
    
In [54]:
    
# Look at key columns for the first few obsids
observations[('obsid', 'ra_targ', 'dec_targ') +
             ('ra_pnt', 'dec_pnt') +
             noms + ('yag', 'zag')][:5]
    
    Out[54]:
In [55]:
    
def plot_yag_zag(observations, det):
    """
    Plot the local target position in Yag, Zag coordinates
    (referenced to the nominal frame).
    """
    ok = observations['detector'] == det
    plt.plot(observations['yag'][ok], observations['zag'][ok], '.')
    plt.xlim(-18, -12)
    plt.ylim(4, 8)
    plt.grid()
    plt.title(det)
    plt.ylabel('zag (arcsec)')
    plt.xlabel('yag (arcsec)')
    
In [27]:
    
# Now we are convinced that there is a predictable offset
# between the target coordinate and the nominal frame.
plt.figure(figsize=(12,4))
plt.subplot(1, 2, 1)
plot_yag_zag(observations, 'ACIS-S')
plt.subplot(1, 2, 2)
plot_yag_zag(observations, 'ACIS-I')
    
    
In [24]:
    
# Define a fake nominal frame as ra, dec, roll = 0.
# Then define a "zero" target location ra0, dec0 that has
# the same Yag, Zag values 
q0 = Quat([0, 0, 0])
ra0, dec0 = -15.5 / 3600., 6.0 / 3600.
np.array(radec2yagzag(ra0, dec0, q0)) * 3600.
    
    Out[24]:
It's basically the difference between two rotation matrices in our system:
ACA_MISALIGN: MNC (HRMA optical axis) to ACA frame misalignment.ODB_SI_ALIGN: Misalignment used to transform from science target coordinates to ACA (PCAD) pointing direction that gets used on-board.My recollection is that the fact that these are not the same is a relic of a decision during OAC, but I'm not entirely certain of that.
In [63]:
    
calalign = Table.read('/soft/ciao/CALDB/data/chandra/pcad/align/pcadD2012-09-13alignN0009.fits',
                     hdu='CALALIGN')
    
In [64]:
    
calalign
    
    Out[64]:
In [65]:
    
# ACIS-S misalign in arcsec (pay attention to elements in row 0, columns 1 & 2)
dy, dz = np.degrees(calalign['ACA_MISALIGN'][0][0, [1,2]]) * 3600
print('dy = {:.2f} arcsec, dz = {:.2f} arcsec'.format(dy, dz))
    
    
In [66]:
    
# ACIS-I misalign in arcsec (pay attention to elements in row 0, columns 1 & 2)
dy, dz = np.degrees(calalign['ACA_MISALIGN'][1][0, [1,2]]) * 3600
print('dy = {:.2f} arcsec, dz = {:.2f} arcsec'.format(dy, dz))
    
    
In [67]:
    
ODB_SI_ALIGN  = np.array([[1.0, 3.3742E-4, 2.7344E-4],                    
                          [-3.3742E-4, 1.0, 0.0],          
                          [-2.7344E-4, 0.0, 1.0]])
    
In [68]:
    
# ACA_MISALIGN * ODB_SI_ALIGN delta alignment (ACIS-S)
dy, dz = np.degrees(calalign['ACA_MISALIGN'][0].dot(ODB_SI_ALIGN)[0, [1,2]]) * 3600
print('dy = {:.2f} arcsec, dz = {:.2f} arcsec'.format(dy, dz))
    
    
In [69]:
    
# ACA_MISALIGN * ODB_SI_ALIGN delta alignment (ACIS-I)
dy, dz = np.degrees(calalign['ACA_MISALIGN'][1].dot(ODB_SI_ALIGN)[0, [1,2]]) * 3600
print('dy = {:.2f} arcsec, dz = {:.2f} arcsec'.format(dy, dz))
    
    
In [70]:
    
plt.figure(figsize=(12,4))
plt.subplot(1, 2, 1)
plot_yag_zag(observations, 'ACIS-S')
plt.plot([-14.96], [6.04], 'or')
plt.subplot(1, 2, 2)
plot_yag_zag(observations, 'ACIS-I')
plt.plot([-15.93], [6.58], 'or')
    
    Out[70]:
    
In [35]:
    
ciaoenv = Ska.Shell.getenv('source /soft/ciao/bin/ciao.sh')
    
In [36]:
    
dmcoords_cmd = ['dmcoords', 'none',
                'asolfile=none',
                'detector=acis',
                'opt=cel',
                'ra={ra}', 
                'dec={dec}',
                'celfmt=deg', 
                'ra_nom=0',
                'dec_nom=0',
                'roll_nom=0',  
                'ra_asp=")ra_nom"',
                'dec_asp=")dec_nom"',
                'roll_asp=")roll_nom"',      
                'sim="{simx} 0 {simz}"',
                'displace="0 {dy} {dz} 0 0 0"',       
                'verbose=0']
dmcoords_cmd = ' '.join(dmcoords_cmd)
    
In [37]:
    
ciaorun = functools.partial(Ska.Shell.bash, env=ciaoenv)
    
In [38]:
    
def dmcoords_chipx_chipy(det, dy, dz):
    ra0, dec0 = -15.5 / 3600., 6.0 / 3600.
    ciaorun('punlearn dmcoords')
    ciaorun(dmcoords_cmd.format(ra=ra0, dec=dec0, 
                                simx=sim_x_nom[det], simz=sim_z_nom[det],
                                dy=dy, dz=dz))
    return [float(x) for x in ciaorun('pget dmcoords chipx chipy')]
    
In [39]:
    
def get_dy_dz(obsid):
    asolfiles = asp_l1.get_files(obsid=obsid, content=['ASPSOL'])
    asol = Table.read(asolfiles[0])
    min_dy, median_dy, max_dy = (np.min(asol['dy']), 
                              np.median(asol['dy']),
                              np.max(asol['dy']))
    min_dz, median_dz, max_dz = (np.min(asol['dz']), 
                              np.median(asol['dz']),
                              np.max(asol['dz']))
    return min_dy, median_dy, max_dy, min_dz, median_dz, max_dz
    
In [40]:
    
dmcoords_chipx_chipy(det='ACIS-I', dy=-1.13993, dz=-0.99711)
    
    Out[40]:
In [41]:
    
dati['obsid', 'year'][:4]
    
    Out[41]:
In [42]:
    
min_dy, median_dy, max_dy, min_dz, median_dz, max_dz = get_dy_dz(5480)
print dmcoords_chipx_chipy(det='ACIS-I', dy=-median_dy, dz=-median_dz)
    
    
In [43]:
    
dats['obsid', 'year'][:4]
    
    Out[43]:
In [44]:
    
min_dy, median_dy, max_dy, min_dz, median_dz, max_dz = get_dy_dz(6730)
print dmcoords_chipx_chipy(det='ACIS-S', dy=-median_dy, dz=-median_dz)
    
    
In [45]:
    
det = 'ACIS-I'
cx11, cy11 = dmcoords_chipx_chipy(det=det, dy=-1.0, dz=-1.0)
cx01, cy01 = dmcoords_chipx_chipy(det=det, dy=0.0, dz=-1.0)
cx12, cy12 = dmcoords_chipx_chipy(det=det, dy=-1.0, dz=-2.0)
print cx11, cy11
print cx01, cy01
print cx12, cy12
    
    
In [46]:
    
print('chipx = {:.2f} + {:.2f} * dz'.format((cx01 - cx12) + cx01, cx01 - cx12))
print('chipy = {:.2f} - {:.2f} * dy'.format(cy01, -(cy01 - cy11)))
    
    
In [47]:
    
det = 'ACIS-S'
cx11, cy11 = dmcoords_chipx_chipy(det=det, dy=-1.0, dz=-1.0)
cx01, cy01 = dmcoords_chipx_chipy(det=det, dy=0.0, dz=-1.0)
cx12, cy12 = dmcoords_chipx_chipy(det=det, dy=-1.0, dz=-2.0)
print cx11, cy11
print cx01, cy01
print cx12, cy12
    
    
In [48]:
    
print('chipx = {:.2f} + {:.2f} * dy'.format(cx01, cx01 - cx11))
print('chipy = {:.2f} + {:.2f} * dz'.format(cy01 + (cy01 - cy12), cy01 - cy12))