1
+ import sys
1
2
import numpy as np
3
+ from itertools import combinations
2
4
3
5
from ...config import Config
6
+ from ...constants import Constants as c
7
+ from .gauss import gaussIOD
8
+ from ..propagate import propagateOrbits
4
9
5
- __all__ = ["selectObservations" ]
10
+ __all__ = ["selectObservations" ,
11
+ "iod" ]
6
12
7
- def selectObservations (observations , method = "first+middle+last" , columnMapping = Config .columnMapping ):
13
+ MU = c .G * c .M_SUN
14
+
15
+
16
+ def selectObservations (observations , method = "combinations" , columnMapping = Config .columnMapping ):
8
17
"""
9
18
Selects which three observations to use for IOD depending on the method.
10
19
11
20
Methods:
12
21
'first+middle+last' : Grab the first, middle and last observations in time.
13
22
'thirds' : Grab the middle observation in the first third, second third, and final third.
23
+ 'combinations' : Return the observation IDs corresponding to every possible combination of three observations with
24
+ non-coinciding observation times.
14
25
15
26
Parameters
16
27
----------
17
28
observations : `~pandas.DataFrame`
18
29
Pandas DataFrame containing observations with at least a column of observation IDs and a column
19
30
of exposure times.
20
- method : {'first+middle+last', 'thirds}, optional
31
+ method : {'first+middle+last', 'thirds', 'combinations' }, optional
21
32
Which method to use to select observations.
22
- [Default = `first+middle+last` ]
33
+ [Default = 'combinations' ]
23
34
columnMapping : dict, optional
24
35
Column name mapping of observations to internally used column names.
25
36
[Default = `~thor.Config.columnMapping`]
26
37
27
38
Returns
28
39
-------
29
- obs_id : `~numpy.ndarray' (3 or 0)
40
+ obs_id : `~numpy.ndarray' (N, 3 or 0)
30
41
An array of selected observation IDs. If three unique observations could
31
- not be selected than returns an empty array.
42
+ not be selected then returns an empty array.
32
43
"""
44
+ obs_ids = observations [columnMapping ["obs_id" ]].values
45
+ if len (obs_ids ) < 3 :
46
+ return np .array ([])
47
+
48
+ indexes = np .arange (0 , len (obs_ids ))
49
+ times = observations [columnMapping ["exp_mjd" ]].values
50
+ selected = np .array ([])
51
+
33
52
if method == "first+middle+last" :
34
- selected = np .percentile (observations [columnMapping ["exp_mjd" ]].values , [0 , 50 , 100 ], interpolation = "nearest" )
53
+ selected_times = np .percentile (times ,
54
+ [0 , 50 , 100 ],
55
+ interpolation = "nearest" )
56
+ selected_index = np .intersect1d (times , selected_times , return_indices = True )[1 ]
57
+ selected_index = np .array ([selected_index ])
58
+
35
59
elif method == "thirds" :
36
- selected = np .percentile (observations [columnMapping ["exp_mjd" ]].values , [1 / 6 * 100 , 50 , 5 / 6 * 100 ], interpolation = "nearest" )
60
+ selected_times = np .percentile (times ,
61
+ [1 / 6 * 100 , 50 , 5 / 6 * 100 ],
62
+ interpolation = "nearest" )
63
+ selected_index = np .intersect1d (times , selected_times , return_indices = True )[1 ]
64
+ selected_index = np .array ([selected_index ])
65
+
66
+ elif method == "combinations" :
67
+ # Make all possible combinations of 3 observations
68
+ selected_index = np .array ([np .array (index ) for index in combinations (indexes , 3 )])
69
+
37
70
else :
38
71
raise ValueError ("method should be one of {'first+middle+last', 'thirds'}" )
39
-
40
- if len (np .unique (selected )) != 3 :
41
- print ("Could not find three observations that satisfy the criteria." )
72
+
73
+ # Make sure each returned combination of observation ids have at least 3 unique
74
+ # times
75
+ keep = []
76
+ for i , comb in enumerate (times [selected_index ]):
77
+ if len (np .unique (comb )) == 3 :
78
+ keep .append (i )
79
+ keep = np .array (keep )
80
+
81
+ # Return an empty array if no observations satisfy the criteria
82
+ if len (keep ) == 0 :
42
83
return np .array ([])
43
84
44
- index = np .intersect1d (observations [columnMapping ["exp_mjd" ]].values , selected , return_indices = True )[1 ]
45
- return observations [columnMapping ["obs_id" ]].values [index ]
46
-
85
+ return obs_ids [selected_index [keep , :]]
86
+
87
+
88
+ def iod (observations ,
89
+ observation_selection_method = "combinations" ,
90
+ iterate = True ,
91
+ light_time = True ,
92
+ max_iter = 50 ,
93
+ tol = 1e-15 ,
94
+ propagatorKwargs = {
95
+ "observatoryCode" : "I11" ,
96
+ "mjdScale" : "UTC" ,
97
+ "dynamical_model" : "2" ,
98
+ },
99
+ mu = MU ,
100
+ columnMapping = Config .columnMapping ):
101
+
102
+ # Extract column names
103
+ obs_id_col = columnMapping ["obs_id" ]
104
+ ra_col = columnMapping ["RA_deg" ]
105
+ dec_col = columnMapping ["Dec_deg" ]
106
+ time_col = columnMapping ["exp_mjd" ]
107
+ ra_err_col = columnMapping ["RA_sigma_deg" ]
108
+ dec_err_col = columnMapping ["Dec_sigma_deg" ]
109
+ obs_x_col = columnMapping ["obs_x_au" ]
110
+ obs_y_col = columnMapping ["obs_y_au" ]
111
+ obs_z_col = columnMapping ["obs_z_au" ]
112
+
113
+ # Extract observation IDs, sky-plane positions, sky-plane position uncertainties, times of observation,
114
+ # and the location of the observer at each time
115
+ obs_ids_all = observations [obs_id_col ].values
116
+ coords_eq_ang_all = observations [observations [obs_id_col ].isin (obs_ids_all )][[ra_col , dec_col ]].values
117
+ coords_eq_ang_err_all = observations [observations [obs_id_col ].isin (obs_ids_all )][[ra_err_col , dec_err_col ]].values
118
+ coords_obs_all = observations [observations [obs_id_col ].isin (obs_ids_all )][[obs_x_col , obs_y_col , obs_z_col ]].values
119
+ times_all = observations [observations [obs_id_col ].isin (obs_ids_all )][time_col ].values
120
+
121
+ # Select observation IDs to use for IOD
122
+ obs_ids = selectObservations (observations , method = observation_selection_method , columnMapping = columnMapping )
123
+
124
+ min_chi2 = 1e10
125
+ best_orbit = None
126
+ best_obs_ids = None
127
+
128
+ for ids in obs_ids :
129
+ # Grab sky-plane positions of the selected observations, the heliocentric ecliptic position of the observer,
130
+ # and the times at which the observations occur
131
+ coords_eq_ang = observations [observations [obs_id_col ].isin (ids )][[ra_col , dec_col ]].values
132
+ coords_obs = observations [observations [obs_id_col ].isin (ids )][[obs_x_col , obs_y_col , obs_z_col ]].values
133
+ times = observations [observations [obs_id_col ].isin (ids )][time_col ].values
134
+
135
+ # Run IOD
136
+ orbits_iod = gaussIOD (coords_eq_ang , times , coords_obs , light_time = light_time , iterate = iterate , max_iter = max_iter , tol = tol )
137
+ if np .all (np .isnan (orbits_iod )) == True :
138
+ continue
139
+
140
+ # Propagate initial orbit to all observation times
141
+ orbits = propagateOrbits (orbits_iod [:, 1 :], orbits_iod [:, 0 ], times_all , ** propagatorKwargs )
142
+ orbits = orbits [['orbit_id' , 'mjd' , 'RA_deg' , 'Dec_deg' ,
143
+ 'HEclObj_X_au' , 'HEclObj_Y_au' , 'HEclObj_Z_au' ,
144
+ 'HEclObj_dX/dt_au_p_day' , 'HEclObj_dY/dt_au_p_day' , 'HEclObj_dZ/dt_au_p_day' ]].values
145
+
146
+ # For each unique initial orbit calculate residuals and chi-squared
147
+ # Find the orbit which yields the lowest chi-squared
148
+ orbit_ids = np .unique (orbits [:, 0 ])
149
+ for i , orbit_id in enumerate (orbit_ids ):
150
+ orbit = orbits [np .where (orbits [:, 0 ] == orbit_id )]
151
+
152
+ pred_dec = np .radians (orbit [:, 3 ])
153
+ residual_ra = (coords_eq_ang_all [:, 0 ] - orbit [:, 2 ]) * np .cos (pred_dec )
154
+ residual_dec = (coords_eq_ang_all [:, 1 ] - orbit [:, 3 ])
155
+
156
+ chi2 = np .sum (residual_ra ** 2 / coords_eq_ang_err_all [:, 0 ]** 2 + residual_dec ** 2 / coords_eq_ang_err_all [:, 1 ]** 2 ) / (2 * len (residual_ra ) - 6 )
157
+
158
+ if chi2 < min_chi2 :
159
+ best_orbit = orbits_iod [i , :]
160
+ best_obs_ids = ids
161
+ min_chi2 = chi2
162
+
163
+ return best_orbit , best_obs_ids , min_chi2
164
+
165
+
0 commit comments