A key part of the process to apply dynamic ACA offsets to each observation is computing the required DY/DZ aspect solution value to move the aimpoint to the specified CHIPX/CHIPY coordinate. This notebook computes these transforms for each ACIS and HRC chip.
The final results are used in the chandra_aca.drift
module.
This notebook was originally based on the absolute_pointing_uncertainty
notebook
in this repository.
In [1]:
from __future__ import print_function, division
import argparse
import re
import time
from itertools import izip, cycle
import functools
from pprint import pprint
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
from sherpa import ui
import matplotlib.pyplot as plt
%matplotlib inline
In [2]:
# Sherpa is too chatty so just turn off all logging (attempts to do this more selectively
# did not work on first try)
import logging
logging.disable(50)
In [3]:
# Nominal SIM focus and TSC positions. This is run once with cell below and values
# are filled in to avoid repeating this exercise. Originally use:
# sim_x_nom = {}
# sim_z_nom = {}
sim_x_nom = {
'ACIS-I': -0.78090834371672724,
'ACIS-S': -0.68282252473119054,
'HRC-I': -1.0388663562382989,
'HRC-S': -1.526339935833849}
sim_z_nom = {
'ACIS-I': -233.58743446082869,
'ACIS-S': -190.1400660498719,
'HRC-I': 126.98297998998621,
'HRC-S': 250.46603308020099}
In [4]:
if not sim_x_nom or not sim_z_nom:
from Ska.DBI import DBI
db = DBI(server='sybase', dbi='sybase', user='aca_read')
dat = db.fetchall('select detector, sim_x, sim_z from obspar where obsid>12000 and obsid<18000')
for det in ('ACIS-S', 'ACIS-I', 'HRC-S', 'HRC-I'):
ok = dat['detector'] == det
sim_x_nom[det] = np.median(dat['sim_x'][ok])
sim_z_nom[det] = np.median(dat['sim_z'][ok])
db.conn.close()
from pprint import pprint
pprint(sim_x_nom)
pprint(sim_z_nom)
In [5]:
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 = 'optics_aimpoint/{}_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
aimpoint_data_cache[det] = dat
return aimpoint_data_cache[det]
In [6]:
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 [7]:
dats = get_zero_offset_aimpoint_data('ACIS-S', 2010)
dati = get_zero_offset_aimpoint_data('ACIS-I', 2010)
hrcs = get_zero_offset_aimpoint_data('HRC-S', 2010)
hrci = get_zero_offset_aimpoint_data('HRC-I', 2010)
observations = vstack([dats, dati, hrcs, hrci])
In [8]:
# 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
observations['ra_pnt'] = 0.0
observations['dec_pnt'] = 0.0
observations['detector'] = 'ACIS-S'
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']
obs['ra_pnt'] = obspar['ra_pnt']
obs['dec_pnt'] = obspar['dec_pnt']
obs['detector'] = obspar['detector']
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 [9]:
ok = observations['detector'] == 'HRC-I'
observations[ok][:5]
Out[9]:
In [10]:
ciaoenv = Ska.Shell.getenv('source /soft/ciao/bin/ciao.sh')
In [11]:
dmcoords_cmd = ['dmcoords', 'none',
'asolfile=none',
'detector="{detector}"',
'fpsys="{fpsys}"',
'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 [12]:
ciaorun = functools.partial(Ska.Shell.bash, env=ciaoenv)
There is normally around an 17 arcsec offset between the target and nominal coordinates in the obspar or event file header values. 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 [13]:
def get_obspars(start='2010:001', stop='2016:001'):
from Ska.DBI import DBI
from cxotime import CxoTime
tstart = CxoTime(start).secs
tstop = CxoTime(stop).secs
db = DBI(server='sybase', dbi='sybase', user='aca_read')
obspars = db.fetchall('select obsid, ra_nom, dec_nom, roll_nom, ra_targ, dec_targ, detector, '
'date_obs, y_det_offset, z_det_offset, sched_exp_time from obspar'
' where tstart>{} and tstop<{} and obsid<40000 and sched_exp_time>8000'.format(tstart, tstop))
db.conn.close()
return obspars
In [14]:
calalign = Table.read('/soft/ciao/CALDB/data/chandra/pcad/align/pcadD2012-09-13alignN0009.fits',
hdu='CALALIGN')
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 [15]:
calalign
Out[15]:
In [16]:
# These are the values of ra_targ, dec_targ which will result in the target being at
# the zero-offset aimpoint for ra_pnt = ra_nom = dec_pnt = dec_nom = roll = 0
detectors = ('ACIS-S', 'ACIS-I', 'HRC-S', 'HRC-I')
ra_dec_0 = {}
for cal in calalign:
rot = cal['ACA_MISALIGN'].dot(ODB_SI_ALIGN)
qrot = Quat(rot)
ra = -np.degrees(qrot.q[2] * 2)
dec = np.degrees(qrot.q[1] * 2)
det = cal['INSTR_ID'].strip()
ra_dec_0[det] = (ra, dec)
print(det, ra * 3600, dec * 3600)
In [17]:
obspars = Table(get_obspars('2006:001'))
In [18]:
obspars['yag'] = np.zeros(len(obspars))
obspars['zag'] = np.zeros(len(obspars))
In [19]:
for i, op in enumerate(obspars):
q = Quat([op['ra_nom'], op['dec_nom'], op['roll_nom']])
obspars['yag'][i], obspars['zag'][i] = radec2yagzag(op['ra_targ'], op['dec_targ'], q)
obspars['yag'] = obspars['yag'] * 3600 - obspars['y_det_offset'] * 60
obspars['zag'] = obspars['zag'] * 3600 - obspars['z_det_offset'] * 60
In [20]:
obspars[:5]
Out[20]:
In [21]:
plt.figure(figsize=(10, 10))
from itertools import count
for color, det, i in zip('gbrc', detectors, count()):
ok = obspars['detector'] == det
op = obspars[ok]
if len(op) > 5:
plt.subplot(2, 2, i + 1)
plt.plot(op['yag'], op['zag'], '.', color=color, alpha=0.4)
plt.title(det)
print(det, np.median(op['yag']), np.median(op['zag']))
plt.xlim(-25, -5)
plt.ylim(-10, 15)
for color, det, i in zip('gbrc', detectors, count()):
plt.subplot(2, 2, i + 1)
x, y = (val * 3600 for val in ra_dec_0[det])
plt.plot(x, y, 'o', color='k', markersize=10)
plt.plot(x, y, 'o', color=color, markersize=7)
plt.grid()
In [22]:
obspars[:2]
Out[22]:
In [23]:
def dmcoords_chipx_chipy(det, dy, dz, verbose=False):
"""
Get the dmcoords-computed chipx and chipy for given detector and
aspect solution DY and DZ values. NOTE: the ``dy`` and ``dz`` inputs
to dmcoords are flipped in sign from the ASOL values. Generally the
ASOL DY/DZ are positive and dmcoord input values are negative. This
sign flip is handled *here*, so input to this is ASOL DY/DZ.
:param det: detector (ACIS-S, ACIS-I, HRC-S, HRC-I)
:param dy: aspect solution DY value (mm)
:param dz: aspect solution DZ value (mm)
"""
# See the absolute_pointing_uncertainty notebook in this repo for the
# detailed derivation of this -15.5, 6.0 arcsec offset factor. See the
# cell below for the summary version.
ra0, dec0 = ra_dec_0[det]
ciaorun('punlearn dmcoords')
fpsys_map = {'HRC-I': 'HI1',
'HRC-S': 'HS2',
'ACIS-I': 'ACIS',
'ACIS-S': 'ACIS'}
cmd = dmcoords_cmd.format(ra=ra0, dec=dec0,
detector=(det if det.startswith('HRC') else 'ACIS'),
fpsys=fpsys_map[det],
simx=sim_x_nom[det], simz=sim_z_nom[det],
dy=-dy, dz=-dz)
ciaorun(cmd)
if verbose:
print(cmd)
return [float(x) for x in ciaorun('pget dmcoords chipx chipy chip_id')]
In [24]:
def get_dy_dz(obsid):
"""
Get statistical summary data for aspect solution DY/DZ for ``obsid``.
:param obsid: ObsID
:returns: min_dy, median_dy, max_dy, min_dz, median_dz, max_dz (mm)
"""
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 [25]:
# What are CHIPX/Y for HRC-I at DY/Z = 0, 0
dmcoords_chipx_chipy('HRC-I', 0, 0)
Out[25]:
In [26]:
dati['obsid', 'ap_chipx', 'ap_chipy'][:1]
Out[26]:
In [27]:
min_dy, median_dy, max_dy, min_dz, median_dz, max_dz = get_dy_dz(11613)
dmcoords_chipx_chipy(det='ACIS-I', dy=median_dy, dz=median_dz)
Out[27]:
In [28]:
dats['obsid', 'ap_chipx', 'ap_chipy'][:1]
Out[28]:
In [29]:
min_dy, median_dy, max_dy, min_dz, median_dz, max_dz = get_dy_dz(12351)
dmcoords_chipx_chipy(det='ACIS-S', dy=median_dy, dz=median_dz)
Out[29]:
In [30]:
hrcs['obsid', 'ap_chipx', 'ap_chipy'][:1]
Out[30]:
In [31]:
min_dy, median_dy, max_dy, min_dz, median_dz, max_dz = get_dy_dz(10665)
dmcoords_chipx_chipy(det='HRC-S', dy=median_dy, dz=median_dz, verbose=True)
Out[31]:
In [32]:
hrci['obsid', 'ap_chipx', 'ap_chipy'][:1]
Out[32]:
In [33]:
min_dy, median_dy, max_dy, min_dz, median_dz, max_dz = get_dy_dz(10980)
dmcoords_chipx_chipy(det='HRC-I', dy=median_dy, dz=median_dz, verbose=True)
Out[33]:
In [34]:
# The third number of the output is the chip_id
dmcoords_chipx_chipy(det='ACIS-I', dy=-10.0, dz=-10.0)
Out[34]:
In [35]:
dmcoords_chipx_chipy(det='ACIS-I', dy=10.0, dz=10.0)
Out[35]:
In [36]:
dmcoords_chipx_chipy(det='ACIS-I', dy=10.0, dz=-10.0)
Out[36]:
In [37]:
dmcoords_chipx_chipy(det='ACIS-I', dy=-10.0, dz=10.0)
Out[37]:
In [38]:
dmcoords_chipx_chipy(det='HRC-S', dy=100, dz=0)
Out[38]:
In [39]:
dmcoords_chipx_chipy(det='HRC-S', dy=-100, dz=0)
Out[39]:
In [40]:
def dyz_to_chipxy(pars, x):
"""
Define a function to transform from DY/Z to CHIPX/Y
CHIPX, CHIPY = c0 + cyz * [DY, DZ] (dot product)
"""
c0x, c0y, cyx, czx, cyy, czy = pars
c0 = np.array([[c0x],
[c0y]])
cyz = np.array([[cyx, czx],
[cyy, czy]])
x = np.array(x).reshape(-1, 2).transpose()
y = c0 + cyz.dot(x)
return y.transpose().flatten()
In [41]:
pars = [971.91, 963.07, 0.0, +41.74, -41.74, 0.0]
x = [-1, -1, 0, -1, -1, -2.0]
# x = [-1, -1]
dyz_to_chipxy(pars, x)
Out[41]:
In [42]:
def fit_xform(det, chip_id, dy0, dz0):
"""
Fit the transform model to 3 points forming an "L" on the chip around
the ``dy0`` and ``dz0`` DY/Z values.
:param det: detector (can be lower case)
:param chip_id: numerical chip ID using dmcoord convention (ACIS=0 to 9, HRC=0 to 3)
:param dy0: asol DY center value (mm)
:param dz0: asol DZ center value (mm)
:returns: sherpa fit_results() object
"""
det = det.upper()
x = []
y = []
for ddy, ddz in ((0, 0), (1, 0), (0, 1)):
dy = dy0 + ddy
dz = dz0 + ddz
cx, cy, cid = dmcoords_chipx_chipy(det, dy, dz)
if cid != chip_id:
raise ValueError('Chip mismatch {} != {}'.format(cid, chip_id))
x.extend([dy, dz])
y.extend([cx, cy])
ui.load_arrays(1, np.array(x), np.array(y))
ui.load_user_model(dyz_to_chipxy, 'xform_mdl')
ui.add_user_pars('xform_mdl', ['c0x', 'c0y', 'cyx', 'czx', 'cyy', 'czy'])
ui.set_model(xform_mdl)
ui.fit()
return ui.get_fit_results()
In [43]:
fr = fit_xform('ACIS-I', 3, 0.0, 0.0)
fr.parvals
Out[43]:
In [44]:
fr = fit_xform(det='HRC-I', chip_id=0, dy0=0.0, dz0=0.0)
fr.parvals
Out[44]:
In [45]:
det_ids = (('acis-i', 3, 0, 0),
('acis-i', 0, 10, -10),
('acis-i', 1, -10, -10),
('acis-i', 2, 10, 10),
('acis-s', 7, 0, 0),
('acis-s', 6, 25, 0),
('acis-s', 5, 50, 0),
('acis-s', 4, 75, 0),
('acis-s', 8, -25, 0),
('acis-s', 9, -50, 0),
('hrc-i', 0, 0, 0),
('hrc-s', 1, -100, 0),
('hrc-s', 2, 0, 0),
('hrc-s', 3, 100, 0),
)
In [46]:
xforms = {}
In [47]:
for det, chip_id, dy0, dz0 in det_ids:
key = det.upper(), chip_id
if key in xforms:
continue
print(key)
fr = fit_xform(det, chip_id, dy0, dz0)
xforms[key] = fr.parvals
In [48]:
print(dyz_to_chipxy(xforms['ACIS-I', 1], [-10, -10]))
print(dmcoords_chipx_chipy(det='ACIS-I', dy=-10.0, dz=-10.0))
In [49]:
print(dyz_to_chipxy(xforms['ACIS-S', 7], [0, 0]))
print(dmcoords_chipx_chipy(det='ACIS-S', dy=0.0, dz=0.0))
In [50]:
print(dyz_to_chipxy(xforms['HRC-I', 0], [0, 0]))
print(dmcoords_chipx_chipy(det='HRC-I', dy=0.0, dz=0.0))
In [51]:
print(dyz_to_chipxy(xforms['HRC-S', 2], [0, 0]))
print(dmcoords_chipx_chipy(det='HRC-S', dy=0.0, dz=0.0))
In [52]:
xforms_out = {}
for key, val in xforms.items():
vals = [round(x, 3) for x in val]
xforms_out[key] = {'c0': [vals[0], vals[1]],
'cyz': [[vals[2], vals[3]],
[vals[4], vals[5]]]}
In [53]:
from pprint import pprint
pprint(xforms_out)
In [ ]: