Fit the flight acquisition probability model in 2017

Fit values here were computed 2017-Aug-9

This version introduces a dependence on the search box size. Search box sizes of 160 or 180 arcsec (required for at least 3 star slots) were used in normal operations starting in the MAR2017 products. This followed two PMSTA anomalies.

In addition this version uses the 2017 dark current model from chandra_aca version 3.15. This requires computing the warm pixel fraction values instead of using the values provided in the acqusition database.


In [1]:
from __future__ import division

import re
import numpy as np
import matplotlib.pyplot as plt
from astropy.table import Table
from astropy.time import Time
import tables
from scipy import stats
import tables3_api
from chandra_aca.dark_model import get_warm_fracs

%matplotlib inline

Final 2017 fit values


In [2]:
SOTA2017_FIT_NO_1P5 = [4.38145,  # scl0
                       6.22480,  # scl1
                       2.20862,  # scl2
                       -2.24494,  # off0
                       0.32180,  # off1
                       0.08306,  # off2
                       0.00384,  # p_bright_fail
                       ]

In [3]:
SOTA2017_FIT_ONLY_1P5 = [4.73283,  # scl0
                         7.63540,  # scl1
                         4.56612,  # scl2
                         -1.49046,  # off0
                         0.53391,  # off1
                         -0.37074,  # off2
                         0.00199,  # p_bright_fail
                         ]

Final 2015 fit values


In [4]:
SOTA2015_FIT_ALL = [3.9438714542029976, 5.4601129927961134, 1.6582423213669775, 
                    -2.0646518576907495, 0.36414269305801689, -0.0075143036207362852, 
                    0.003740065500207244]

In [5]:
SOTA2015_FIT_NO_1P5 = [4.092016310373646, 6.5415918325159641, 1.8191919043258409, 
                       -2.2301709573082413, 0.30337711472920426, 0.10116735012955963, 
                       0.0043395964215468185]

In [6]:
SOTA2015_FIT_ONLY_1P5 = [4.786710417762472, 4.839392687262392, 1.8646719319052267, 
                         -1.4926740399312248, 0.76412972998935347, -0.20229644263097146, 
                         0.0016270748026844457]

Fit code


In [7]:
with tables.open_file('/proj/sot/ska/data/acq_stats/acq_stats.h5', 'r') as h5:
    cols = h5.root.data.cols
    names = {'tstart': 'guide_tstart',
             'obsid': 'obsid',
             'obc_id': 'acqid',
             'halfwidth': 'halfw',
             'warm_pix': 'n100_warm_frac',
             'mag': 'mag_aca',
             'known_bad': 'known_bad',
             'color': 'color1',
            'img_func': 'img_func', 
            'ion_rad': 'ion_rad',
            'sat_pix': 'sat_pix',
            'ccd_temp': 'ccd_temp'}
    acqs = Table([getattr(cols, h5_name)[:] for h5_name in names.values()],
                 names=list(names.keys()))

    year_q0 = 1999.0 + 31. / 365.25  # Jan 31 approximately

In [8]:
acqs['year'] = Time(acqs['tstart'], format='cxcsec').decimalyear.astype('f4')
acqs['quarter'] = (np.trunc((acqs['year'] - year_q0) * 4)).astype('f4')
acqs['color_1p5'] = np.where(acqs['color'] == 1.5, 1, 0)

In [9]:
# Filter for year and mag
ok = (acqs['year'] > 2007) & (acqs['mag'] > 6.0) & (acqs['mag'] < 11.0)

In [10]:
# Filter known bad obsids
print('Filtering known bad obsids, start len = {}'.format(np.count_nonzero(ok)))
bad_obsids = [
    # Venus
    2411,2414,6395,7306,7307,7308,7309,7311,7312,7313,7314,7315,7317,7318,7406,583,
    7310,9741,9742,9743,9744,9745,9746,9747,9749,9752,9753,9748,7316,15292,16499,
    16500,16501,16503,16504,16505,16506,16502,
    ]
for badid in bad_obsids:
    ok = ok & (acqs['obsid'] != badid)
print('Filtering known bad obsids, end len = {}'.format(np.count_nonzero(ok)))


Filtering known bad obsids, start len = 139075
Filtering known bad obsids, end len = 138915

In [11]:
data_all = acqs[ok]
data_all.sort('year')
data_all['mag10'] = data_all['mag'] - 10.0

In [12]:
# Adjust probability (in probit space) for box size.  See:
# https://github.com/sot/skanb/blob/master/pea-test-set/fit_box_size_acq_prob.ipynb
b1 = 0.96
b2 = -0.30
box0 = (data_all['halfwidth'] - 120) / 120  # normalized version of box, equal to 0.0 at nominal default
data_all['box_delta'] = b1 * box0 + b2 * box0**2

Histogram of warm pixel fraction (and use current dark model, not values in database)


In [13]:
# Compute warm fracs using current dark model. This takes a couple of minutes
warm_fracs = [get_warm_fracs(100, date=tstart, T_ccd=ccd_temp)
              for tstart, ccd_temp in zip(data_all['tstart'], data_all['ccd_temp'])]

In [14]:
n, bins, patches = plt.hist(data_all['warm_pix'], bins=100, label='acq database')
plt.grid()
plt.xlabel('Warm pixel fraction')

plt.hist(warm_fracs, bins=bins, facecolor='r', alpha=0.5, label='current dark model')
plt.legend();



In [15]:
# Substitute current dark model values instead of acq database
data_all['warm_pix'] = warm_fracs

In [16]:
data_all = data_all.group_by('quarter')
data_mean = data_all.groups.aggregate(np.mean)


WARNING: Cannot aggregate column 'img_func' with type '|S7' [astropy.table.groups]

In [17]:
def p_fail(pars, m10, wp, box_delta=0.0):
    """
    Acquisition probability model

    :param pars: 7 parameters (3 x offset, 3 x scale, p_fail for bright stars)
    :param m10: mag - 10
    :param wp: warm pixel fraction
    :param box: search box half width (arcsec)
    """
    scl0, scl1, scl2 = pars[0:3]
    off0, off1, off2 = pars[3:6]
    p_bright_fail = pars[6]


    scale = scl0 + scl1 * m10 + scl2 * m10**2
    offset = off0 + off1 * m10 + off2 * m10**2

    p_fail = offset + scale * wp + box_delta
    p_fail = stats.norm.cdf(p_fail)  # probit transform
    p_fail[m10 < -1.5] = p_bright_fail  # For stars brighter than 8.5 mag use a constant

    return p_fail

def p_acq_fail(data=None):
    """
    Sherpa fit function wrapper to ensure proper use of data in fitting.
    """
    if data is None:
        data = data_all
        
    m10 = data['mag10']
    wp = data['warm_pix']
    box_delta = data['box_delta']

    def sherpa_func(pars, x):
        return p_fail(pars, m10, wp, box_delta)

    return sherpa_func

In [18]:
def fit_sota_model(data_mask=None, ms_disabled=False):
    from sherpa import ui

    obc_id = data_all['obc_id']
    if ms_disabled:
        obc_id |= (data_all['img_func'] == 'star') & ~data_all['ion_rad'] & ~data_all['sat_pix']    
    data_all['fail'] = np.where(obc_id, 0.0, 1.0)

    data = data_all if data_mask is None else data_all[data_mask]

    data_id = 1
    ui.set_method('simplex')
    ui.set_stat('cash')
    ui.load_user_model(p_acq_fail(data), 'model')
    ui.add_user_pars('model', ['scl0', 'scl1', 'scl2', 'off0', 'off1', 'off2', 'p_bright_fail'])
    ui.set_model(data_id, 'model')
    ui.load_arrays(data_id, np.array(data['year']), np.array(data['fail'], dtype=np.float))

    # Initial fit values from fit of all data
    start_vals = iter(SOTA2015_FIT_ALL)  # Offset
    fmod = ui.get_model_component('model')
    for name in ('scl', 'off'):
        for num in (0, 1, 2):
            comp_name = name + str(num)
            setattr(fmod, comp_name, next(start_vals))
            comp = getattr(fmod, comp_name)
            comp.min = -100000
            comp.max = 100000
            # ui.freeze(comp)
    
    fmod.p_bright_fail = 0.025
    fmod.p_bright_fail.min = 0.0
    fmod.p_bright_fail.max = 1.0
    # ui.freeze(fmod.p_bright_fail)
    
    ui.fit(data_id)
    # conf = ui.get_confidence_results()
    return ui.get_fit_results()

Plotting and validation


In [19]:
def plot_fit_grouped(pars, group_col, group_bin, mask=None, log=False, colors='br', label=None):
    data = data_all if mask is None else data_all[mask]
    data['model'] = p_acq_fail(data)(pars, None)

    group = np.trunc(data[group_col] / group_bin)
    data = data.group_by(group)
    data_mean = data.groups.aggregate(np.mean)
    len_groups = np.diff(data.groups.indices)
    fail_sigmas = np.sqrt(data_mean['fail'] * len_groups) / len_groups
    
    plt.errorbar(data_mean[group_col], data_mean['fail'], yerr=fail_sigmas, fmt='.' + colors[0], label=label)
    plt.plot(data_mean[group_col], data_mean['model'], '-' + colors[1])
    if log:
        ax = plt.gca()
        ax.set_yscale('log')

In [20]:
def mag_filter(mag0, mag1):
    ok = (data_all['mag'] > mag0) & (data_all['mag'] < mag1)
    return ok

In [21]:
def wp_filter(wp0, wp1):
    ok = (data_all['warm_pix'] > wp0) & (data_all['warm_pix'] < wp1)
    return ok

In [22]:
def print_fit_results(fit, label):
    label = label + ' = ['
    print(label, end='')
    space = ''
    for parname, parval in zip(fit.parnames, fit.parvals):
        parname = re.sub(r'model\.', '', parname)
        print(f'{space}{parval:.5f},  # {parname}')
        space = ' ' * len(label)
    print(space + ']')

In [23]:
def plot_fit_all(fit, mask=None):
    print(fit)
    parvals = [par.val for par in model.pars]
    print(parvals)
    if mask is None:
        mask = np.ones(len(data_all), dtype=bool)
    
    plt.figure()
    plot_fit_grouped(parvals, 'mag', 0.25, wp_filter(0.10, 0.20) & mask, log=False, colors='cm', label='0.10 < WP < 0.2')
    plot_fit_grouped(parvals, 'mag', 0.25, wp_filter(0.0, 0.10) & mask, log=False, colors='br', label='0 < WP < 0.10')
    plt.legend(loc='upper left');
    plt.ylim(0.001, 1.0);
    plt.xlim(9, 11)
    plt.grid()
    
    plt.figure()
    plot_fit_grouped(parvals, 'warm_pix', 0.02, mag_filter(10, 10.6) & mask, log=True, colors='cm', label='10 < mag < 10.6')
    plot_fit_grouped(parvals, 'warm_pix', 0.02, mag_filter(9, 10) & mask, log=True, colors='br', label='9 < mag < 10')
    plt.legend(loc='best')
    plt.grid()
    
    plt.figure()
    plot_fit_grouped(parvals, 'year', 0.25, mag_filter(10, 10.6) & mask, colors='cm', label='10 < mag < 10.6')
    plot_fit_grouped(parvals, 'year', 0.25, mag_filter(9.5, 10) & mask, colors='br', label='9.5 < mag < 10')
    plot_fit_grouped(parvals, 'year', 0.25, mag_filter(9.0, 9.5) & mask,  colors='gk', label='9.0 < mag < 9.5')
    plt.legend(loc='best')
    plt.grid()
    
    plt.figure()
    plot_fit_grouped(parvals, 'year', 0.25, mag_filter(10, 10.6) & mask, colors='cm', label='10 < mag < 10.6', log=True)
    plot_fit_grouped(parvals, 'year', 0.25, mag_filter(9.5, 10) & mask, colors='br', label='9.5 < mag < 10', log=True)
    plot_fit_grouped(parvals, 'year', 0.25, mag_filter(9.0, 9.5) & mask,  colors='gk', label='9.0 < mag < 9.5', log=True)
    plt.legend(loc='best')
    plt.grid();

Color != 1.5 fit


In [24]:
print('Hang tight, this could take a few minutes')
# fit = fit_sota_model(data_all['color'] == 1.5, ms_disabled=True)
mask = data_all['color'] != 1.5
fit_n1p5 = fit_sota_model(mask, ms_disabled=True)


Hang tight, this could take a few minutes
Dataset               = 1
Method                = neldermead
Statistic             = cash
Initial fit statistic = 19329.5
Final fit statistic   = 18422.6 at function evaluation 1228
Data points           = 114193
Degrees of freedom    = 114186
Change in statistic   = 906.957
   model.scl0     4.38145     
   model.scl1     6.2248      
   model.scl2     2.20862     
   model.off0     -2.24494    
   model.off1     0.321802    
   model.off2     0.083065    
   model.p_bright_fail   0.00383546  

In [25]:
print_fit_results(fit_n1p5, 'SOTA2017_FIT_NO_1P5')


SOTA2017_FIT_NO_1P5 = [4.38145,  # scl0
                       6.22480,  # scl1
                       2.20862,  # scl2
                       -2.24494,  # off0
                       0.32180,  # off1
                       0.08306,  # off2
                       0.00384,  # p_bright_fail
                       ]

In [26]:
plot_fit_all(fit_n1p5, mask=mask)


datasets       = (1,)
itermethodname = none
methodname     = neldermead
statname       = cash
succeeded      = True
parnames       = ('model.scl0', 'model.scl1', 'model.scl2', 'model.off0', 'model.off1', 'model.off2', 'model.p_bright_fail')
parvals        = (4.3814492494899708, 6.2247982796091321, 2.2086234577312558, -2.2449389368817179, 0.32180182895871901, 0.083064980362986013, 0.0038354621866884936)
statval        = 18422.5669973412
istatval       = 19329.523702280036
dstatval       = 906.956704939
numpoints      = 114193
dof            = 114186
qval           = None
rstat          = None
message        = Optimization terminated successfully
nfev           = 1228
[4.3814492494899708, 6.2247982796091321, 2.2086234577312558, -2.2449389368817179, 0.32180182895871901, 0.083064980362986013, 0.0038354621866884936]
WARNING: Cannot aggregate column 'img_func' with type '|S7' [astropy.table.groups]

Color == 1.5 fit


In [27]:
print('Hang tight, this could take a few minutes')
mask = data_all['color'] == 1.5
fit_1p5 = fit_sota_model(mask, ms_disabled=True)


Hang tight, this could take a few minutes
Dataset               = 1
Method                = neldermead
Statistic             = cash
Initial fit statistic = 6864.53
Final fit statistic   = 5905.95 at function evaluation 1414
Data points           = 24722
Degrees of freedom    = 24715
Change in statistic   = 958.582
   model.scl0     4.73283     
   model.scl1     7.6354      
   model.scl2     4.56612     
   model.off0     -1.49046    
   model.off1     0.533912    
   model.off2     -0.370744   
   model.p_bright_fail   0.00198955  

In [28]:
print_fit_results(fit_1p5, 'SOTA2017_FIT_ONLY_1P5')


SOTA2017_FIT_ONLY_1P5 = [4.73283,  # scl0
                         7.63540,  # scl1
                         4.56612,  # scl2
                         -1.49046,  # off0
                         0.53391,  # off1
                         -0.37074,  # off2
                         0.00199,  # p_bright_fail
                         ]

In [29]:
plot_fit_all(fit_1p5, mask=mask)


datasets       = (1,)
itermethodname = none
methodname     = neldermead
statname       = cash
succeeded      = True
parnames       = ('model.scl0', 'model.scl1', 'model.scl2', 'model.off0', 'model.off1', 'model.off2', 'model.p_bright_fail')
parvals        = (4.7328281202184606, 7.6353986485446388, 4.56612179019519, -1.4904565129779004, 0.53391150895271899, -0.37074362233184105, 0.0019895547582680141)
statval        = 5905.9492983433665
istatval       = 6864.53162859849
dstatval       = 958.582330255
numpoints      = 24722
dof            = 24715
qval           = None
rstat          = None
message        = Optimization terminated successfully
nfev           = 1414
[4.7328281202184606, 7.6353986485446388, 4.56612179019519, -1.4904565129779004, 0.53391150895271899, -0.37074362233184105, 0.0019895547582680141]
WARNING: Cannot aggregate column 'img_func' with type '|S7' [astropy.table.groups]

Compare 2017 to 2015 coefficients

Failure prob vs. mag for Wp=(0.1, 0.2, 0.3)


In [30]:
mag = np.linspace(9, 11, 30)
for wp in (0.1, 0.2, 0.3):
    plt.plot(mag, p_fail(SOTA2015_FIT_NO_1P5, mag-10, wp), 'r', 
             label='2015 model' if wp == 0.1 else None)
    plt.plot(mag, p_fail(SOTA2017_FIT_NO_1P5, mag-10, wp), 'b', 
             label='2017 model' if wp == 0.1 else None)

plt.grid()
plt.xlabel('Mag')
plt.ylim(0, 1)
plt.title('Failure prob vs. mag for Wp=(0.1, 0.2, 0.3)')
plt.legend(loc='upper left')
plt.ylabel('Prob');


Failure prob vs. Wp for mag=(10.0, 10.25, 10.5)


In [31]:
for mag in (10.0, 10.25, 10.5):
    wp = np.linspace(0, 0.4, 30)
    plt.plot(wp, p_fail(SOTA2015_FIT_NO_1P5, mag-10, wp), 'r',
                        label='2015 model' if mag == 10.0 else None)
    plt.plot(wp, p_fail(SOTA2017_FIT_NO_1P5, mag-10, wp), 'b',
                        label='2017 model' if mag == 10.0 else None)

plt.grid()
plt.xlabel('Warm pix frac')
plt.ylim(0, 1)
plt.title('Failure prob vs. Wp for mag=(10.0, 10.25, 10.5)')
plt.ylabel('Fail prob');



In [ ]: