grss 4.2.0__tar.gz → 4.3.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 (76) hide show
  1. {grss-4.2.0/grss.egg-info → grss-4.3.2}/PKG-INFO +1 -1
  2. {grss-4.2.0 → grss-4.3.2}/grss/fit/fit_ades.py +1 -0
  3. {grss-4.2.0 → grss-4.3.2}/grss/fit/fit_optical.py +2 -1
  4. {grss-4.2.0 → grss-4.3.2}/grss/fit/fit_radar.py +0 -9
  5. {grss-4.2.0 → grss-4.3.2}/grss/fit/fit_simulation.py +241 -74
  6. {grss-4.2.0 → grss-4.3.2}/grss/kernels/get_kernels.py +10 -10
  7. {grss-4.2.0 → grss-4.3.2}/grss/utils.py +13 -0
  8. grss-4.3.2/grss/version.txt +1 -0
  9. {grss-4.2.0 → grss-4.3.2/grss.egg-info}/PKG-INFO +1 -1
  10. {grss-4.2.0 → grss-4.3.2}/include/gr15.h +18 -6
  11. {grss-4.2.0 → grss-4.3.2}/include/grss.h +2 -1
  12. {grss-4.2.0 → grss-4.3.2}/include/simulation.h +44 -22
  13. {grss-4.2.0 → grss-4.3.2}/include/stm.h +6 -0
  14. {grss-4.2.0 → grss-4.3.2}/src/force.cpp +41 -0
  15. {grss-4.2.0 → grss-4.3.2}/src/gr15.cpp +102 -42
  16. {grss-4.2.0 → grss-4.3.2}/src/grss.cpp +64 -53
  17. {grss-4.2.0 → grss-4.3.2}/src/interpolate.cpp +18 -26
  18. {grss-4.2.0 → grss-4.3.2}/src/observe.cpp +101 -66
  19. {grss-4.2.0 → grss-4.3.2}/src/pck.cpp +3 -2
  20. {grss-4.2.0 → grss-4.3.2}/src/simulation.cpp +173 -43
  21. {grss-4.2.0 → grss-4.3.2}/src/stm.cpp +51 -0
  22. {grss-4.2.0 → grss-4.3.2}/tests/cpp/prop/pck_map.cpp +5 -5
  23. grss-4.2.0/grss/version.txt +0 -1
  24. {grss-4.2.0 → grss-4.3.2}/CMakeLists.txt +0 -0
  25. {grss-4.2.0 → grss-4.3.2}/LICENSE +0 -0
  26. {grss-4.2.0 → grss-4.3.2}/MANIFEST.in +0 -0
  27. {grss-4.2.0 → grss-4.3.2}/README.md +0 -0
  28. {grss-4.2.0 → grss-4.3.2}/build_cpp.sh +0 -0
  29. {grss-4.2.0 → grss-4.3.2}/docs/source/conf.py +0 -0
  30. {grss-4.2.0 → grss-4.3.2}/extern/get_cspice.py +0 -0
  31. {grss-4.2.0 → grss-4.3.2}/grss/__init__.py +0 -0
  32. {grss-4.2.0 → grss-4.3.2}/grss/debias/get_debiasing_data.py +0 -0
  33. {grss-4.2.0 → grss-4.3.2}/grss/fit/__init__.py +0 -0
  34. {grss-4.2.0 → grss-4.3.2}/grss/fit/fit_utils.py +0 -0
  35. {grss-4.2.0 → grss-4.3.2}/grss/prop/__init__.py +0 -0
  36. {grss-4.2.0 → grss-4.3.2}/grss/prop/prop_parallel.py +0 -0
  37. {grss-4.2.0 → grss-4.3.2}/grss/prop/prop_unscented.py +0 -0
  38. {grss-4.2.0 → grss-4.3.2}/grss/prop/prop_utils.py +0 -0
  39. {grss-4.2.0 → grss-4.3.2}/grss.egg-info/SOURCES.txt +0 -0
  40. {grss-4.2.0 → grss-4.3.2}/grss.egg-info/dependency_links.txt +0 -0
  41. {grss-4.2.0 → grss-4.3.2}/grss.egg-info/requires.txt +0 -0
  42. {grss-4.2.0 → grss-4.3.2}/grss.egg-info/top_level.txt +0 -0
  43. {grss-4.2.0 → grss-4.3.2}/include/approach.h +0 -0
  44. {grss-4.2.0 → grss-4.3.2}/include/elements.h +0 -0
  45. {grss-4.2.0 → grss-4.3.2}/include/force.h +0 -0
  46. {grss-4.2.0 → grss-4.3.2}/include/interpolate.h +0 -0
  47. {grss-4.2.0 → grss-4.3.2}/include/observe.h +0 -0
  48. {grss-4.2.0 → grss-4.3.2}/include/parallel.h +0 -0
  49. {grss-4.2.0 → grss-4.3.2}/include/pck.h +0 -0
  50. {grss-4.2.0 → grss-4.3.2}/include/spk.h +0 -0
  51. {grss-4.2.0 → grss-4.3.2}/include/timeconvert.h +0 -0
  52. {grss-4.2.0 → grss-4.3.2}/include/utilities.h +0 -0
  53. {grss-4.2.0 → grss-4.3.2}/pyproject.toml +0 -0
  54. {grss-4.2.0 → grss-4.3.2}/requirements.txt +0 -0
  55. {grss-4.2.0 → grss-4.3.2}/setup.cfg +0 -0
  56. {grss-4.2.0 → grss-4.3.2}/setup.py +0 -0
  57. {grss-4.2.0 → grss-4.3.2}/src/CMakeLists.txt +0 -0
  58. {grss-4.2.0 → grss-4.3.2}/src/approach.cpp +0 -0
  59. {grss-4.2.0 → grss-4.3.2}/src/elements.cpp +0 -0
  60. {grss-4.2.0 → grss-4.3.2}/src/parallel.cpp +0 -0
  61. {grss-4.2.0 → grss-4.3.2}/src/spk.cpp +0 -0
  62. {grss-4.2.0 → grss-4.3.2}/src/timeconvert.cpp +0 -0
  63. {grss-4.2.0 → grss-4.3.2}/src/utilities.cpp +0 -0
  64. {grss-4.2.0 → grss-4.3.2}/tests/cpp/prop/CMakeLists.txt +0 -0
  65. {grss-4.2.0 → grss-4.3.2}/tests/cpp/prop/apophis.cpp +0 -0
  66. {grss-4.2.0 → grss-4.3.2}/tests/cpp/prop/didymos.cpp +0 -0
  67. {grss-4.2.0 → grss-4.3.2}/tests/cpp/prop/spk_map.cpp +0 -0
  68. {grss-4.2.0 → grss-4.3.2}/tests/python/fit/chesley.ipynb +0 -0
  69. {grss-4.2.0 → grss-4.3.2}/tests/python/fit/eggl.ipynb +0 -0
  70. {grss-4.2.0 → grss-4.3.2}/tests/python/fit/farnocchia.ipynb +0 -0
  71. {grss-4.2.0 → grss-4.3.2}/tests/python/fit/shantanunaidu.ipynb +0 -0
  72. {grss-4.2.0 → grss-4.3.2}/tests/python/prop/apophis.ipynb +0 -0
  73. {grss-4.2.0 → grss-4.3.2}/tests/python/prop/didymos.ipynb +0 -0
  74. {grss-4.2.0 → grss-4.3.2}/tests/python/prop/icarus.ipynb +0 -0
  75. {grss-4.2.0 → grss-4.3.2}/tests/python/prop/moshup.ipynb +0 -0
  76. {grss-4.2.0 → grss-4.3.2}/tests/python/run_tests.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: grss
3
- Version: 4.2.0
3
+ Version: 4.3.2
4
4
  Summary: GRSS: Gauss-Radau Small-body Simulator
5
5
  Author: Rahil Makadia
6
6
  Author-email: makadia2@illinois.edu
@@ -32,6 +32,7 @@ ades_add_columns = {
32
32
  }
33
33
  ades_grss_columns = {
34
34
  'obsTimeMJD': 'float', 'obsTimeMJDTDB': 'float', 'cosDec': 'float',
35
+ 'resChi': 'float',
35
36
  }
36
37
  ades_column_types = ades_keep_columns | ades_add_columns | ades_grss_columns
37
38
 
@@ -479,7 +479,8 @@ def apply_debiasing_scheme(obs_df, lowres, verbose):
479
479
  unbias_counter += len(group)
480
480
  continue
481
481
  if cat_code not in biased_catalogs:
482
- print(f"\tUnknown star catalog: {cat}")
482
+ if verbose:
483
+ print(f"\tUnknown star catalog: {cat}")
483
484
  obs_df.loc[group.index, 'biasRA'] = 0.0
484
485
  obs_df.loc[group.index, 'biasDec'] = 0.0
485
486
  continue
@@ -71,7 +71,6 @@ def add_radar_obs(obs_df, t_min_tdb=None, t_max_tdb=None, verbose=False):
71
71
  print(f"Read in {num_obs} radar observations from JPL radar API.")
72
72
  data = raw_data['data']
73
73
  time_range_count = 0
74
- pre1972_count = 0
75
74
  for i in range(num_obs):
76
75
  obs = data[num_obs-i-1]
77
76
  date = Time(obs[1], format='iso', scale='utc')
@@ -105,14 +104,6 @@ def add_radar_obs(obs_df, t_min_tdb=None, t_max_tdb=None, verbose=False):
105
104
  obs_df.loc[idx,'frq'] = freq
106
105
  obs_df.loc[idx,'sigDelay'] = obs_sigma if delay else np.nan
107
106
  obs_df.loc[idx,'sigDoppler'] = obs_sigma if doppler else np.nan
108
- if date.utc.mjd < 41317:
109
- pre1972_count += 1
110
- obs_df.loc[idx,'selAst'] = 'd'
111
- if pre1972_count > 0:
112
- print(f"\tWARNING: {pre1972_count} radar observations were taken before 1972.\n"
113
- "\t\tThese residuals cannot be reliably computed because high-accuracy Earth "
114
- "orientation kernels are not available for this time period.\n"
115
- "\t\tForce deleting them...")
116
107
  if verbose:
117
108
  print(f"\tFiltered to {num_obs-time_range_count} observations that satisfy the "
118
109
  "time range constraints.")
@@ -46,7 +46,7 @@ class IterationParams:
46
46
  self.covariance = covariance
47
47
  variance = np.sqrt(np.diag(self.covariance))
48
48
  self.variance = dict(zip([f'var_{k}' for k in self.x_nom.keys()], variance))
49
- self.obs = obs
49
+ self.obs = obs.copy()
50
50
  force_del_idx = self.obs[self.obs['selAst'] == 'd'].index
51
51
  auto_del_idx = self.obs[self.obs['selAst'] == 'D'].index
52
52
  self.rejected_idx = np.concatenate((force_del_idx, auto_del_idx))
@@ -549,7 +549,7 @@ class FitSimulation:
549
549
  self._compute_obs_weights()
550
550
  self.n_iter = 0
551
551
  self.n_iter_max = n_iter_max
552
- self.iters = []
552
+ self.iters = [[]]
553
553
  self.de_kernel = de_kernel
554
554
  self.de_kernel_path = default_kernel_path
555
555
  self.analytic_partials = False
@@ -561,6 +561,12 @@ class FitSimulation:
561
561
  for key in nongrav_info:
562
562
  self.fixed_propsim_params[key] = nongrav_info[key]
563
563
  self.fixed_propsim_params['events'] = events if events is not None else []
564
+ self.prior_est = None
565
+ self.prior_sigs = None
566
+ self._priors_given = False
567
+ self._xbar0 = None
568
+ self._info0 = None
569
+ self._prior_constant = None
564
570
  self.reject_outliers = True
565
571
  self.reject_criteria = [3.0, 2.8]
566
572
  # number of rejected obs is the count of ['D', 'd'] in selAst
@@ -621,6 +627,38 @@ class FitSimulation:
621
627
  raise ValueError(msg)
622
628
  return None
623
629
 
630
+ def _check_priors(self):
631
+ """
632
+ Check the prior estimates and sigmas provided by the user
633
+ and make sure they are valid.
634
+
635
+ Returns
636
+ -------
637
+ None : NoneType
638
+ None
639
+
640
+ Raises
641
+ ------
642
+ ValueError
643
+ If the prior estimates and sigmas do not have the same length.
644
+ """
645
+ # check that the keys in self.x_init are the same as in self.prior_est and self.prior_sigs
646
+ if self.prior_est is not None and self.prior_sigs is not None:
647
+ self._priors_given = True
648
+ if len(self.prior_est) != len(self.prior_sigs):
649
+ raise ValueError("Prior estimates and sigmas must have the same length.")
650
+ for key in self.prior_est.keys():
651
+ if key not in self.x_init:
652
+ raise ValueError(f"Key {key} not found in initial state vector.")
653
+ self._info0 = np.zeros_like(self.covariance_init)
654
+ self._xbar0 = np.zeros(self.n_fit)
655
+ for key in self.prior_est.keys():
656
+ idx = list(self.x_init.keys()).index(key)
657
+ self._info0[idx, idx] = 1/self.prior_sigs[key]**2
658
+ self._xbar0[idx] = self.prior_est[key] - self.x_init[key]
659
+ self._prior_constant = list(self.x_nom.values())+self._xbar0
660
+ return None
661
+
624
662
  def _flatten_and_clean(self, arr):
625
663
  """
626
664
  Flatten an array and remove any NaN values.
@@ -653,14 +691,18 @@ class FitSimulation:
653
691
  weights = self.simulated_obs['weights']
654
692
  if not len(times) == len(modes) == len(weights):
655
693
  raise ValueError("Simulated observation data must have the same length.")
694
+ perm_id = self.obs['permID'][0]
695
+ prov_id = self.obs['provID'][0]
656
696
  for i, time in enumerate(times):
657
697
  idx = len(self.obs)
658
698
  mode = modes[i]
659
699
  if mode not in {'SIM_CCD', 'SIM_OCC', 'SIM_RAD_DEL', 'SIM_RAD_DOP'}:
660
700
  raise ValueError(f"Unknown simulated observation mode {mode}.")
661
701
  weight = weights[i]
662
- self.obs.loc[idx, 'permID'] = 'SIM_'+str(self.obs['permID'][0])
663
- self.obs.loc[idx, 'provID'] = 'SIM_'+str(self.obs['provID'][0])
702
+ if isinstance(perm_id, str):
703
+ self.obs.loc[idx, 'permID'] = f'SIM_{perm_id}'
704
+ if isinstance(prov_id, str):
705
+ self.obs.loc[idx, 'provID'] = f'SIM_{prov_id}'
664
706
  self.obs.loc[idx, 'obsTime'] = f'{time.utc.isot}Z'
665
707
  self.obs.loc[idx, 'obsTimeMJD'] = time.utc.mjd
666
708
  self.obs.loc[idx, 'obsTimeMJDTDB'] = time.tdb.mjd
@@ -801,7 +843,7 @@ class FitSimulation:
801
843
  """
802
844
  # pylint: disable=no-member
803
845
  t_eval_past = self.obs.obsTimeMJD.values[self.past_obs_idx]
804
- tf_past = np.min(t_eval_past)
846
+ tf_past = np.min(self.obs.obsTimeMJDTDB.values[self.past_obs_idx])
805
847
  prop_sim_past = libgrss.PropSimulation(name, self.t_sol,
806
848
  self.de_kernel, self.de_kernel_path)
807
849
  prop_sim_past.tEvalMargin = 1.0
@@ -845,7 +887,7 @@ class FitSimulation:
845
887
  """
846
888
  # pylint: disable=no-member
847
889
  t_eval_future = self.obs.obsTimeMJD.values[self.future_obs_idx]
848
- tf_future = np.max(t_eval_future)
890
+ tf_future = np.max(self.obs.obsTimeMJDTDB.values[self.future_obs_idx])
849
891
  prop_sim_future = libgrss.PropSimulation(name, self.t_sol,
850
892
  self.de_kernel, self.de_kernel_path)
851
893
  prop_sim_future.tEvalMargin = 1.0
@@ -986,12 +1028,24 @@ class FitSimulation:
986
1028
  events = []
987
1029
  for i in range(len(self.fixed_propsim_params['events'])):
988
1030
  fixed_event = tuple(self.fixed_propsim_params['events'][i])
1031
+ fixed_event_impulsive = len(fixed_event) == 5
1032
+ fixed_event_continuous = len(fixed_event) == 6
1033
+ if fixed_event_continuous and fixed_event[4] == 0.0:
1034
+ raise ValueError("Continuous event time constant cannot be 0.")
1035
+ assert fixed_event_impulsive or fixed_event_continuous
989
1036
  event = [None]*5
990
- event[0] = fixed_event[0]
991
- event[1] = x_dict[f"dvx{i}"] if f"dvx{i}" in x_dict.keys() else fixed_event[1]
992
- event[2] = x_dict[f"dvy{i}"] if f"dvy{i}" in x_dict.keys() else fixed_event[2]
993
- event[3] = x_dict[f"dvz{i}"] if f"dvz{i}" in x_dict.keys() else fixed_event[3]
994
- event[4] = x_dict[f"mult{i}"] if f"mult{i}" in x_dict.keys() else fixed_event[4]
1037
+ event[0] = fixed_event[0] # time of delta-v
1038
+ if fixed_event_impulsive:
1039
+ event[1] = x_dict[f"dvx{i}"] if f"dvx{i}" in x_dict.keys() else fixed_event[1]
1040
+ event[2] = x_dict[f"dvy{i}"] if f"dvy{i}" in x_dict.keys() else fixed_event[2]
1041
+ event[3] = x_dict[f"dvz{i}"] if f"dvz{i}" in x_dict.keys() else fixed_event[3]
1042
+ event[4] = x_dict[f"mult{i}"] if f"mult{i}" in x_dict.keys() else fixed_event[4]
1043
+ elif fixed_event_continuous:
1044
+ event[1] = x_dict[f"ax{i}"] if f"ax{i}" in x_dict.keys() else fixed_event[1]
1045
+ event[2] = x_dict[f"ay{i}"] if f"ay{i}" in x_dict.keys() else fixed_event[2]
1046
+ event[3] = x_dict[f"az{i}"] if f"az{i}" in x_dict.keys() else fixed_event[3]
1047
+ event[4] = x_dict[f"dt{i}"] if f"dt{i}" in x_dict.keys() else fixed_event[4]
1048
+ event.append(fixed_event[5])
995
1049
  events.append(tuple(event))
996
1050
  return events
997
1051
 
@@ -1017,16 +1071,42 @@ class FitSimulation:
1017
1071
  prop_sim_future : libgrss.PropSimulation object
1018
1072
  PropSimulation object for the future.
1019
1073
  """
1020
- for event in events:
1021
- t_event = event[0]
1022
- dvx = event[1]
1023
- dvy = event[2]
1024
- dvz = event[3]
1025
- multiplier = event[4]
1074
+ est_keys = self.x_nom.keys()
1075
+ for i, event_list in enumerate(events):
1076
+ t_event = event_list[0]
1077
+ dvx_or_ax = event_list[1]
1078
+ dvy_or_ay = event_list[2]
1079
+ dvz_or_az = event_list[3]
1080
+ multiplier_or_dt = event_list[4]
1081
+ continuous_flag = event_list[5] if len(event_list) == 6 else False
1082
+ event = libgrss.Event()
1083
+ event.t = t_event
1084
+ event.bodyName = integ_body.name
1085
+ event.isContinuous = continuous_flag
1086
+ if continuous_flag:
1087
+ event.expAccel0 = [dvx_or_ax, dvy_or_ay, dvz_or_az]
1088
+ event.tau = multiplier_or_dt
1089
+ event.expAccel0Est = (
1090
+ f"ax{i}" in est_keys and
1091
+ f"ay{i}" in est_keys and
1092
+ f"az{i}" in est_keys
1093
+ )
1094
+ event.tauEst = f"dt{i}" in est_keys
1095
+ event.eventEst = event.expAccel0Est or event.tauEst
1096
+ else:
1097
+ event.deltaV = [dvx_or_ax, dvy_or_ay, dvz_or_az]
1098
+ event.multiplier = multiplier_or_dt
1099
+ event.deltaVEst = (
1100
+ f"dvx{i}" in est_keys and
1101
+ f"dvy{i}" in est_keys and
1102
+ f"dvz{i}" in est_keys
1103
+ )
1104
+ event.multiplierEst = f"mult{i}" in est_keys
1105
+ event.eventEst = event.deltaVEst or event.multiplierEst
1026
1106
  if t_event < self.t_sol:
1027
- prop_sim_past.add_event(integ_body, t_event, [dvx, dvy, dvz], multiplier)
1107
+ prop_sim_past.add_event(event)
1028
1108
  else:
1029
- prop_sim_future.add_event(integ_body, t_event, [dvx, dvy, dvz], multiplier)
1109
+ prop_sim_future.add_event(event)
1030
1110
  return prop_sim_past, prop_sim_future
1031
1111
 
1032
1112
  def _get_perturbed_state(self, key):
@@ -1055,6 +1135,7 @@ class FitSimulation:
1055
1135
  fd_delta : float
1056
1136
  Finite difference perturbation.
1057
1137
  """
1138
+ fd_pert = np.inf
1058
1139
  if self.fit_cartesian:
1059
1140
  if key in {'x', 'y', 'z'}:
1060
1141
  fd_pert = 1e-8
@@ -1074,6 +1155,14 @@ class FitSimulation:
1074
1155
  fd_pert = 1e0
1075
1156
  if key[:3] in {'dvx', 'dvy', 'dvz'}:
1076
1157
  fd_pert = 1e-11
1158
+ if key[:2] in {'ax', 'ay', 'az'}:
1159
+ fd_pert = 1e-11
1160
+ if key[:2] == 'dt':
1161
+ # 10 percent of the time constant
1162
+ fd_pert = 0.1*self.x_nom[key]
1163
+ if fd_pert == np.inf:
1164
+ raise ValueError("Finite difference perturbation not defined"
1165
+ " for the given state parameter.")
1077
1166
  x_plus = self.x_nom.copy()
1078
1167
  x_minus = self.x_nom.copy()
1079
1168
  # fd_pert = finite difference perturbation to nominal state for calculating derivatives
@@ -1285,7 +1374,7 @@ class FitSimulation:
1285
1374
  radius_nonzero = self.fixed_propsim_params['radius'] != 0.0
1286
1375
  fac = 0.0
1287
1376
  if radius_nonzero:
1288
- rel_dists = np.linalg.norm(np.array(state_eval)[:, :3], axis=1)
1377
+ rel_dists = np.linalg.norm([state[:3] for state in state_eval], axis=1)
1289
1378
  lmbda = 0.3 # from fuentes-munoz et al. 2024
1290
1379
  au2m = 1.495978707e11
1291
1380
  fac = (lmbda*self.fixed_propsim_params['radius']/au2m/rel_dists*180/np.pi*3600)**2
@@ -1296,7 +1385,7 @@ class FitSimulation:
1296
1385
  | special_codes["spacecraft"]
1297
1386
  )
1298
1387
  for i in range(len(self.observer_info_lengths)):
1299
- if modes[i] not in {'RAD', 'SIM_RAD_DEL', 'SIM_RAD_DOP', 'SIM_OCC'}:
1388
+ if modes[i] not in {'RAD', 'SIM_RAD_DEL', 'SIM_RAD_DOP', 'SIM_CCD', 'SIM_OCC'}:
1300
1389
  sig_ra = sig_ra_vals[i]
1301
1390
  sig_dec = sig_dec_vals[i]
1302
1391
  sig_corr = sig_corr_vals[i]
@@ -1357,7 +1446,8 @@ class FitSimulation:
1357
1446
  size = 1
1358
1447
  else:
1359
1448
  raise ValueError("Observer info length not recognized.")
1360
- part = np.zeros((size, self.n_fit))
1449
+ # part is partial of observable with respect to state at observation time
1450
+ part = np.zeros((size, 6))
1361
1451
  if self.past_obs_exist and i < len_past_idx:
1362
1452
  sim_idx = i
1363
1453
  optical_partials = past_optical_partials
@@ -1368,14 +1458,16 @@ class FitSimulation:
1368
1458
  optical_partials = future_optical_partials
1369
1459
  radar_partials = future_radar_partials
1370
1460
  state = future_states
1371
- stm = libgrss.reconstruct_stm(state[sim_idx][6:])
1461
+ # stm is partial of state at observation time with respect to nominal state
1462
+ stm = libgrss.reconstruct_stm(state[sim_idx][6:])[:6]
1372
1463
  if is_optical:
1373
1464
  part[0, :6] = optical_partials[sim_idx, :6]*cos_dec[i]
1374
1465
  part[1, :6] = optical_partials[sim_idx, 6:12]
1375
1466
  else:
1376
1467
  part[0, :6] = radar_partials[sim_idx, :6]
1468
+ # partial is partial of observable with respect to nominal state
1377
1469
  partial = part @ stm
1378
- partials[partials_idx:partials_idx+size, :] = partial
1470
+ partials[partials_idx:partials_idx+size, :partial.shape[1]] = partial
1379
1471
  partials_idx += size
1380
1472
  return partials
1381
1473
 
@@ -1533,6 +1625,7 @@ class FitSimulation:
1533
1625
  chi_sq = 0
1534
1626
  j = 0
1535
1627
  sel_ast = self.obs['selAst'].values
1628
+ res_chisq_vals = np.nan*np.ones(len(self.observer_info_lengths))
1536
1629
  for i, obs_info_len in enumerate(self.observer_info_lengths):
1537
1630
  if obs_info_len in {4, 7}:
1538
1631
  size = 2
@@ -1541,8 +1634,8 @@ class FitSimulation:
1541
1634
  else:
1542
1635
  raise ValueError("Observer info length not recognized.")
1543
1636
  resid = residuals[i]
1544
- # calculate chi-squared for each residual
1545
- if start_rejecting:
1637
+ # calculate chi-squared for each residual if after the first iteration
1638
+ if self.n_iter > 1:
1546
1639
  obs_cov = self.obs_cov[i]
1547
1640
  obs_partials = partials[j:j+size, :]
1548
1641
  if sel_ast[i] in {'D', 'd'}:
@@ -1554,22 +1647,29 @@ class FitSimulation:
1554
1647
  else:
1555
1648
  resid_cov_det = resid_cov[0,0]*resid_cov[1,1] - resid_cov[0,1]*resid_cov[1,0]
1556
1649
  resid_cov_inv = np.array([[resid_cov[1,1], -resid_cov[0,1]],
1557
- [-resid_cov[1,0], resid_cov[0,0]]])/resid_cov_det
1650
+ [-resid_cov[1,0], resid_cov[0,0]]])/resid_cov_det
1558
1651
  residual_chi_squared = resid @ resid_cov_inv @ resid.T
1559
- # outlier rejection, only reject RA/Dec measurements
1560
- if size == 2:
1561
- if residual_chi_squared > chi_reject**2 and sel_ast[i] not in {'a', 'd'}:
1562
- if sel_ast[i] == 'A':
1563
- self.num_rejected += 1
1564
- sel_ast[i] = 'D'
1565
- elif residual_chi_squared < chi_recover**2 and sel_ast[i] == 'D':
1566
- sel_ast[i] = 'A'
1567
- self.num_rejected -= 1
1652
+ res_chisq_vals[i] = residual_chi_squared
1653
+ # outlier rejection, only reject RA/Dec measurements
1654
+ if start_rejecting and size == 2:
1655
+ if abs(residual_chi_squared) > chi_reject**2 and sel_ast[i] not in {'a', 'd'}:
1656
+ if sel_ast[i] == 'A':
1657
+ self.num_rejected += 1
1658
+ sel_ast[i] = 'D'
1659
+ elif abs(residual_chi_squared) < chi_recover**2 and sel_ast[i] == 'D':
1660
+ sel_ast[i] = 'A'
1661
+ self.num_rejected -= 1
1568
1662
  if sel_ast[i] not in {'D', 'd'}:
1569
1663
  rms_u += resid @ resid.T
1570
1664
  chi_sq += resid @ self.obs_weight[i] @ resid.T
1571
1665
  j += size
1666
+ # # write res_chisq_vals to file if any values are negative
1667
+ # if np.any(res_chisq_vals < 0):
1668
+ # print("WARNING: Negative outlier rejection chi-squared values detected. "
1669
+ # "Writing to file 'warn_neg_chisq_vals.txt'.")
1670
+ # np.savetxt("warn_neg_chisq_vals.txt", res_chisq_vals, fmt="%.16f")
1572
1671
  self.obs['selAst'] = sel_ast
1672
+ self.obs['resChi'] = res_chisq_vals**0.5
1573
1673
  rejected_fraction = self.num_rejected/len(self.obs)
1574
1674
  if rejected_fraction > 0.25:
1575
1675
  print("WARNING: More than 25% of observations rejected. Consider changing the",
@@ -1651,8 +1751,12 @@ class FitSimulation:
1651
1751
  delta_x : array
1652
1752
  The state correction.
1653
1753
  """
1654
- atwa = np.zeros((self.n_fit, self.n_fit))
1655
- atwb = np.zeros(self.n_fit)
1754
+ if self._priors_given:
1755
+ atwa = self._info0.copy()
1756
+ atwb = (self._info0 @ self._xbar0).copy()
1757
+ else:
1758
+ atwa = np.zeros((self.n_fit, self.n_fit))
1759
+ atwb = np.zeros(self.n_fit)
1656
1760
  j = 0
1657
1761
  sel_ast = self.obs['selAst'].values
1658
1762
  for i, obs_info_len in enumerate(self.observer_info_lengths):
@@ -1670,11 +1774,11 @@ class FitSimulation:
1670
1774
  j += size
1671
1775
  # use pseudo-inverse if the data arc is less than 7 days
1672
1776
  if self.obs.obsTimeMJD.max() - self.obs.obsTimeMJD.min() < 7.0:
1673
- cov = np.linalg.pinv(atwa, rcond=1e-20, hermitian=True)
1777
+ self.covariance = np.linalg.pinv(atwa, rcond=1e-20, hermitian=True)
1674
1778
  else:
1675
- cov = np.array(libgrss.matrix_inverse(atwa))
1676
- delta_x = cov @ atwb
1677
- return delta_x.ravel(), cov
1779
+ self.covariance = np.array(libgrss.matrix_inverse(atwa))
1780
+ delta_x = self.covariance @ atwb
1781
+ return delta_x.ravel()
1678
1782
 
1679
1783
  def filter_lsq(self, verbose=True):
1680
1784
  """
@@ -1690,35 +1794,51 @@ class FitSimulation:
1690
1794
  None : NoneType
1691
1795
  None
1692
1796
  """
1797
+ self._check_priors()
1693
1798
  start_rejecting = False
1694
1799
  if verbose:
1695
1800
  print("Iteration\t\tUnweighted RMS\t\tWeighted RMS",
1696
1801
  "\t\tChi-squared\t\tReduced Chi-squared")
1802
+ delta_x = np.zeros(self.n_fit)
1697
1803
  for i in range(self.n_iter_max):
1804
+ if self._priors_given:
1805
+ self._xbar0 -= delta_x
1806
+ a_priori_constant = list(self.x_nom.values())+self._xbar0
1807
+ a_priori_constant_violation = np.linalg.norm(a_priori_constant-self._prior_constant)
1808
+ if a_priori_constant_violation >= 1e-11:
1809
+ print(f"a priori constraint constant violation before iteration {i+1}: "
1810
+ f"{a_priori_constant_violation}. Solution may not be trustworthy. "
1811
+ "Violation for each estimated parameter:")
1812
+ print(dict(zip(self.x_nom.keys(), a_priori_constant-self._prior_constant)))
1698
1813
  self.n_iter = i+1
1699
1814
  # get residuals and partials
1700
1815
  residuals, partials = self._get_residuals_and_partials()
1701
1816
  # calculate rms and reject outliers here if desired
1702
1817
  rms_u, rms_w, chi_sq = self._get_rms_and_reject_outliers(partials, residuals,
1703
1818
  start_rejecting)
1704
- # get initial guess
1705
- curr_state = np.array(list(self.x_nom.values()))
1819
+ # get current state
1820
+ curr_state = list(self.x_nom.values())
1706
1821
  # get state correction
1707
- delta_x, cov = self._get_lsq_state_correction(partials, residuals)
1708
- # get new covariance
1709
- self.covariance = cov
1710
- # get new state
1711
- if i == 0:
1712
- # add prefit iteration
1713
- self._add_iteration(0, rms_u, rms_w, chi_sq)
1822
+ delta_x = self._get_lsq_state_correction(partials, residuals)
1714
1823
  if self.constraint_dir is not None:
1715
1824
  constr_hat = self.constraint_dir/np.linalg.norm(self.constraint_dir)
1716
1825
  delta_x -= np.dot(delta_x, constr_hat)*constr_hat
1826
+ # make sure eccentricity is non-negative and
1827
+ # any keys starting with dt are 0.1 at minimum
1828
+ for key in self.x_nom:
1829
+ if self.fit_cometary and key == 'e':
1830
+ idx = list(self.x_nom.keys()).index(key)
1831
+ if curr_state[idx] + delta_x[idx] < 0.0:
1832
+ delta_x[idx] = -curr_state[idx]
1833
+ print("WARNING: Eccentricity is negative per least squares "
1834
+ "state correction. Setting to 0.0. This solution "
1835
+ "may not be trustworthy.")
1836
+ if key[:2] == 'dt':
1837
+ idx = list(self.x_nom.keys()).index(key)
1838
+ if curr_state[idx] + delta_x[idx] < 0.1:
1839
+ delta_x[idx] = 0.1 - curr_state[idx]
1840
+ # get new nominal state
1717
1841
  next_state = curr_state + delta_x
1718
- if self.fit_cometary and next_state[0] < 0.0:
1719
- next_state[0] = 0.0
1720
- print("WARNING: Eccentricity is negative per least squares state correction. "
1721
- "Setting to 0.0. This solution may not be trustworthy.")
1722
1842
  self.x_nom = dict(zip(self.x_nom.keys(), next_state))
1723
1843
  # add iteration
1724
1844
  self._add_iteration(i+1, rms_u, rms_w, chi_sq)
@@ -1744,11 +1864,17 @@ class FitSimulation:
1744
1864
  print(msg)
1745
1865
  if not self.reject_outliers:
1746
1866
  break
1867
+ # add postfit iteration if converged
1868
+ if self.converged:
1869
+ residuals, partials = self._get_residuals_and_partials()
1870
+ rms_u, rms_w, chi_sq = self._get_rms_and_reject_outliers(partials, residuals,
1871
+ start_rejecting)
1872
+ self._add_iteration(self.n_iter+1, rms_u, rms_w, chi_sq)
1747
1873
  if self.n_iter == self.n_iter_max and not self.converged:
1748
1874
  print("WARNING: Maximum number of iterations reached without converging.")
1749
1875
  return None
1750
1876
 
1751
- def print_summary(self, iter_idx=-1, out_stream=sys.stdout):
1877
+ def print_summary(self, iter_idx=None, out_stream=sys.stdout):
1752
1878
  """
1753
1879
  Prints a summary of the orbit fit calculations at a given iteration.
1754
1880
 
@@ -1762,10 +1888,14 @@ class FitSimulation:
1762
1888
  None : NoneType
1763
1889
  None
1764
1890
  """
1891
+ iter_idx = len(self.iters)-1 if iter_idx is None else iter_idx
1765
1892
  data = self.iters[iter_idx]
1766
1893
  arc = self.obs.obsTimeMJD.max() - self.obs.obsTimeMJD.min()
1767
- str_to_print = "Summary of the orbit fit calculations at iteration "
1768
- str_to_print += f"{data.iter_number} (of {self.n_iter}):\n"
1894
+ str_to_print = "Summary of the orbit fit calculations"
1895
+ if data.iter_number <= self.n_iter:
1896
+ str_to_print += f" after iteration {data.iter_number} (of {self.n_iter}):\n"
1897
+ else:
1898
+ str_to_print += " after postfit pass:\n"
1769
1899
  str_to_print += "==============================================================\n"
1770
1900
  str_to_print += f"RMS unweighted: {data.unweighted_rms}\n"
1771
1901
  str_to_print += f"RMS weighted: {data.weighted_rms}\n"
@@ -1781,7 +1911,7 @@ class FitSimulation:
1781
1911
  str_to_print += "\t\t\tUncertainty\t\t\tChange\t\t\t\tChange (sigma)\n"
1782
1912
  init_variance = np.sqrt(np.diag(self.covariance_init))
1783
1913
  final_variance = np.sqrt(np.diag(self.covariance))
1784
- init_sol = self.iters[0].x_nom
1914
+ init_sol = self.x_init
1785
1915
  final_sol = data.x_nom
1786
1916
  with np.errstate(divide='ignore'):
1787
1917
  for i, key in enumerate(init_sol.keys()):
@@ -1817,7 +1947,9 @@ class FitSimulation:
1817
1947
  """
1818
1948
  plt.rcParams['font.size'] = 12
1819
1949
  plt.rcParams['axes.labelsize'] = 12
1820
- ticks = np.arange(1, self.n_iter+1, 1)
1950
+ ticks = np.arange(1, self.n_iter+2, 1)
1951
+ labels = list(ticks)
1952
+ labels[-1] = "Postfit"
1821
1953
  start_idx = 1
1822
1954
  iters_for_plot = self.iters[start_idx:]
1823
1955
  final_iter = iters_for_plot[-1]
@@ -1827,7 +1959,8 @@ class FitSimulation:
1827
1959
  [iteration.unweighted_rms for iteration in iters_for_plot],
1828
1960
  label=f"Final Unweighted RMS={final_iter.unweighted_rms:.3e}")
1829
1961
  plt.xticks(ticks)
1830
- plt.xlabel("Iteration #")
1962
+ plt.gca().set_xticklabels(labels)
1963
+ plt.xlabel("Iteration")
1831
1964
  plt.ylabel("Unweighted RMS")
1832
1965
  plt.legend()
1833
1966
  plt.subplot(2, 2, 2)
@@ -1835,7 +1968,8 @@ class FitSimulation:
1835
1968
  [iteration.weighted_rms for iteration in iters_for_plot],
1836
1969
  label=f"Final Weighted RMS={final_iter.weighted_rms:.3e}")
1837
1970
  plt.xticks(ticks)
1838
- plt.xlabel("Iteration #")
1971
+ plt.gca().set_xticklabels(labels)
1972
+ plt.xlabel("Iteration")
1839
1973
  plt.ylabel("Weighted RMS")
1840
1974
  plt.legend()
1841
1975
  plt.subplot(2, 2, 3)
@@ -1843,7 +1977,8 @@ class FitSimulation:
1843
1977
  [iteration.chi_squared for iteration in iters_for_plot],
1844
1978
  label=fr"Final $\chi^2$={final_iter.chi_squared:.3e}")
1845
1979
  plt.xticks(ticks)
1846
- plt.xlabel("Iteration #")
1980
+ plt.gca().set_xticklabels(labels)
1981
+ plt.xlabel("Iteration")
1847
1982
  plt.ylabel(r"$\chi^2$")
1848
1983
  plt.legend()
1849
1984
  plt.subplot(2, 2, 4)
@@ -1851,7 +1986,8 @@ class FitSimulation:
1851
1986
  [iteration.reduced_chi_squared for iteration in iters_for_plot],
1852
1987
  label=fr"Final Reduced $\chi^2$={final_iter.reduced_chi_squared:.3e}")
1853
1988
  plt.xticks(ticks)
1854
- plt.xlabel("Iteration #")
1989
+ plt.gca().set_xticklabels(labels)
1990
+ plt.xlabel("Iteration")
1855
1991
  plt.ylabel(r"Reduced $\chi^2$")
1856
1992
  plt.legend()
1857
1993
  plt.tight_layout()
@@ -1890,6 +2026,8 @@ class FitSimulation:
1890
2026
  'w': 'deg', 'i': 'deg', 'x': 'AU', 'y': 'AU', 'z': 'AU',
1891
2027
  'vx': 'AU/day', 'vy': 'AU/day', 'vz': 'AU/day',
1892
2028
  'a1': 'AU/day^2', 'a2': 'AU/day^2', 'a3': 'AU/day^2',
2029
+ 'dv': 'AU/day', 'ax': 'AU/day^2', 'ay': 'AU/day^2', 'az': 'AU/day^2',
2030
+ 'dt': 'day'
1893
2031
  }
1894
2032
  with open(filename, 'x', encoding='utf-8') as f:
1895
2033
  f.write(section_full + '\n')
@@ -1928,12 +2066,25 @@ class FitSimulation:
1928
2066
  if key in ['om', 'w', 'i']:
1929
2067
  val *= 180/np.pi
1930
2068
  sig *= 180/np.pi
1931
- if key in ['a1', 'a2', 'a3']:
2069
+ if key[:2] in {'a1', 'a2', 'a3', 'dv', 'ax', 'ay', 'az'}:
1932
2070
  f.write(f"{key.upper()}: {val:.11e} \u00B1 {sig:.11e}")
1933
2071
  else:
1934
2072
  f.write(f"{key.upper()}: {val:.11f} \u00B1 {sig:.11f}")
1935
- if key in units_dict:
1936
- f.write(f" {units_dict[key]}")
2073
+ if key[:2] in units_dict:
2074
+ f.write(f" {units_dict[key[:2]]}")
2075
+ if self._priors_given and key in self.prior_est:
2076
+ p_val = self.prior_est[key]
2077
+ p_sig = self.prior_sigs[key]
2078
+ if key in ['om', 'w', 'i']:
2079
+ p_val *= 180/np.pi
2080
+ p_sig *= 180/np.pi
2081
+ if key[:2] in {'a1', 'a2', 'a3', 'dv', 'ax', 'ay', 'az'}:
2082
+ f.write(f" (a priori: {p_val:.11e} \u00B1 {p_sig:.11e}")
2083
+ else:
2084
+ f.write(f" (a priori: {p_val:.11f} \u00B1 {p_sig:.11f}")
2085
+ if key[:2] in units_dict:
2086
+ f.write(f" {units_dict[key[:2]]}")
2087
+ f.write(")")
1937
2088
  f.write("\n")
1938
2089
  f.write("\n")
1939
2090
  f.write("Initial Covariance Matrix:\n")
@@ -1955,12 +2106,25 @@ class FitSimulation:
1955
2106
  if key in ['om', 'w', 'i']:
1956
2107
  val *= 180/np.pi
1957
2108
  sig *= 180/np.pi
1958
- if key in ['a1', 'a2', 'a3']:
2109
+ if key[:2] in {'a1', 'a2', 'a3', 'dv', 'ax', 'ay', 'az'}:
1959
2110
  f.write(f"{key.upper()}: {val:.11e} \u00B1 {sig:.11e}")
1960
2111
  else:
1961
2112
  f.write(f"{key.upper()}: {val:.11f} \u00B1 {sig:.11f}")
1962
- if key in units_dict:
1963
- f.write(f" {units_dict[key]}")
2113
+ if key[:2] in units_dict:
2114
+ f.write(f" {units_dict[key[:2]]}")
2115
+ if self._priors_given and key in self.prior_est:
2116
+ p_val = self.prior_est[key]
2117
+ p_sig = self.prior_sigs[key]
2118
+ if key in ['om', 'w', 'i']:
2119
+ p_val *= 180/np.pi
2120
+ p_sig *= 180/np.pi
2121
+ if key[:2] in {'a1', 'a2', 'a3', 'dv', 'ax', 'ay', 'az'}:
2122
+ f.write(f" (a priori: {p_val:.11e} \u00B1 {p_sig:.11e}")
2123
+ else:
2124
+ f.write(f" (a priori: {p_val:.11f} \u00B1 {p_sig:.11f}")
2125
+ if key[:2] in units_dict:
2126
+ f.write(f" {units_dict[key[:2]]}")
2127
+ f.write(")")
1964
2128
  f.write("\n")
1965
2129
  f.write("\n")
1966
2130
  f.write("Final Covariance Matrix:\n")
@@ -2015,8 +2179,11 @@ class FitSimulation:
2015
2179
  f.write(f'{iter_summary_buff}{iter_summary_str}{iter_summary_buff}\n')
2016
2180
  f.write(subsection_full + '\n')
2017
2181
  for itrn in self.iters[1:]:
2018
- f.write("Summary of the orbit fit calculations at iteration")
2019
- f.write(f" {itrn.iter_number} (of {self.n_iter}):\n")
2182
+ f.write("Summary of the orbit fit calculations")
2183
+ if itrn.iter_number <= self.n_iter:
2184
+ f.write(f" after iteration {itrn.iter_number} (of {self.n_iter}):\n")
2185
+ else:
2186
+ f.write(" after postfit pass:\n")
2020
2187
  f.write(f"RMS unweighted: {itrn.unweighted_rms}\n")
2021
2188
  f.write(f"RMS weighted: {itrn.weighted_rms}\n")
2022
2189
  f.write(f"chi-squared: {itrn.chi_squared}\n")
@@ -2025,7 +2192,7 @@ class FitSimulation:
2025
2192
  f.write(f'{"Variable":<8}{"Initial Value":>20}')
2026
2193
  f.write(f'{"Uncertainty":>20}{"Fitted Value":>20}')
2027
2194
  f.write(f'{"Uncertainty":>20}{"Change":>20}{"Change":>8}'+' (\u03C3)\n')
2028
- init_sol = self.iters[0].x_nom
2195
+ init_sol = self.x_init
2029
2196
  final_sol = itrn.x_nom
2030
2197
  with np.errstate(divide='ignore'):
2031
2198
  for i, key in enumerate(init_sol.keys()):
@@ -2046,7 +2213,7 @@ class FitSimulation:
2046
2213
  f.write(f'{final_val-init_val:+18.11e}{half_tab}')
2047
2214
  f.write(f'{(final_val-init_val)/init_unc:+10.3f}\n')
2048
2215
  f.write("\n")
2049
- if itrn.iter_number != self.n_iter:
2216
+ if itrn.iter_number <= self.n_iter:
2050
2217
  f.write(subsection_full + '\n')
2051
2218
 
2052
2219
  mean_0 = np.array(list(self.x_init.values()))