Skip to content

Commit 9c4ce7e

Browse files
committed
Replace Curtis's iterator with the state transition matrix iterator (retrieves position to within 100 meters and velocity to within 10 cm/s)
1 parent 97474e8 commit 9c4ce7e

3 files changed

Lines changed: 220 additions & 131 deletions

File tree

thor/orbits/iod/gauss.py

Lines changed: 177 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import numpy as np
2-
from scipy import roots
2+
from numpy import roots
3+
from numba import jit
34

45
from ...constants import Constants as c
56
from ...coordinates import equatorialToEclipticCartesian
@@ -16,6 +17,8 @@
1617
"_calcLambdas",
1718
"_calcRhos",
1819
"_calcFG",
20+
"_calcM",
21+
"_calcStateTransitionMatrix",
1922
"_iterateGaussIOD",
2023
"calcGauss",
2124
"gaussIOD"
@@ -24,130 +27,207 @@
2427
MU = c.G * c.M_SUN
2528
C = 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

157237
def 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

Comments
 (0)