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",
30 Return canonical heliocentric Keplerian orbital elements for the Earth,
31 suitable for MOID computations, in the J2000 mean ecliptic and equinox
34 The elements describe a *fixed Keplerian ellipse* that approximates the
35 Earth's mean orbit at epoch J2000. This is the standard convention used
36 in MOID literature and software (e.g., Milani & Gronchi, NEODyS, JPL)
37 when computing the MOID between small bodies and the Earth.
42 A namedtuple with fields:
45 Semi-major axis in astronomical units (AU).
46 Canonical value: 1.00000011 AU
49 Eccentricity (dimensionless).
50 Canonical value: 0.01671022
53 Inclination to the ecliptic in degrees.
54 Canonical value: 0.00005 deg
56 Note: by definition the ecliptic plane is the Earth's mean orbital
57 plane, so the true inclination is ~0. A very small non-zero value
58 is used in practice to avoid singularities in rotation matrices
59 and angle definitions.
62 Longitude of the ascending node in degrees, measured in the
63 ecliptic plane from the J2000 vernal equinox.
64 Canonical value: -11.26064 deg (equivalent to 348.73936 deg)
67 Argument of perihelion in degrees, measured from the ascending
68 node along the Earth's orbital plane.
69 Canonical value: 102.94719 deg
73 * These values are the widely used J2000 "mean elements" of the Earth’s
74 heliocentric orbit, compatible with VSOP87-style low-order models and
75 with the orbital elements quoted in many celestial mechanics references.
77 * They are intended for *geometric* computations such as MOID, where one
78 wants a fixed Keplerian ellipse representing the Earth's orbit, rather
79 than the true, time-varying perturbed Earth orbit.
81 * If you are using the MOIDSolver defined above, you can plug this
82 directly into the solver as the Earth orbit, e.g.:
84 earth = earth_orbit_J2000()
85 result = solver.compute(asteroid_elements, earth)
87 or reverse the order depending on which way you want to label the
90 * The epoch associated with these elements is J2000.0; MOID is a purely
91 geometric quantity, so there is no mean anomaly / phase dependence.
95 These values (or very close variants) are quoted in many sources, e.g.:
97 - Explanatory Supplement to the Astronomical Almanac
98 - VSOP87/IAU-style mean orbital element lists for planets
99 - Milani, A., & Gronchi, G. F., "Theory of Orbit Determination"
100 - Online resources summarizing J2000 planetary elements
113 MOID solver using an adaptive 2D grid in (f1, f2).
116 - Semi-major axes a: AU
117 - Distances / MOID: AU
119 - μ: astropy quantity (km^3/s^2) or float
120 - Public angles: degrees
121 - Internal angles: radians
129 tol_MOID_abs=1e-7 * u.AU,
134 self.
mu = mu.to_value(u.km**3 / u.s**2)
if hasattr(mu,
"unit")
else float(mu)
151 """Rz(Omega) * Rx(inc) * Rz(omega)."""
171 Q = np.empty((3, 3), float)
173 Q[0, 0] = cO * A00 - sO * A10
174 Q[0, 1] = cO * A01 - sO * A11
175 Q[0, 2] = cO * A02 - sO * A12
177 Q[1, 0] = sO * A00 + cO * A10
178 Q[1, 1] = sO * A01 + cO * A11
179 Q[1, 2] = sO * A02 + cO * A12
189 """Return p in AU, rotation matrix Q."""
190 p = a_AU * (1 - e * e)
196 """Vectorized positions in AU."""
201 r = p_AU / (1 + e * cf)
206 X = Q[0, 0] * x_pf + Q[0, 1] * y_pf
207 Y = Q[1, 0] * x_pf + Q[1, 1] * y_pf
208 Z = Q[2, 0] * x_pf + Q[2, 1] * y_pf
209 return np.column_stack((X, Y, Z))
213 """Return (r in AU, v in km/s)."""
218 r_AU = p_AU / (1 + e * cf)
222 X = Q[0, 0] * x_pf + Q[0, 1] * y_pf
223 Y = Q[1, 0] * x_pf + Q[1, 1] * y_pf
224 Z = Q[2, 0] * x_pf + Q[2, 1] * y_pf
225 r_vec_AU = np.array([X, Y, Z], float)
229 fac = np.sqrt(mu_km3_s2 / p_km)
232 vy_pf = fac * (e + cf)
234 VX = Q[0, 0] * vx_pf + Q[0, 1] * vy_pf
235 VY = Q[1, 0] * vx_pf + Q[1, 1] * vy_pf
236 VZ = Q[2, 0] * vx_pf + Q[2, 1] * vy_pf
237 v_vec_kms = np.array([VX, VY, VZ], float)
239 return r_vec_AU, v_vec_kms
247 Adaptive search in AU.
248 Returns: (MOID_AU, f1_best, f2_best)
268 for level
in range(max_ref + 1):
269 f1_lo = center1 - width1 / 2
270 f1_hi = center1 + width1 / 2
271 f2_lo = center2 - width2 / 2
272 f2_hi = center2 + width2 / 2
274 f1_grid = (f1_lo + (np.arange(n) + 0.5) * (f1_hi - f1_lo) / n) % two_pi
275 f2_grid = (f2_lo + (np.arange(n) + 0.5) * (f2_hi - f2_lo) / n) % two_pi
280 diff = r1[:,
None, :] - r2[
None, :, :]
281 d2 = np.einsum(
"ijk,ijk->ij", diff, diff)
283 i0, j0 = np.unravel_index(np.argmin(d2), d2.shape)
284 d2_local = d2[i0, j0]
285 f1_local, f2_local = f1_grid[i0], f2_grid[j0]
287 if d2_local < best_d2:
292 moid = float(np.sqrt(best_d2))
295 delta = abs(moid - prev_moid)
296 scale = max(abs(moid), abs(prev_moid), 1.0)
297 if delta <= tol_abs + tol_rel * scale:
306 return moid, best_f1, best_f2
314 Compute MOID and related quantities.
316 el = (a_AU, e, inc_deg, Ω_deg, ω_deg)
317 Returns MOIDResult namedtuple.
319 a1, e1, i1d, O1d, w1d = el1
320 a2, e2, i2d, O2d, w2d = el2
338 dv_kms = float(np.linalg.norm(v1 - v2))
340 lon = np.degrees(np.arctan2(r1[1], r1[0])) % 360
341 f1_deg = np.degrees(f1_best) % 360
342 f2_deg = np.degrees(f2_best) % 360
347 EclipticLongitude_deg=lon,
348 TrueAnomaly1_deg=f1_deg,
349 TrueAnomaly2_deg=f2_deg,
356if __name__ ==
"__main__":
361 tol_MOID_abs=1e-8 * u.AU,
366 el1 = (1.0, 0.1, 5.0, 30.0, 45.0)
367 el2 = (1.5, 0.2, 15.0, 60.0, 10.0)
369 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)