2from astropy
import units
as u
3from astropy.constants
import au
as AU_CONST, GM_sun
4from collections
import namedtuple
7AU_KM = AU_CONST.to_value(u.km)
10EarthElements = namedtuple(
12 [
"a_AU",
"e",
"inc_deg",
"Omega_deg",
"omega_deg"],
16MOIDResult = namedtuple(
21 "EclipticLongitude_deg",
29 """Return Earth’s heliocentric Keplerian elements at a given epoch.
31 Evaluates a linear secular model for Earth’s orbital elements
32 in the J2000 ecliptic and equinox frame. The model coefficients
33 are from JPL’s "Keplerian Elements for Approximate Positions of
34 the Major Planets" (Standish 1992, Table 1), valid for
39 epoch_mjd : float, optional
40 Epoch as Modified Julian Date. Default is 51544.5
41 (J2000.0 = 2000 Jan 1.5 TDB).
46 A namedtuple with fields:
49 Semi-major axis in AU.
53 Inclination to the ecliptic in degrees. Set to a small
54 nonzero value (0.00005°) to avoid singularities in
55 rotation matrices; the true value is ~0 by definition
56 since the ecliptic *is* Earth’s mean orbital plane.
58 Longitude of the ascending node in degrees. Zero by
59 definition in the ecliptic frame.
61 Argument of perihelion in degrees. Since Omega = 0 in
62 the ecliptic frame, this equals the longitude of
67 The linear model for each element is::
69 element(T) = element_0 + element_dot * T
71 where T is Julian centuries from J2000.0. The coefficients are:
73 ====== =============== =================
74 Param Value at J2000 Rate (per century)
75 ====== =============== =================
76 a 1.00000261 AU +0.00000562 AU
77 e 0.01671123 -0.00004392
78 ϖ 102.93768193° +0.32327364°
79 ====== =============== =================
81 Inclination and Omega are fixed at ~0 (ecliptic frame).
83 MOID is a purely geometric quantity (no mean anomaly / phase
84 dependence), but the *shape and orientation* of Earth’s orbit
85 do evolve slowly. Evaluating at the asteroid’s osculating
86 element epoch ensures the two orbits are compared at a
91 Standish, E.M. (1992). "Keplerian Elements for Approximate
92 Positions of the Major Planets." Solar System Dynamics Group,
93 JPL. https://ssd.jpl.nasa.gov/planets/approx_pos.html
95 T = (epoch_mjd - 51544.5) / 36525.0
97 a_AU=1.00000261 + 0.00000562 * T,
98 e=0.01671123 - 0.00004392 * T,
101 omega_deg=102.93768193 + 0.32327364 * T,
106 """Return Earth’s elements at J2000.0. Deprecated; use
107 `earth_orbit(epoch_mjd)` instead.
114 MOID solver using an adaptive 2D grid in (f1, f2).
117 - Semi-major axes a: AU
118 - Distances / MOID: AU
120 - μ: astropy quantity (km^3/s^2) or float
121 - Public angles: degrees
122 - Internal angles: radians
130 tol_MOID_abs=1e-7 * u.AU,
135 self.
mu = mu.to_value(u.km**3 / u.s**2)
if hasattr(mu,
"unit")
else float(mu)
152 """Rz(Omega) * Rx(inc) * Rz(omega)."""
172 Q = np.empty((3, 3), float)
174 Q[0, 0] = cO * A00 - sO * A10
175 Q[0, 1] = cO * A01 - sO * A11
176 Q[0, 2] = cO * A02 - sO * A12
178 Q[1, 0] = sO * A00 + cO * A10
179 Q[1, 1] = sO * A01 + cO * A11
180 Q[1, 2] = sO * A02 + cO * A12
190 """Return p in AU, rotation matrix Q."""
191 p = a_AU * (1 - e * e)
197 """Vectorized positions in AU."""
202 r = p_AU / (1 + e * cf)
207 X = Q[0, 0] * x_pf + Q[0, 1] * y_pf
208 Y = Q[1, 0] * x_pf + Q[1, 1] * y_pf
209 Z = Q[2, 0] * x_pf + Q[2, 1] * y_pf
210 return np.column_stack((X, Y, Z))
214 """Return (r in AU, v in km/s)."""
219 r_AU = p_AU / (1 + e * cf)
223 X = Q[0, 0] * x_pf + Q[0, 1] * y_pf
224 Y = Q[1, 0] * x_pf + Q[1, 1] * y_pf
225 Z = Q[2, 0] * x_pf + Q[2, 1] * y_pf
226 r_vec_AU = np.array([X, Y, Z], float)
230 fac = np.sqrt(mu_km3_s2 / p_km)
233 vy_pf = fac * (e + cf)
235 VX = Q[0, 0] * vx_pf + Q[0, 1] * vy_pf
236 VY = Q[1, 0] * vx_pf + Q[1, 1] * vy_pf
237 VZ = Q[2, 0] * vx_pf + Q[2, 1] * vy_pf
238 v_vec_kms = np.array([VX, VY, VZ], float)
240 return r_vec_AU, v_vec_kms
248 Adaptive search in AU.
249 Returns: (MOID_AU, f1_best, f2_best)
269 for level
in range(max_ref + 1):
270 f1_lo = center1 - width1 / 2
271 f1_hi = center1 + width1 / 2
272 f2_lo = center2 - width2 / 2
273 f2_hi = center2 + width2 / 2
275 f1_grid = (f1_lo + (np.arange(n) + 0.5) * (f1_hi - f1_lo) / n) % two_pi
276 f2_grid = (f2_lo + (np.arange(n) + 0.5) * (f2_hi - f2_lo) / n) % two_pi
281 diff = r1[:,
None, :] - r2[
None, :, :]
282 d2 = np.einsum(
"ijk,ijk->ij", diff, diff)
284 i0, j0 = np.unravel_index(np.argmin(d2), d2.shape)
285 d2_local = d2[i0, j0]
286 f1_local, f2_local = f1_grid[i0], f2_grid[j0]
288 if d2_local < best_d2:
293 moid = float(np.sqrt(best_d2))
296 delta = abs(moid - prev_moid)
297 scale = max(abs(moid), abs(prev_moid), 1.0)
298 if delta <= tol_abs + tol_rel * scale:
307 return moid, best_f1, best_f2
315 Compute MOID and related quantities.
317 el = (a_AU, e, inc_deg, Ω_deg, ω_deg)
318 Returns MOIDResult namedtuple.
320 a1, e1, i1d, O1d, w1d = el1
321 a2, e2, i2d, O2d, w2d = el2
339 dv_kms = float(np.linalg.norm(v1 - v2))
341 lon = np.degrees(np.arctan2(r1[1], r1[0])) % 360
342 f1_deg = np.degrees(f1_best) % 360
343 f2_deg = np.degrees(f2_best) % 360
348 EclipticLongitude_deg=lon,
349 TrueAnomaly1_deg=f1_deg,
350 TrueAnomaly2_deg=f2_deg,
357if __name__ ==
"__main__":
362 tol_MOID_abs=1e-8 * u.AU,
367 el1 = (1.0, 0.1, 5.0, 30.0, 45.0)
368 el2 = (1.5, 0.2, 15.0, 60.0, 10.0)
370 result = solver.compute(el1, el2)
_make_orbit_params(cls, a_AU, e, inc, Om, om)
_rv_from_params(p_AU, e, Q, f, mu_km3_s2)
_moid_grid_search(self, p1, e1, Q1, p2, e2, Q2)
_make_rotation_matrix(inc, Om, om)
__init__(self, mu=GM_sun, n_samples=128, refine_factor=5.0, tol_MOID_abs=1e-7 *u.AU, tol_MOID_rel=1e-6, max_refine=10)
_orbit_positions(p_AU, e, Q, f)
earth_orbit(epoch_mjd=51544.5)