ccf5880269
- 基于平均下合周期与节点相位预筛候选,减少无效星历计算;跳过无法求解的单个会合周期,避免有限覆盖星历导致搜索提前终止 - 引入影轴 γ、半影半径及地球尺度几何判定,结合站心可见性识别地心无接触、仅部分地区可见的掠凌 - 添加金星 243 年、水星 46 年周期族编号,并明确编号适用范围及未分配时的零值语义 - 为 SVG、GeoJSON 和 KML 增加站心部分凌日可见区,适配接触等时线、几何元数据、图层样式及取景;为地心无接触凌日事件增加正射图,按可见区球面面积加权居中
171 lines
6.7 KiB
Go
171 lines
6.7 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
"b612.me/astro/internal/ephem"
|
|
)
|
|
|
|
func TestPlanetTransitGlobalGrazes(tester *testing.T) {
|
|
for _, model := range []ephem.Model{ephem.Semianalytic, ephem.Legacy} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, year := range []int{-540, 2854} {
|
|
transit := NextVenusTransitWithInstance(JDCalc(year, 1, 1), inst)
|
|
if !transit.Valid || JD2DateByZone(transit.Greatest, time.UTC, false).Year() != year {
|
|
tester.Fatalf("model=%d year=%d: missed global event: %+v", model, year, transit)
|
|
}
|
|
if transit.HasExternal || transit.HasInternal || transit.ExternalIngress != 0 || transit.ExternalEgress != 0 {
|
|
tester.Fatalf("global graze fabricated geocentric contacts: %+v", transit)
|
|
}
|
|
if math.Abs(transit.Gamma) <= transit.PenumbralRadius || math.Abs(transit.Gamma) >= transit.PenumbralRadius+1.01 {
|
|
tester.Fatalf("unexpected shadow geometry: %+v", transit)
|
|
}
|
|
observer := Observer{Longitude: -135, Latitude: -60}
|
|
local := PlanetTransitLocalWithInstance(transit, observer, inst)
|
|
if !local.Valid || !local.Visible || local.HasInternal || !local.ExternalIngress.Valid || !local.ExternalEgress.Valid {
|
|
tester.Fatalf("model=%d year=%d: missing visible local graze: %+v", model, year, local)
|
|
}
|
|
if year == 2854 {
|
|
if other := PlanetTransitLocalWithInstance(transit, Observer{Latitude: 45}, inst); other.Valid {
|
|
tester.Fatal("northern site must miss the 2854 event")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanetTransitSeriesNumbers(tester *testing.T) {
|
|
for _, sample := range []struct{ planet, year, number, period int }{
|
|
{2, 1631, 6, 243}, {2, 1639, 4, 243}, {2, 2004, 3, 243}, {2, 2012, 5, 243}, {2, 2611, 4, 243}, {2, 2854, 4, 243},
|
|
{1, 1957, 9, 46}, {1, 2003, 9, 46}, {1, 2006, 8, 46}, {1, 2016, 7, 46}, {1, 2019, 6, 46}, {1, 2174, 11, 46},
|
|
} {
|
|
query := JDCalc(sample.year, 1, 1)
|
|
result := NextVenusTransit(query)
|
|
if sample.planet == 1 {
|
|
result = NextMercuryTransit(query)
|
|
}
|
|
if result.Series.Number != sample.number || result.Series.PeriodYears != sample.period {
|
|
tester.Fatalf("planet=%d year=%d: series=%+v, want %d/%d", sample.planet, sample.year, result.Series, sample.number, sample.period)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanetTransitSeriesUnassigned(tester *testing.T) {
|
|
for _, sample := range []struct {
|
|
cfg planetTransitConfig
|
|
year int
|
|
}{
|
|
{mercuryTransitConfig(), 1500}, {mercuryTransitConfig(), 2400},
|
|
{venusTransitConfig(), -2100}, {venusTransitConfig(), 4100},
|
|
} {
|
|
series := planetTransitSeriesAt(JDCalc(sample.year, 1, 1), sample.cfg)
|
|
if series.Number != 0 || series.PeriodYears == 0 {
|
|
tester.Fatalf("unassigned series: %+v", series)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanetTransitNearInternalEdge(tester *testing.T) {
|
|
for _, year := range []int{1631, 2611} {
|
|
transit := NextVenusTransit(JDCalc(year, 1, 1))
|
|
if !transit.Valid || !transit.HasExternal || !transit.HasInternal {
|
|
tester.Fatalf("year=%d: missing geocentric contacts", year)
|
|
}
|
|
full, partial := false, false
|
|
for latitude := -75; latitude <= 75; latitude += 30 {
|
|
for longitude := -180; longitude < 180; longitude += 45 {
|
|
local := PlanetTransitLocal(transit, Observer{Longitude: float64(longitude), Latitude: float64(latitude)})
|
|
if !local.Valid || !local.Visible {
|
|
continue
|
|
}
|
|
if local.HasInternal {
|
|
full = true
|
|
} else {
|
|
partial = true
|
|
}
|
|
}
|
|
}
|
|
if !full || !partial {
|
|
tester.Fatalf("year=%d: full=%v partial=%v", year, full, partial)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanetTransitInputBounds(tester *testing.T) {
|
|
for _, query := range []float64{math.NaN(), math.Inf(1), math.Inf(-1), math.MaxFloat64} {
|
|
if result := NextVenusTransit(query); result.Valid {
|
|
tester.Fatal("non-finite query produced a transit")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanetTransitResultRebuild(tester *testing.T) {
|
|
greatest := time.Date(2854, 12, 14, 13, 0, 0, 0, time.UTC)
|
|
if PlanetTransitResultFromTimes(2, time.Time{}, time.Time{}, greatest, time.Time{}, time.Time{}).Valid {
|
|
tester.Fatal("contact rebuild accepted missing C1/C4")
|
|
}
|
|
for _, window := range [][2]time.Time{
|
|
{{}, greatest.Add(time.Hour)}, {greatest, greatest.Add(time.Hour)}, {greatest.Add(-time.Hour), greatest},
|
|
} {
|
|
if PlanetTransitResultFromWindow(2, window[0], greatest, window[1]).Valid {
|
|
tester.Fatalf("window rebuild accepted %v", window)
|
|
}
|
|
}
|
|
transit := NextVenusTransit(JDCalc(2854, 1, 1))
|
|
rebuilt := PlanetTransitResultFromWindow(2, JD2DateByZone(transit.WindowStart, time.UTC, false),
|
|
JD2DateByZone(transit.Greatest, time.UTC, false), JD2DateByZone(transit.WindowEnd, time.UTC, false))
|
|
observer := Observer{Longitude: -135, Latitude: -60}
|
|
want, got := PlanetTransitLocal(transit, observer), PlanetTransitLocal(rebuilt, observer)
|
|
if !got.Valid || math.Abs(got.ExternalIngress.JD-want.ExternalIngress.JD)*86400 > 0.01 || math.Abs(got.ExternalEgress.JD-want.ExternalEgress.JD)*86400 > 0.01 {
|
|
tester.Fatalf("window rebuild changed local contacts: got %+v want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
// boundedCoverageEngine 把内置星历限制在一个时间窗内,窗外的请求按越界报错。
|
|
type boundedCoverageEngine struct {
|
|
ephem.Engine
|
|
start, end float64
|
|
}
|
|
|
|
func (bounded boundedCoverageEngine) within(jd float64) bool {
|
|
return jd >= bounded.start && jd <= bounded.end
|
|
}
|
|
|
|
func (bounded boundedCoverageEngine) State(target, center ephem.Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
|
if !bounded.within(jdTDB) {
|
|
return [3]float64{}, [3]float64{}, ephem.ErrOutOfRange
|
|
}
|
|
return bounded.Engine.State(target, center, jdTDB)
|
|
}
|
|
|
|
func (bounded boundedCoverageEngine) Apparent(target ephem.Body, jdTDB float64) (pos [3]float64, lightTime, distance float64, err error) {
|
|
if !bounded.within(jdTDB) {
|
|
return [3]float64{}, 0, 0, ephem.ErrOutOfRange
|
|
}
|
|
return bounded.Engine.(ephem.ApparentEngine).Apparent(target, jdTDB)
|
|
}
|
|
|
|
func (bounded boundedCoverageEngine) Coverage(ephem.Body) (start, end float64, ok bool) {
|
|
return bounded.start, bounded.end, true
|
|
}
|
|
|
|
// 有限覆盖星历:搜索起点前的候选落在覆盖之外时,不得因此丢掉覆盖内的凌日。
|
|
func TestPlanetTransitBoundedEphemerisCoverage(tester *testing.T) {
|
|
base := ephem.NewModelInstance(ephem.Semianalytic).Engine()
|
|
start := JDCalc(2019, 1, 1)
|
|
bounded := ephem.NewInstance(boundedCoverageEngine{Engine: base, start: start, end: JDCalc(2021, 1, 1)})
|
|
query := start + 60
|
|
transit := NextMercuryTransitWithInstance(query, bounded)
|
|
if !transit.Valid {
|
|
tester.Fatal("bounded ephemeris lost the 2019-11-11 transit inside its coverage")
|
|
}
|
|
if year := JD2DateByZone(transit.Greatest, time.UTC, false).Year(); year != 2019 {
|
|
tester.Fatalf("bounded ephemeris returned year %d, want 2019", year)
|
|
}
|
|
if transit.HasExternal != true || transit.HasInternal != true {
|
|
tester.Fatalf("bounded ephemeris returned an incomplete transit: %+v", transit)
|
|
}
|
|
}
|