238 lines
9.6 KiB
Go
238 lines
9.6 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestPlanetTransitLocalContactsNearGeocentric(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
transit PlanetTransitResult
|
||
|
|
maxShift float64 // 站心与地心接触的最大偏移,单位分钟
|
||
|
|
}{
|
||
|
|
{"venus-2012", NextVenusTransit(JDCalc(2012, 1, 1)), 9},
|
||
|
|
{"mercury-2016", NextMercuryTransit(JDCalc(2016, 1, 1)), 2},
|
||
|
|
}
|
||
|
|
observers := []Observer{
|
||
|
|
{Longitude: 151.2, Latitude: -33.9},
|
||
|
|
{Longitude: 116.4, Latitude: 39.9},
|
||
|
|
{Longitude: -157.8, Latitude: 21.3},
|
||
|
|
{Longitude: -0.1, Latitude: 51.5, Height: 100},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
if !tc.transit.Valid {
|
||
|
|
t.Fatalf("%s: geocentric transit not found", tc.name)
|
||
|
|
}
|
||
|
|
snapshot, ok := newPlanetTransitSnapshot(tc.transit, nil)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("%s: snapshot failed", tc.name)
|
||
|
|
}
|
||
|
|
for _, observer := range observers {
|
||
|
|
local := PlanetTransitLocal(tc.transit, observer)
|
||
|
|
if !local.Valid || !local.HasInternal {
|
||
|
|
t.Fatalf("%s %+v: local transit invalid", tc.name, observer)
|
||
|
|
}
|
||
|
|
pairs := []struct {
|
||
|
|
local PlanetTransitLocalContact
|
||
|
|
geo float64
|
||
|
|
kind PlanetTransitContactKind
|
||
|
|
}{
|
||
|
|
{local.ExternalIngress, tc.transit.ExternalIngress, PlanetTransitC1},
|
||
|
|
{local.InternalIngress, tc.transit.InternalIngress, PlanetTransitC2},
|
||
|
|
{local.InternalEgress, tc.transit.InternalEgress, PlanetTransitC3},
|
||
|
|
{local.ExternalEgress, tc.transit.ExternalEgress, PlanetTransitC4},
|
||
|
|
}
|
||
|
|
for _, pair := range pairs {
|
||
|
|
shift := math.Abs(pair.local.JD-pair.geo) * 1440
|
||
|
|
if shift > tc.maxShift {
|
||
|
|
t.Fatalf("%s %+v C%d: shift %.2f min", tc.name, observer, pair.kind, shift)
|
||
|
|
}
|
||
|
|
state, _ := snapshot.stateAt(UTC2TT(pair.local.JD), observer)
|
||
|
|
metric := state.external
|
||
|
|
if pair.kind == PlanetTransitC2 || pair.kind == PlanetTransitC3 {
|
||
|
|
metric = state.internal
|
||
|
|
}
|
||
|
|
if math.Abs(metric) > 0.01 {
|
||
|
|
t.Fatalf("%s %+v C%d: residual %.4f arcsec", tc.name, observer, pair.kind, metric)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// 直接星历求值核对插值缓存:凌甚时刻的站心角距与缓存结果一致。
|
||
|
|
tt := UTC2TT(local.Greatest.JD)
|
||
|
|
cfg := venusTransitConfig()
|
||
|
|
if tc.transit.PlanetIndex == 1 {
|
||
|
|
cfg = mercuryTransitConfig()
|
||
|
|
}
|
||
|
|
sidereal := ApparentSiderealTime(TT2UT1(tt)) * 15
|
||
|
|
ra, dec := HSunApparentRaDecN(tt, -1)
|
||
|
|
sunRA, sunDec := topocentricRaDecWithSidereal(ra, dec, observer.Latitude, observer.Longitude, sidereal, EarthAwayN(tt, -1), observer.Height)
|
||
|
|
ra, dec = cfg.apparentRaDecN(tt, -1)
|
||
|
|
planetRA, planetDec := topocentricRaDecWithSidereal(ra, dec, observer.Latitude, observer.Longitude, sidereal, cfg.earthDistanceN(tt, -1), observer.Height)
|
||
|
|
direct := StarAngularSeparation(sunRA, sunDec, planetRA, planetDec) * 3600
|
||
|
|
if diff := math.Abs(direct - local.MinimumSeparationArcsec); diff > 0.01 {
|
||
|
|
t.Fatalf("%s %+v: cached separation off by %.4f arcsec", tc.name, observer, diff)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPlanetTransitContactIsochronesMatchLocalTimes(t *testing.T) {
|
||
|
|
transit := NextVenusTransit(JDCalc(2012, 1, 1))
|
||
|
|
for _, kind := range []PlanetTransitContactKind{PlanetTransitC1, PlanetTransitC4} {
|
||
|
|
lines := PlanetTransitContactIsochrones(transit, kind, 2*time.Minute)
|
||
|
|
if len(lines) == 0 {
|
||
|
|
t.Fatalf("C%d: no isochrones", kind)
|
||
|
|
}
|
||
|
|
checked := 0
|
||
|
|
for _, line := range lines {
|
||
|
|
for _, segment := range line.Segments {
|
||
|
|
for index := 0; index < len(segment); index += 7 {
|
||
|
|
point := segment[index]
|
||
|
|
local := PlanetTransitLocal(transit, Observer{Longitude: point.Longitude, Latitude: point.Latitude})
|
||
|
|
contact := local.ExternalIngress
|
||
|
|
if kind == PlanetTransitC4 {
|
||
|
|
contact = local.ExternalEgress
|
||
|
|
}
|
||
|
|
if diff := math.Abs(contact.JD-line.JD) * 86400; diff > 3 {
|
||
|
|
t.Fatalf("C%d %.5f at (%.2f,%.2f): local contact off by %.1f s", kind, line.JD, point.Longitude, point.Latitude, diff)
|
||
|
|
}
|
||
|
|
if contact.SunAltitudeDeg < planetTransitSunHorizonAltitudeDeg-0.01 {
|
||
|
|
t.Fatalf("C%d at (%.2f,%.2f): Sun below horizon %.2f", kind, point.Longitude, point.Latitude, contact.SunAltitudeDeg)
|
||
|
|
}
|
||
|
|
checked++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if checked < 20 {
|
||
|
|
t.Fatalf("C%d: only %d points checked", kind, checked)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPlanetTransitLocalFramesLimitAndDedup(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
transit PlanetTransitResult
|
||
|
|
}{
|
||
|
|
{"venus-2012", NextVenusTransit(JDCalc(2012, 1, 1))},
|
||
|
|
{"mercury-2016", NextMercuryTransit(JDCalc(2016, 1, 1))},
|
||
|
|
}
|
||
|
|
observer := Observer{Longitude: 116.4, Latitude: 39.9}
|
||
|
|
for _, tc := range cases {
|
||
|
|
if !tc.transit.Valid {
|
||
|
|
t.Fatalf("%s: geocentric transit not found", tc.name)
|
||
|
|
}
|
||
|
|
local := PlanetTransitLocal(tc.transit, observer)
|
||
|
|
if !local.Valid || !local.HasInternal {
|
||
|
|
t.Fatalf("%s: local transit invalid", tc.name)
|
||
|
|
}
|
||
|
|
frames := PlanetTransitLocalFrames(tc.transit, local, 1e-9)
|
||
|
|
if len(frames) == 0 || len(frames) > planetTransitLocalMaxFrames {
|
||
|
|
t.Fatalf("%s: got %d frames, want 1..%d", tc.name, len(frames), planetTransitLocalMaxFrames)
|
||
|
|
}
|
||
|
|
for index := 1; index < len(frames); index++ {
|
||
|
|
if frames[index].JD <= frames[index-1].JD {
|
||
|
|
t.Fatalf("%s: duplicate or unsorted frame at %d: %.12f <= %.12f", tc.name, index, frames[index].JD, frames[index-1].JD)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1999-11-15 水星掠凌:地心最小角距只比内切限小约 1.9″,南半球站心视差把它推出内切限,
|
||
|
|
// 这些地点只剩一触、四触;北半球仍有二触、三触。内切等时线只能落在仍有内切接触的地点。
|
||
|
|
func TestPlanetTransitLocalGrazingMercury1999(t *testing.T) {
|
||
|
|
transit := NextMercuryTransit(JDCalc(1999, 1, 1))
|
||
|
|
if !transit.Valid || !transit.HasInternal {
|
||
|
|
t.Fatalf("1999 geocentric transit must exist with internal contacts: %+v", transit)
|
||
|
|
}
|
||
|
|
limit := transit.SunSemidiameterArcsec - transit.PlanetSemidiameterArcsec
|
||
|
|
if margin := limit - transit.MinimumSeparationArcsec; margin <= 0 || margin > 5 {
|
||
|
|
t.Fatalf("geocentric margin %.2f arcsec, want a near-grazing transit", margin)
|
||
|
|
}
|
||
|
|
grazing := PlanetTransitLocal(transit, Observer{Longitude: 0, Latitude: -60})
|
||
|
|
if !grazing.Valid || grazing.HasInternal || grazing.InternalIngress.Valid || grazing.InternalEgress.Valid {
|
||
|
|
t.Fatalf("southern site must see a grazing transit: %+v", grazing)
|
||
|
|
}
|
||
|
|
if grazing.MinimumSeparationArcsec <= grazing.SunSemidiameterArcsec-grazing.PlanetSemidiameterArcsec {
|
||
|
|
t.Fatalf("grazing site separation %.2f inside the internal limit", grazing.MinimumSeparationArcsec)
|
||
|
|
}
|
||
|
|
if !(grazing.ExternalIngress.JD < grazing.Greatest.JD && grazing.Greatest.JD < grazing.ExternalEgress.JD) {
|
||
|
|
t.Fatalf("grazing site contacts out of order: %+v", grazing)
|
||
|
|
}
|
||
|
|
internal := PlanetTransitLocal(transit, Observer{Longitude: 0, Latitude: 40})
|
||
|
|
if !internal.Valid || !internal.HasInternal ||
|
||
|
|
!(internal.ExternalIngress.JD < internal.InternalIngress.JD && internal.InternalIngress.JD < internal.Greatest.JD &&
|
||
|
|
internal.Greatest.JD < internal.InternalEgress.JD && internal.InternalEgress.JD < internal.ExternalEgress.JD) {
|
||
|
|
t.Fatalf("northern site must see ordered internal contacts: %+v", internal)
|
||
|
|
}
|
||
|
|
lines := PlanetTransitContactIsochrones(transit, PlanetTransitC2, 20*time.Second)
|
||
|
|
if len(lines) == 0 {
|
||
|
|
t.Fatal("C2 isochrones missing")
|
||
|
|
}
|
||
|
|
checked := 0
|
||
|
|
for _, line := range lines {
|
||
|
|
for _, segment := range line.Segments {
|
||
|
|
for index := 0; index < len(segment); index += 5 {
|
||
|
|
point := segment[index]
|
||
|
|
local := PlanetTransitLocal(transit, Observer{Longitude: point.Longitude, Latitude: point.Latitude})
|
||
|
|
if !local.HasInternal {
|
||
|
|
t.Fatalf("C2 isochrone point (%.2f,%.2f) has no internal contact", point.Longitude, point.Latitude)
|
||
|
|
}
|
||
|
|
// 掠凌边缘内切时刻随余量按平方根变化,梯度发散,网格插值在余量 <0.5″ 处可差到 20 s。
|
||
|
|
if local.SunSemidiameterArcsec-local.PlanetSemidiameterArcsec-local.MinimumSeparationArcsec < 0.5 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if diff := math.Abs(local.InternalIngress.JD-line.JD) * 86400; diff > 3 {
|
||
|
|
t.Fatalf("C2 at (%.2f,%.2f): local contact off by %.1f s", point.Longitude, point.Latitude, diff)
|
||
|
|
}
|
||
|
|
checked++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if checked < 10 {
|
||
|
|
t.Fatalf("only %d C2 points checked", checked)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 步长过细时等时线不应被截断:条数不超上限,且仍覆盖整个接触时段。
|
||
|
|
func TestPlanetTransitContactIsochronesFineStepCoversSpan(t *testing.T) {
|
||
|
|
transit := NextVenusTransit(JDCalc(2012, 1, 1))
|
||
|
|
coarse := PlanetTransitContactIsochrones(transit, PlanetTransitC1, 2*time.Minute)
|
||
|
|
fine := PlanetTransitContactIsochrones(transit, PlanetTransitC1, 5*time.Second)
|
||
|
|
if len(coarse) == 0 || len(fine) == 0 {
|
||
|
|
t.Fatal("isochrones missing")
|
||
|
|
}
|
||
|
|
if len(fine) > planetTransitIsochroneMaxLevels {
|
||
|
|
t.Fatalf("%d lines exceed the cap", len(fine))
|
||
|
|
}
|
||
|
|
coarseSpan := (coarse[len(coarse)-1].JD - coarse[0].JD) * 1440
|
||
|
|
fineSpan := (fine[len(fine)-1].JD - fine[0].JD) * 1440
|
||
|
|
if fineSpan < coarseSpan-4 {
|
||
|
|
t.Fatalf("fine step covers %.1f min, coarse %.1f min", fineSpan, coarseSpan)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPlanetTransitSnapshotRejectsBadInput(t *testing.T) {
|
||
|
|
transit := NextVenusTransit(JDCalc(2012, 1, 1))
|
||
|
|
cases := map[string]func(*PlanetTransitResult){
|
||
|
|
"no external": func(r *PlanetTransitResult) { r.HasExternal = false },
|
||
|
|
"huge span": func(r *PlanetTransitResult) { r.ExternalEgress = r.ExternalIngress + 1e6 },
|
||
|
|
"greatest outside": func(r *PlanetTransitResult) { r.Greatest = r.ExternalEgress + 0.1 },
|
||
|
|
"greatest NaN": func(r *PlanetTransitResult) { r.Greatest = math.NaN() },
|
||
|
|
}
|
||
|
|
for name, mutate := range cases {
|
||
|
|
bad := transit
|
||
|
|
mutate(&bad)
|
||
|
|
if _, ok := newPlanetTransitSnapshot(bad, nil); ok {
|
||
|
|
t.Fatalf("%s: snapshot must be rejected", name)
|
||
|
|
}
|
||
|
|
if local := PlanetTransitLocal(bad, Observer{}); local.Valid {
|
||
|
|
t.Fatalf("%s: local result must be invalid", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, ok := newPlanetTransitSnapshot(transit, nil); !ok {
|
||
|
|
t.Fatal("valid transit rejected")
|
||
|
|
}
|
||
|
|
}
|