Fit binned-floor acquisition probability model in 2018-11

This is the FLIGHT acquisition and probability model calculated in 2018-11.

Key features of the model for color != 1.5 stars (with good mag estimates)

  • Incorporates ASVT data for t_ccd >= -10 C in order to provide reasonable estimates of probability in regimes with little or no flight data.
  • Fits a quadradatic model for p_fail (probit) as a function of t_ccd in a series of magnitude bins. The mag bins are driven by magnitudes of ASVT simulated data.
  • Model now includes a floor parameter that sets a hard lower limit on p_fail. This is seen in data and represents other factors that cause acquisition failure independent of t_ccd. In other words, even for an arbitrarily cold CCD there will still be a small fraction of acquisition failures. For flight data this can include spoilers or an ionizing radiation flag.
  • As in past models, the p_fail model is adjusted by a box_delta term which applies a search-box dependent offset in probit space. The box_delta term is defined to have a value of 0.0 for box halfwidth = 120.
  • The global model (for arbitrary mag) is computed by linearly interpolating the binned quadratic coefficients as a function of mag. The previous flight model (spline) did a global mag - t_ccd fit using a 5-element spline in the mag direction.

Key features of the model for color == 1.5 stars (with poor mag estimates)

  • Post AGASC 1.7, there is inadequate data to independently perform the binned fitting.
  • Instead assume a magnitude error distribution which is informed by examining the observed distribution of dmag = mag_obs - mag_aca (observed - catalog). This turns out to be well-represented by an exp(-abs(dmag) / dmag_scale) distribution. This contrasts with a gaussian that scales as exp(dmag^2).
  • Use the assumed mag error distribution and sample the color != 1.5 star probabilities accordingly and compute the weighted mean failure probability.
  • Flight data show a steeper falloff for dmag > 0 (stars observed to be fainter than expected) than for dmag < 0. As noted by JC this likely includes a survival effect that stars which are actually much fainter don't get acquired and do not get into the sample. Indeed using the observed distribution gives a poor fit to flight data, so dmag_scale for dmag > 0 was arbitrarily increased from 2.8 to 4.0 in order to better fit flight data.

Model details

  • In order to get a good match to flight data for faint stars near -11 C, it was necessary to apply an ad-hoc correction to ASVT data for mag > 10.1. The correction effectively made the model assume smaller search box sizes, so for the canonical 120 arcsec box the model p_fail is slightly increased relative to the raw failure rate from ASVT.
  • The mag = 8.0 data from ASVT show a dependence on search-box size that is flipped from usual. There are more failures for smaller search boxes, though we are dealing with small number statistics (up to 3 fails per bin). This caused problems in the fitting, so for this bin the box_delta term was simplied zeroed out and a good fit was obtained in the automatic fit process. Since p_fail is quite low in all cases this has little practical impact either way.
  • Fitting now uses binomial statistics to compute the fit statistic during model parameter optimization. Previously it was using a poisson statistic which is similar except near 1.0. The poisson statistic is built in to Sherpa and was easier, but the new binomial statistic is formally correct and behaves better for probabilities near 1.0.

Paradigm shift for production model implementation

  • This model is complicated, and the color = 1.5 star case is computationally intensive.
  • Instead of transfering the analytic algorithm and fit values into chandra_aca.star_probs for production use, take a new approach of generating a 3-d grid of p_fail (in probit space) as a function of mag, t_ccd, and halfwidth. Do this for color != 1.5 and color = 1.5.
  • The ranges are 5.0 <= mag <= 12.0, -16 <= t_ccd <= -1, and 60 <= halfwidth <= 180. Values outside that range are clipped.
  • This separates the model generation from the production model calculation.
  • Gridded 3-d linear interpolation is used in chandra_aca and is quite fast.
  • The gridded value files are about 150 kb, and make it easy to generate new models without changing code in chandra_aca (except for a hard-coded value for the default model).

In [1]:
import sys
import os
from itertools import count
from pathlib import Path

# Custom stuff, including dev version of chandra_aca with the grid model
# implementation.
sys.path.insert(0, str(Path(os.environ['HOME'], 'git', 'skanb', 'pea-test-set')))

import utils as asvt_utils
import numpy as np
import matplotlib.pyplot as plt
from astropy.table import Table, vstack
from astropy.time import Time
import tables
from scipy import stats
from scipy.interpolate import CubicSpline
from Chandra.Time import DateTime
from astropy.table import Table
from chandra_aca.star_probs import get_box_delta, broadcast_arrays

%matplotlib inline

In [2]:
np.random.seed(0)

In [3]:
SKA = Path(os.environ['SKA'])

Get acq stats data and clean


In [4]:
# Make a map of AGASC_ID to AGACS 1.7 MAG_ACA.  The acq_stats.h5 file has whatever MAG_ACA
# was in place at the time of planning the loads.
# Define new term `red_mag_err` which is used here in place of the 
# traditional COLOR1 == 1.5 test.
with tables.open_file(str(SKA / 'data' / 'agasc' / 'miniagasc_1p7.h5'), 'r') as h5:
    agasc_mag_aca = h5.root.data.col('MAG_ACA')
    agasc_id = h5.root.data.col('AGASC_ID')
    has_color3 = h5.root.data.col('RSV3') != 0  # 
    red_star = np.isclose(h5.root.data.col('COLOR1'), 1.5)
    mag_aca_err = h5.root.data.col('MAG_ACA_ERR') / 100
    red_mag_err = red_star & ~has_color3  # MAG_ACA, MAG_ACA_ERR is potentially inaccurate

In [5]:
agasc1p7_idx = {id: idx for id, idx in zip(agasc_id, count())}
agasc1p7 = Table([agasc_mag_aca, mag_aca_err, red_mag_err], 
                 names=['mag_aca', 'mag_aca_err', 'red_mag_err'], copy=False)

In [6]:
acq_file = str(SKA / 'data' / 'acq_stats' / 'acq_stats.h5')
with tables.open_file(str(acq_file), '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_aca': 'mag_aca',
             'mag_obs': 'mean_trak_mag',
             'known_bad': 'known_bad',
             'color': 'color1',
            'img_func': 'img_func', 
            'ion_rad': 'ion_rad',
            'sat_pix': 'sat_pix',
             'agasc_id': 'agasc_id',
             't_ccd': 'ccd_temp',
            'slot': 'slot'}
    acqs = Table([getattr(cols, h5_name)[:] for h5_name in names.values()],
                 names=list(names.keys()))

In [7]:
year_q0 = 1999.0 + 31. / 365.25  # Jan 31 approximately
acqs['year'] = Time(acqs['tstart'], format='cxcsec').decimalyear.astype('f4')
acqs['quarter'] = (np.trunc((acqs['year'] - year_q0) * 4)).astype('f4')

In [8]:
# Create 'fail' column, rewriting history as if the OBC always
# ignore the MS flag in ID'ing acq stars.
#
# CHECK: is ion_rad being ignored on-board?
# Answer: Not as of 2018-11
#
obc_id = acqs['obc_id']
obc_id_no_ms = (acqs['img_func'] == 'star') & ~acqs['sat_pix'] & ~acqs['ion_rad']
acqs['fail'] = np.where(obc_id | obc_id_no_ms, 0.0, 1.0)

# Re-map acq_stats database magnitudes for AGASC 1.7
acqs['mag_aca'] = [agasc1p7['mag_aca'][agasc1p7_idx[agasc_id]] for agasc_id in acqs['agasc_id']]
acqs['red_mag_err'] = [agasc1p7['red_mag_err'][agasc1p7_idx[agasc_id]] for agasc_id in acqs['agasc_id']]
acqs['mag_aca_err'] = [agasc1p7['mag_aca_err'][agasc1p7_idx[agasc_id]] for agasc_id in acqs['agasc_id']]

# Add a flag to distinguish flight from ASVT data
acqs['asvt'] = False

In [9]:
# Filter for year and mag (previously used data through 2007:001)
#
# UPDATE this to be between 4 to 5 years from time of recalibration.
#
year_min = 2014.5
year_max = DateTime('2018-10-30').frac_year
acq_ok = ((acqs['year'] > year_min) & (acqs['year'] < year_max) & 
          (acqs['mag_aca'] > 7.0) & (acqs['mag_aca'] < 11) &
          (~np.isclose(acqs['color'], 0.7)))

In [10]:
# Filter known bad obsids.  NOTE: this is no longer doing anything, but
# consider updating the list of known bad obsids or obtaining programmically?

print('Filtering known bad obsids, start len = {}'.format(np.count_nonzero(acq_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:
    acq_ok = acq_ok & (acqs['obsid'] != badid)
print('Filtering known bad obsids, end len = {}'.format(np.count_nonzero(acq_ok)))


Filtering known bad obsids, start len = 56547
Filtering known bad obsids, end len = 56547

Get ASVT data and make it look more like acq stats data


In [11]:
peas = Table.read('pea_analysis_results_2018_299_CCD_temp_performance.csv', format='ascii.csv')
peas = asvt_utils.flatten_pea_test_data(peas)
peas = peas[peas['ccd_temp'] > -10.5]

In [12]:
# Version of ASVT PEA data that is more flight-like
fpeas = Table([peas['star_mag'], peas['ccd_temp'], peas['search_box_hw']],
              names=['mag_aca', 't_ccd', 'halfwidth'])
fpeas['year'] = np.random.uniform(2019.0, 2019.5, size=len(peas))
fpeas['color'] = 1.0
fpeas['quarter'] = (np.trunc((fpeas['year'] - year_q0) * 4)).astype('f4')
fpeas['fail'] = 1.0 - peas['search_success']
fpeas['asvt'] = True
fpeas['red_mag_err'] = False
fpeas['mag_obs'] = 0.0

Combine flight acqs and ASVT data


In [13]:
data_all = vstack([acqs[acq_ok]['year', 'fail', 'mag_aca', 't_ccd', 'halfwidth', 'quarter', 
                                'color', 'asvt', 'red_mag_err', 'mag_obs'], 
                   fpeas])
data_all.sort('year')

Compute box probit delta term based on box size


In [14]:
# Adjust probability (in probit space) for box size. 
data_all['box_delta'] = get_box_delta(data_all['halfwidth'])

# Put in an ad-hoc penalty on ASVT data that introduces up to a -0.3 shift
# on probit probability.  It goes from 0.0 for mag < 10.1 up to 0.3 at mag=10.4.
ok = data_all['asvt']
box_delta_tweak = (data_all['mag_aca'][ok] - 10.1).clip(0, 0.3)
data_all['box_delta'][ok] -= box_delta_tweak

# Another ad-hoc tweak: the mag=8.0 data show more failures at smaller
# box sizes.  This confounds the fitting.  For this case only just
# set the box deltas to zero and this makes the fit work.
ok = data_all['asvt'] & (data_all['mag_aca'] == 8)
data_all['box_delta'][ok] = 0.0

In [15]:
data_all = data_all.group_by('quarter')
data_all0 = data_all.copy()  # For later augmentation with simulated red_mag_err stars
data_mean = data_all.groups.aggregate(np.mean)

Model definition


In [16]:
def t_ccd_normed(t_ccd):
    return (t_ccd + 8.0) / 8.0

def p_fail(pars, 
           t_ccd, tc2=None,
           box_delta=0, rescale=True, probit=False):
    """
    Acquisition probability model

    :param pars: p0, p1, p2 (quadratic in t_ccd) and floor (min p_fail)
    :param t_ccd: t_ccd (degC) or scaled t_ccd if rescale is False.
    :param tc2: (scaled t_ccd) ** 2, this is just for faster fitting
    :param box_delta: delta p_fail for search box size
    :param rescale: rescale t_ccd to about -1 to 1 (makes P0, P1, P2 better-behaved)
    :param probit: return probability as probit instead of 0 to 1.
    """
    p0, p1, p2, floor = pars

    tc = t_ccd_normed(t_ccd) if rescale else t_ccd
    
    if tc2 is None:
        tc2 = tc ** 2
    
    # Make sure box_delta has right dimensions
    tc, box_delta = np.broadcast_arrays(tc, box_delta)

    # Compute the model.  Also clip at +10 to avoid values that are
    # exactly 1.0 at 64-bit precision.
    probit_p_fail = (p0 + p1 * tc + p2 * tc2 + box_delta).clip(floor, 10)

    # Possibly transform from probit to linear probability
    out = probit_p_fail if probit else stats.norm.cdf(probit_p_fail)
    return out

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
        
    tc = t_ccd_normed(data['t_ccd'])
    tc2 = tc ** 2
    box_delta = data['box_delta']
    
    def sherpa_func(pars, x=None):
        return p_fail(pars, tc, tc2, box_delta, rescale=False)

    return sherpa_func

Model fitting functions


In [17]:
def calc_binom_stat(data, model, staterror=None, syserror=None, weight=None, bkg=None):
    """
    Calculate log-likelihood for a binomial probability distribution
    for a single trial at each point.
    
    Defining p = model, then probability of seeing data == 1 is p and
    probability of seeing data == 0 is (1 - p).  Note here that ``data``
    is strictly either 0.0 or 1.0, and np.where interprets those float
    values as False or True respectively.
    """
    fit_stat = -np.sum(np.log(np.where(data, model, 1.0 - model)))    
    return fit_stat, np.ones(1)

In [18]:
def fit_poly_model(data):
    from sherpa import ui
    
    comp_names = ['p0', 'p1', 'p2', 'floor']

    data_id = 1
    ui.set_method('simplex')
    
    # Set up the custom binomial statistics
    ones = np.ones(len(data))
    ui.load_user_stat('binom_stat', calc_binom_stat, lambda x: ones)
    ui.set_stat(binom_stat)

    # Define the user model
    ui.load_user_model(p_acq_fail(data), 'model')
    ui.add_user_pars('model', comp_names)
    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
    fmod = ui.get_model_component('model')

    # Define initial values / min / max
    # This is the p_fail value at t_ccd = -8.0
    fmod.p0 = -2.605
    fmod.p0.min = -10
    fmod.p0.max = 10

    # Linear slope of p_fail
    fmod.p1 = 2.5
    fmod.p1.min = 0.0
    fmod.p1.max = 10
    
    # Quadratic term.  Only allow negative curvature, and not too much at that.
    fmod.p2 = 0.0
    fmod.p2.min = -1
    fmod.p2.max = 0

    # Floor to p_fail.
    fmod.floor = -2.6
    fmod.floor.min = -2.6
    fmod.floor.max = -0.5

    ui.fit(data_id)

    return ui.get_fit_results()

Plotting and validation


In [19]:
def plot_fails_mag_aca_vs_t_ccd(mag_bins, data_all, year0=2014.0):
    ok = (data_all['year'] > year0) & ~data_all['fail'].astype(bool)
    da = data_all[ok]
    fuzzx = np.random.uniform(-0.3, 0.3, len(da))
    fuzzy = np.random.uniform(-0.125, 0.125, len(da))
    plt.plot(da['t_ccd'] + fuzzx, da['mag_aca'] + fuzzy, '.C0', markersize=4)

    ok = (data_all['year'] > year0) & data_all['fail'].astype(bool)
    da = data_all[ok]
    fuzzx = np.random.uniform(-0.3, 0.3, len(da))
    fuzzy = np.random.uniform(-0.125, 0.125, len(da))
    plt.plot(da['t_ccd'] + fuzzx, da['mag_aca'] + fuzzy, '.C1', markersize=4, alpha=0.8)
    
    # plt.xlim(-18, -10)
    # plt.ylim(7.0, 11.1)
    x0, x1 = plt.xlim()
    for y in mag_bins:
        plt.plot([x0, x1], [y, y], '-', color='r', linewidth=2, alpha=0.8)
    plt.xlabel('T_ccd (C)')
    plt.ylabel('Mag_aca')
    plt.title(f'Acq successes (blue) and failures (orange) since {year0}')
    plt.grid()

In [20]:
def plot_fit_grouped(data, group_col, group_bin, log=False, colors='br', label=None, probit=False):
    
    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)
    data_fail = data_mean['fail']
    model_fail = np.array(data_mean['model'])
    
    fail_sigmas = np.sqrt(data_fail * len_groups) / len_groups
    
    # Possibly plot the data and model probabilities in probit space
    if probit:
        dp = stats.norm.ppf(np.clip(data_fail + fail_sigmas, 1e-6, 1-1e-6))
        dm = stats.norm.ppf(np.clip(data_fail - fail_sigmas, 1e-6, 1-1e-6))
        data_fail = stats.norm.ppf(data_fail)
        model_fail = stats.norm.ppf(model_fail)
        fail_sigmas = np.vstack([data_fail - dm, dp - data_fail])
            
    plt.errorbar(data_mean[group_col], data_fail, yerr=fail_sigmas, 
                 fmt='.' + colors[1], label=label, markersize=8)
    plt.plot(data_mean[group_col], model_fail, '-' + colors[0])
    
    if log:
        ax = plt.gca()
        ax.set_yscale('log')

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

In [22]:
def t_ccd_filter(t_ccd0, t_ccd1):
    ok = (data_all['t_ccd'] > t_ccd0) & (data_all['t_ccd'] < t_ccd1)
    return ok

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

Define magnitude bins for fitting and show data


In [24]:
mag_centers = np.array([6.3, 8.1, 9.1, 9.55, 9.75, 10.0, 10.25, 10.55, 10.75, 11.0])
mag_bins = (mag_centers[1:] + mag_centers[:-1]) / 2
mag_means = np.array([8.0, 9.0, 9.5, 9.75, 10.0, 10.25, 10.5, 10.75])

In [25]:
for m0, m1, mm in zip(mag_bins[:-1], mag_bins[1:], mag_means):
    ok = (data_all['asvt'] == False) & (data_all['mag_aca'] >= m0) & (data_all['mag_aca'] < m1)
    print(f"m0={m0:.2f} m1={m1:.2f} mean_mag={data_all['mag_aca'][ok].mean():.2f} vs. {mm}")


m0=7.20 m1=8.60 mean_mag=8.05 vs. 8.0
m0=8.60 m1=9.32 mean_mag=8.99 vs. 9.0
m0=9.32 m1=9.65 mean_mag=9.48 vs. 9.5
m0=9.65 m1=9.88 mean_mag=9.76 vs. 9.75
m0=9.88 m1=10.12 mean_mag=9.99 vs. 10.0
m0=10.12 m1=10.40 mean_mag=10.23 vs. 10.25
m0=10.40 m1=10.65 mean_mag=10.50 vs. 10.5
m0=10.65 m1=10.88 mean_mag=10.75 vs. 10.75

In [26]:
plt.figure(figsize=(10, 14))
for subplot, halfwidth in enumerate([60, 80, 100, 120, 140, 160, 180]):
    plt.subplot(4, 2, subplot + 1)
    ok = (data_all['halfwidth'] > halfwidth - 10) & (data_all['halfwidth'] <= halfwidth + 10)
    plot_fails_mag_aca_vs_t_ccd(mag_bins, data_all[ok])
    plt.title(f'Acq success (blue) fail (orange) box={halfwidth}')
plt.tight_layout()


Color != 1.5 fit (this is MOST acq stars)


In [27]:
# fit = fit_sota_model(data_all['color'] == 1.5, ms_disabled=True)
mask_no_1p5 = ((data_all['red_mag_err'] == False) & 
               (data_all['t_ccd'] > -18) &
               (data_all['t_ccd'] < -0.5))

In [28]:
mag0s, mag1s = mag_bins[:-1], mag_bins[1:]
fits = {}
masks = []
for m0, m1 in zip(mag0s, mag1s):
    print(m0, m1)
    mask = mask_no_1p5 & mag_filter(m0, m1) #  & t_ccd_filter(-10.5, 0)
    print(np.count_nonzero(mask))
    masks.append(mask)
    fits[m0, m1] = fit_poly_model(data_all[mask])


7.2 8.6
14778
WARNING: imaging routines will not be available, 
failed to import sherpa.image.ds9_backend due to 
'RuntimeErr: DS9Win unusable: Could not find ds9 on your PATH'
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 531.548
Final fit statistic   = 426.434 at function evaluation 595
Data points           = 14778
Degrees of freedom    = 14774
Change in statistic   = 105.114
   model.p0       -2.85506    
   model.p1       1.97065     
   model.p2       -0.999991   
   model.floor    -2.6        
WARNING: parameter value model.floor is at its minimum boundary -2.6
8.6 9.325
17464
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 1379.38
Final fit statistic   = 1295.14 at function evaluation 533
Data points           = 17464
Degrees of freedom    = 17460
Change in statistic   = 84.2363
   model.p0       -2.42935    
   model.p1       1.95681     
   model.p2       -0.410955   
   model.floor    -2.27537    
9.325 9.65
9439
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 2470.12
Final fit statistic   = 1909.74 at function evaluation 609
Data points           = 9439
Degrees of freedom    = 9435
Change in statistic   = 560.377
   model.p0       -2.26226    
   model.p1       3.87905     
   model.p2       -0.999976   
   model.floor    -1.74938    
9.65 9.875
6633
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 3475.1
Final fit statistic   = 1932.42 at function evaluation 537
Data points           = 6633
Degrees of freedom    = 6629
Change in statistic   = 1542.68
   model.p0       -0.78502    
   model.p1       2.29318     
   model.p2       -1.64621e-06
   model.floor    -1.57095    
9.875 10.125
6233
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 5080.19
Final fit statistic   = 2207.07 at function evaluation 559
Data points           = 6233
Degrees of freedom    = 6229
Change in statistic   = 2873.12
   model.p0       -0.157143   
   model.p1       1.95089     
   model.p2       -0.298061   
   model.floor    -1.54602    
10.125 10.4
4736
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 7647.68
Final fit statistic   = 1926.32 at function evaluation 550
Data points           = 4736
Degrees of freedom    = 4732
Change in statistic   = 5721.35
   model.p0       0.855904    
   model.p1       2.09381     
   model.p2       -0.987743   
   model.floor    -1.35466    
10.4 10.65
2681
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 8131.56
Final fit statistic   = 1053.67 at function evaluation 715
Data points           = 2681
Degrees of freedom    = 2677
Change in statistic   = 7077.89
   model.p0       1.50838     
   model.p1       1.95444     
   model.p2       -1          
   model.floor    -0.915928   
10.65 10.875
2049
Dataset               = 1
Method                = neldermead
Statistic             = binom_stat
Initial fit statistic = 7795.51
Final fit statistic   = 556.91 at function evaluation 774
Data points           = 2049
Degrees of freedom    = 2045
Change in statistic   = 7238.6
   model.p0       1.84592     
   model.p1       1.25826     
   model.p2       -0.635686   
   model.floor    -1.33166    

In [29]:
colors = [f'kC{i}' for i in range(9)]

plt.figure(figsize=(13, 4))
for subplot in (1, 2):
    plt.subplot(1, 2, subplot)
    probit = (subplot == 2)
    for m0_m1, color, mask, mag_mean in zip(list(fits), colors, masks, mag_means):
        fit = fits[m0_m1]
        data = data_all[mask]
        data['model'] = p_acq_fail(data)(fit.parvals)        
        plot_fit_grouped(data, 't_ccd', 2.0, 
                         probit=probit, colors=[color, color], label=str(mag_mean))
    plt.grid()
    if probit:
        plt.ylim(-3.5, 2.5)
    plt.ylabel('Probit(p_fail)' if probit else 'p_fail')
    plt.xlabel('T_ccd');
    plt.legend(fontsize='small')



In [30]:
# This computes probabilities for 120 arcsec boxes, corresponding to raw data
t_ccds = np.linspace(-16, -0, 20)
plt.figure(figsize=(13, 4))

for subplot in (1, 2):
    plt.subplot(1, 2, subplot)
    probit = (subplot == 2)
    for m0_m1, color, mag_mean in zip(list(fits), colors, mag_means):
        fit = fits[m0_m1]
        probs = p_fail(fit.parvals, t_ccds)
        if probit:
            probs = stats.norm.ppf(probs)
        plt.plot(t_ccds, probs, label=f'{mag_mean:.2f}')

    plt.legend()
    plt.xlabel('T_ccd')
    plt.ylabel('P_fail' if subplot == 1 else 'Probit(p_fail)')
    plt.title('P_fail for halfwidth=120')
    plt.grid()



In [31]:
mag_bin_centers = np.concatenate([[5.0], mag_means, [13.0]])
fit_parvals = []
for fit in fits.values():
    fit_parvals.extend(fit.parvals)

fit_parvals = np.array(fit_parvals).reshape(-1, 4)
parvals_mag12 = [[5, 0, 0, 0]]
parvals_mag5 = [[-5, 0, 0, -3]]
fit_parvals = np.concatenate([parvals_mag5, fit_parvals, parvals_mag12])
fit_parvals = fit_parvals.transpose()
for ps, parname in zip(fit_parvals, fit.parnames):
    plt.plot(mag_bin_centers, ps, '.-', label=parname)

plt.legend(fontsize='small')
plt.title('Model coefficients vs. mag')
plt.xlabel('Mag_aca')
plt.grid()


Define model for color=1.5 stars

  • Post AGASC 1.7, there is inadequate data to independently perform the binned fitting.
  • Instead assume a magnitude error distribution which is informed by examining the observed distribution of dmag = mag_obs - mag_aca (observed - catalog). This turns out to be well-represented by an exp(-abs(dmag) / dmag_scale) distribution. This contrasts with a gaussian that scales as exp(dmag^2).
  • Use the assumed mag error distribution and sample the color != 1.5 star probabilities accordingly and compute the weighted mean failure probability.

Examine distribution of mag error for color=1.5 stars


In [32]:
def plot_mag_errs(acqs, red_mag_err):
    ok = ((acqs['red_mag_err'] == red_mag_err) & 
          (acqs['mag_obs'] > 0) & 
          (acqs['img_func'] == 'star'))
    dok = acqs[ok]
    dmag = dok['mag_obs'] - dok['mag_aca']
    plt.figure(figsize=(14, 4.5))
    plt.subplot(1, 3, 1)
    plt.plot(dok['mag_aca'], dmag, '.')
    plt.plot(dok['mag_aca'], dmag, ',', alpha=0.3)
    plt.xlabel('mag_aca (catalog)')
    plt.ylabel('Mag err')
    plt.title('Mag err (observed - catalog) vs mag_aca')
    plt.xlim(5, 11.5)
    plt.ylim(-4, 2)
    plt.grid()
    
    plt.subplot(1, 3, 2)
    plt.hist(dmag, bins=np.arange(-3, 4, 0.2), log=True);
    plt.grid()
    plt.xlabel('Mag err')
    plt.title('Mag err (observed - catalog)')
    plt.xlim(-4, 2)
    
    plt.subplot(1, 3, 3)
    plt.hist(dmag, bins=100, cumulative=-1, normed=True)
    plt.xlim(-1, 1)
    plt.xlabel('Mag err')
    plt.title('Mag err (observed - catalog)')
    plt.grid()

In [33]:
plot_mag_errs(acqs, red_mag_err=True)
plt.subplot(1, 3, 2)
plt.plot([-2.8, 0], [1, 7000], 'r');
plt.plot([0, 4.0], [7000, 1], 'r');
plt.xlim(-4, 4);


Define an analytical approximation for distribution with ad-hoc positive tail


In [34]:
# Define parameters / metadata for floor model
FLOOR = {'fit_parvals': fit_parvals,
         'mag_bin_centers': mag_bin_centers}

In [35]:
def calc_1p5_mag_err_weights():
    x = np.linspace(-2.8, 4, 18)
    ly = 3.8 * (1 - np.abs(x) / np.where(x > 0, 4.0, 2.8))
    y = 10 ** ly
    return x, y / y.sum()

In [36]:
FLOOR['mag_errs_1p5'], FLOOR['mag_err_weights_1p5'] = calc_1p5_mag_err_weights()

In [37]:
plt.semilogy(FLOOR['mag_errs_1p5'], FLOOR['mag_err_weights_1p5'])
plt.grid()


Global model for arbitrary mag, t_ccd, color, and halfwidth


In [38]:
def floor_model_acq_prob(mag, t_ccd, color=0.6, halfwidth=120, probit=False):
    """
    Acquisition probability model

    :param mag: Star magnitude(s)
    :param t_ccd: CCD temperature(s)
    :param color: Star color (compared to 1.5 to decide which p_fail model to use)
    :param halfwidth: Search box size (arcsec)
    :param probit: Return probit of failure probability
    
    :returns: acquisition failure probability
    """

    parvals = FLOOR['fit_parvals']
    mag_bin_centers = FLOOR['mag_bin_centers']
    mag_errs_1p5 = FLOOR['mag_errs_1p5']
    mag_err_weights_1p5 = FLOOR['mag_err_weights_1p5']

    # Make sure inputs have right dimensions
    is_scalar, t_ccds, mags, halfwidths, colors = broadcast_arrays(t_ccd, mag, halfwidth, color)
    box_deltas = get_box_delta(halfwidths) 

    p_fails = []
    for t_ccd, mag, box_delta, color in zip(t_ccds.flat, mags.flat, box_deltas.flat, colors.flat):
        if np.isclose(color, 1.5):
            pars_list = [[np.interp(mag + mag_err_1p5, mag_bin_centers, ps) for ps in parvals]
                         for mag_err_1p5 in mag_errs_1p5]
            weights = mag_err_weights_1p5
            if probit:
                raise ValueError('cannot use probit=True with color=1.5 stars')
        else:
            pars_list = [[np.interp(mag, mag_bin_centers, ps) for ps in parvals]]
            weights = [1]

        pf = sum(weight * p_fail(pars, t_ccd, box_delta=box_delta, probit=probit)
                 for pars, weight in zip(pars_list, weights))
        p_fails.append(pf)
    
    out = np.array(p_fails).reshape(t_ccds.shape)
    return out

In [39]:
mags, t_ccds = np.mgrid[8.75:10.75:30j, -16:-4:30j]
plt.figure(figsize=(13, 4))
for subplot, color in enumerate([1.0, 1.5]):
    plt.subplot(1, 2, subplot + 1)
    p_fails = floor_model_acq_prob(mags, t_ccds, probit=False, color=color)

    cs = plt.contour(t_ccds, mags, p_fails, levels=[0.05, 0.1, 0.2, 0.5, 0.75, 0.9], 
                     colors=['g', 'g', 'b', 'c', 'm', 'r'])
    plt.clabel(cs, inline=1, fontsize=10)
    plt.grid()
    plt.xlim(-17, -4)
    plt.ylim(8.5, 11.0)
    plt.xlabel('T_ccd (degC)')
    plt.ylabel('Mag_ACA')
    plt.title(f'Failure probability color={color}');



In [40]:
mags = np.linspace(8, 11, 301)
plt.figure()
for t_ccd in np.arange(-16, -0.9, 1):
    p_fails = floor_model_acq_prob(mags, t_ccd, probit=True)
    plt.plot(mags, p_fails)
plt.grid()
plt.xlim(8, 11)


Out[40]:
(8, 11)

Compare to flight data for halfwidth=120

Selecting only data with halfwidth=120 is a clean, model-independent way to compare the model to raw flight statistics.

Setup functions to get appropriate data


In [41]:
from scipy.stats import binom

def calc_binned_pfail(data_all, mag, dmag, t_ccd, dt):
    da = data_all[~data_all['asvt'] & (data_all['halfwidth'] == 120)]
    fail = da['fail'].astype(bool)
    ok = (np.abs(da['mag_aca'] - mag) < dmag) & (np.abs(da['t_ccd'] - t_ccd) < dt)
    n_fail = np.count_nonzero(fail[ok])
    n_acq = np.count_nonzero(ok)
    p_fail = n_fail / n_acq
    p_fail_lower = binom.ppf(0.17, n_acq, n_fail / n_acq) / n_acq
    p_fail_upper = binom.ppf(0.84, n_acq, n_fail / n_acq) / n_acq
    mean_t_ccd = np.mean(da['t_ccd'][ok])
    mean_mag = np.mean(da['mag_aca'][ok])
    return p_fail, p_fail_lower, p_fail_upper, mean_t_ccd, mean_mag, n_fail, n_acq

In [42]:
pfs_list = []
for mag in (10.0, 10.3, 10.55):
    pfs = []
    for t_ccd in np.linspace(-15, -11, 5):
        pf = calc_binned_pfail(data_all, mag, 0.2, t_ccd, 0.5)
        pfs.append(pf)
        print(f'mag={mag} mean_mag_aca={pf[4]:.2f} t_ccd=f{pf[3]:.2f} p_fail={pf[-2]}/{pf[-1]}={pf[0]:.2f}')
    pfs_list.append(pfs)


mag=10.0 mean_mag_aca=10.00 t_ccd=f-14.92 p_fail=82/1345=0.06
mag=10.0 mean_mag_aca=10.00 t_ccd=f-14.09 p_fail=94/1319=0.07
mag=10.0 mean_mag_aca=10.01 t_ccd=f-13.00 p_fail=121/1250=0.10
mag=10.0 mean_mag_aca=10.02 t_ccd=f-11.94 p_fail=80/622=0.13
mag=10.0 mean_mag_aca=10.04 t_ccd=f-11.10 p_fail=45/227=0.20
mag=10.3 mean_mag_aca=10.25 t_ccd=f-14.92 p_fail=91/843=0.11
mag=10.3 mean_mag_aca=10.25 t_ccd=f-14.09 p_fail=96/779=0.12
mag=10.3 mean_mag_aca=10.26 t_ccd=f-13.01 p_fail=161/747=0.22
mag=10.3 mean_mag_aca=10.26 t_ccd=f-11.94 p_fail=135/406=0.33
mag=10.3 mean_mag_aca=10.25 t_ccd=f-11.12 p_fail=57/133=0.43
mag=10.55 mean_mag_aca=10.49 t_ccd=f-14.92 p_fail=67/286=0.23
mag=10.55 mean_mag_aca=10.49 t_ccd=f-14.08 p_fail=72/268=0.27
mag=10.55 mean_mag_aca=10.49 t_ccd=f-12.99 p_fail=132/290=0.46
mag=10.55 mean_mag_aca=10.50 t_ccd=f-11.96 p_fail=91/174=0.52
mag=10.55 mean_mag_aca=10.47 t_ccd=f-11.10 p_fail=33/45=0.73

Compare model to flight for color != 1.5 stars


In [43]:
def plot_floor_and_flight(color):
    from chandra_aca.star_probs import acq_success_prob

    # This computes probabilities for 120 arcsec boxes, corresponding to raw data
    t_ccds = np.linspace(-16, -6, 20)
    mag_acas = np.array([9.5, 10.0, 10.25, 10.5, 10.75])

    for ii, mag_aca in enumerate(reversed(mag_acas)):
        flight_probs = 1 - acq_success_prob(date='2018-05-01T00:00:00', 
                                            t_ccd=t_ccds, mag=mag_aca, color=color)
        new_probs = floor_model_acq_prob(mag_aca, t_ccds, color=color)
        plt.plot(t_ccds, flight_probs, '--', color=f'C{ii}')
        plt.plot(t_ccds, new_probs, '-', color=f'C{ii}', label=f'mag_aca={mag_aca}')

    if color != 1.5:
        # pf1, pf2 have p_fail, p_fail_lower, p_fail_upper, mean_t_ccd, mean_mag_aca, n_fail, n_acq
        for pfs, clr in zip(pfs_list, ('C3', 'C2', 'C1')):
            for pf in pfs:
                yerr = np.array([pf[0] - pf[1], pf[2] - pf[0]]).reshape(2, 1)
                plt.errorbar(pf[3], pf[0], xerr=0.5, yerr=yerr, color=clr)

    # plt.xlim(-16, None)
    plt.legend()
    plt.xlabel('T_ccd')
    plt.ylabel('P_fail')
    plt.title(f'P_fail (color={color}: new (solid) and flight (dashed)')
    plt.grid()

In [44]:
plot_floor_and_flight(color=1.0)


Compare model to flight for color = 1.5 stars


In [45]:
plt.figure(figsize=(13, 4))
plt.subplot(1, 2, 1)
for m0, m1, color in [(9, 9.5, 'C0'), (9.5, 10, 'C1'), (10, 10.3, 'C2'), (10.3, 10.7, 'C3')]:
    ok = data_all['red_mag_err'] & mag_filter(m0, m1) & t_ccd_filter(-16, -10)
    data = data_all[ok]
    data['model'] = floor_model_acq_prob(data['mag_aca'], data['t_ccd'], color=1.5, halfwidth=data['halfwidth'])
    plot_fit_grouped(data, 't_ccd', 2.0, 
                     probit=False, colors=[color, color], label=f'{m0}-{m1}')
plt.ylim(0, 1.0)
plt.legend(fontsize='small')
plt.grid()
plt.xlabel('T_ccd')
plt.title('COLOR1=1.5 acquisition probabilities')

plt.subplot(1, 2, 2)
plot_floor_and_flight(color=1.5)


Write model as a 3-d grid to a gzipped FITS file


In [46]:
def write_model_as_fits(model_name,
                        comment=None,
                        mag0=5, mag1=12, n_mag=141,  # 0.05 mag spacing
                        t_ccd0=-16, t_ccd1=-1, n_t_ccd=31,  # 0.5 degC spacing
                        halfw0=60, halfw1=180, n_halfw=7,  # 20 arcsec spacing
                        ):
    from astropy.io import fits
    
    mags = np.linspace(mag0, mag1, n_mag)
    t_ccds = np.linspace(t_ccd0, t_ccd1, n_t_ccd)
    halfws = np.linspace(halfw0, halfw1, n_halfw)
    mag, t_ccd, halfw = np.meshgrid(mags, t_ccds, halfws, indexing='ij')

    print('Computing probs, stand by...')
    
    # COLOR = 1.5 (stars with poor mag estimates)
    p_fails = floor_model_acq_prob(mag, t_ccd, halfwidth=halfw, probit=False, color=1.5)
    p_fails_probit_1p5 = stats.norm.ppf(p_fails)

    # COLOR not 1.5 (most stars)
    p_fails_probit = floor_model_acq_prob(mag, t_ccd, halfwidth=halfw, probit=True, color=1.0)
    
    hdu = fits.PrimaryHDU()
    if comment:
        hdu.header['comment'] = comment
    hdu.header['date'] = DateTime().fits
    hdu.header['mdl_name'] = model_name
    hdu.header['mag_lo'] = mags[0]
    hdu.header['mag_hi'] = mags[-1]
    hdu.header['mag_n'] = len(mags)
    hdu.header['t_ccd_lo'] = t_ccds[0]
    hdu.header['t_ccd_hi'] = t_ccds[-1]
    hdu.header['t_ccd_n'] = len(t_ccds)
    hdu.header['halfw_lo'] = halfws[0]
    hdu.header['halfw_hi'] = halfws[-1]
    hdu.header['halfw_n'] = len(halfws)

    hdu1 = fits.ImageHDU(p_fails_probit.astype(np.float32))
    hdu1.header['comment'] = 'COLOR1 != 1.5 (good mag estimates)'
    
    hdu2 = fits.ImageHDU(p_fails_probit_1p5.astype(np.float32))
    hdu2.header['comment'] = 'COLOR1 == 1.5 (poor mag estimates)'

    hdus = fits.HDUList([hdu, hdu1, hdu2])
    hdus.writeto(f'grid-{model_name}.fits.gz', overwrite=True)

In [47]:
comment = 'Created with fit_acq_model-2018-11-binned-poly-binom-floor.ipynb in aca_stats repository'
write_model_as_fits('floor-2018-11', comment=comment)


Computing probs, stand by...

In [48]:
import warnings
import scipy

STAR_PROBS_DATA_DIR = '.'
GRID_FUNCS = {}

def broadcast_arrays_flatten(*args):
    is_scalar = all(np.array(arg).ndim == 0 for arg in args)
    if is_scalar:
        return [()] + list(args)

    args = np.atleast_1d(*args)
    outs = np.broadcast_arrays(*args)
    shape = outs[0].shape
    outs = [out.ravel() for out in outs]
    return [shape] + outs

def clip_and_warn(name, val, val_lo, val_hi, model):
    val = np.asarray(val)
    if np.any((val > val_hi) | (val < val_lo)):
        warnings.warn('\nModel {} computed between {} <= {} <= {}, '
                      'clipping input {}(s) outside that range.'
                      .format(model, name, val_lo, val_hi, name))
        val = np.clip(val, val_lo, val_hi)

    return val

def grid_model_acq_prob(mag=10.0, t_ccd=-12.0, color=0.6, halfwidth=120, probit=False,
                        model='grid-floor-2018-11'):

    if model not in GRID_FUNCS:
        from astropy.io import fits
        from scipy.interpolate import RegularGridInterpolator

        filename = os.path.join(STAR_PROBS_DATA_DIR, model) + '.fits.gz'
        if not os.path.exists(filename):
            raise IOError('model file {} does not exist'.format(filename))

        hdus = fits.open(filename)
        hdu0 = hdus[0]
        probit_p_fail_no_1p5 = hdus[1].data
        probit_p_fail_1p5 = hdus[2].data

        hdr = hdu0.header
        grid_mags = np.linspace(hdr['mag_lo'], hdr['mag_hi'], hdr['mag_n'])
        grid_t_ccds = np.linspace(hdr['t_ccd_lo'], hdr['t_ccd_hi'], hdr['t_ccd_n'])
        grid_halfws = np.linspace(hdr['halfw_lo'], hdr['halfw_hi'], hdr['halfw_n'])

        assert probit_p_fail_no_1p5.shape == (len(grid_mags), len(grid_t_ccds), len(grid_halfws))
        assert probit_p_fail_1p5.shape == probit_p_fail_no_1p5.shape

        func_no_1p5 = RegularGridInterpolator(points=(grid_mags, grid_t_ccds, grid_halfws),
                                              values=probit_p_fail_no_1p5)
        func_1p5 = RegularGridInterpolator(points=(grid_mags, grid_t_ccds, grid_halfws),
                                           values=probit_p_fail_1p5)
        mag_lo = hdr['mag_lo']
        mag_hi = hdr['mag_hi']
        t_ccd_lo = hdr['t_ccd_lo']
        t_ccd_hi = hdr['t_ccd_hi']
        halfw_lo = hdr['halfw_lo']
        halfw_hi = hdr['halfw_hi']

        GRID_FUNCS[model] = {'filename': filename,
                             'func_no_1p5': func_no_1p5,
                             'func_1p5': func_1p5,
                             'mag_lo': mag_lo,
                             'mag_hi': mag_hi,
                             't_ccd_lo': t_ccd_lo,
                             't_ccd_hi': t_ccd_hi,
                             'halfw_lo': halfw_lo,
                             'halfw_hi': halfw_hi}
    else:
        gfm = GRID_FUNCS[model]
        func_no_1p5 = gfm['func_no_1p5']
        func_1p5 = gfm['func_1p5']
        mag_lo = gfm['mag_lo']
        mag_hi = gfm['mag_hi']
        t_ccd_lo = gfm['t_ccd_lo']
        t_ccd_hi = gfm['t_ccd_hi']
        halfw_lo = gfm['halfw_lo']
        halfw_hi = gfm['halfw_hi']

    mag = clip_and_warn('mag', mag, mag_lo, mag_hi, model)
    t_ccd = clip_and_warn('t_ccd', t_ccd, t_ccd_lo, t_ccd_hi, model)
    halfwidth = clip_and_warn('halfw', halfwidth, halfw_lo, halfw_hi, model)

    # Broadcast all inputs to a common shape.  If they are all scalars
    # then shape=().  The returns values are flattened, so the final output
    # needs to be reshape at the end.
    shape, t_ccds, mags, colors, halfwidths = broadcast_arrays_flatten(
        t_ccd, mag, color, halfwidth)

    if shape:
        # One or more inputs are arrays, output is array with shape
        is_1p5 = np.isclose(colors, 1.5)
        not_1p5 = ~is_1p5
        p_fail = np.zeros(len(mags), dtype=np.float64)
        points = np.vstack([mags, t_ccds, halfwidths]).transpose()
        if np.any(is_1p5):
            p_fail[is_1p5] = func_1p5(points[is_1p5])
        if np.any(not_1p5):
            p_fail[not_1p5] = func_no_1p5(points[not_1p5])
        p_fail.shape = shape

    else:
        # Scalar case
        func = func_1p5 if np.isclose(color, 1.5) else func_no_1p5
        p_fail = func([[mag, t_ccd, halfwidth]])

    # Convert p_fail to p_success (remembering at this point p_fail is probit)
    p_success = -p_fail
    if not probit:
        p_success = scipy.stats.norm.cdf(p_success)

    return p_success

In [49]:
colors = [f'kC{i}' for i in range(9)]

plt.figure(figsize=(13, 4))
for subplot in (1, 2):
    plt.subplot(1, 2, subplot)
    probit = (subplot == 2)
    for m0_m1, color, mask, mag_mean in zip(list(fits), colors, masks, mag_means):
        fit = fits[m0_m1]
        data = data_all[mask]
        data['model'] = 1 - grid_model_acq_prob(data['mag_aca'], data['t_ccd'],
                                             halfwidth=data['halfwidth'])
        plot_fit_grouped(data, 't_ccd', 2.0, 
                         probit=probit, colors=[color, color], label=str(mag_mean))
    plt.grid()
    if probit:
        plt.ylim(-3.5, 2.5)
    plt.ylabel('Probit(p_fail)' if probit else 'p_fail')
    plt.xlabel('T_ccd');
    plt.legend(fontsize='small')


/Users/aldcroft/miniconda3/envs/ska3/lib/python3.6/site-packages/ipykernel/__main__.py:23: UserWarning: 
Model grid-floor-2018-11 computed between t_ccd <= -16.0 <= -1.0, clipping input t_ccd(s) outside that range.
/Users/aldcroft/miniconda3/envs/ska3/lib/python3.6/site-packages/ipykernel/__main__.py:23: UserWarning: 
Model grid-floor-2018-11 computed between halfw <= 60.0 <= 180.0, clipping input halfw(s) outside that range.

In [50]:
# Check chandra_aca implementation vs. native model from this notebook
mags = np.linspace(5, 12, 40)
t_ccds = np.linspace(-16, -1, 40)
halfws = np.linspace(60, 180, 7)
mag, t_ccd, halfw = np.meshgrid(mags, t_ccds, halfws, indexing='ij')

# First color != 1.5
#  Notebook
nb_probs = floor_model_acq_prob(mag, t_ccd, halfwidth=halfw, probit=True, color=1.0)
#  Chandra_aca.  Note that grid_model returns p_success, so need to negate it.
ca_probs = -grid_model_acq_prob(mag, t_ccd, halfwidth=halfw, probit=True, color=1.0)

assert nb_probs.shape == ca_probs.shape
print('Max difference is {:.3f}'.format(np.max(np.abs(nb_probs - ca_probs))))
assert np.allclose(nb_probs, ca_probs, rtol=0, atol=0.1)


Max difference is 0.067

In [51]:
d_probs = (nb_probs - ca_probs)[:, :, 3]
plt.imshow(d_probs, origin='lower', extent=[-16, -1, 5, 12], aspect='auto', cmap='jet')
plt.colorbar();
plt.title('Delta between probit p_fail: analytical vs. gridded');



In [52]:
mags = np.linspace(8, 11, 200)
plt.figure()
for ii, t_ccd in enumerate(np.arange(-16, -0.9, 2)):
    p_fails = floor_model_acq_prob(mags, t_ccd, probit=True)
    plt.plot(mags, p_fails, color=f'C{ii}')
    p_success = grid_model_acq_prob(mags, t_ccd, probit=True)
    plt.plot(mags, -p_success, color=f'C{ii}')
plt.grid()
plt.xlim(8, 11)


Out[52]:
(8, 11)

Generate regression data for chandra_aca

The real testing is done here with a copy of the functions from chandra_aca, but now generate some regression test data as a smoke test that things are working on all platforms.


In [53]:
mags = [9, 9.5, 10.5]
t_ccds = [-10, -5]
halfws = [60, 120, 160]
mag, t_ccd, halfw = np.meshgrid(mags, t_ccds, halfws, indexing='ij')

probs = floor_model_acq_prob(mag, t_ccd, halfwidth=halfw, probit=True, color=1.0)
print(repr(probs.round(3).flatten()))

probs = floor_model_acq_prob(mag, t_ccd, halfwidth=halfw, probit=False, color=1.5)
probs = stats.norm.ppf(probs)
print(repr(probs.round(3).flatten()))


array([-2.275, -2.275, -2.275, -2.275, -1.753, -1.467, -1.749, -1.749,
       -1.749, -1.503, -0.948, -0.662,  0.402,  0.957,  1.244,  1.546,
        2.101,  2.387])
array([-1.657, -1.53 , -1.455, -1.311, -1.033, -0.863, -1.167, -0.974,
       -0.875, -0.695, -0.382, -0.204,  0.386,  0.758,  0.938,  1.133,
        1.476,  1.639])

In [ ]:

Appending: playing with smoothing the fit_parvals

Unfortunuately this introduces noticable degradation in the comparison with flight / ASVT data.


In [54]:
from scipy.interpolate import UnivariateSpline
from astropy.convolution import convolve, Box1DKernel
n_sm = 200
x = fit_parvals_mags = np.linspace(5, 13, n_sm)
fit_parvals_smooth = np.zeros(shape=(4, n_sm))

for ii in range(4):
    y = np.interp(x=x, xp=mag_bin_centers, fp=fit_parvals[ii])
    func = UnivariateSpline(x, y, k=3, s=20)
    plt.plot(x, y, color=f'C{ii}')
    ysm = convolve(y, Box1DKernel(10), boundary='extend')
    plt.plot(x, ysm, '--', color=f'C{ii}')
    plt.xlim(8, 11)
    fit_parvals_smooth[ii] = ysm

# FLOOR = {'fit_parvals': fit_parvals_smooth,
#          'mag_bin_centers': fit_parvals_mags}



In [ ]: