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