2026-05-22 12:24:41 +08:00
|
|
|
|
package basic
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type innerBaselineFile struct {
|
|
|
|
|
|
Events []innerBaselineEvent `json:"events"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type innerBaselineEvent struct {
|
|
|
|
|
|
Planet string `json:"planet"`
|
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
|
NAOJHintJST string `json:"naoj_hint_jst"`
|
|
|
|
|
|
Precision string `json:"precision"`
|
|
|
|
|
|
CandidateJST string `json:"candidate_jst"`
|
|
|
|
|
|
VerifiedJST string `json:"verified_jst"`
|
|
|
|
|
|
CandidateSource string `json:"candidate_source"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func loadInnerBaseline(t *testing.T) innerBaselineFile {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
|
|
|
|
paths := [][]string{
|
|
|
|
|
|
{
|
|
|
|
|
|
"testdata/jpl_inner_event_baseline.json",
|
|
|
|
|
|
"basic/testdata/jpl_inner_event_baseline.json",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"testdata/jpl_inner_event_baseline_21c_sample.json",
|
|
|
|
|
|
"basic/testdata/jpl_inner_event_baseline_21c_sample.json",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"testdata/jpl_inner_event_baseline_20c_sample.json",
|
|
|
|
|
|
"basic/testdata/jpl_inner_event_baseline_20c_sample.json",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"testdata/jpl_inner_event_baseline_22c_sample.json",
|
|
|
|
|
|
"basic/testdata/jpl_inner_event_baseline_22c_sample.json",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
var merged innerBaselineFile
|
|
|
|
|
|
for index, candidates := range paths {
|
|
|
|
|
|
var (
|
|
|
|
|
|
data []byte
|
|
|
|
|
|
err error
|
|
|
|
|
|
)
|
|
|
|
|
|
for _, path := range candidates {
|
|
|
|
|
|
data, err = os.ReadFile(path)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
var baseline innerBaselineFile
|
|
|
|
|
|
if err := json.Unmarshal(data, &baseline); err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
merged.Events = append(merged.Events, baseline.Events...)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if err != nil && index == 0 {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(merged.Events) == 0 {
|
|
|
|
|
|
t.Fatal("empty inner baseline file")
|
|
|
|
|
|
}
|
|
|
|
|
|
return merged
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseInnerBaselineTime(t *testing.T, value string) time.Time {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
loc := time.FixedZone("JST", 9*3600)
|
|
|
|
|
|
layouts := []string{
|
|
|
|
|
|
"2006-01-02 15:04:05 MST",
|
|
|
|
|
|
"2006-01-02 15:04 MST",
|
|
|
|
|
|
"2006-01-02 15:04:05",
|
|
|
|
|
|
"2006-01-02 15:04",
|
|
|
|
|
|
}
|
|
|
|
|
|
var err error
|
|
|
|
|
|
for _, layout := range layouts {
|
|
|
|
|
|
when, parseErr := time.ParseInLocation(layout, value, loc)
|
|
|
|
|
|
if parseErr == nil {
|
|
|
|
|
|
return when
|
|
|
|
|
|
}
|
|
|
|
|
|
err = parseErr
|
|
|
|
|
|
}
|
|
|
|
|
|
t.Fatalf("parse baseline time %q: %v", value, err)
|
|
|
|
|
|
return time.Time{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func innerBaselineTolerance(event innerBaselineEvent) time.Duration {
|
|
|
|
|
|
switch event.Kind {
|
2026-09-17 12:27:40 +08:00
|
|
|
|
case "IC", "SC":
|
|
|
|
|
|
// 2197 年附近双方 ΔT 外推已差约 2 分钟(校正了 EarthPI 的 0.00046·T² 项之后,
|
|
|
|
|
|
// 三个 2197 样本从 −119.5 s 移到 −121…−126 s),因此与留点同样留 3 分钟余量;
|
|
|
|
|
|
// 真实的搜索错误是小时/天级,靠这个容差仍能抓住。
|
|
|
|
|
|
// Around 2197 the two ΔT extrapolations already differ by about two minutes (after
|
|
|
|
|
|
// correcting the EarthPI 0.00046*T^2 term, three 2197 samples moved from -119.5 s to
|
|
|
|
|
|
// -121..-126 s), so conjunctions get the same 3-minute margin as the stations.
|
|
|
|
|
|
// Real search errors are hours or days, so this still catches them.
|
|
|
|
|
|
return 3 * time.Minute
|
|
|
|
|
|
case "P2R", "R2P":
|
|
|
|
|
|
// 留是 RA 的极值点:星历模型中 ~1e-6 度/天的变化率差异就会被放大成分钟级时刻差,
|
|
|
|
|
|
// 且 JPL 给出的 UT 时刻还叠加了各自的 ΔT 外推(2197 年附近两者已差 ~2 分钟)。
|
|
|
|
|
|
// 这里留 3 分钟余量,仍远小于任何真实的搜索错误(小时/天级)。
|
|
|
|
|
|
return 3 * time.Minute
|
2026-05-22 12:24:41 +08:00
|
|
|
|
case "GEE", "GEW":
|
|
|
|
|
|
return 90 * time.Minute
|
|
|
|
|
|
default:
|
|
|
|
|
|
return 2 * time.Minute
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func innerEventFuncs(t *testing.T, event innerBaselineEvent) (func(float64) float64, func(float64) float64) {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
switch event.Planet + ":" + event.Kind {
|
|
|
|
|
|
case "Mercury:IC":
|
|
|
|
|
|
return LastMercuryInferiorConjunctionInclusive, NextMercuryInferiorConjunctionInclusive
|
|
|
|
|
|
case "Mercury:SC":
|
|
|
|
|
|
return LastMercurySuperiorConjunctionInclusive, NextMercurySuperiorConjunctionInclusive
|
|
|
|
|
|
case "Mercury:P2R":
|
|
|
|
|
|
return LastMercuryProgradeToRetrogradeInclusive, NextMercuryProgradeToRetrogradeInclusive
|
|
|
|
|
|
case "Mercury:R2P":
|
|
|
|
|
|
return LastMercuryRetrogradeToProgradeInclusive, NextMercuryRetrogradeToProgradeInclusive
|
|
|
|
|
|
case "Mercury:GEE":
|
|
|
|
|
|
return LastMercuryGreatestElongationEastInclusive, NextMercuryGreatestElongationEastInclusive
|
|
|
|
|
|
case "Mercury:GEW":
|
|
|
|
|
|
return LastMercuryGreatestElongationWestInclusive, NextMercuryGreatestElongationWestInclusive
|
|
|
|
|
|
case "Venus:IC":
|
|
|
|
|
|
return LastVenusInferiorConjunctionInclusive, NextVenusInferiorConjunctionInclusive
|
|
|
|
|
|
case "Venus:SC":
|
|
|
|
|
|
return LastVenusSuperiorConjunctionInclusive, NextVenusSuperiorConjunctionInclusive
|
|
|
|
|
|
case "Venus:P2R":
|
|
|
|
|
|
return LastVenusProgradeToRetrogradeInclusive, NextVenusProgradeToRetrogradeInclusive
|
|
|
|
|
|
case "Venus:R2P":
|
|
|
|
|
|
return LastVenusRetrogradeToProgradeInclusive, NextVenusRetrogradeToProgradeInclusive
|
|
|
|
|
|
case "Venus:GEE":
|
|
|
|
|
|
return LastVenusGreatestElongationEastInclusive, NextVenusGreatestElongationEastInclusive
|
|
|
|
|
|
case "Venus:GEW":
|
|
|
|
|
|
return LastVenusGreatestElongationWestInclusive, NextVenusGreatestElongationWestInclusive
|
|
|
|
|
|
default:
|
|
|
|
|
|
t.Fatalf("unsupported event %s:%s", event.Planet, event.Kind)
|
|
|
|
|
|
return nil, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func assertInnerBaselineEvent(t *testing.T, event innerBaselineEvent, lastFn, nextFn func(float64) float64) {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
when := parseInnerBaselineTime(t, event.VerifiedJST)
|
|
|
|
|
|
before := when.Add(-24 * time.Hour)
|
|
|
|
|
|
after := when.Add(24 * time.Hour)
|
2026-09-23 18:55:12 +08:00
|
|
|
|
next := JD2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
|
|
|
|
|
|
last := JD2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
|
2026-05-22 12:24:41 +08:00
|
|
|
|
tolerance := innerBaselineTolerance(event)
|
|
|
|
|
|
|
|
|
|
|
|
if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
|
|
|
|
|
|
t.Fatalf("%s %s next mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, next, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
|
|
|
|
|
|
}
|
|
|
|
|
|
if diff := last.Sub(when); diff < -tolerance || diff > tolerance {
|
|
|
|
|
|
t.Fatalf("%s %s last mismatch: got %s want %s tol=%s hint=%s candidate=%s via=%s", event.Planet, event.Kind, last, when, tolerance, event.NAOJHintJST, event.CandidateJST, event.CandidateSource)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestInnerPlanetTruthAgainstJPL(t *testing.T) {
|
|
|
|
|
|
baseline := loadInnerBaseline(t)
|
|
|
|
|
|
for _, event := range baseline.Events {
|
|
|
|
|
|
event := event
|
|
|
|
|
|
name := strings.Join([]string{event.Planet, event.Kind, event.VerifiedJST}, "_")
|
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
|
lastFn, nextFn := innerEventFuncs(t, event)
|
|
|
|
|
|
assertInnerBaselineEvent(t, event, lastFn, nextFn)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|