Skip to content

Commit e8101ae

Browse files
Merge pull request #98 from rahil-makadia/dev
direction of motion residuals; upgraded outlier rejection and low-information handling
2 parents 1488899 + e4fd795 commit e8101ae

File tree

6 files changed

+163
-134
lines changed

6 files changed

+163
-134
lines changed

grss/fit/fit_simulation.py

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,22 +1344,26 @@ def _get_computed_obs(self, prop_sim_past, prop_sim_future, integ_body_idx):
13441344
raise ValueError("Observer info length not recognized.")
13451345
nom_body = integ_body_idx == 0
13461346
if nom_body:
1347-
self._inflate_uncertainties(prop_sim_past, prop_sim_future)
1348-
return computed_obs
1347+
computed_obs_dot = self._inflate_uncertainties(prop_sim_past, prop_sim_future)
1348+
else:
1349+
computed_obs_dot = np.nan*np.ones((len(self.obs), 2))
1350+
return computed_obs, computed_obs_dot
13491351

13501352
def _inflate_uncertainties(self, prop_sim_past, prop_sim_future):
13511353
"""
13521354
Apply time uncertainties to the optical observations weights.
13531355
13541356
Parameters
13551357
----------
1356-
computed_obs_dot : array
1357-
Computed optical observations dot.
1358+
prop_sim_past : libgrss.PropSimulation object
1359+
PropSimulation object for the past observations.
1360+
prop_sim_future : libgrss.PropSimulation object
1361+
PropSimulation object for the future observations.
13581362
13591363
Returns
13601364
-------
1361-
None : NoneType
1362-
None
1365+
computed_obs_dot : array
1366+
Computed optical observations time derivatives.
13631367
"""
13641368
stations = self.obs.stn.values
13651369
sig_times = self.obs.sigTime.values
@@ -1446,7 +1450,7 @@ def _inflate_uncertainties(self, prop_sim_past, prop_sim_future):
14461450
self.obs_cov[i] = cov
14471451
self.obs_weight[i] = inv
14481452
self.obs.sigTime = sig_times
1449-
return None
1453+
return computed_obs_dot
14501454

14511455
def _get_analytic_partials(self, prop_sim_past, prop_sim_future):
14521456
"""
@@ -1532,9 +1536,9 @@ def _get_numeric_partials(self, prop_sim_past, prop_sim_future, perturbation_inf
15321536
_ = list(self.x_nom.keys())[i]
15331537
_, _, _, _, _, _, fd_delta = perturbation_info[i]
15341538
# get computed_obs for perturbed states
1535-
computed_obs_plus = self._get_computed_obs(prop_sim_past, prop_sim_future,
1539+
computed_obs_plus, _ = self._get_computed_obs(prop_sim_past, prop_sim_future,
15361540
integ_body_idx=2*i+1)
1537-
computed_obs_minus = self._get_computed_obs(prop_sim_past, prop_sim_future,
1541+
computed_obs_minus, _ = self._get_computed_obs(prop_sim_past, prop_sim_future,
15381542
integ_body_idx=2*i+2)
15391543
computed_obs_plus = self._flatten_and_clean(computed_obs_plus)
15401544
computed_obs_minus = self._flatten_and_clean(computed_obs_minus)
@@ -1586,10 +1590,13 @@ def _get_residuals_and_partials(self):
15861590
# get partials
15871591
partials = self._get_partials(prop_sim_past, prop_sim_future, perturbation_info)
15881592
# get residuals
1589-
computed_obs = self._get_computed_obs(prop_sim_past, prop_sim_future, integ_body_idx=0)
1593+
computed_obs, computed_obs_dot = self._get_computed_obs(prop_sim_past, prop_sim_future, integ_body_idx=0)
15901594
residuals = [None]*len(self.obs)
15911595
ra_res = self.obs['resRA'].values
15921596
dec_res = self.obs['resDec'].values
1597+
at_res = [np.nan]*len(ra_res)
1598+
at_sec_res = [np.nan]*len(ra_res)
1599+
ct_res = [np.nan]*len(ra_res)
15931600
delay_res = self.obs['resDelay'].values
15941601
doppler_res = self.obs['resDoppler'].values
15951602
fields = ['mode', 'ra', 'dec', 'cosDec', 'biasRA', 'biasDec', 'delay', 'doppler']
@@ -1611,8 +1618,17 @@ def _get_residuals_and_partials(self):
16111618
ra_res[i] = ra*3600*cos_dec - bias_ra - computed_obs[i, 0]
16121619
dec_res[i] = dec*3600 - bias_dec - computed_obs[i, 1]
16131620
residuals[i] = np.array([ra_res[i], dec_res[i]])
1621+
# rotation angle from RA/Dec to along/cross-track
1622+
obs_dot = computed_obs_dot[i, :]
1623+
rot_angle = np.arctan2(obs_dot[1], obs_dot[0])
1624+
at_res[i] = (ra_res[i]*np.cos(rot_angle) + dec_res[i]*np.sin(rot_angle))
1625+
ct_res[i] = (-ra_res[i]*np.sin(rot_angle) + dec_res[i]*np.cos(rot_angle))
1626+
at_sec_res[i] = at_res[i]/np.linalg.norm(obs_dot)*86400.0 # convert to seconds
16141627
self.obs['resRA'] = ra_res
16151628
self.obs['resDec'] = dec_res
1629+
self.obs['resAT'] = at_res
1630+
self.obs['resCT'] = ct_res
1631+
self.obs['resATsec'] = at_sec_res
16161632
self.obs['resDelay'] = delay_res
16171633
self.obs['resDoppler'] = doppler_res
16181634
return residuals, partials
@@ -1656,11 +1672,9 @@ def _get_rms_and_reject_outliers(self, partials, residuals, start_rejecting):
16561672
"Default values are chi_reject=3.0 and chi_recover=2.8 "
16571673
"(Implemented as FitSimulation.reject_criteria=[3.0, 2.8])")
16581674
full_cov = self.covariance
1659-
rms_u = 0
1660-
chi_sq = 0
16611675
j = 0
16621676
sel_ast = self.obs['selAst'].values
1663-
res_chisq_vals = np.nan*np.ones(len(self.observer_info_lengths))
1677+
outlier_chisq_vals = np.nan*np.ones(len(self.observer_info_lengths))
16641678
for i, obs_info_len in enumerate(self.observer_info_lengths):
16651679
if obs_info_len in {4, 7}:
16661680
size = 2
@@ -1680,20 +1694,34 @@ def _get_rms_and_reject_outliers(self, partials, residuals, start_rejecting):
16801694
resid_cov_det = resid_cov[0,0]*resid_cov[1,1] - resid_cov[0,1]*resid_cov[1,0]
16811695
resid_cov_inv = np.array([[resid_cov[1,1], -resid_cov[0,1]],
16821696
[-resid_cov[1,0], resid_cov[0,0]]])/resid_cov_det
1683-
outlier_chisq = resid @ resid_cov_inv @ resid.T
1684-
# outlier rejection, only reject RA/Dec measurements
1685-
if abs(outlier_chisq) > chi_reject**2 and sel_ast[i] not in {'a', 'd'}:
1697+
outlier_chisq_vals[i] = resid @ resid_cov_inv @ resid.T
1698+
j += size
1699+
if start_rejecting:
1700+
idx_to_check = np.where((sel_ast == 'A') | (sel_ast == 'a'))[0]
1701+
if len(idx_to_check) == 0:
1702+
raise ValueError("No accepted observations in fit.")
1703+
max_outlier_chisq = np.nanmax(outlier_chisq_vals[idx_to_check])
1704+
rej_chisq_thresh = max(chi_reject**2, 0.25*max_outlier_chisq)
1705+
rec_chisq_thresh = chi_recover**2
1706+
# outlier rejection, only reject RA/Dec measurements
1707+
rms_u = 0
1708+
chi_sq = 0
1709+
res_chisq_vals = np.nan*np.ones(len(self.observer_info_lengths))
1710+
for i in range(len(self.observer_info_lengths)):
1711+
if start_rejecting:
1712+
outlier_chisq = outlier_chisq_vals[i]
1713+
if abs(outlier_chisq) > rej_chisq_thresh and sel_ast[i] not in {'a', 'd'}:
16861714
if sel_ast[i] == 'A':
16871715
self.num_rejected += 1
16881716
sel_ast[i] = 'D'
1689-
elif abs(outlier_chisq) < chi_recover**2 and sel_ast[i] == 'D':
1717+
elif abs(outlier_chisq) < rec_chisq_thresh and sel_ast[i] == 'D':
16901718
sel_ast[i] = 'A'
16911719
self.num_rejected -= 1
1720+
resid = residuals[i]
16921721
res_chisq_vals[i] = resid @ self.obs_weight[i] @ resid.T
16931722
if sel_ast[i] not in {'D', 'd'}:
16941723
rms_u += resid @ resid.T
16951724
chi_sq += res_chisq_vals[i]
1696-
j += size
16971725
# # write res_chisq_vals to file if any values are negative
16981726
# if np.any(res_chisq_vals < 0):
16991727
# print("WARNING: Negative outlier rejection chi-squared values detected. "
@@ -1806,12 +1834,12 @@ def _get_lsq_state_correction(self, partials, residuals):
18061834
atwb += partials[j:j+size, :].T @ self.obs_weight[i] @ residuals[i]
18071835
j += size
18081836
self.info_mats.append(atwa.copy())
1809-
# use pseudo-inverse if the data arc is less than 7 days
1837+
# use pseudo-inverse if the information matrix is ill-conditioned
18101838
data_arc = self.obs.obsTimeMJD.max() - self.obs.obsTimeMJD.min()
1811-
if data_arc < 1.0:
1812-
self.covariance = np.linalg.pinv(atwa, rcond=1e-10, hermitian=True)
1813-
elif data_arc < 7.0:
1814-
self.covariance = np.linalg.pinv(atwa, rcond=1e-20, hermitian=True)
1839+
cond_num = np.linalg.cond(atwa)
1840+
if data_arc < 10.0 and cond_num > 1e15:
1841+
# print(f"Condition number of the information matrix: {cond_num:.2e}")
1842+
self.covariance = np.linalg.pinv(atwa, rcond=1e3/cond_num, hermitian=True)
18151843
else:
18161844
self.covariance = np.array(libgrss.matrix_inverse(atwa))
18171845
delta_x = self.covariance @ atwb
@@ -2176,10 +2204,11 @@ def save(self, filename):
21762204
widths = {
21772205
'obsTime': 29, 'stn': 7, 'ra': 16, 'dec': 18,
21782206
'sigRA': 16, 'sigDec': 17, 'resRA': 16, 'resDec': 15,
2207+
'resAT': 16, 'resCT': 16, 'resATsec': 16
21792208
}
21802209
f.write(f'|{"Observations".center(69)}|')
21812210
f.write(f'{"Sigmas".center(32)}|')
2182-
f.write(f'{"Residuals".center(29)}|')
2211+
f.write(f'{"Residuals".center(77)}|')
21832212
f.write("\n")
21842213
f.write(f'{"Time [UTC]".center(widths["obsTime"])}')
21852214
f.write(f'{"Obs".center(widths["stn"])}')
@@ -2189,6 +2218,9 @@ def save(self, filename):
21892218
f.write(f'{"Dec[as]/Dop[Hz]".center(widths["sigDec"])}')
21902219
f.write(f'{f"RA[as]/Del[{chr(0x00b5)}s]".center(widths["resRA"])}')
21912220
f.write(f'{"Dec[as]/Dop[Hz]".center(widths["resDec"])}')
2221+
f.write(f'{"AT[as]".center(widths["resAT"])}')
2222+
f.write(f'{"CT[as]".center(widths["resCT"])}')
2223+
f.write(f'{"ATsec[s]".center(widths["resATsec"])}')
21922224
f.write("\n")
21932225
for i, obs in self.obs[::-1].iterrows():
21942226
f.write(f'{obs["selAst"]+obs["obsTime"]:<{widths["obsTime"]}}')
@@ -2208,6 +2240,9 @@ def save(self, filename):
22082240
f.write(f'{obs["sigDec"]:^{widths["sigDec"]}.4f}')
22092241
f.write(f'{obs["resRA"]:^+{widths["resRA"]}.7f}')
22102242
f.write(f'{obs["resDec"]:^+{widths["resDec"]}.7f}')
2243+
f.write(f'{obs["resAT"]:^+{widths["resAT"]}.7f}')
2244+
f.write(f'{obs["resCT"]:^+{widths["resCT"]}.7f}')
2245+
f.write(f'{obs["resATsec"]:^+{widths["resATsec"]}.7f}')
22112246
f.write("\n")
22122247
f.write("\n")
22132248

grss/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
4.4.2
1+
4.5.0

tests/python/fit/chesley.ipynb

Lines changed: 27 additions & 29 deletions
Large diffs are not rendered by default.

tests/python/fit/eggl.ipynb

Lines changed: 27 additions & 28 deletions
Large diffs are not rendered by default.

tests/python/fit/farnocchia.ipynb

Lines changed: 21 additions & 22 deletions
Large diffs are not rendered by default.

tests/python/fit/shantanunaidu.ipynb

Lines changed: 28 additions & 30 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)