krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

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}