Packages/MoonriseKit/Tests/SkyEngineTests/KeplerTests.swift
52 lines · 2880 bytes
1import Foundation
2import Testing
3@testable import SkyEngine
4
5private func jd(_ jd: Double) -> Date { Date(timeIntervalSince1970: (jd - 2_440_587.5) * 86_400) }
6private func auDist(_ a: SIMD3<Double>, _ b: SIMD3<Double>) -> Double { ((a - b) * (a - b)).sum().squareRoot() }
7
8@Test func ceresMatchesHorizons() throws {
9 // JPL Horizons, heliocentric J2000 ecliptic, 2026-10-19 00:00 TDB.
10 let ceres = try #require(KeplerOrbit.dwarfPlanets.first { $0.id == "ceres" })
11 let p = ceres.position(at: jd(2461332.5))
12 #expect(auDist(p, SIMD3(0.09160098, 2.65726102, 0.06724773)) < 0.003)
13}
14
15@Test func halleyMatchesHorizons() throws {
16 let halley = try #require(KeplerOrbit.comets.first { $0.id == "halley" })
17 // At its 1986 perihelion: within 0.05 AU, about a day of its motion there (Horizons uses a perturbed solution).
18 #expect(auDist(halley.position(at: jd(2446470.5)), SIMD3(0.34233380, -0.44659254, 0.16779698)) < 0.05)
19 // ...and near aphelion now, where two-body drift is a few tenths of an AU at 36 AU.
20 #expect(auDist(halley.position(at: jd(2461332.5)), SIMD3(-19.34593976, 27.47052259, -9.85160687)) < 1)
21}
22
23@Test func showersFindParentsAndPeaks() {
24 let perseids = MeteorShower.major.first { $0.name == "Perseids" }!
25 #expect(perseids.parent == "swift-tuttle")
26 var cal = Calendar(identifier: .gregorian)
27 cal.timeZone = TimeZone(identifier: "UTC")!
28 let aug10 = cal.date(from: DateComponents(year: 2027, month: 8, day: 10, hour: 12))!
29 #expect(abs(perseids.daysToPeak(from: aug10, calendar: cal) - 2) < 0.01)
30 let dec30 = cal.date(from: DateComponents(year: 2026, month: 12, day: 30, hour: 12))!
31 let quadrantids = MeteorShower.major.first { $0.name == "Quadrantids" }!
32 #expect(abs(quadrantids.daysToPeak(from: dec30, calendar: cal) - 4) < 0.01)
33}
34
35@Test func galileanMoonDistances() {
36 let km = SkyEngine.galileanMoons(at: Date(timeIntervalSince1970: 1_792_400_000)).map { ($0 * $0).sum().squareRoot() * 1.495978707e8 }
37 // Mean distances 421,800 / 671,100 / 1,070,400 / 1,882,700 km; orbits are nearly circular.
38 for (d, mean) in zip(km, [421_800.0, 671_100, 1_070_400, 1_882_700]) { #expect(abs(d - mean) / mean < 0.02) }
39}
40
41@Test func enckeReturnsFeb2027() throws {
42 // Short periods are where two-body prediction holds; JPL lists Encke's next perihelion as 2027-02-10.
43 let encke = try #require(KeplerOrbit.comets.first { $0.id == "encke" })
44 let next = encke.nextPerihelion(after: Date(timeIntervalSince1970: 1_792_400_000))
45 #expect(abs(next.timeIntervalSince(Date(timeIntervalSince1970: 1_802_217_600)) / 86_400) < 3)
46}
47
48@Test func halleyPathSpansItsOrbit() throws {
49 let halley = try #require(KeplerOrbit.comets.first { $0.id == "halley" })
50 let r = halley.orbitPath().map { ($0 * $0).sum().squareRoot() }
51 #expect(abs(r.min()! - 0.575) < 0.001 && abs(r.max()! - 35.28) < 0.01)
52}