044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
385 lines
15 KiB
Go
385 lines
15 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
|
|
"b612.me/astro/internal/ephem"
|
|
)
|
|
|
|
type p3ProbeEngine struct {
|
|
angle float64
|
|
distance float64
|
|
}
|
|
|
|
func (engine p3ProbeEngine) State(target, center ephem.Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
|
angle := engine.angle + float64(target-center)*0.01
|
|
distance := engine.distance
|
|
if distance == 0 {
|
|
distance = 1
|
|
}
|
|
pos[0] = math.Cos(angle) * astronomicalUnitKM * distance
|
|
pos[1] = math.Sin(angle) * astronomicalUnitKM * distance
|
|
return pos, vel, nil
|
|
}
|
|
|
|
func (engine p3ProbeEngine) Coverage(target ephem.Body) (start, end float64, ok bool) {
|
|
return 2400000, 2500000, target != ephem.SSB
|
|
}
|
|
|
|
func (engine p3ProbeEngine) Apparent(target ephem.Body, jdTT float64) (dir [3]float64, distKM, tauDays float64, err error) {
|
|
angle := engine.angle + float64(target)*0.01
|
|
dir[0] = math.Cos(angle)
|
|
dir[1] = math.Sin(angle)
|
|
distance := engine.distance
|
|
if distance == 0 {
|
|
distance = 1
|
|
}
|
|
return dir, astronomicalUnitKM * distance, 0.005, nil
|
|
}
|
|
|
|
func TestP3SunMoonFunctionsUseExplicitInstances(t *testing.T) {
|
|
previous := ephem.Active()
|
|
ephem.SetActive(ephem.Legacy)
|
|
t.Cleanup(func() { ephem.SetActive(previous) })
|
|
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
|
|
jd := 2451545.0
|
|
|
|
sunFirst := HSunApparentLoNWithInstance(jd, -1, first)
|
|
sunSecond := HSunApparentLoNWithInstance(jd, -1, second)
|
|
if math.Abs(angleDifference(sunFirst, sunSecond)) < 1 {
|
|
t.Fatalf("sun apparent longitude did not follow instance: %.6f vs %.6f", sunFirst, sunSecond)
|
|
}
|
|
|
|
moonFirst := HMoonApparentLoNWithInstance(jd, -1, first)
|
|
moonSecond := HMoonApparentLoNWithInstance(jd, -1, second)
|
|
if math.Abs(angleDifference(moonFirst, moonSecond)) < 1 {
|
|
t.Fatalf("moon apparent longitude did not follow instance: %.6f vs %.6f", moonFirst, moonSecond)
|
|
}
|
|
|
|
if got, want := HSunApparentLoN(jd, -1), HSunApparentLoNWithInstance(jd, -1, nil); math.Abs(angleDifference(got, want)) > 1e-12 {
|
|
t.Fatalf("nil instance changed legacy-compatible result: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func TestP3PlanetFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
|
|
jd := 2451545.0
|
|
|
|
for _, sample := range []struct {
|
|
name string
|
|
first float64
|
|
second float64
|
|
}{
|
|
{"MercuryL", MercuryLWithInstance(jd, first), MercuryLWithInstance(jd, second)},
|
|
{"VenusL", VenusLWithInstance(jd, first), VenusLWithInstance(jd, second)},
|
|
{"MarsL", MarsLWithInstance(jd, first), MarsLWithInstance(jd, second)},
|
|
{"JupiterL", JupiterLWithInstance(jd, first), JupiterLWithInstance(jd, second)},
|
|
{"SaturnL", SaturnLWithInstance(jd, first), SaturnLWithInstance(jd, second)},
|
|
{"UranusL", UranusLWithInstance(jd, first), UranusLWithInstance(jd, second)},
|
|
{"NeptuneL", NeptuneLWithInstance(jd, first), NeptuneLWithInstance(jd, second)},
|
|
{"MercuryApparentLo", MercuryApparentLoWithInstance(jd, first), MercuryApparentLoWithInstance(jd, second)},
|
|
{"NeptuneApparentLo", NeptuneApparentLoWithInstance(jd, first), NeptuneApparentLoWithInstance(jd, second)},
|
|
} {
|
|
if math.Abs(angleDifference(sample.first, sample.second)) < 1 {
|
|
t.Fatalf("%s did not follow explicit instance: %.6f vs %.6f", sample.name, sample.first, sample.second)
|
|
}
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"MercuryL", MercuryLWithInstance(jd, inst)},
|
|
{"MercuryR", MercuryRWithInstance(jd, inst)},
|
|
{"MercuryApparentRa", MercuryApparentRaWithInstance(jd, inst)},
|
|
{"NeptuneApparentDec", NeptuneApparentDecWithInstance(jd, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
previous := ephem.Active()
|
|
ephem.SetActive(ephem.Legacy)
|
|
t.Cleanup(func() { ephem.SetActive(previous) })
|
|
if got, want := MercuryLWithInstance(jd, nil), MercuryL(jd); got != want {
|
|
t.Fatalf("nil instance changed MercuryL: %.15f vs %.15f", got, want)
|
|
}
|
|
if got, want := NeptuneApparentRaWithInstance(jd, nil), NeptuneApparentRa(jd); got != want {
|
|
t.Fatalf("nil instance changed NeptuneApparentRa: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func TestP3MoonPhaseFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
|
|
jd := 2451545.0
|
|
|
|
phaseFirst := MoonPhaseWithInstance(jd, first)
|
|
phaseSecond := MoonPhaseWithInstance(jd, second)
|
|
if math.Abs(phaseFirst-phaseSecond) < 1e-6 {
|
|
t.Fatalf("moon phase did not follow explicit instance: %.12f vs %.12f", phaseFirst, phaseSecond)
|
|
}
|
|
seekFirst := SunMoonSeekWithInstance(jd, 0, first)
|
|
seekSecond := SunMoonSeekWithInstance(jd, 0, second)
|
|
if math.Abs(angleDifference(seekFirst, seekSecond)) < 1 {
|
|
t.Fatalf("sun-moon phase residual did not follow explicit instance: %.6f vs %.6f", seekFirst, seekSecond)
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"CalcMoonSHByJDE", CalcMoonSHByJDEWithInstance(jd, 0, inst)},
|
|
{"CalcMoonXHByJDE", CalcMoonXHByJDEWithInstance(jd, 0, inst)},
|
|
{"MoonPhase", MoonPhaseWithInstance(jd, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if got, want := MoonPhaseWithInstance(jd, nil), MoonPhase(jd); got != want {
|
|
t.Fatalf("nil instance changed MoonPhase: %.15f vs %.15f", got, want)
|
|
}
|
|
if got, want := CalcMoonSHByJDEWithInstance(jd, 0, nil), CalcMoonSHByJDE(jd, 0); got != want {
|
|
t.Fatalf("nil instance changed CalcMoonSHByJDE: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func TestP3SolarTermFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
|
|
jd := 2451545.0
|
|
|
|
firstResidual := JQLospecWithInstance(jd, 0, first)
|
|
secondResidual := JQLospecWithInstance(jd, 0, second)
|
|
if math.Abs(angleDifference(firstResidual, secondResidual)) < 1 {
|
|
t.Fatalf("solar-term longitude did not follow explicit instance: %.6f vs %.6f", firstResidual, secondResidual)
|
|
}
|
|
if GetShiErCiWithInstance(jd, first) == GetShiErCiWithInstance(jd, second) {
|
|
t.Fatalf("twelve-division label did not follow explicit instance")
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"GetJQTime", GetJQTimeWithInstance(2024, 270, inst)},
|
|
{"GetWuHouTime", GetWuHouTimeWithInstance(2024, 0, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
if got := GetJieqiLoopsWithInstance(2024, 3, inst); len(got) != 3 {
|
|
t.Fatalf("GetJieqiLoops model %d returned %d values", model, len(got))
|
|
}
|
|
}
|
|
|
|
if got, want := JQLospecWithInstance(jd, 0, nil), JQLospec(jd, 0); got != want {
|
|
t.Fatalf("nil instance changed JQLospec: %.15f vs %.15f", got, want)
|
|
}
|
|
if got, want := GetJQTimeWithInstance(2024, 270, nil), GetJQTime(2024, 270); got != want {
|
|
t.Fatalf("nil instance changed GetJQTime: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func TestP3SunObservationFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
|
|
jd := 2451545.0
|
|
|
|
firstHeight := SunHeightWithInstance(jd, 121.47, 31.23, 8, first)
|
|
secondHeight := SunHeightWithInstance(jd, 121.47, 31.23, 8, second)
|
|
if math.Abs(firstHeight-secondHeight) < 1 {
|
|
t.Fatalf("sun height did not follow explicit instance: %.6f vs %.6f", firstHeight, secondHeight)
|
|
}
|
|
firstAzimuth := SunAzimuthWithInstance(jd, 121.47, 31.23, 8, first)
|
|
secondAzimuth := SunAzimuthWithInstance(jd, 121.47, 31.23, 8, second)
|
|
if math.Abs(angleDifference(firstAzimuth, secondAzimuth)) < 1 {
|
|
t.Fatalf("sun azimuth did not follow explicit instance: %.6f vs %.6f", firstAzimuth, secondAzimuth)
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"SunTime", SunTimeWithInstance(jd, inst)},
|
|
{"CulminationTime", CulminationTimeWithInstance(jd, 121.47, 8, inst)},
|
|
{"SunTimeAngle", SunTimeAngleWithInstance(jd, 121.47, 31.23, 8, inst)},
|
|
{"SunHeight", SunHeightWithInstance(jd, 121.47, 31.23, 8, inst)},
|
|
{"SunAzimuth", SunAzimuthWithInstance(jd, 121.47, 31.23, 8, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if got, want := SunHeightWithInstance(jd, 121.47, 31.23, 8, nil), SunHeight(jd, 121.47, 31.23, 8); got != want {
|
|
t.Fatalf("nil instance changed SunHeight: %.15f vs %.15f", got, want)
|
|
}
|
|
if got, want := SunAzimuthWithInstance(jd, 121.47, 31.23, 8, nil), SunAzimuth(jd, 121.47, 31.23, 8); got != want {
|
|
t.Fatalf("nil instance changed SunAzimuth: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
// 显式 Legacy 实例的均时差与中天时刻必须复现包级 Legacy 结果。
|
|
func TestSunTimeWithLegacyInstanceMatchesPackageLevel(t *testing.T) {
|
|
previous := ephem.Active()
|
|
ephem.SetActive(ephem.Legacy)
|
|
t.Cleanup(func() { ephem.SetActive(previous) })
|
|
inst := ephem.NewModelInstance(ephem.Legacy)
|
|
for _, jd := range []float64{2451545.0, 2460384.5, 2460569.5} {
|
|
if got, want := SunTimeWithInstance(jd, inst), SunTime(jd); math.Abs(got-want) > 1e-9 {
|
|
t.Fatalf("SunTime jd %.1f: instance %.9f h, package %.9f h", jd, got, want)
|
|
}
|
|
if got, want := SunTimeNWithInstance(jd, 20, inst), SunTimeN(jd, 20); math.Abs(got-want) > 1e-9 {
|
|
t.Fatalf("SunTimeN jd %.1f: instance %.9f h, package %.9f h", jd, got, want)
|
|
}
|
|
if got, want := CulminationTimeWithInstance(jd, 116.4, 8, inst), CulminationTime(jd, 116.4, 8); math.Abs(got-want)*86400 > 1e-3 {
|
|
t.Fatalf("CulminationTime jd %.1f: differs by %.3f s", jd, (got-want)*86400)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestP3SunMoonAliasFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2})
|
|
jd := 2451545.0
|
|
|
|
if math.Abs(angleDifference(SunApparentRaWithInstance(jd, first), SunApparentRaWithInstance(jd, second))) < 1 {
|
|
t.Fatal("SunApparentRa alias did not follow explicit instance")
|
|
}
|
|
if math.Abs(angleDifference(MoonGeocentricApparentRaWithInstance(jd, first), MoonGeocentricApparentRaWithInstance(jd, second))) < 1 {
|
|
t.Fatal("MoonGeocentricApparentRa alias did not follow explicit instance")
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"SunApparentRa", SunApparentRaWithInstance(jd, inst)},
|
|
{"SunTrueDec", SunTrueDecWithInstance(jd, inst)},
|
|
{"MoonGeocentricTrueRa", MoonGeocentricTrueRaWithInstance(jd, inst)},
|
|
{"MoonTrueDec", MoonTrueDecWithInstance(jd, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if got, want := SunApparentRaWithInstance(jd, nil), SunApparentRa(jd); got != want {
|
|
t.Fatalf("nil instance changed SunApparentRa: %.15f vs %.15f", got, want)
|
|
}
|
|
if got, want := MoonGeocentricTrueRaWithInstance(jd, nil), HMoonGeocentricTrueRa(jd); got != want {
|
|
t.Fatalf("nil instance changed MoonGeocentricTrueRa: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func TestP3PlanetMagnitudeFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0, distance: 0.8})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2, distance: 1.2})
|
|
jd := 2451545.0
|
|
|
|
for _, sample := range []struct {
|
|
name string
|
|
first float64
|
|
second float64
|
|
}{
|
|
{"MercuryMag", MercuryMagWithInstance(jd, first), MercuryMagWithInstance(jd, second)},
|
|
{"VenusMag", VenusMagWithInstance(jd, first), VenusMagWithInstance(jd, second)},
|
|
{"MarsMag", MarsMagWithInstance(jd, first), MarsMagWithInstance(jd, second)},
|
|
{"JupiterMag", JupiterMagWithInstance(jd, first), JupiterMagWithInstance(jd, second)},
|
|
{"UranusMag", UranusMagWithInstance(jd, first), UranusMagWithInstance(jd, second)},
|
|
{"NeptuneMag", NeptuneMagWithInstance(jd, first), NeptuneMagWithInstance(jd, second)},
|
|
} {
|
|
if math.Abs(sample.first-sample.second) < 1e-6 {
|
|
t.Fatalf("%s did not follow explicit instance: %.12f vs %.12f", sample.name, sample.first, sample.second)
|
|
}
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"MercuryMag", MercuryMagWithInstance(jd, inst)},
|
|
{"JupiterMag", JupiterMagWithInstance(jd, inst)},
|
|
{"NeptuneMag", NeptuneMagWithInstance(jd, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if got, want := MercuryMagWithInstance(jd, nil), MercuryMag(jd); got != want {
|
|
t.Fatalf("nil instance changed MercuryMag: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func TestP3SaturnRingFunctionsUseExplicitInstances(t *testing.T) {
|
|
first := ephem.NewInstance(p3ProbeEngine{angle: 0, distance: 0.8})
|
|
second := ephem.NewInstance(p3ProbeEngine{angle: math.Pi / 2, distance: 1.2})
|
|
jd := 2451545.0
|
|
|
|
firstRing := SaturnRingBWithInstance(jd, first)
|
|
secondRing := SaturnRingBWithInstance(jd, second)
|
|
if math.Abs(firstRing-secondRing) < 1e-6 {
|
|
t.Fatalf("Saturn ring opening angle did not follow explicit instance: %.12f vs %.12f", firstRing, secondRing)
|
|
}
|
|
firstMagnitude := SaturnMagWithInstance(jd, first)
|
|
secondMagnitude := SaturnMagWithInstance(jd, second)
|
|
if math.Abs(firstMagnitude-secondMagnitude) < 1e-6 {
|
|
t.Fatalf("Saturn magnitude did not follow explicit instance: %.12f vs %.12f", firstMagnitude, secondMagnitude)
|
|
}
|
|
|
|
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
|
|
inst := ephem.NewModelInstance(model)
|
|
for _, sample := range []struct {
|
|
name string
|
|
value float64
|
|
}{
|
|
{"SaturnRingB", SaturnRingBWithInstance(jd, inst)},
|
|
{"SaturnRingPositionAngle", SaturnRingPositionAngleWithInstance(jd, inst)},
|
|
{"SaturnMag", SaturnMagWithInstance(jd, inst)},
|
|
} {
|
|
if math.IsNaN(sample.value) || math.IsInf(sample.value, 0) {
|
|
t.Fatalf("%s model %d returned non-finite value: %v", sample.name, model, sample.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
if got, want := SaturnMagWithInstance(jd, nil), SaturnMag(jd); got != want {
|
|
t.Fatalf("nil instance changed SaturnMag: %.15f vs %.15f", got, want)
|
|
}
|
|
}
|
|
|
|
func angleDifference(a, b float64) float64 {
|
|
difference := math.Mod(a-b+180, 360)
|
|
if difference < 0 {
|
|
difference += 360
|
|
}
|
|
return difference - 180
|
|
}
|