grss 3.3.0__tar.gz → 3.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {grss-3.3.0/grss.egg-info → grss-3.4.2}/PKG-INFO +4 -3
  2. {grss-3.3.0 → grss-3.4.2}/README.md +3 -2
  3. {grss-3.3.0 → grss-3.4.2}/grss/fit/fit_simulation.py +118 -133
  4. {grss-3.3.0 → grss-3.4.2}/grss/prop/prop_parallel.py +1 -0
  5. {grss-3.3.0 → grss-3.4.2}/grss/prop/prop_utils.py +78 -39
  6. grss-3.4.2/grss/version.txt +1 -0
  7. {grss-3.3.0 → grss-3.4.2/grss.egg-info}/PKG-INFO +4 -3
  8. {grss-3.3.0 → grss-3.4.2}/include/utilities.h +4 -4
  9. {grss-3.3.0 → grss-3.4.2}/src/approach.cpp +1 -1
  10. {grss-3.3.0 → grss-3.4.2}/src/elements.cpp +84 -0
  11. {grss-3.3.0 → grss-3.4.2}/src/grss.cpp +25 -0
  12. {grss-3.3.0 → grss-3.4.2}/src/simulation.cpp +5 -11
  13. {grss-3.3.0 → grss-3.4.2}/src/utilities.cpp +15 -17
  14. grss-3.3.0/grss/version.txt +0 -1
  15. {grss-3.3.0 → grss-3.4.2}/CMakeLists.txt +0 -0
  16. {grss-3.3.0 → grss-3.4.2}/LICENSE +0 -0
  17. {grss-3.3.0 → grss-3.4.2}/MANIFEST.in +0 -0
  18. {grss-3.3.0 → grss-3.4.2}/build_cpp.sh +0 -0
  19. {grss-3.3.0 → grss-3.4.2}/docs/source/conf.py +0 -0
  20. {grss-3.3.0 → grss-3.4.2}/extern/get_cspice.py +0 -0
  21. {grss-3.3.0 → grss-3.4.2}/grss/__init__.py +0 -0
  22. {grss-3.3.0 → grss-3.4.2}/grss/debias/get_debiasing_data.py +0 -0
  23. {grss-3.3.0 → grss-3.4.2}/grss/fit/__init__.py +0 -0
  24. {grss-3.3.0 → grss-3.4.2}/grss/fit/fit_optical.py +0 -0
  25. {grss-3.3.0 → grss-3.4.2}/grss/fit/fit_radar.py +0 -0
  26. {grss-3.3.0 → grss-3.4.2}/grss/fit/fit_utils.py +0 -0
  27. {grss-3.3.0 → grss-3.4.2}/grss/kernels/create_kernel_431.txt +0 -0
  28. {grss-3.3.0 → grss-3.4.2}/grss/kernels/create_kernel_441.txt +0 -0
  29. {grss-3.3.0 → grss-3.4.2}/grss/kernels/get_kernels.py +0 -0
  30. {grss-3.3.0 → grss-3.4.2}/grss/kernels/pck00011.tpc +0 -0
  31. {grss-3.3.0 → grss-3.4.2}/grss/kernels/planets_big16_de431_1950_2350.log +0 -0
  32. {grss-3.3.0 → grss-3.4.2}/grss/kernels/planets_big16_de431_1950_2350.tm +0 -0
  33. {grss-3.3.0 → grss-3.4.2}/grss/kernels/planets_big16_de441_1950_2350.log +0 -0
  34. {grss-3.3.0 → grss-3.4.2}/grss/kernels/planets_big16_de441_1950_2350.tm +0 -0
  35. {grss-3.3.0 → grss-3.4.2}/grss/prop/__init__.py +0 -0
  36. {grss-3.3.0 → grss-3.4.2}/grss/prop/prop_unscented.py +0 -0
  37. {grss-3.3.0 → grss-3.4.2}/grss/utils.py +0 -0
  38. {grss-3.3.0 → grss-3.4.2}/grss.egg-info/SOURCES.txt +0 -0
  39. {grss-3.3.0 → grss-3.4.2}/grss.egg-info/dependency_links.txt +0 -0
  40. {grss-3.3.0 → grss-3.4.2}/grss.egg-info/requires.txt +0 -0
  41. {grss-3.3.0 → grss-3.4.2}/grss.egg-info/top_level.txt +0 -0
  42. {grss-3.3.0 → grss-3.4.2}/include/approach.h +0 -0
  43. {grss-3.3.0 → grss-3.4.2}/include/elements.h +0 -0
  44. {grss-3.3.0 → grss-3.4.2}/include/force.h +0 -0
  45. {grss-3.3.0 → grss-3.4.2}/include/gr15.h +0 -0
  46. {grss-3.3.0 → grss-3.4.2}/include/grss.h +0 -0
  47. {grss-3.3.0 → grss-3.4.2}/include/interpolate.h +0 -0
  48. {grss-3.3.0 → grss-3.4.2}/include/parallel.h +0 -0
  49. {grss-3.3.0 → grss-3.4.2}/include/simulation.h +0 -0
  50. {grss-3.3.0 → grss-3.4.2}/include/spk.h +0 -0
  51. {grss-3.3.0 → grss-3.4.2}/include/stm.h +0 -0
  52. {grss-3.3.0 → grss-3.4.2}/pyproject.toml +0 -0
  53. {grss-3.3.0 → grss-3.4.2}/requirements.txt +0 -0
  54. {grss-3.3.0 → grss-3.4.2}/setup.cfg +0 -0
  55. {grss-3.3.0 → grss-3.4.2}/setup.py +0 -0
  56. {grss-3.3.0 → grss-3.4.2}/src/CMakeLists.txt +0 -0
  57. {grss-3.3.0 → grss-3.4.2}/src/force.cpp +0 -0
  58. {grss-3.3.0 → grss-3.4.2}/src/gr15.cpp +0 -0
  59. {grss-3.3.0 → grss-3.4.2}/src/interpolate.cpp +0 -0
  60. {grss-3.3.0 → grss-3.4.2}/src/parallel.cpp +0 -0
  61. {grss-3.3.0 → grss-3.4.2}/src/spk.cpp +0 -0
  62. {grss-3.3.0 → grss-3.4.2}/src/stm.cpp +0 -0
  63. {grss-3.3.0 → grss-3.4.2}/tests/cpp/prop/CMakeLists.txt +0 -0
  64. {grss-3.3.0 → grss-3.4.2}/tests/cpp/prop/apophis.cpp +0 -0
  65. {grss-3.3.0 → grss-3.4.2}/tests/cpp/prop/didymos.cpp +0 -0
  66. {grss-3.3.0 → grss-3.4.2}/tests/cpp/prop/spk_map.cpp +0 -0
  67. {grss-3.3.0 → grss-3.4.2}/tests/python/fit/chesley.ipynb +0 -0
  68. {grss-3.3.0 → grss-3.4.2}/tests/python/fit/eggl.ipynb +0 -0
  69. {grss-3.3.0 → grss-3.4.2}/tests/python/fit/farnocchia.ipynb +0 -0
  70. {grss-3.3.0 → grss-3.4.2}/tests/python/fit/shantanunaidu.ipynb +0 -0
  71. {grss-3.3.0 → grss-3.4.2}/tests/python/prop/apophis.ipynb +0 -0
  72. {grss-3.3.0 → grss-3.4.2}/tests/python/prop/didymos.ipynb +0 -0
  73. {grss-3.3.0 → grss-3.4.2}/tests/python/prop/icarus.ipynb +0 -0
  74. {grss-3.3.0 → grss-3.4.2}/tests/python/prop/moshup.ipynb +0 -0
  75. {grss-3.3.0 → grss-3.4.2}/tests/python/run_tests.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: grss
3
- Version: 3.3.0
3
+ Version: 3.4.2
4
4
  Summary: GRSS: Gauss-Radau Small-body Simulator
5
5
  Author: Rahil Makadia
6
6
  Author-email: makadia2@illinois.edu
@@ -74,9 +74,10 @@ Requires-Dist: spiceypy>=5.0.0
74
74
 
75
75
  **GRSS** (pronounced "grass"), the *Gauss-Radau Small-body Simulator* is a Python package with a C++ binding for propagating and fitting the orbits of small bodies in the solar system, such as asteroids and comets.
76
76
 
77
- If you use GRSS in your research, please cite the following paper:
77
+ If you use GRSS in your research, please cite at least one of the following:
78
78
 
79
- Makadia et al. (2024), "Measurability of the Heliocentric Momentum Enhancement from a Kinetic Impact: The Double Asteroid Redirection Test (DART) Mission", [Planetary Science Journal, 5, 38.](https://doi.org/10.3847/PSJ/ad1bce)
79
+ * Makadia et al. (2024), "Measurability of the Heliocentric Momentum Enhancement from a Kinetic Impact: The Double Asteroid Redirection Test (DART) Mission", [Planetary Science Journal, 5, 38.](https://doi.org/10.3847/PSJ/ad1bce)
80
+ * Makadia et al. (2023), "GRSS: An open-source small-body science tool for planetary defense", [55th Annual Meeting of the Division for Planetary Sciences.](https://ui.adsabs.harvard.edu/abs/2023DPS....5540502M/abstract)
80
81
 
81
82
  ## Getting Started
82
83
 
@@ -8,9 +8,10 @@
8
8
 
9
9
  **GRSS** (pronounced "grass"), the *Gauss-Radau Small-body Simulator* is a Python package with a C++ binding for propagating and fitting the orbits of small bodies in the solar system, such as asteroids and comets.
10
10
 
11
- If you use GRSS in your research, please cite the following paper:
11
+ If you use GRSS in your research, please cite at least one of the following:
12
12
 
13
- Makadia et al. (2024), "Measurability of the Heliocentric Momentum Enhancement from a Kinetic Impact: The Double Asteroid Redirection Test (DART) Mission", [Planetary Science Journal, 5, 38.](https://doi.org/10.3847/PSJ/ad1bce)
13
+ * Makadia et al. (2024), "Measurability of the Heliocentric Momentum Enhancement from a Kinetic Impact: The Double Asteroid Redirection Test (DART) Mission", [Planetary Science Journal, 5, 38.](https://doi.org/10.3847/PSJ/ad1bce)
14
+ * Makadia et al. (2023), "GRSS: An open-source small-body science tool for planetary defense", [55th Annual Meeting of the Division for Planetary Sciences.](https://ui.adsabs.harvard.edu/abs/2023DPS....5540502M/abstract)
14
15
 
15
16
  ## Getting Started
16
17
 
@@ -17,8 +17,8 @@ class IterationParams:
17
17
  orbit determination process. It is also used for plotting the residuals
18
18
  and chi-squared values for each iteration.
19
19
  """
20
- def __init__(self, iter_number, x_nom, covariance, residuals,
21
- obs_array, obs_weight_rej, observer_codes, rejection_flags):
20
+ def __init__(self, iter_number, residuals, rms_u, rms_w, residual_chi_squared,
21
+ x_nom, covariance, obs_array, rejection_flags):
22
22
  """
23
23
  Constructor for the IterationParams class
24
24
 
@@ -26,18 +26,20 @@ class IterationParams:
26
26
  ----------
27
27
  iter_number : int
28
28
  Iteration number
29
+ residuals : array
30
+ Residuals at the current iteration
31
+ rms_u : float
32
+ Unweighted RMS of the residuals.
33
+ rms_w : float
34
+ Weighted RMS of the residuals.
35
+ residual_chi_squared : list
36
+ List of the residual chi-squared values for each observation.
29
37
  x_nom : dict
30
38
  Dictionary of nominal state vector values at the current iteration
31
39
  covariance : array
32
40
  Covariance matrix at the current iteration
33
- residuals : array
34
- Residuals at the current iteration
35
41
  obs_array : array
36
42
  Observation array for the orbit fit
37
- obs_weight_rej : array
38
- Observation weight matrix after outlier rejection
39
- observer_codes : tuple
40
- Observer locations for each observation in obs_array
41
43
  rejection_flags : list
42
44
  List of rejection flags for each observation in obs_array
43
45
  """
@@ -48,12 +50,18 @@ class IterationParams:
48
50
  self.variance = dict(zip([f'var_{k}' for k in self.x_nom.keys()], variance))
49
51
  self.residuals = residuals
50
52
  self.obs_array = obs_array
51
- self.observer_codes = observer_codes
52
53
  self.is_accepted = np.where(np.logical_not(rejection_flags))[0]
53
54
  self.is_rejected = np.where(rejection_flags)[0]
55
+ # optical_idx is where neither the RA nor dec residuals are NaN
56
+ self.optical_idx = np.where(~np.isnan(self.residuals[:, 0]) &
57
+ ~np.isnan(self.residuals[:, 1]))[0]
58
+ # radar_idx is where either the RA or dec residuals are NaN
59
+ self.radar_idx = np.where(np.isnan(self.residuals[:, 0]) |
60
+ np.isnan(self.residuals[:, 1]))[0]
54
61
  self.sigmas = obs_array[:, 3:5]
55
- self.obs_weight_rej = obs_weight_rej
56
- self._calculate_rms()
62
+ self.unweighted_rms = rms_u
63
+ self.weighted_rms = rms_w
64
+ self.residual_chi_squared = residual_chi_squared
57
65
  self._calculate_chis()
58
66
  self._assemble_info()
59
67
  return None
@@ -76,21 +84,6 @@ class IterationParams:
76
84
  arr = arr[~np.isnan(arr)]
77
85
  return arr
78
86
 
79
- def _calculate_rms(self):
80
- """
81
- Calculate the weighted and unweighted RMS values for the residuals
82
-
83
- Returns
84
- -------
85
- None : NoneType
86
- None
87
- """
88
- resid_arr = self._flatten_and_clean(self.residuals[self.is_accepted])
89
- n_obs = len(resid_arr)
90
- self.unweighted_rms = float(np.sqrt(resid_arr.T @ resid_arr/n_obs))
91
- self.weighted_rms = float(np.sqrt(resid_arr.T @ self.obs_weight_rej @ resid_arr/n_obs))
92
- return None
93
-
94
87
  def _calculate_chis(self):
95
88
  """
96
89
  Calculates the chi and chi-squared values for the residuals
@@ -119,12 +112,8 @@ class IterationParams:
119
112
  None
120
113
  """
121
114
  # sourcery skip: extract-duplicate-method
122
- # optical_idx is where neither the RA nor dec residuals are NaN
123
- optical_idx = np.where(~np.isnan(self.residuals[:, 0]) &
124
- ~np.isnan(self.residuals[:, 1]))[0]
125
- # radar_idx is where either the RA or dec residuals are NaN
126
- radar_idx = np.where(np.isnan(self.residuals[:, 0]) |
127
- np.isnan(self.residuals[:, 1]))[0]
115
+ optical_idx = self.optical_idx
116
+ radar_idx = self.radar_idx
128
117
  optical_info = self.obs_array.copy()
129
118
  optical_info[radar_idx, :] = np.nan
130
119
  ra_obs = optical_info[:, 1]
@@ -384,12 +373,8 @@ class IterationParams:
384
373
  """
385
374
  is_rejected = self.is_rejected
386
375
  is_accepted = self.is_accepted
387
- # optical_idx is where neither the RA nor dec residuals are NaN
388
- optical_idx = np.where(~np.isnan(self.residuals[:, 0]) &
389
- ~np.isnan(self.residuals[:, 1]))[0]
390
- # radar_idx is where either the RA or dec residuals are NaN
391
- radar_idx = np.where(np.isnan(self.residuals[:, 0]) |
392
- np.isnan(self.residuals[:, 1]))[0]
376
+ optical_idx = self.optical_idx
377
+ radar_idx = self.radar_idx
393
378
  # plot chi values
394
379
  factor = 3 if plot_chi_squared else 1
395
380
  plt.figure(figsize=(factor*7,6), dpi=150)
@@ -610,7 +595,6 @@ class FitSimulation:
610
595
  self.fit_events = False
611
596
  self.reject_outliers = True
612
597
  self.reject_criteria = [3.0, 2.8]
613
- self.residual_chi_squared = [None]*len(self.obs_array)
614
598
  self.num_rejected = 0
615
599
  self.converged = False
616
600
  return None
@@ -809,7 +793,7 @@ class FitSimulation:
809
793
  sigma_corr = self.obs_array[:, 5]
810
794
  obs_cov = np.zeros((self.sigmas.size, self.sigmas.size))
811
795
  obs_wgt = np.zeros((self.sigmas.size, self.sigmas.size))
812
- idx_to_remove = []
796
+ idx_to_keep = []
813
797
  for i in range(self.obs_array.shape[0]):
814
798
  obs_info_len = len(self.observer_info[i])
815
799
  if obs_info_len in {4, 7}:
@@ -831,20 +815,22 @@ class FitSimulation:
831
815
  obs_wgt[2*i:2*i+2, 2*i:2*i+2] = np.linalg.inv(sub_cov)
832
816
  else:
833
817
  obs_wgt[2*i:2*i+2, 2*i:2*i+2] = np.diag([1.0/sig_1**2, 1.0/sig_2**2])
834
- if sig_1_nan:
835
- idx_to_remove.append(2*i)
836
- if sig_2_nan:
837
- idx_to_remove.append(2*i+1)
818
+ if not sig_1_nan:
819
+ idx_to_keep.append(2*i)
820
+ if not sig_2_nan:
821
+ idx_to_keep.append(2*i+1)
838
822
  if sig_1_nan and sig_2_nan:
839
823
  raise ValueError("Both sigmas cannot be NaN.")
840
- # remove all rows and columns with NaN values from the obs_cov
841
- obs_cov = np.delete(obs_cov, idx_to_remove, axis=0)
842
- obs_cov = np.delete(obs_cov, idx_to_remove, axis=1)
843
- # remove all rows and columns with NaN values from the obs_wgt
844
- obs_wgt = np.delete(obs_wgt, idx_to_remove, axis=0)
845
- obs_wgt = np.delete(obs_wgt, idx_to_remove, axis=1)
846
- self.obs_cov = obs_cov
847
- self.obs_weight = obs_wgt
824
+ # these next two lines were VERY VERY slow, needed to find a better way
825
+ # self.obs_cov = obs_cov[idx_to_keep, :][:, idx_to_keep]
826
+ # self.obs_weight = obs_wgt[idx_to_keep, :][:, idx_to_keep]
827
+ cov_post = np.zeros((len(idx_to_keep), len(idx_to_keep)))
828
+ wgt_post = np.zeros((len(idx_to_keep), len(idx_to_keep)))
829
+ for i, idx in enumerate(idx_to_keep):
830
+ cov_post[i, i:i+2] = obs_cov[idx, idx:idx+2]
831
+ wgt_post[i, i:i+2] = obs_wgt[idx, idx:idx+2]
832
+ self.obs_cov = cov_post
833
+ self.obs_weight = wgt_post
848
834
  return None
849
835
 
850
836
  def _sort_array_by_another(self, array, sort_by):
@@ -909,10 +895,14 @@ class FitSimulation:
909
895
  prop_sim_past : prop.propSimulation object
910
896
  propSim object for the past observations.
911
897
  """
898
+ # pylint: disable=no-member
912
899
  t_eval_past = self.obs_array[self.past_obs_idx, 0]
913
900
  tf_past = np.min(t_eval_past)
914
901
  prop_sim_past = prop.propSimulation(name, self.t_sol, self.de_kernel, self.de_kernel_path)
915
902
  prop_sim_past.tEvalMargin = 1.0
903
+ # flip t_eval_past and observer_info to go in reverse time order
904
+ t_eval_past = t_eval_past[::-1]
905
+ observer_info = tuple(observer_info[::-1])
916
906
  prop_sim_past.set_integration_parameters(tf_past, t_eval_past, t_eval_utc,
917
907
  eval_apparent_state, converged_light_time,
918
908
  observer_info)
@@ -942,6 +932,7 @@ class FitSimulation:
942
932
  prop_sim_future : prop.propSimulation object
943
933
  propSim object for the future observations.
944
934
  """
935
+ # pylint: disable=no-member
945
936
  t_eval_future = self.obs_array[self.future_obs_idx, 0]
946
937
  tf_future = np.max(t_eval_future)
947
938
  prop_sim_future = prop.propSimulation(name, self.t_sol, self.de_kernel, self.de_kernel_path)
@@ -972,7 +963,7 @@ class FitSimulation:
972
963
  eval_apparent_state = True
973
964
  converged_light_time = True
974
965
  observer_info = np.array(self.observer_info, dtype=tuple)
975
- observer_info_past = tuple(observer_info[self.past_obs_idx])
966
+ observer_info_past = observer_info[self.past_obs_idx]
976
967
  observer_info_future = tuple(observer_info[self.future_obs_idx])
977
968
  prop_sim_past = None
978
969
  prop_sim_future = None
@@ -1042,6 +1033,7 @@ class FitSimulation:
1042
1033
  nongrav_params : prop.NongravParamaters object
1043
1034
  Non-gravitational parameters for the fitted body.
1044
1035
  """
1036
+ # pylint: disable=no-member
1045
1037
  nongrav_params = prop.NongravParamaters()
1046
1038
  a1_val = x_dict['a1'] if 'a1' in x_dict.keys() else self.fixed_propsim_params['a1']
1047
1039
  a2_val = x_dict['a2'] if 'a2' in x_dict.keys() else self.fixed_propsim_params['a2']
@@ -1206,6 +1198,7 @@ class FitSimulation:
1206
1198
  propagated propSimulation object for the future.
1207
1199
  """
1208
1200
  # sourcery skip: low-code-quality
1201
+ # pylint: disable=no-member
1209
1202
  # get propagated states
1210
1203
  prop_sim_past, prop_sim_future = self._get_prop_sims("orbit_fit_sim")
1211
1204
  # create nominal integ_body object
@@ -1304,30 +1297,24 @@ class FitSimulation:
1304
1297
  If the observer information is not well-defined.
1305
1298
  """
1306
1299
  if self.past_obs_exist and self.future_obs_exist:
1307
- optical_obs_past = np.array(prop_sim_past.opticalObs)
1308
- optical_obs_future = np.array(prop_sim_future.opticalObs)
1309
- optical_obs = np.vstack((optical_obs_past, optical_obs_future))
1310
- radar_obs_past = np.array(prop_sim_past.radarObs)
1311
- radar_obs_future = np.array(prop_sim_future.radarObs)
1312
- radar_obs = np.vstack((radar_obs_past, radar_obs_future))
1300
+ optical_obs = np.vstack((prop_sim_past.opticalObs, prop_sim_future.opticalObs))
1301
+ radar_obs = np.vstack((prop_sim_past.radarObs, prop_sim_future.radarObs))
1313
1302
  elif self.past_obs_exist:
1314
- optical_obs = np.array(prop_sim_past.opticalObs)
1315
- radar_obs = np.array(prop_sim_past.radarObs)
1303
+ optical_obs = prop_sim_past.opticalObs
1304
+ radar_obs = prop_sim_past.radarObs
1316
1305
  elif self.future_obs_exist:
1317
- optical_obs = np.array(prop_sim_future.opticalObs)
1318
- radar_obs = np.array(prop_sim_future.radarObs)
1319
- optical_obs = optical_obs[:,2*integ_body_idx:2*integ_body_idx+2]
1320
- radar_obs = radar_obs[:,integ_body_idx]
1306
+ optical_obs = prop_sim_future.opticalObs
1307
+ radar_obs = prop_sim_future.radarObs
1321
1308
  measured_obs = self.obs_array[:, 1:3]
1322
1309
  computed_obs = np.nan*np.ones_like(measured_obs)
1323
1310
  for i in range(len(self.obs_array)):
1324
1311
  obs_info_len = len(self.observer_info[i])
1325
1312
  if obs_info_len in {4, 7}:
1326
- computed_obs[i, :] = optical_obs[i]
1313
+ computed_obs[i, :] = optical_obs[i][2*integ_body_idx:2*integ_body_idx+2]
1327
1314
  elif obs_info_len == 9: # delay measurement
1328
- computed_obs[i, 0] = radar_obs[i]
1315
+ computed_obs[i, 0] = radar_obs[i][integ_body_idx]
1329
1316
  elif obs_info_len == 10: # dopper measurement
1330
- computed_obs[i, 1] = radar_obs[i]
1317
+ computed_obs[i, 1] = radar_obs[i][integ_body_idx]
1331
1318
  else:
1332
1319
  raise ValueError("Observer info length not recognized.")
1333
1320
  return computed_obs
@@ -1481,7 +1468,7 @@ class FitSimulation:
1481
1468
  partials = self._get_partials(prop_sim_past, prop_sim_future, perturbation_info)
1482
1469
  return residuals, partials
1483
1470
 
1484
- def _apply_outlier_rejection(self, partials, weights, residuals):
1471
+ def _get_rms_and_reject_outliers(self, partials, residuals, start_rejecting):
1485
1472
  # sourcery skip: low-code-quality
1486
1473
  """
1487
1474
  Outlier rejection algorithm for the residuals.
@@ -1491,22 +1478,19 @@ class FitSimulation:
1491
1478
  partials : array
1492
1479
  The partials of the observations with respect to the
1493
1480
  initial nominal state.
1494
- weights : array
1495
- The weights of the observations.
1496
1481
  residuals : array
1497
1482
  The residuals of the observations
1483
+ start_rejecting : bool
1484
+ Flag for whether to start rejecting outliers.
1498
1485
 
1499
1486
  Returns
1500
1487
  -------
1501
- partials : array
1502
- The partials of the observations with respect to the
1503
- initial nominal state after outlier rejection.
1504
- weights : array
1505
- The weights of the observations after outlier rejection.
1506
- residuals : array
1507
- The residuals of the observations after outlier rejection.
1508
- residual_chi_squared : array
1509
- The chi squared values of the residuals after outlier rejection.
1488
+ rms_u : float
1489
+ Unweighted RMS of the residuals.
1490
+ rms_w : float
1491
+ Weighted RMS of the residuals.
1492
+ residual_chi_squared : list
1493
+ List of the residual chi-squared values for each observation.
1510
1494
 
1511
1495
  Raises
1512
1496
  ------
@@ -1520,73 +1504,75 @@ class FitSimulation:
1520
1504
  "Use default values if unsure. "
1521
1505
  "Default values are chi_reject=3.0 and chi_recover=2.8 "
1522
1506
  "(Implemented as FitSimulation.reject_criteria=[3.0, 2.8])")
1523
- full_cov = np.linalg.inv(partials.T @ self.obs_weight @ partials)
1507
+ full_cov = self.covariance
1508
+ residual_chi_squared = [None]*len(self.obs_array)
1509
+ self.num_rejected = 0
1510
+ rms_u = 0
1511
+ rms_w = 0
1524
1512
  j = 0
1525
- residual_chi_squared = np.zeros(len(self.obs_array))
1526
- rejected_indices = []
1527
1513
  for i in range(len(self.obs_array)):
1528
1514
  obs_info_len = len(self.observer_info[i])
1529
1515
  if obs_info_len in {4, 7}:
1530
1516
  size = 2
1531
1517
  elif obs_info_len in {9, 10}:
1532
1518
  size = 1
1533
- j += size
1534
- continue
1535
1519
  else:
1536
1520
  raise ValueError("Observer info length not recognized.")
1537
- # calculate chi-squared for each residual
1538
1521
  resid = residuals[j:j+size].reshape((1, size))
1522
+ # calculate chi-squared for each residual
1539
1523
  obs_cov = self.obs_cov[j:j+size, j:j+size]
1540
1524
  obs_partials = partials[j:j+size, :]
1541
1525
  if self.rejection_flag[i]:
1542
1526
  resid_cov = obs_cov + obs_partials @ full_cov @ obs_partials.T
1543
1527
  else:
1544
1528
  resid_cov = obs_cov - obs_partials @ full_cov @ obs_partials.T
1545
- residual_chi_squared[i] = resid @ np.linalg.inv(resid_cov) @ resid.T
1529
+ residual_chi_squared[i] = (resid @ np.linalg.inv(resid_cov) @ resid.T)[0,0]
1546
1530
  # outlier rejection, only reject RA/Dec measurements
1547
- # but not from Gaia/radar tuples
1548
- if isinstance(self.observer_codes[i], str):
1549
- if residual_chi_squared[i] > chi_reject**2 and size == 2:
1531
+ # but not Gaia/radar measurements
1532
+ if start_rejecting and size == 2 and isinstance(self.observer_codes[i], str):
1533
+ if residual_chi_squared[i] > chi_reject**2:
1550
1534
  self.rejection_flag[i] = True
1551
- if self.rejection_flag[i] and residual_chi_squared[i] < chi_recover**2:
1535
+ self.num_rejected += 1
1536
+ elif self.rejection_flag[i] and residual_chi_squared[i] < chi_recover**2:
1552
1537
  self.rejection_flag[i] = False
1553
- if self.rejection_flag[i]:
1554
- rejected_indices.extend((j, j+1))
1538
+ self.num_rejected -= 1
1539
+ if not self.rejection_flag[i]:
1540
+ rms_u += (resid @ resid.T)[0,0]
1541
+ rms_w += (resid @ self.obs_weight[j:j+size, j:j+size] @ resid.T)[0,0]
1555
1542
  j += size
1556
- # remove rejected residuals from partials, weights, and residuals
1557
- partials = np.delete(partials, rejected_indices, axis=0)
1558
- weights = np.delete(weights, rejected_indices, axis=0)
1559
- weights = np.delete(weights, rejected_indices, axis=1)
1560
- residuals = np.delete(residuals, rejected_indices, axis=0)
1561
- # print(rejected_indices)
1562
- self.num_rejected = len(rejected_indices)//2
1563
1543
  rejected_fraction = self.num_rejected/len(self.obs_array)
1564
1544
  if rejected_fraction > 0.25:
1565
1545
  print("WARNING: More than 25% of observations rejected. Consider changing chi_reject",
1566
1546
  "and chi_recover values, or turning off outlier rejection altogether.")
1567
- self.residual_chi_squared = residual_chi_squared
1568
- return partials, weights, residuals, residual_chi_squared
1547
+ rms_u = np.sqrt(rms_u/self.n_obs)
1548
+ rms_w = np.sqrt(rms_w/self.n_obs)
1549
+ return rms_u, rms_w, residual_chi_squared
1569
1550
 
1570
- def _add_iteration(self, iter_number, residuals, obs_weight_rej):
1551
+ def _add_iteration(self, iter_number, residuals, rms_u, rms_w, residual_chi_squared):
1571
1552
  """
1572
1553
  Adds an iteration to the list of iterations in the FitSimulation object.
1573
1554
 
1574
1555
  Parameters
1575
1556
  ----------
1576
1557
  iter_number : int
1577
- The iteration number.
1558
+ Iteration number.
1578
1559
  residuals : array
1579
- The residuals of the observations.
1580
- obs_weight_rej : array
1581
- Observation weight matrix after outlier rejection.
1560
+ Residuals of the observations.
1561
+ rms_u : float
1562
+ Unweighted RMS of the residuals.
1563
+ rms_w : float
1564
+ Weighted RMS of the residuals.
1565
+ residual_chi_squared : list
1566
+ List of the residual chi-squared values for each observation.
1582
1567
 
1583
1568
  Returns
1584
1569
  -------
1585
1570
  None : NoneType
1586
1571
  None
1587
1572
  """
1588
- self.iters.append(IterationParams(iter_number, self.x_nom, self.covariance, residuals,
1589
- self.obs_array, obs_weight_rej, self.observer_codes,
1573
+ self.iters.append(IterationParams(iter_number, residuals, rms_u, rms_w,
1574
+ residual_chi_squared, self.x_nom,
1575
+ self.covariance, self.obs_array,
1590
1576
  self.rejection_flag))
1591
1577
  return None
1592
1578
 
@@ -1600,7 +1586,7 @@ class FitSimulation:
1600
1586
  None
1601
1587
  """
1602
1588
  if self.n_iter > 1:
1603
- del_rms_convergence = 1e-3
1589
+ del_rms_convergence = 1e-4
1604
1590
  curr_rms = self.iters[-1].weighted_rms
1605
1591
  prev_rms = self.iters[-2].weighted_rms
1606
1592
  del_rms = abs(prev_rms - curr_rms)/prev_rms
@@ -1608,7 +1594,7 @@ class FitSimulation:
1608
1594
  self.converged = True
1609
1595
  return None
1610
1596
 
1611
- def _get_lsq_state_correction(self, partials, weights, residuals):
1597
+ def _get_lsq_state_correction(self, partials, residuals):
1612
1598
  """
1613
1599
  Get the state correction using least-squares.
1614
1600
 
@@ -1617,8 +1603,6 @@ class FitSimulation:
1617
1603
  partials : array
1618
1604
  The partials of the observations with respect to the
1619
1605
  initial nominal state.
1620
- weights : array
1621
- The weights of the observations.
1622
1606
  residuals : array
1623
1607
  The residuals of the observations
1624
1608
 
@@ -1631,8 +1615,6 @@ class FitSimulation:
1631
1615
  atwb = np.zeros((self.n_fit, 1))
1632
1616
  j = 0
1633
1617
  for i in range(len(self.obs_array)):
1634
- if j >= len(residuals):
1635
- break
1636
1618
  obs_info_len = len(self.observer_info[i])
1637
1619
  if obs_info_len in {4, 7}:
1638
1620
  size = 2
@@ -1643,13 +1625,16 @@ class FitSimulation:
1643
1625
  if self.rejection_flag[i]:
1644
1626
  j += size
1645
1627
  continue
1646
- atwa += partials[j:j+size, :].T @ weights[j:j+size, j:j+size] @ partials[j:j+size, :]
1647
- atwb += (partials[j:j+size, :].T @ weights[j:j+size, j:j+size]
1628
+ atwa += (partials[j:j+size, :].T @ self.obs_weight[j:j+size, j:j+size]
1629
+ @ partials[j:j+size, :])
1630
+ atwb += (partials[j:j+size, :].T @ self.obs_weight[j:j+size, j:j+size]
1648
1631
  @ residuals[j:j+size].reshape((size, 1)))
1649
1632
  j += size
1650
- # atwa = partials.T @ weights @ partials
1651
- # atwb = partials.T @ weights @ residuals
1652
- cov = np.linalg.inv(atwa)
1633
+ # use pseudo-inverse if the data arc is less than 7 days
1634
+ if self.obs_array[-1,0]-self.obs_array[0,0] < 7.0:
1635
+ cov = np.linalg.pinv(atwa, rcond=1e-20, hermitian=True)
1636
+ else:
1637
+ cov = np.array(prop.matrix_inverse(atwa))
1653
1638
  delta_x = cov @ atwb
1654
1639
  return delta_x.ravel(), cov
1655
1640
 
@@ -1675,29 +1660,28 @@ class FitSimulation:
1675
1660
  self.n_iter = i+1
1676
1661
  # get residuals and partials
1677
1662
  residuals, partials = self._get_residuals_and_partials()
1678
- weights = self.obs_weight
1663
+ clean_residuals = self._flatten_and_clean(residuals)
1664
+ # calculate rms and reject outliers here if desired
1665
+ rms_u, rms_w, res_chi_sq = self._get_rms_and_reject_outliers(partials, clean_residuals,
1666
+ start_rejecting)
1679
1667
  if i == 0:
1680
1668
  # add prefit iteration
1681
- prefit_residuals = residuals.copy()
1682
- self._add_iteration(0, prefit_residuals, weights)
1683
- clean_residuals = self._flatten_and_clean(residuals)
1684
- # reject outliers here
1685
- if self.reject_outliers and start_rejecting:
1686
- a_rej, w_rej, b_rej, _ = self._apply_outlier_rejection(partials, weights,
1687
- clean_residuals)
1688
- else:
1689
- a_rej, w_rej, b_rej = partials, weights, clean_residuals
1669
+ self._add_iteration(0, residuals, rms_u, rms_w, res_chi_sq)
1690
1670
  # get initial guess
1691
1671
  curr_state = np.array(list(self.x_nom.values()))
1692
1672
  # get state correction
1693
- delta_x, cov = self._get_lsq_state_correction(a_rej, w_rej, b_rej)
1673
+ delta_x, cov = self._get_lsq_state_correction(partials, clean_residuals)
1694
1674
  # get new state
1695
1675
  next_state = curr_state + delta_x
1676
+ if self.fit_cometary and next_state[0] < 0.0:
1677
+ next_state[0] = 0.0
1678
+ print("WARNING: Eccentricity is negative per least squares state correction. "
1679
+ "Setting to 0.0. This solution may not be trustworthy.")
1696
1680
  self.x_nom = dict(zip(self.x_nom.keys(), next_state))
1697
1681
  # get new covariance
1698
1682
  self.covariance = cov
1699
1683
  # add iteration
1700
- self._add_iteration(i+1, residuals, w_rej)
1684
+ self._add_iteration(i+1, residuals, rms_u, rms_w, res_chi_sq)
1701
1685
  if verbose:
1702
1686
  print(f"{self.iters[-1].iter_number}\t\t\t",
1703
1687
  f"{self.iters[-1].unweighted_rms:.3f}\t\t\t",
@@ -1749,7 +1733,7 @@ class FitSimulation:
1749
1733
  print("--------------------------------------------------------------")
1750
1734
  print(f"Solution Time: MJD {self.t_sol:0.3f} TDB =",
1751
1735
  f"{Time(self.t_sol, format='mjd', scale='tdb').iso} TDB")
1752
- print(f"Solution Observation Arc: {arc:0.2f} days")
1736
+ print(f"Solution Observation Arc: {arc:0.2f} days ({arc/365.25:0.2f} years)")
1753
1737
  print("--------------------------------------------------------------")
1754
1738
  print("Fitted Variable\t\tInitial Value\t\t\tUncertainty\t\t\tFitted Value",
1755
1739
  "\t\t\tUncertainty\t\t\tChange\t\t\t\tChange (sigma)")
@@ -1885,6 +1869,7 @@ def _generate_simulated_obs(ref_sol, ref_cov, ref_ng_info, events, modified_obs_
1885
1869
  If the reference solution does not contain a full cartesian
1886
1870
  or cometary state.
1887
1871
  """
1872
+ # pylint: disable=no-member
1888
1873
  obs_sigma_dict = { 'astrometry': 1, # arcsec
1889
1874
  'occultation': 0.25, # fraction of body angular diameter
1890
1875
  'delay': 2, # microseconds
@@ -2,6 +2,7 @@
2
2
  import numpy as np
3
3
  import pandas as pd
4
4
  from joblib import Parallel, delayed
5
+ # pylint: disable=no-name-in-module
5
6
  from . import prop_simulation as prop
6
7
 
7
8
  __all__ = [ 'cluster_ca_or_impacts',