Skip to content

Commit 3f73953

Browse files
authored
Add files via upload
1 parent 1796673 commit 3f73953

2 files changed

Lines changed: 370 additions & 0 deletions

File tree

src/norm_diff_class.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/usr/bin/env python
2+
"""
3+
4+
norm_diff_class.py
5+
6+
Purpose: Apply previously made calibration file to calibrate raw data
7+
(frequency correct and normalize), which produces the diffraction
8+
pattern given to Fresnel Inversion code.
9+
10+
Revisions:
11+
gjs_produce_normalized_diffraction_pattern.py
12+
Mar 07 2018 - gsteranka - Original version. Tentative rough draft, since
13+
haven't yet chosen format of calibration file, and
14+
also don't have Ryan's code to find window width
15+
from a given window type and resolution.
16+
Mar 19 2018 - gsteranka - Edited to take new tentative final form of cal
17+
file, which has the columns as 1) SPM 2) Predicted
18+
sky frequency 3) Residual frequency fit
19+
4) Freespace power spline fit 5) Frequency offset
20+
fit. Also edited to save an obs file and return
21+
everything in the obs file too
22+
produce_normalized_diffraction_pattern.py
23+
Mar 20 2018 - gsteranka - Eliminate debug steps, and switched obs_file to be
24+
optional inputs
25+
norm_diff_class.py
26+
Mar 22 2018 - gsteranka - Edit previous code to be a class, with Fresnel
27+
inversion inputs as attributes
28+
Mar 30 2018 - gsteranka - Edited reading in of geometry file to read in csv
29+
format
30+
Apr 03 2018 - gsteranka - When first defining p_norm_vals, do it by
31+
normalizing IQ_c_desired, not IQ_m_desired
32+
Apr 09 2018 - gsteranka - Obs files now save absolute value of rho dot,
33+
which eliminates a lot of error checking in
34+
Fresnel Inversion step. Added
35+
"fill_value='extrapolate'" to rho_interp_func,
36+
p_free_interp_func, spm_desired_func, and
37+
f_sky_pred_func
38+
"""
39+
40+
import numpy as np
41+
import pandas as pd
42+
from scipy.interpolate import interp1d
43+
import spiceypy as spice
44+
import sys
45+
46+
from rsr_reader import RSRReader
47+
from resample_IQ import resample_IQ
48+
49+
50+
class NormDiff(object):
51+
"""Produce a normalized diffraction pattern using a previously made
52+
calibration file (cal_file). Contains everything needed for Fresnel
53+
Inversion as attributes.
54+
55+
Attributes:
56+
rho_km_desired (np.ndarray): Ring intercept radius values, in km,
57+
at final spacing specified in dr_km
58+
spm_desired (np.ndarray): Set of SPM values corresponding to
59+
rho_km_desired
60+
p_norm_vals (np.ndarray): Power normalized to 1. This is the diffraction
61+
pattern that is input to the Fresnel inversion step
62+
phase_rad_vals (np.ndarray): Phase of the complex signal, in radians.
63+
This is the other part of the diffraction pattern that is input to
64+
the Fresnel Inversion step
65+
B_rad_vals (np.ndarray): Ring opening angle of Saturn
66+
D_km_vals (np.ndarray): Spacecraft-RIP (Ring Intercept Point) distance
67+
F_km_vals (np.ndarray): Fresnel scale, in km
68+
f_sky_pred_vals (np.ndarray): Predicted sky frequency, in Hz.
69+
phi_rad_vals (np.ndarray): Observed ring azimuth, in radians
70+
rho_dot_kms_vals (np.ndarray): Ring intercept point velocity
71+
"""
72+
73+
def __init__(self, rsr_file, dr_km, rho_km_range, res_km, window_type,
74+
geo_file, cal_file, dr_km_tol=0.01,
75+
decimate_16khz_to_1khz=False):
76+
"""
77+
Instantiation defines all attributes of instance.
78+
79+
Args:
80+
rsr_file (str): Full path name of RSR file to process
81+
dr_km (float): Desired final radial spacing of processed data, in km
82+
rho_km_range (list): Radius range to process over, in km
83+
res_km (float): Resolution to later do Fresnel Inversion at, in km
84+
window_type (str): Window type to later do Fresnel Inversion at
85+
geo_file (str): Full path name of a geoemtry file corresponding to
86+
rsr_file
87+
cal_file (str): Full path name of a calibration file corresponding to
88+
rsr_file
89+
dr_km_tol (float): Optional keyword argument, in km, that specifies the
90+
maximum distance the starting point of the final set of radius
91+
values can be away from an integer number of dr_km. For example, if
92+
you say dr_km_tol=0.01 and dr_km=0.25, the starting point of the
93+
final set of rho values could be 70000.26 km, but not 70000.261 km
94+
decimate_16khz_to_1khz (bool): Optional boolean keyword argument that
95+
specifies whether or not to decimate a 16khz sample rate file down
96+
to 1khz sample rate. Can only specify when rsr_file is 16khz. Value
97+
of this should match what you said in RSRfile.get_IQ method when you
98+
initially made the calibration file. Defualt is False
99+
"""
100+
101+
# Magical code to find radius range necessary for Fresnel Inversion.
102+
# For now just use the input radius range
103+
rho_km_range_inversion = rho_km_range
104+
105+
# Read geometry file for radius-SPM conversion
106+
geo = pd.read_csv(geo_file, header=None)
107+
spm_geo = np.asarray(geo[0][:])
108+
rho_km_geo = np.asarray(geo[3][:])
109+
B_rad_geo = np.asarray(geo[6][:]) * spice.rpd()
110+
D_km_geo = np.asarray(geo[7][:])
111+
phi_rad_geo = np.asarray(geo[5][:]) * spice.rpd()
112+
rho_dot_kms_geo = np.asarray(geo[8][:])
113+
F_km_geo = np.asarray(geo[10][:])
114+
115+
# Function to convert SPM to rho
116+
rho_interp_func = interp1d(spm_geo, rho_km_geo, fill_value='extrapolate')
117+
118+
# Find SPM range corresponding to rho range. Bottom-most line of this
119+
# block is to handle ingress files
120+
spm_start = max(spm_geo[rho_km_geo <= rho_km_range[0]])
121+
spm_end = min(spm_geo[rho_km_geo >= rho_km_range[1]])
122+
spm_range = [spm_start, spm_end]
123+
spm_range = [min(spm_range), max(spm_range)]
124+
125+
rsr_inst = RSRReader(rsr_file)
126+
(spm_vals, IQ_m) = rsr_inst.get_IQ(spm_range=spm_range,
127+
decimate_16khz_to_1khz=
128+
decimate_16khz_to_1khz)
129+
rho_km_vals = rho_interp_func(spm_vals)
130+
131+
cal = pd.read_csv(cal_file, delim_whitespace=True, header=None)
132+
133+
spm_cal = np.asarray(cal[0][:])
134+
f_sky_pred_cal = np.asarray(cal[1][:])
135+
p_free_cal = np.asarray(cal[3][:])
136+
f_offset_fit_cal = np.asarray(cal[4][:])
137+
rho_km_cal = rho_interp_func(spm_cal)
138+
139+
# If specified rho range that cal file can't cover
140+
if (spm_range[0]<min(spm_cal)) | (spm_range[1]>max(spm_cal)):
141+
print('ERROR (NormDiff): Specified cal file is missing \n'
142+
+'points required to pre-process points in rho_km_range.\n'
143+
+'Calibration file rho range is '+str(min(rho_km_cal))+'km, '
144+
+str(max(rho_km_cal))+'km, while specified rho_km_range is \n'
145+
+str(rho_km_range[0])+'km, '+str(rho_km_range[1])+'km')
146+
sys.exit()
147+
148+
# Inteprolate frequency offset to finer spacing in preparation
149+
# for integration
150+
dt = 0.1
151+
n_pts = round((spm_cal[-1] - spm_cal[0])/dt)
152+
spm_cal_interp = spm_cal[0] + dt*np.arange(n_pts)
153+
f_offset_fit_func = interp1d(spm_cal, f_offset_fit_cal,
154+
fill_value='extrapolate')
155+
f_offset_fit_interp = f_offset_fit_func(spm_cal_interp)
156+
157+
# Integrate frequency offset to detrend IQ_m
158+
f_detrend_interp = np.cumsum(f_offset_fit_interp)*dt
159+
f_detrend_interp_rad = f_detrend_interp * spice.twopi()
160+
f_detrend_rad_func = interp1d(spm_cal_interp, f_detrend_interp_rad,
161+
fill_value='extrapolate')
162+
f_detrend_rad = f_detrend_rad_func(spm_vals)
163+
164+
# Detrend raw measured I and Q
165+
IQ_c = IQ_m*np.exp(-1j*f_detrend_rad)
166+
167+
(rho_km_desired, IQ_c_desired) = resample_IQ(rho_km_vals, IQ_c, dr_km,
168+
dr_km_tol=dr_km_tol)
169+
170+
# Interpolate freespace power (spline fit) to rho_km_desired
171+
p_free_interp_func = interp1d(rho_km_cal, p_free_cal,
172+
fill_value='extrapolate')
173+
p_free = p_free_interp_func(rho_km_desired)
174+
175+
# Construct power from IQ_c_desired and not IQ_m_desired or resampled
176+
# power, because p_free was constructed from downsampled IQ_c. It's
177+
# important to not use IQ_m_desired because I and Q each vary
178+
# sinusoidally, meaning that if you resample too much, they get
179+
# averaged to near zero.
180+
p_norm_vals = (abs(IQ_c_desired)**2)/p_free
181+
phase_rad_vals = np.arctan2(np.imag(IQ_c_desired),
182+
np.real(IQ_c_desired))
183+
184+
spm_desired_func = interp1d(rho_km_geo, spm_geo,
185+
fill_value='extrapolate')
186+
spm_desired = spm_desired_func(rho_km_desired)
187+
188+
self.__set_attributes(rho_km_desired, spm_desired, p_norm_vals,
189+
phase_rad_vals,
190+
rho_km_geo, B_rad_geo, D_km_geo, F_km_geo,
191+
phi_rad_geo, rho_dot_kms_geo,
192+
rho_km_cal, f_sky_pred_cal)
193+
194+
195+
def __set_attributes(self, rho_km_desired, spm_desired, p_norm_vals,
196+
phase_rad_vals,
197+
rho_km_geo, B_rad_geo, D_km_geo, F_km_geo, phi_rad_geo,
198+
rho_dot_kms_geo,
199+
rho_km_cal, f_sky_pred_cal):
200+
"""Private method called by __init__ to set attributes of the
201+
instance"""
202+
203+
self.rho_km_desired = rho_km_desired
204+
self.spm_desired = spm_desired
205+
self.p_norm_vals = p_norm_vals
206+
self.phase_rad_vals = phase_rad_vals
207+
208+
# Ring opening angle at final spacing
209+
B_rad_func = interp1d(rho_km_geo, B_rad_geo, fill_value='extrapolate')
210+
self.B_rad_vals = B_rad_func(rho_km_desired)
211+
212+
# Spacecraft - RIP distance at final spacing
213+
D_km_func = interp1d(rho_km_geo, D_km_geo, fill_value='extrapolate')
214+
self.D_km_vals = D_km_func(rho_km_desired)
215+
216+
# Fresnel scale at final spacing
217+
F_km_func = interp1d(rho_km_geo, F_km_geo, fill_value='extrapolate')
218+
self.F_km_vals = F_km_func(rho_km_desired)
219+
220+
# Sky frequency at final spacing
221+
f_sky_pred_func = interp1d(rho_km_cal, f_sky_pred_cal,
222+
fill_value='extrapolate')
223+
self.f_sky_pred_vals = f_sky_pred_func(rho_km_desired)
224+
225+
# Observed Ring Azimuth at final spacing
226+
phi_rad_func = interp1d(rho_km_geo, phi_rad_geo, fill_value='extrapolate')
227+
self.phi_rad_vals = phi_rad_func(rho_km_desired)
228+
229+
# RIP velocity at final spacing
230+
rho_dot_kms_func = interp1d(rho_km_geo, rho_dot_kms_geo,
231+
fill_value='extrapolate')
232+
self.rho_dot_kms_vals = rho_dot_kms_func(rho_km_desired)
233+
234+
235+
def save_obs(self, obs_file):
236+
"""Save an obs file, which contains all of the attributes in the
237+
instance, and therefore has everything needed for Fresnel Inversion
238+
239+
Args:
240+
obs_file (str): Full path name of obs file to be created
241+
"""
242+
243+
# Save obs file with absolute value of rho dot. With this and
244+
# increasing rho_km_desired, ingress files can be put into
245+
# inversion code without any error checks to differentiate
246+
# from egress
247+
np.savetxt(obs_file,
248+
np.c_[self.rho_km_desired, self.spm_desired,
249+
self.p_norm_vals, self.phase_rad_vals,
250+
self.B_rad_vals, self.D_km_vals, self.F_km_vals,
251+
self.f_sky_pred_vals, self.phi_rad_vals,
252+
abs(self.rho_dot_kms_vals)],
253+
fmt='%32.16f '*10)
254+
255+
256+
if __name__ == '__main__':
257+
pass

src/resample_IQ.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env python
2+
"""
3+
4+
resample_IQ.py
5+
6+
Purpose: Resample I and Q from uniformly spaced time to uniformly spaced
7+
radius. Normally, you resample from raw resolution data.
8+
9+
Revisions:
10+
gjs_resample_IQ.py
11+
Feb 27 2018 - gsteranka - Original version
12+
Mar 01 2018 - gsteranka - Took pre_resample function outside of resample_IQ
13+
function, since I found out it doesn't need to be
14+
inside to be recognized in a call to resample_IQ
15+
from another file
16+
Mar 07 2018 - gsteranka - Input is a complex number IQ_c, instead of
17+
separate real and imaginary parts
18+
Mar 08 2018 - gsteranka - Change input arrays so rho starts within dr_km_tol
19+
of an integer number of dr_km. Makes it so after
20+
resampling, everything is at a more even number
21+
resample_IQ.py
22+
Mar 20 2018 - gsteranka - Copy to official version and remove debug steps
23+
Apr 09 2018 - gsteranka - Edited pre_resample function to handle ingerss
24+
files as well as egress
25+
"""
26+
27+
import numpy as np
28+
from scipy import signal
29+
from scipy.interpolate import interp1d
30+
31+
def pre_resample(rho_km, vec, freq):
32+
"""Sub-function inside of resample_IQ to put vectors to uniform spaced
33+
radius at a similar spacing as raw resolution. For ingress occultations,
34+
this step implicitly reverses the radius scale when interpolating"""
35+
36+
# Average radius spacing over region
37+
ts_avg = abs(rho_km[-1] - rho_km[0])/float(len(rho_km) - 1)
38+
p = 1
39+
q = int(round(1.0/(ts_avg*freq)))
40+
dr_grid = float(p)/(q*freq)
41+
42+
# Uniform radius grid at near-raw resolution to interpolate to
43+
# For ingress, this implicitly reverses radius scale!
44+
n_pts = round(abs(rho_km[-1] - rho_km[0])/dr_grid)
45+
rho_grid = min(rho_km) + dr_grid*np.arange(n_pts)
46+
47+
# Interpolate to near-raw resolution
48+
# For ingress, this implicitly reverses radius scale!
49+
vec_grid_interp = interp1d(rho_km, vec, kind='linear',
50+
fill_value='extrapolate')
51+
vec_grid = vec_grid_interp(rho_grid)
52+
53+
return rho_grid, vec_grid, p, q
54+
55+
56+
def resample_IQ(rho_km, IQ_c, dr_desired, dr_km_tol=0.01, TEST=False):
57+
"""Function to resample I and Q to uniformly spaced radius. Based off of
58+
Matlab's resample function
59+
60+
Example:
61+
>>> fit_inst = FreqOffsetFit(RSR_inst, geo_inst, f_spm, f_offset, \
62+
f_USO, kernels, k=k, rho_exclude=rho_exclude)
63+
>>> (spm_raw, IQ_c_raw) = fit_inst.get_IQ_c()
64+
>>> (rho_km_desired, IQ_c_resampled) = resample_IQ(rho_km_raw, \
65+
IQ_c_raw, dr_desired, dr_km_tol=dr_km_tol, TEST=TEST)
66+
67+
Args:
68+
rho_km (np.ndarray): Set of ring intercept point values at initial
69+
resolution before resampling
70+
IQ_c (np.ndarray): Frequency corrected complex signal at initial
71+
resolution before resampling
72+
dr_desired (float): Desired final radial spacing to resample to
73+
dr_km_tol (float): Maximum distance from an integer number of
74+
dr_desired that the first rho value will be at. Makes the final
75+
set of rho values more regular-looking. For example, if you say
76+
dr_km_tol=0.01 with a dr_desired of 0.25, your final set of rho
77+
values might look something like [70000.26, 70000.51, ...]
78+
TEST (bool): Testing variable to print out the first few resampled
79+
results
80+
"""
81+
82+
# Begin raw resolution rho within dr_km_tol of integer number of dr_desired
83+
rho_km_remainder = rho_km % dr_desired
84+
rho_km_start_ind = int((np.argwhere(rho_km_remainder < dr_km_tol))[0])
85+
rho_km = rho_km[rho_km_start_ind:-1]
86+
IQ_c = IQ_c[rho_km_start_ind:-1]
87+
88+
I_c = np.real(IQ_c)
89+
Q_c = np.imag(IQ_c)
90+
91+
# Pre-resampling steps. Interpolates to uniform radius at near-raw spacing
92+
(rho_km_uniform, I_c_uniform, p, q) = pre_resample(rho_km, I_c,
93+
1.0/dr_desired)
94+
(rho_km_uniform, Q_c_uniform, p, q) = pre_resample(rho_km, Q_c,
95+
1.0/dr_desired)
96+
97+
# Downsample by factor q to desired final spacing
98+
I_c_desired = signal.resample_poly(I_c_uniform, p, q)
99+
Q_c_desired = signal.resample_poly(Q_c_uniform, p, q)
100+
101+
rho_km_desired = rho_km_uniform[0] + dr_desired*np.arange(len(I_c_desired))
102+
103+
if TEST:
104+
print('First 10 rho, I_c, Q_c:')
105+
for i in range(10):
106+
print('%24.16f %24.16f %24.16f' %
107+
(rho_km_desired[i], I_c_desired[i], Q_c_desired[i]))
108+
109+
return rho_km_desired, I_c_desired + 1j*Q_c_desired
110+
111+
112+
if __name__ == '__main__':
113+
pass

0 commit comments

Comments
 (0)