-
Notifications
You must be signed in to change notification settings - Fork 0
/
lorri_driftscan_sim.py
227 lines (159 loc) · 7.34 KB
/
lorri_driftscan_sim.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Tue Jan 24 11:44:27 2017
@author: throop
"""
# lorri_driftscan_sim -- simulate a LORRI driftscan of a given image
# This is for HBT's NH MU69 MT KBO/MT1.3-G4-4.2
# https://www.spaceops.swri.org/nh/wiki/index.php/KBO/MT1.3-G4-4.2
# HBT 24-Jan-2017
# General python imports
import pdb
import glob
import math # We use this to get pi. Documentation says math is 'always available'
# but apparently it still must be imported.
import os.path
import os
import subprocess
import astropy
from astropy.io import fits
from astropy.table import Table
import astropy.table # I need the unique() function here. Why is in in table and not Table??
import matplotlib
import matplotlib.pyplot as plt # pyplot
from matplotlib.figure import Figure
import numpy as np
import astropy.modeling
from scipy.optimize import curve_fit
#from pylab import * # So I can change plot size.
# Pylab defines the 'plot' command
import spiceypy as sp
#from itertools import izip # To loop over groups in a table -- see astropy tables docs
from astropy.wcs import WCS
from astroquery.vo_conesearch import conesearch
from astropy import units as u # Units library
from astropy.coordinates import SkyCoord # To define coordinates to use in star search
#from photutils import datasets
# HBT imports
import hbt
dir = '/Users/throop/data/NH_misc/'
file = dir + 'lor_0235025618_0x633_sci_1.fit' # NGC3532 - open cluster - not a good choice - 0.4 sec 4X4
#file = dir + 'lor_0235132638_0x633_sci_1.fit'
#file = dir + 'lor_0268266239_0x630_sci_1.fit' # 0.1 sec opnav 1X1
#
ang_deadband_deg = 0.03
hbt.figsize((8,8)) # Image size
stretch_percent = 99
ang_rate_nominal = 30e-6 # Angular drift rate, radians/sec
length_sec = 29.9 # output exposure time
rate_drift = 30e-6 # rad/sec. For 30 urad/sec, that is 5 1x1 pixels/sec, or 150 pixels in a 30-sec exposure.
# That's pretty good -- i.e., most images will have zero thruster hits in them.
num_steps = 500 # Add this many frames to create output frame
hdulist = fits.open(file)
im = hdulist['PRIMARY'].data
stretch = astropy.visualization.PercentileInterval(stretch_percent)
plt.set_cmap('Greys_r')
header = hdulist['PRIMARY'].header
exptime = header['EXPTIME']
plt.imshow(stretch(im))
IS_4X4 = (header['SFORMAT'] == '4X4') # Is the image we loaded in 4X4 format?
# Get the angular pixel size
ang_fov = 0.3*u.degree.to('radian') # radians in LORRI FOV
if IS_4X4:
ang_pix = ang_fov / 256 # Radians per pixe, 4X4
else:
ang_pix = ang_fov / 1024 # Radians per pixel, 1X1
ang_deadband = ang_deadband_deg * hbt.d2r # Deadband size
drift_per_sec_pix = rate_drift / ang_pix
num_sum = num_steps # corect?? XXX I had to add this line; not sure if it is right or not.
dt = length_sec / num_sum # Timestep
t = hbt.frange(0, length_sec, num_sum)
# Need to make a function which
ang_x = np.zeros(num_steps) # Angle, in radians, at each timestep
ang_y = np.zeros(num_steps)
ang_rate_x = np.zeros(num_steps) # Drift rate, in radians/second, at each timestep
ang_rate_y = np.zeros(num_steps)
# Set the position and drift rate at initial timestep
ang_x[0] = 0
ang_y[0] = 0
ang_rate_x[:] = ang_rate_nominal
ang_rate_y[:] = ang_rate_nominal/2
for i in range(num_steps):
ang_x[i] = ang_x[i-1] + ang_rate_x[i]*dt
ang_y[i] = ang_y[i-1] + ang_rate_y[i]*dt
if (abs(ang_x[i]) > ang_deadband):
ang_rate_x[i:-1] *= -1
if (abs(ang_y[i]) > ang_deadband):
ang_rate_y[i:-1] *= -1
im_out = im.copy()
for i in range(num_steps):
dx_pix = int(ang_x[i] / ang_pix)
dy_pix = int(ang_y[i] / ang_pix)
im_i = np.roll(im, dx_pix, 0) # Shift it
im_i = np.roll(im_i, dy_pix, 1) # Sum it
im_out = im_i + im_out
# print ("Rolled {}, {}".format(dx_pix, dy_pix))
# Convert to 4x4 if it is not.
#if not(IS_4X4):
# im_out = im_out[::4,::4] # Downsample it to 4x4
ax = plt.imshow((im_out))
ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)
plt.title('LORRI driftscan {} sec, {} urad/sec, deadband {} deg'.format(length_sec, rate_drift*1e6, ang_deadband_deg))
file_out = dir + 'out/lorri_driftscan_t{}_rate{}_db{}.png'.format(length_sec, rate_drift*1e6, ang_deadband_deg)
plt.savefig(file_out)
print('Wrote: ' + file_out)
plt.show()
#==============================================================================
# Now, do an unrelated calculation to determine the SNR that we'd get for a driftscan observation
# if it happened to go across a ring.
#==============================================================================
#%%
file_tm_dayside = '/Users/throop/git/NH_rings/kernels_nh_pluto_mu69_tcm22.tm' # Sort of hacked this together
utc_ca = '2019 1 Jan 07:00:00'
sp.furnsh(file_tm_dayside)
#Get the typical
et_ca = sp.utc2et(utc_ca)
hour = 3600
minute = 60
dt = -12 * hour # What is the offset of our observation, from KBO C/A. K-1h, K+2d, etc.
ddt = 1*minute # Time offset for my calculation of velocity
width_pix_rad_4x4 = 4 * (0.3*hbt.d2r / 1024) # LORRI 4x4 pixel size, in radians
width_pix_rad_1x1 = (0.3*hbt.d2r / 1024) # LORRI 4x4 pixel size, in radians
et_0 = et_ca + dt
(state_0, lt_0) = sp.spkezr('MU69', et_0, 'J2000', 'LT+S', 'New Horizons')
(state_1, lt_1) = sp.spkezr('MU69', et_0 + ddt, 'J2000', 'LT+S', 'New Horizons')
(junk, ra_0, dec_0) = sp.recrad(state_0[0:3])
(junk, ra_1, dec_1) = sp.recrad(state_1[0:3])
omega_kbo = sp.vsep(state_0[0:3], state_1[0:3]) / ddt # Radians per second of sky motion that the KBO has, from NH
dist_kbo = sp.vnorm(state_0[0:3]) # Distance to KBO, in km
# Calculate the shadow velocity of the KBO
v_shadow_kbo = omega_kbo * dist_kbo # km/sec of the shadow
# Calculate the time resolution of the LORRI driftscan.
# That is, how long does it take for LORRI to drift one 4x4 LORRI pixel?
# [A: Basically one sec: 0.681 sec, technically.]
dt_lorri_driftscan = width_pix_rad_4x4 / rate_drift
# Calculate Roche radius
r_kbo = 16.5 * u.km
r_roche = 2.5 * r_kbo
r_roche_km = r_roche.to('km').value
res_occ_kbo = v_shadow_kbo / dt_lorri_driftscan # Resolution, in km
print("-----")
print("T = K{:-.2f} h, dist = {:.2f} km".format(dt/hour, dist_kbo))
print("KBO Shadow velocity = {:.2f} km/s".format(v_shadow_kbo))
print("LORRI drift rate (deadband) = {:.2e} rad/sec = {:.2f} 4x4 pix/sec.".format(
rate_drift, rate_drift / width_pix_rad_4x4))
print("LORRI time resolution in a driftscan (ie, time to move one pixel) = {:.2f} sec.".format(dt_lorri_driftscan))
print("Time for KBO to move one LORRI 4x4 pixel = {:.2f} sec".format(
width_pix_rad_4x4 * dist_kbo / v_shadow_kbo))
# Compare speeds of KBO and of LORRI drift
print("LORRI is drifting {:.2f} x as fast as KBO".format(rate_drift / omega_kbo))
# Get our final answer, for the spatial resolution we can observe with driftscan
# XXX I don't think this one is correct below
print("Final spatial resolution of a driftscan observation at K{:-.2f} h = {:.2f} km".format(
dt/hour, v_shadow_kbo / dt_lorri_driftscan))
print("At this same time, the 4x4 imaging resolution of LORRI is: {:.2f} km/pix".format(
dist_kbo * width_pix_rad_4x4))
print("r_roche = {:.2f} km = {:.2f} LORRI 4x4 pix.".format(r_roche_km,
r_roche_km /dist_kbo / width_pix_rad_4x4))