2026-05-01 22:38:44 +08:00
|
|
|
package basic
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestOrbitCircularStateAtEpoch(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{
|
|
|
|
|
EpochJD: orbitReferenceJD,
|
|
|
|
|
A: 2,
|
|
|
|
|
E: 0,
|
|
|
|
|
I: 0,
|
|
|
|
|
Omega: 0,
|
|
|
|
|
W: 0,
|
|
|
|
|
M0: 0,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD, elements)
|
|
|
|
|
assertSameFloat(t, "x", vector[0], 2)
|
|
|
|
|
assertSameFloat(t, "y", vector[1], 0)
|
|
|
|
|
assertSameFloat(t, "z", vector[2], 0)
|
|
|
|
|
|
|
|
|
|
lon, lat, distance := OrbitHeliocentricEclipticJ2000(orbitReferenceJD, elements)
|
|
|
|
|
assertSameFloat(t, "lon", lon, 0)
|
|
|
|
|
assertSameFloat(t, "lat", lat, 0)
|
|
|
|
|
assertSameFloat(t, "distance", distance, 2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOrbitQuarterPeriodOnCircularOrbit(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{
|
|
|
|
|
EpochJD: orbitReferenceJD,
|
|
|
|
|
A: 1,
|
|
|
|
|
E: 0,
|
|
|
|
|
I: 0,
|
|
|
|
|
Omega: 0,
|
|
|
|
|
W: 0,
|
|
|
|
|
M0: 0,
|
|
|
|
|
}
|
|
|
|
|
quarterPeriodDays := 90 / OrbitMeanMotion(elements)
|
|
|
|
|
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD+quarterPeriodDays, elements)
|
|
|
|
|
|
|
|
|
|
if math.Abs(vector[0]) > 1e-10 || math.Abs(vector[1]-1) > 1e-10 || math.Abs(vector[2]) > 1e-10 {
|
|
|
|
|
t.Fatalf("quarter-period vector mismatch: got %+v", vector)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOrbitMeanAndTrueAnomalyCircularMatch(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{
|
|
|
|
|
EpochJD: orbitReferenceJD,
|
|
|
|
|
A: 1.523679,
|
|
|
|
|
E: 0,
|
|
|
|
|
I: 1.85,
|
|
|
|
|
Omega: 49.5,
|
|
|
|
|
W: 286.5,
|
|
|
|
|
M0: 123.4,
|
|
|
|
|
}
|
|
|
|
|
jd := orbitReferenceJD + 123.456
|
|
|
|
|
meanAnomaly := OrbitMeanAnomaly(jd, elements)
|
|
|
|
|
trueAnomaly := OrbitTrueAnomaly(jd, elements)
|
|
|
|
|
if math.Abs(meanAnomaly-trueAnomaly) > 1e-12 {
|
|
|
|
|
t.Fatalf("circular mean/true anomaly mismatch: mean=%.18f true=%.18f", meanAnomaly, trueAnomaly)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPerihelionFormMatchesClassicalEllipticState(t *testing.T) {
|
|
|
|
|
classical := OrbitElements{
|
|
|
|
|
EpochJD: 2457305.5,
|
|
|
|
|
A: 3.462249489765068,
|
|
|
|
|
E: 0.6409081306555051,
|
|
|
|
|
I: 7.040294906760007,
|
|
|
|
|
Omega: 50.13557380441372,
|
|
|
|
|
W: 12.79824973415729,
|
|
|
|
|
M0: 8.859927418758764,
|
|
|
|
|
}
|
|
|
|
|
perihelion := OrbitElements{
|
|
|
|
|
Q: 1.243265641416762,
|
|
|
|
|
E: classical.E,
|
|
|
|
|
I: classical.I,
|
|
|
|
|
Omega: classical.Omega,
|
|
|
|
|
W: classical.W,
|
|
|
|
|
TpJD: 2457247.588657863465,
|
|
|
|
|
}
|
|
|
|
|
jd := 2457308.5
|
|
|
|
|
classicalVector := OrbitHeliocentricXYZJ2000(jd, classical)
|
|
|
|
|
perihelionVector := OrbitHeliocentricXYZJ2000(jd, perihelion)
|
|
|
|
|
for i := range classicalVector {
|
|
|
|
|
if math.Abs(classicalVector[i]-perihelionVector[i]) > 1e-11 {
|
|
|
|
|
t.Fatalf("component %d mismatch: classical=%.18f perihelion=%.18f", i, classicalVector[i], perihelionVector[i])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParabolicPerihelionStateAtTp(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{Q: 1, E: 1, I: 0, Omega: 0, W: 0, TpJD: orbitReferenceJD}
|
|
|
|
|
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD, elements)
|
|
|
|
|
assertSameFloat(t, "x", vector[0], 1)
|
|
|
|
|
assertSameFloat(t, "y", vector[1], 0)
|
|
|
|
|
assertSameFloat(t, "z", vector[2], 0)
|
|
|
|
|
if !math.IsNaN(OrbitMeanAnomaly(orbitReferenceJD, elements)) {
|
|
|
|
|
t.Fatalf("parabolic mean anomaly should be NaN")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestHyperbolicOrbitProducesFiniteState(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{Q: 0.5, E: 1.2, I: 12, Omega: 30, W: 45, TpJD: orbitReferenceJD}
|
|
|
|
|
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD+20, elements)
|
|
|
|
|
for i, component := range vector {
|
|
|
|
|
if math.IsNaN(component) || math.IsInf(component, 0) {
|
|
|
|
|
t.Fatalf("component %d not finite: %.18f", i, component)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOrbitInvalidEllipticElementsReturnNaN(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{EpochJD: orbitReferenceJD, A: 1, E: 1.1}
|
|
|
|
|
if !math.IsNaN(OrbitMeanMotion(elements)) {
|
|
|
|
|
t.Fatalf("expected NaN mean motion for invalid elements")
|
|
|
|
|
}
|
|
|
|
|
vector := OrbitHeliocentricXYZJ2000(orbitReferenceJD, elements)
|
|
|
|
|
for i, component := range vector {
|
|
|
|
|
if !math.IsNaN(component) {
|
|
|
|
|
t.Fatalf("component %d expected NaN, got %.18f", i, component)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-15 23:54:15 +08:00
|
|
|
|
|
|
|
|
func TestParabolicRadiusMonotonicFarBeforePerihelion(t *testing.T) {
|
|
|
|
|
elements := OrbitElements{Q: 0.005, E: 1, I: 0, Omega: 0, W: 0, TpJD: orbitReferenceJD}
|
|
|
|
|
previous := math.Inf(1)
|
|
|
|
|
for years := 1000.0; years <= 20000; years += 1000 {
|
|
|
|
|
before := OrbitHeliocentricXYZJ2000(orbitReferenceJD-years*365.25, elements)
|
|
|
|
|
after := OrbitHeliocentricXYZJ2000(orbitReferenceJD+years*365.25, elements)
|
|
|
|
|
rb := math.Hypot(math.Hypot(before[0], before[1]), before[2])
|
|
|
|
|
ra := math.Hypot(math.Hypot(after[0], after[1]), after[2])
|
|
|
|
|
if math.Abs(rb-ra) > 1e-9*ra {
|
|
|
|
|
t.Fatalf("%v yr: radius before %.9f, after %.9f", years, rb, ra)
|
|
|
|
|
}
|
|
|
|
|
if previous != math.Inf(1) && ra <= previous {
|
|
|
|
|
t.Fatalf("%v yr: radius %.9f not increasing from %.9f", years, ra, previous)
|
|
|
|
|
}
|
|
|
|
|
previous = ra
|
|
|
|
|
}
|
|
|
|
|
}
|