|
| 1 | +#!/usr/bin/env python |
| 2 | +""" |
| 3 | +
|
| 4 | +power_normalization.py |
| 5 | +
|
| 6 | +Purpose: Normalize frequency-corrected power using a spline fit of specified |
| 7 | + order. |
| 8 | +
|
| 9 | +NOTE: Dependent on final format of geometry instances ("geo_inst") |
| 10 | +
|
| 11 | +Revisions: |
| 12 | + gjs_power_normalization.py |
| 13 | + Feb 26 2018 - gsteranka - Original version |
| 14 | + gjs_power_normalization_v2.py |
| 15 | + Mar 06 2018 - gsteranka - Edited so user only needs to instantiate and |
| 16 | + run get_spline_fit, which now returns spline fit |
| 17 | + for a specified array of SPM values. |
| 18 | + gjs_power_normalization_v3.py |
| 19 | + Mar 19 2018 - gsteranka - Edited so default inputs are set as attributes in |
| 20 | + __init__, and the inputs in get_spline_fit can |
| 21 | + override those defaults as the new attribute |
| 22 | + values. Done this way to keep track of what the |
| 23 | + previous fit specifications were inside of the |
| 24 | + instance. |
| 25 | + power_normalization.py |
| 26 | + Mar 20 2018 - gsteranka - Edited to get rid of debug steps |
| 27 | + Mar 20 2018 - gsteranka - Fixed bugs with spm_vals_down getting defined |
| 28 | + wrong, and with default knots sometimes stretching |
| 29 | + beyond the input times to make a spline fit for. |
| 30 | + Added error check if no input points fall within |
| 31 | + specified freespace regions |
| 32 | + Apr 04 2018 - gsteranka - In initial downsampling step before resample_poly, |
| 33 | + downsample IQ_c_raw instead of p_obs_raw. This |
| 34 | + accompanies the recent change in norm_diff_class.py |
| 35 | + Apr 04 2018 - gsteranka - In loop to make knots_spm and knots_rho, change i |
| 36 | + index to int(i) |
| 37 | + Apr 05 2018 - gsteranka - Added index sorting right before splrep to take |
| 38 | + ingress files, since splrep expects x values to be |
| 39 | + in order, which isn't true for ingress |
| 40 | +""" |
| 41 | + |
| 42 | +import numpy as np |
| 43 | +import pandas as pd |
| 44 | +from scipy import signal |
| 45 | +from scipy.interpolate import interp1d |
| 46 | +from scipy.interpolate import splrep |
| 47 | +from scipy.interpolate import splev |
| 48 | +import sys |
| 49 | + |
| 50 | +class Normalization(object): |
| 51 | + """Class to get a power normalizing spline fit for frequency corrected |
| 52 | + data uniformly spaced in time at raw resolution. |
| 53 | +
|
| 54 | + Example: |
| 55 | + >>> fit_inst = FreqOffsetFit(rsr_inst, geo_inst, f_spm, f_offset, \ |
| 56 | + f_uso, kernels, k=k, rho_exclude=rho_exclude) |
| 57 | + >>> (spm_raw, IQ_c_raw) = fit_inst.get_IQ_c() |
| 58 | + >>> norm_inst = Normalization(spm_raw, IQ_c_raw, geo_inst) |
| 59 | + >>> spm_fit = np.arange(spm_raw[0], spm_raw[-1], 1.0) |
| 60 | + >>> spline_fit = norm_inst.get_spline_fit(spm_fit, k=k, \ |
| 61 | + knots_km=knots_km, dt_down=dt_down, \ |
| 62 | + freespace_km=freespace_km, TEST=TEST) |
| 63 | +
|
| 64 | + Attributes: |
| 65 | + _k (int): Order of the spline fit. Default order is 2 |
| 66 | + _knots_km (list): List of knots for the spline fit. Should only |
| 67 | + be at places where there is data. Specify in km |
| 68 | + _dt_down (float): Time spacing to downsample to before making |
| 69 | + a spline fit |
| 70 | + _freespace_km (list): Set of radius values, in km, to treat as |
| 71 | + free space. These are the regions that the spline fit is made |
| 72 | + to. Specify in km. Be sure to include a region for each knot |
| 73 | + specified |
| 74 | + __spm_raw (np.ndarray): Raw resolution SPM values |
| 75 | + __IQ_c_raw (np.ndarray): Raw resolution frequency corrected I and Q |
| 76 | + __rho_interp_func (scipy.interpolate.interpolate.interp1d): interp1d |
| 77 | + function to get set of rho values in km for a set of SPM |
| 78 | + """ |
| 79 | + |
| 80 | + def __init__(self, spm_raw, IQ_c_raw, geo_inst, TEST=False): |
| 81 | + """Instantiation defines raw resolution SPM, frequency corrected I |
| 82 | + and Q, and a function to interpolate radius to any SPM value. These |
| 83 | + are set as attributes |
| 84 | +
|
| 85 | + Args: |
| 86 | + spm_raw (np.ndarray): Raw resolution array of SPM values |
| 87 | + IQ_c_raw (np.ndarray): Frequency corrected complex signal at |
| 88 | + raw resolution |
| 89 | + geo_inst: Instance of geometry class. Contains attributes |
| 90 | + t_oet_spm and rho_km_vals. Can create mock version from a |
| 91 | + geometry file using geo_file_into_instance.py |
| 92 | + TEST (bool): Optional boolean argument that, if True, prints out |
| 93 | + intermediate values""" |
| 94 | + |
| 95 | + self.__spm_raw = spm_raw |
| 96 | + self.__IQ_c_raw = IQ_c_raw |
| 97 | + |
| 98 | + # Function to interpolate spm to rho values in km |
| 99 | + rho_interp_func = interp1d(np.asarray(geo_inst.t_oet_spm_vals), |
| 100 | + np.asarray(geo_inst.rho_km_vals), |
| 101 | + fill_value='extrapolate') |
| 102 | + self.__rho_interp_func = rho_interp_func |
| 103 | + |
| 104 | + # Default arguments for spline fit. Chosen because: |
| 105 | + # k: Order higher than 2 gives too much freedom to spline fit. |
| 106 | + # knots_km: By experimentation, these tend to give the most |
| 107 | + # consistently good spline fits. Sometimes need to |
| 108 | + # adjust if a file's free space power cuts off sooner |
| 109 | + # or begins later than expected |
| 110 | + # freespace_km: These regions are always free space and |
| 111 | + # uninterrupted by diffraction pattern or an |
| 112 | + # eccentric ringlet. These accompany the default |
| 113 | + # knots |
| 114 | + self._k = 2 |
| 115 | + self._knots_km = np.array([70445., 87400., 117730., 119950., 133550., |
| 116 | + 194269.]) |
| 117 | + self._dt_down = 0.5 |
| 118 | + self._freespace_km = [[69100.0, 73500], [87350.0, 87450.0], |
| 119 | + [117690.0, 117780.0], [119850.0, 120020.0], |
| 120 | + [133500.0, 133650.0], [137000.0, 194400.0]] |
| 121 | + |
| 122 | + if TEST: |
| 123 | + sample_rate_hz = int(round(1.0/(self.__spm_raw[1] - |
| 124 | + self.__spm_raw[0]))) |
| 125 | + print('\nGeometry SPM, rho:') |
| 126 | + for i in range(10): |
| 127 | + print(geo_inst.t_oet_spm_vals[i], geo_inst.rho_km_vals[i]) |
| 128 | + print('\nRaw resolution SPM, rho at 1 sec. interval:') |
| 129 | + for i in range(10): |
| 130 | + print(self.__spm_raw[i*sample_rate_hz], |
| 131 | + self.__rho_interp_func(self.__spm_raw[i*sample_rate_hz])) |
| 132 | + |
| 133 | + |
| 134 | + def __downsample_IQ(self, dt_down, TEST=False): |
| 135 | + """Downsample complex signal to specified time spacing to avoid |
| 136 | + diffraction pattern ruining spline fit |
| 137 | +
|
| 138 | + Args: |
| 139 | + dt_down (float): Time spacing to downsample to |
| 140 | + TEST (bool): If True, prints downsampled results |
| 141 | +
|
| 142 | + Returns: |
| 143 | + spm_vals_down (np.ndarray): SPM values after downsampling |
| 144 | + rho_km_vals_down (np.ndarray): Rho values after downsampling |
| 145 | + p_obs_down (np.ndarray): Observed power after downsampling""" |
| 146 | + |
| 147 | + # Downsampling coefficient q |
| 148 | + dt_raw = self.__spm_raw[1] - self.__spm_raw[0] |
| 149 | + q = round(dt_down / dt_raw) |
| 150 | + |
| 151 | + # Downsample IQ_c by factor of q and not power because this is to |
| 152 | + # match the resampling done in norm_diff_class.py, where it also |
| 153 | + # resamples IQ_c |
| 154 | + IQ_c_down = signal.resample_poly(self.__IQ_c_raw, 1, q) |
| 155 | + p_obs_down = abs(IQ_c_down)**2 |
| 156 | + |
| 157 | + # New SPM, rho, and p_obs, at the downsampled resolution |
| 158 | + spm_vals_down = np.linspace(self.__spm_raw[0], self.__spm_raw[-1], |
| 159 | + num=len(p_obs_down)) |
| 160 | + rho_km_vals_down = self.__rho_interp_func(spm_vals_down) |
| 161 | + |
| 162 | + if TEST: |
| 163 | + print('\nDownsampled SPM, rho:') |
| 164 | + for i in range(10): |
| 165 | + print(spm_vals_down[i], rho_km_vals_down[i]) |
| 166 | + |
| 167 | + return spm_vals_down, rho_km_vals_down, p_obs_down |
| 168 | + |
| 169 | + |
| 170 | + def get_spline_fit(self, spm_fit, k=None, knots_km=None, |
| 171 | + dt_down=None, freespace_km=None, TEST=False): |
| 172 | + """Make spline fit to observed downsampled power at specified set |
| 173 | + of times spm_fit. Specified keyword arguments will override the |
| 174 | + corresponding defaults set in __init__ |
| 175 | +
|
| 176 | + Args: |
| 177 | + spm_fit (np.ndarray): SPM values to evaluate the spline fit at |
| 178 | + k (int): Order of the spline fit. Default order is 2 |
| 179 | + knots_km (list): List of knots for the spline fit. Should only |
| 180 | + be at places where there is data. Specify in km |
| 181 | + dt_down (float): Time spacing to downsample to before making |
| 182 | + a spline fit |
| 183 | + freespace_km (list): Set of radius values, in km, to treat as |
| 184 | + free space. These are the regions that the spline fit is made |
| 185 | + to. Specify in km. Be sure to include a region for each knot |
| 186 | + specified |
| 187 | + TEST (bool): If True, print out intermediate values |
| 188 | +
|
| 189 | + Returns: |
| 190 | + spline_fit: Spline fit to observed power at the specified times |
| 191 | + spm_fit""" |
| 192 | + |
| 193 | + # Update defaults if any keyword arguments were specified |
| 194 | + if k is not None: |
| 195 | + self._k = k |
| 196 | + if knots_km is not None: |
| 197 | + self._knots_km = knots_km |
| 198 | + if dt_down is not None: |
| 199 | + self._dt_down = dt_down |
| 200 | + if freespace_km is not None: |
| 201 | + self._freespace_km = freespace_km |
| 202 | + |
| 203 | + # Use whatever most up-to-date arguments are |
| 204 | + k = self._k |
| 205 | + knots_km = self._knots_km |
| 206 | + dt_down = self._dt_down |
| 207 | + freespace_km = self._freespace_km |
| 208 | + |
| 209 | + # Downsample I and Q to the time spacing dt_down |
| 210 | + (spm_vals_down, rho_km_vals_down, |
| 211 | + p_obs_down) = self.__downsample_IQ(dt_down, TEST=TEST) |
| 212 | + |
| 213 | + # Define freespace regions |
| 214 | + ind_free = [] |
| 215 | + for i in range(len(freespace_km)): |
| 216 | + ind_free.append(np.argwhere((rho_km_vals_down > freespace_km[i][0]) |
| 217 | + & (rho_km_vals_down < freespace_km[i][1]))) |
| 218 | + ind_free = np.reshape(np.concatenate(ind_free), -1) |
| 219 | + |
| 220 | + # Limit downsampled data to freespace regions |
| 221 | + spm_vals_free = spm_vals_down[ind_free] |
| 222 | + rho_km_vals_free = rho_km_vals_down[ind_free] |
| 223 | + p_obs_free = p_obs_down[ind_free] |
| 224 | + |
| 225 | + # Find knots in range of specified times |
| 226 | + knots_km_where = np.argwhere( |
| 227 | + np.logical_and(knots_km>min(rho_km_vals_free), |
| 228 | + knots_km<max(rho_km_vals_free))) |
| 229 | + |
| 230 | + # If specified times don't have freespace region, you can't make a fit |
| 231 | + if len(knots_km_where) == 0: |
| 232 | + print('ERROR (Normalization.get_spline_fit): No specified times \n'+ |
| 233 | + 'fall within freespace range. Freespace ranges are '+ |
| 234 | + str(freespace_km)) |
| 235 | + sys.exit() |
| 236 | + |
| 237 | + # Select data points closest to selected knots |
| 238 | + n_knots_km = len(knots_km_where) |
| 239 | + knots_spm = np.zeros(n_knots_km) |
| 240 | + knots_rho = np.zeros(n_knots_km) |
| 241 | + for i in knots_km_where:#range(n_knots_km): |
| 242 | + _ind = np.argmin(abs(rho_km_vals_free - knots_km[int(i)])) |
| 243 | + knots_spm[i] = spm_vals_free[_ind] |
| 244 | + knots_rho[i] = rho_km_vals_free[_ind] |
| 245 | + |
| 246 | + if TEST: |
| 247 | + print('\nSelected knots:') |
| 248 | + print(knots_spm) |
| 249 | + print(knots_rho) |
| 250 | + |
| 251 | + # Make and evaluate the spline fit. Sort spm_vals_free and p_obs_free |
| 252 | + # in case it's ingress, since indices were gotten from rho values, |
| 253 | + # which are not in order for ingress |
| 254 | + ind_sort = np.argsort(spm_vals_free) |
| 255 | + ind_knot_sort = np.argsort(knots_spm) |
| 256 | + spline_rep = splrep(spm_vals_free[ind_sort], p_obs_free[ind_sort], |
| 257 | + k=k, t=knots_spm[ind_knot_sort]) |
| 258 | + spline_fit = splev(spm_fit, spline_rep) |
| 259 | + |
| 260 | + return spline_fit |
| 261 | + |
| 262 | + |
| 263 | +if __name__ == '__main__': |
| 264 | + pass |
0 commit comments