11import numpy as np
2- from scipy import roots
2+ from numpy import roots
3+ from numba import jit
34
45from ...constants import Constants as c
56from ...coordinates import equatorialToEclipticCartesian
1617 "_calcLambdas" ,
1718 "_calcRhos" ,
1819 "_calcFG" ,
20+ "_calcM" ,
21+ "_calcStateTransitionMatrix" ,
1922 "_iterateGaussIOD" ,
2023 "calcGauss" ,
2124 "gaussIOD"
2427MU = c .G * c .M_SUN
2528C = c .C
2629
27- #@jit(["f8(f8[::1], f8[::1], f8[::1])"], nopython=True)
28- def _calcV (rho1hat , rho2hat , rho3hat ):
30+ def _calcV (rho1_hat , rho2_hat , rho3_hat ):
2931 # Vector triple product that gives the area of
3032 # the "volume of the parallelepiped" or according to
3133 # to Milani et al. 2008: 3x volume of the pyramid with vertices q, r1, r2, r3.
3234 # Note that vector triple product rules apply here.
33- return np .dot (np .cross (rho1hat , rho2hat ), rho3hat )
35+ return np .dot (np .cross (rho1_hat , rho2_hat ), rho3_hat )
3436
35- #@jit(["f8(f8[::1], f8[::1], f8[::1], f8[::1], f8[::1], f8, f8, f8)"], nopython=True)
36- def _calcA (q1 , q2 , q3 , rho1hat , rho3hat , t31 , t32 , t21 ):
37+ def _calcA (q1 , q2 , q3 , rho1_hat , rho3_hat , t31 , t32 , t21 ):
3738 # Equation 21 from Milani et al. 2008
38- return np .linalg .norm (q2 )** 3 * np .dot (np .cross (rho1hat , rho3hat ), (t32 * q1 - t31 * q2 + t21 * q3 ))
39+ return np .linalg .norm (q2 )** 3 * np .dot (np .cross (rho1_hat , rho3_hat ), (t32 * q1 - t31 * q2 + t21 * q3 ))
3940
40- #@jit(["f8(f8[::1], f8[::1], f8[::1], f8[::1], f8, f8, f8)"], nopython=True)
41- def _calcB (q1 , q3 , rho1hat , rho3hat , t31 , t32 , t21 ):
41+ def _calcB (q1 , q3 , rho1_hat , rho3_hat , t31 , t32 , t21 , mu = MU ):
4242 # Equation 19 from Milani et al. 2008
43- return MU / 6 * t32 * t21 * np .dot (np .cross (rho1hat , rho3hat ), ((t31 + t32 ) * q1 + (t31 + t21 ) * q3 ))
43+ return mu / 6 * t32 * t21 * np .dot (np .cross (rho1_hat , rho3_hat ), ((t31 + t32 ) * q1 + (t31 + t21 ) * q3 ))
4444
45- #@jit(["UniTuple(f8, 2)(f8, f8, f8, f8)"], nopython=True)
46- def _calcLambdas (r2_mag , t31 , t32 , t21 ):
45+ def _calcLambdas (r2_mag , t31 , t32 , t21 , mu = MU ):
4746 # Equations 16 and 17 from Milani et al. 2008
48- lambda1 = t32 / t31 * (1 + MU / (6 * r2_mag ** 3 ) * (t31 ** 2 - t32 ** 2 ))
49- lambda3 = t21 / t31 * (1 + MU / (6 * r2_mag ** 3 ) * (t31 ** 2 - t21 ** 2 ))
47+ lambda1 = t32 / t31 * (1 + mu / (6 * r2_mag ** 3 ) * (t31 ** 2 - t32 ** 2 ))
48+ lambda3 = t21 / t31 * (1 + mu / (6 * r2_mag ** 3 ) * (t31 ** 2 - t21 ** 2 ))
5049 return lambda1 , lambda3
5150
52- #@jit(["UniTuple(f8[::1], 3)(f8, f8, f8[::1], f8[::1], f8[::1], f8[::1], f8[::1], f8[::1], f8)"], nopython=True)
53- def _calcRhos (lambda1 , lambda3 , q1 , q2 , q3 , rho1hat , rho2hat , rho3hat , V ):
51+ def _calcRhos (lambda1 , lambda3 , q1 , q2 , q3 , rho1_hat , rho2_hat , rho3_hat , V ):
5452 # This can be derived by taking a series of scalar products of the coplanarity condition equation
5553 # with cross products of unit vectors in the direction of the observer, in particular, see Chapter 9 in
5654 # Milani's book on the theory of orbit determination
5755 numerator = - lambda1 * q1 + q2 - lambda3 * q3
58- rho1_mag = np .dot (numerator , np .cross (rho2hat , rho3hat )) / (lambda1 * V )
59- rho2_mag = np .dot (numerator , np .cross (rho1hat , rho3hat )) / V
60- rho3_mag = np .dot (numerator , np .cross (rho1hat , rho2hat )) / (lambda3 * V )
61- return np .dot (rho1_mag , rho1hat ), np .dot (rho2_mag , rho2hat ), np .dot (rho3_mag , rho3hat )
56+ rho1_mag = np .dot (numerator , np .cross (rho2_hat , rho3_hat )) / (lambda1 * V )
57+ rho2_mag = np .dot (numerator , np .cross (rho1_hat , rho3_hat )) / V
58+ rho3_mag = np .dot (numerator , np .cross (rho1_hat , rho2_hat )) / (lambda3 * V )
59+ rho1 = np .dot (rho1_mag , rho1_hat )
60+ rho2 = np .dot (rho2_mag , rho2_hat )
61+ rho3 = np .dot (rho3_mag , rho3_hat )
62+ return rho1 , rho2 , rho3
6263
63- def _calcFG (r2_mag , t32 , t21 ):
64+ def _calcFG (r2_mag , t32 , t21 , mu = MU ):
6465 # Calculate the Lagrange coefficients (Gauss f and g series)
65- f1 = 1 - (1 / 2 ) * MU / r2_mag ** 3 * (- t21 )** 2
66- f3 = 1 - (1 / 2 ) * MU / r2_mag ** 3 * t32 ** 2
67- g1 = - t21 - (1 / 6 ) * MU / r2_mag ** 3 * (- t21 )** 2
68- g3 = t32 - (1 / 6 ) * MU / r2_mag ** 3 * t32 ** 2
66+ f1 = 1 - (1 / 2 ) * mu / r2_mag ** 3 * (- t21 )** 2
67+ f3 = 1 - (1 / 2 ) * mu / r2_mag ** 3 * t32 ** 2
68+ g1 = - t21 - (1 / 6 ) * mu / r2_mag ** 3 * (- t21 )** 2
69+ g3 = t32 - (1 / 6 ) * mu / r2_mag ** 3 * t32 ** 2
6970 return f1 , g1 , f3 , g3
7071
71- def _iterateGaussIOD (orbit , t21 , t32 , q1 , q2 , q3 , rho1hat , rho2hat , rho3hat , mu = MU , max_iter = 10 , tol = 1e-15 ):
72+ def _calcM (r0_mag , r_mag , f , g , f_dot , g_dot , c0 , c1 , c2 , c3 , c4 , c5 , alpha , chi , mu = MU ):
73+ # Universal variables will differ between different texts and works in the literature.
74+ # c0, c1, c2, c3, c4, c5 are expected to be
75+ w = chi / np .sqrt (mu )
76+ alpha_alt = - mu * alpha
77+ U0 = (1 - alpha_alt * chi ** 2 ) * c0
78+ U1 = (chi - alpha_alt * chi ** 3 ) * c1 / np .sqrt (mu )
79+ U2 = chi ** 2 * c2 / mu
80+ U3 = chi ** 3 * c3 / mu ** (3 / 2 )
81+ U4 = chi ** 4 * c4 / mu ** (2 )
82+ U5 = chi ** 5 * c5 / mu ** (5 / 2 )
83+
84+ F = f_dot
85+ G = g_dot
86+
87+ # Equations 18 and 19 in Sheppard 1985
88+ U = (U2 * U3 + w * U4 - 3 * U5 ) / 3
89+ W = g * U2 + 3 * mu * U
90+
91+ # Calculate elements of the M matrix
92+ m11 = (U0 / (r_mag * r0_mag ) + 1 / r0_mag ** 2 + 1 / r_mag ** 2 ) * F - (mu ** 2 * W ) / (r_mag * r0_mag )** 3
93+ m12 = F * U1 / r_mag + (G - 1 ) / r_mag ** 2
94+ m13 = (G - 1 ) * U1 / r_mag - (mu * W ) / r_mag ** 3
95+ m21 = - F * U1 / r0_mag - (f - 1 ) / r0_mag ** 2
96+ m22 = - F * U2
97+ m23 = - (G - 1 ) * U2
98+ m31 = (f - 1 ) * U1 / r0_mag - (mu * W ) / r0_mag ** 3
99+ m32 = (f - 1 ) * U2
100+ m33 = g * U2 - W
101+
102+ # Combine elements into matrix
103+ M = np .array ([
104+ [m11 , m12 , m13 ],
105+ [m21 , m22 , m23 ],
106+ [m31 , m32 , m33 ],
107+ ])
108+
109+ return M
110+
111+ def _calcStateTransitionMatrix (M , r0 , v0 , f , g , f_dot , g_dot , r , v ):
112+ I = np .identity (3 )
113+ state_0 = np .vstack ((r0 , v0 ))
114+ state_1 = np .vstack ((r , v ))
115+
116+ phi11 = f * I + state_1 .T @ M [1 :, :2 ] @ state_0
117+ phi12 = g * I + state_1 .T @ M [1 :, 1 :] @ state_0
118+ phi21 = f_dot * I + state_1 .T @ M [:2 , :2 ] @ state_0
119+ phi22 = g_dot * I + state_1 .T @ M [:2 , 1 :] @ state_0
120+
121+ phi = np .block ([
122+ [phi11 , phi12 ],
123+ [phi21 , phi22 ]
124+ ])
125+ return phi
126+
127+ def _iterateGaussIOD (orbit , t21 , t32 , q1 , q2 , q3 , rho1 , rho2 , rho3 , mu = MU , max_iter = 10 , tol = 1e-15 ):
72128 # Iterate over the polynomial solution from Gauss using the universal anomaly
73129 # formalism until the solution converges or the maximum number of iterations is reached
74130
75131 # Calculate variables that won't change per iteration
76132 sqrt_mu = np .sqrt (mu )
77- V = _calcV (rho1hat , rho2hat , rho3hat )
78133
79- # Set up temporary variables that change iteration to iteration
80- orbit_iter = orbit
81- rho1_ratio = 1e10
82- rho2_ratio = 1e10
83- rho3_ratio = 1e10
84- rho1_mag = - 1e10
85- rho2_mag = - 1e10
86- rho3_mag = - 1e10
134+ # Calculate magntiude and unit rho vectors
135+ rho1_mag = np .linalg .norm (rho1 )
136+ rho2_mag = np .linalg .norm (rho2 )
137+ rho3_mag = np .linalg .norm (rho3 )
138+ rho1_hat = rho1 / rho1_mag
139+ rho2_hat = rho2 / rho2_mag
140+ rho3_hat = rho3 / rho3_mag
87141
142+ orbit_iter = orbit
88143 i = 0
89- while (( np . abs ( rho1_ratio ) > tol )
90- & ( np . abs ( rho2_ratio ) > tol )
91- & ( np . abs ( rho3_ratio ) > tol )):
92-
144+ for i in range ( max_iter ):
145+ # Grab orbit position and velocity vectors
146+ # These should belong to the state of the object at the time of the second
147+ # observation after applying Gauss's method the first time
93148 r = orbit_iter [:3 ]
94149 v = orbit_iter [3 :]
95150 v_mag = np .linalg .norm (v )
96151 r_mag = np .linalg .norm (r )
97-
152+
153+ # Calculate the inverse semi-major axis
154+ # Note: the definition of alpha will change between different works in the literature.
155+ # Here alpha is defined as 1 / a where a is the semi-major axis of the orbit
156+ alpha = - v_mag ** 2 / mu + 2 / r_mag
157+
98158 # Calculate the universal anomaly for both the first and third observations
99- # then calculate the Lagrange coefficients
159+ # then calculate the Lagrange coefficients and the state for each observation.
160+ # Use those to calculate the state transition matrix
100161 for j , dt in enumerate ([- t21 , t32 ]):
101- alpha = - v_mag ** 2 / mu + 2 / r_mag
102-
103- chi = calcChi (orbit_iter , dt , mu = mu , max_iter = max_iter , tol = tol )
162+ # Calculate the universal anomaly
163+ # Universal anomaly here is defined in such a way that it satisfies the following
164+ # differential equation:
165+ # d\chi / dt = \sqrt{mu} / r
166+ chi = calcChi (orbit_iter , dt , mu = mu , max_iter = 10 , tol = tol )
104167 chi2 = chi ** 2
105168
106- psi = alpha * chi2
169+ # Calculate the values of the Stumpff functions
170+ psi = alpha * chi2
107171 c0 , c1 , c2 , c3 , c4 , c5 = calcStumpff (psi )
108172
109- f = 1 - chi ** 2 / r_mag * c2
173+ # Calculate the Lagrange coefficients
174+ # and the corresponding state vector
175+ f = 1 - chi2 / r_mag * c2
110176 g = dt - 1 / sqrt_mu * chi ** 3 * c3
111177
178+ r_new = f * r + g * v
179+ r_new_mag = np .linalg .norm (r_new )
180+
181+ f_dot = sqrt_mu / (r_mag * r_new_mag ) * (alpha * chi ** 3 * c3 - chi )
182+ g_dot = 1 - chi2 / r_new_mag * c2
183+
184+ v_new = f_dot * r + g_dot * v
185+
186+ # Calculate M matrix and use it to calculate the state transition matrix
187+ M = _calcM (r_mag , r_new_mag , f , g , f_dot , g_dot , c0 , c1 , c2 , c3 , c4 , c5 , alpha , chi , mu = mu )
188+ STM = _calcStateTransitionMatrix (M , r , v , f , g , f_dot , g_dot , r_new , v_new )
189+
112190 if j == 0 :
113- g1 = g
114- f1 = f
191+ STM1 = STM
192+ v1 = v_new
193+ r1 = r_new
115194 else :
116- g3 = g
117- f3 = f
118-
119- # Calculate the coplanarity coefficients
120- lambda1 = g3 / (f1 * g3 - f3 * g1 )
121- lambda3 = - g1 / (f1 * g3 - f3 * g1 )
122-
123- # Calculate new topocentric observer to target vectors
124- rho1_temp , rho2_temp , rho3_temp = _calcRhos (lambda1 , lambda3 ,
125- q1 , q2 ,
126- q3 , rho1hat ,
127- rho2hat , rho3hat ,
128- V )
129-
130- # Calculate heliocentric position vector
131- r1 = q1 + rho1_temp
132- r2 = q2 + rho2_temp
133- r3 = q3 + rho3_temp
134-
135- # Update topocentric observer to target magnitudes ratios
136- rho1_ratio = rho1_mag / np .linalg .norm (rho1_temp )
137- rho2_ratio = rho2_mag / np .linalg .norm (rho2_temp )
138- rho3_ratio = rho3_mag / np .linalg .norm (rho3_temp )
139-
140- # Update running topocentric observer to target magnitudes variables
141- rho1_mag = np .linalg .norm (rho1_temp )
142- rho2_mag = np .linalg .norm (rho2_temp )
143- rho3_mag = np .linalg .norm (rho3_temp )
195+ STM3 = STM
196+ v3 = v_new
197+ r3 = r_new
198+
199+ # Create phi error vector: as the estimate of the orbit
200+ # improves the elements in this vector should approach 0.
201+ phi = np .hstack ((
202+ r1 - q1 - rho1_mag * rho1_hat ,
203+ r - q2 - rho2_mag * rho2_hat ,
204+ r3 - q3 - rho3_mag * rho3_hat ))
205+ if np .linalg .norm (phi ) == 0 :
206+ break
144207
145- # Calculate the velocity at the second observation
146- v2 = 1 / (f1 * g3 - f3 * g1 ) * (- f3 * r1 + f1 * r3 )
208+ dphi = np .zeros ((9 , 9 ), dtype = float )
209+ dphi [0 :3 , 0 :3 ] = STM1 [0 :3 , 0 :3 ] # dr1/dr2
210+ dphi [3 :6 , 0 :3 ] = np .identity (3 ) # dr2/dr2
211+ dphi [6 :9 , 0 :3 ] = STM3 [0 :3 , 0 :3 ] # dr3/dr2
147212
148- # Update the guess of the orbit
149- orbit_iter = np .concatenate ((r2 , v2 ))
150-
213+ dphi [0 :3 , 3 :6 ] = STM1 [0 :3 , 3 :6 ] # dr1/dv2
214+ dphi [3 :6 , 3 :6 ] = np .zeros ((3 , 3 )) # dr2/dv2
215+ dphi [6 :9 , 3 :6 ] = STM3 [0 :3 , 3 :6 ] # dr3/dv2
216+
217+ dphi [0 :3 ,6 ] = - v1 / C - rho1_hat
218+
219+ dphi [0 :3 ,7 ] = v1 / C
220+ dphi [3 :6 ,7 ] = - rho2_hat
221+ dphi [6 :9 ,7 ] = v3 / C
222+
223+ dphi [6 :9 ,8 ] = - v3 / C - rho3_hat
224+
225+ delta = np .linalg .solve (dphi , phi )
226+ orbit_iter -= delta [0 :6 ]
227+ rho1_mag -= delta [6 ]
228+ rho2_mag -= delta [7 ]
229+ rho3_mag -= delta [8 ]
230+
151231 i += 1
152232 if i >= max_iter :
153233 break
@@ -156,7 +236,7 @@ def _iterateGaussIOD(orbit, t21, t32, q1, q2, q3, rho1hat, rho2hat, rho3hat, mu=
156236
157237def calcGauss (r1 , r2 , r3 , t1 , t2 , t3 ):
158238 """
159- Calculates the velocity vector at the location of the second position vector (r2) with Gauss'
239+ Calculates the velocity vector at the location of the second position vector (r2) with Gauss's
160240 original method.
161241
162242 .. math::
@@ -240,18 +320,18 @@ def gaussIOD(coords_eq_ang, t, coords_obs, velocity_method="gibbs", iterate=True
240320 Up to three preliminary orbits (as cartesian state vectors).
241321 """
242322 rho = equatorialToEclipticCartesian (equatorialAngularToCartesian (np .radians (coords_eq_ang )))
243- rho1hat = rho [0 , :]
244- rho2hat = rho [1 , :]
245- rho3hat = rho [2 , :]
323+ rho1_hat = rho [0 , :]
324+ rho2_hat = rho [1 , :]
325+ rho3_hat = rho [2 , :]
246326 q1 = coords_obs [0 ,:]
247327 q2 = coords_obs [1 ,:]
248328 q3 = coords_obs [2 ,:]
249329 q2_mag = np .linalg .norm (q2 )
250330
251331 # Make sure rhohats are unit vectors
252- rho1hat = rho1hat / np .linalg .norm (rho1hat )
253- rho2hat = rho2hat / np .linalg .norm (rho2hat )
254- rho3hat = rho3hat / np .linalg .norm (rho3hat )
332+ rho1_hat = rho1_hat / np .linalg .norm (rho1_hat )
333+ rho2_hat = rho2_hat / np .linalg .norm (rho2_hat )
334+ rho3_hat = rho3_hat / np .linalg .norm (rho3_hat )
255335
256336 t1 = t [0 ]
257337 t2 = t [1 ]
@@ -260,10 +340,10 @@ def gaussIOD(coords_eq_ang, t, coords_obs, velocity_method="gibbs", iterate=True
260340 t21 = t2 - t1
261341 t32 = t3 - t2
262342
263- A = _calcA (q1 , q2 , q3 , rho1hat , rho3hat , t31 , t32 , t21 )
264- B = _calcB (q1 , q3 , rho1hat , rho3hat , t31 , t32 , t21 )
265- V = _calcV (rho1hat , rho2hat , rho3hat )
266- coseps2 = np .dot (q2 , rho2hat ) / q2_mag
343+ A = _calcA (q1 , q2 , q3 , rho1_hat , rho3_hat , t31 , t32 , t21 )
344+ B = _calcB (q1 , q3 , rho1_hat , rho3_hat , t31 , t32 , t21 )
345+ V = _calcV (rho1_hat , rho2_hat , rho3_hat )
346+ coseps2 = np .dot (q2 , rho2_hat ) / q2_mag
267347 C0 = V * t31 * q2_mag ** 4 / B
268348 h0 = - A / B
269349
@@ -292,11 +372,11 @@ def gaussIOD(coords_eq_ang, t, coords_obs, velocity_method="gibbs", iterate=True
292372 orbits = []
293373 for r2_mag in r2_mags :
294374 lambda1 , lambda3 = _calcLambdas (r2_mag , t31 , t32 , t21 )
295- rho1 , rho2 , rho3 = _calcRhos (lambda1 , lambda3 , q1 , q2 , q3 , rho1hat , rho2hat , rho3hat , V )
375+ rho1 , rho2 , rho3 = _calcRhos (lambda1 , lambda3 , q1 , q2 , q3 , rho1_hat , rho2_hat , rho3_hat , V )
296376
297377 # Test if we get the same rho2 as using equation 22 in Milani et al. 2008
298378 rho2_mag = (h0 - q2_mag ** 3 / r2_mag ** 3 ) * q2_mag / C0
299- np .testing .assert_almost_equal (np .dot (rho2_mag , rho2hat ), rho2 )
379+ np .testing .assert_almost_equal (np .dot (rho2_mag , rho2_hat ), rho2 )
300380
301381 r1 = q1 + rho1
302382 r2 = q2 + rho2
@@ -315,10 +395,10 @@ def gaussIOD(coords_eq_ang, t, coords_obs, velocity_method="gibbs", iterate=True
315395 if iterate == True :
316396 orbit = _iterateGaussIOD (orbit , t21 , t32 ,
317397 q1 , q2 , q3 ,
318- rho1hat , rho2hat , rho3hat ,
319- mu = MU , max_iter = max_iter , tol = tol )
398+ rho1 , rho2 , rho3 ,
399+ mu = mu , max_iter = max_iter , tol = tol )
320400
321- if np .linalg .norm (v2 ) >= C :
401+ if np .linalg .norm (orbit [ 3 :] ) >= C :
322402 print ("Velocity is greater than speed of light!" )
323403 orbits .append (orbit )
324404
0 commit comments