feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
// Package moon 轻量月球链路:少量摄动项的月球近似、轻量站心修正、月相月龄和升落搜索,角度单位为度。
|
||||
// Package moon is the lightweight lunar chain: a few-perturbation-term Moon approximation, light topocentric correction, phase, age and rise/set search; angles are degrees.
|
||||
package moon
|
||||
+11
-4
@@ -60,27 +60,34 @@ func ApparentRaDec(date time.Time, lon, lat float64) (float64, float64) {
|
||||
return state.RightAscension, state.Declination
|
||||
}
|
||||
|
||||
func topocentricHorizontal(date time.Time, lon, lat float64) (altitude, azimuth, hourAngle float64) {
|
||||
jd := basic.Date2JDE(date.UTC())
|
||||
state := lite.MoonTopocentric(jd, lon, lat, 0)
|
||||
return lite.HorizontalCoordinates(state.RightAscension, state.Declination, jd, lon, lat)
|
||||
}
|
||||
|
||||
// HourAngle 轻量时角 / lightweight hour angle.
|
||||
func HourAngle(date time.Time, lon, lat float64) float64 {
|
||||
_, _, hourAngle := lite.HorizontalCoordinates(ApparentRa(date, lon, lat), ApparentDec(date, lon, lat), basic.Date2JDE(date.UTC()), lon, lat)
|
||||
_, _, hourAngle := topocentricHorizontal(date, lon, lat)
|
||||
return hourAngle
|
||||
}
|
||||
|
||||
// Azimuth 轻量方位角 / lightweight azimuth.
|
||||
func Azimuth(date time.Time, lon, lat float64) float64 {
|
||||
_, azimuth, _ := lite.HorizontalCoordinates(ApparentRa(date, lon, lat), ApparentDec(date, lon, lat), basic.Date2JDE(date.UTC()), lon, lat)
|
||||
_, azimuth, _ := topocentricHorizontal(date, lon, lat)
|
||||
return azimuth
|
||||
}
|
||||
|
||||
// Altitude 轻量高度角 / lightweight altitude.
|
||||
func Altitude(date time.Time, lon, lat float64) float64 {
|
||||
altitude, _, _ := lite.HorizontalCoordinates(ApparentRa(date, lon, lat), ApparentDec(date, lon, lat), basic.Date2JDE(date.UTC()), lon, lat)
|
||||
altitude, _, _ := topocentricHorizontal(date, lon, lat)
|
||||
return altitude
|
||||
}
|
||||
|
||||
// Zenith 轻量天顶距 / lightweight zenith distance.
|
||||
func Zenith(date time.Time, lon, lat float64) float64 {
|
||||
return 90 - Altitude(date, lon, lat)
|
||||
altitude, _, _ := topocentricHorizontal(date, lon, lat)
|
||||
return 90 - altitude
|
||||
}
|
||||
|
||||
// SunMoonLoDiff 轻量日月黄经差 / lightweight Moon-Sun ecliptic-longitude difference.
|
||||
|
||||
@@ -2,9 +2,12 @@ package moon
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
lite "b612.me/astro/lite/internal"
|
||||
fullmoon "b612.me/astro/moon"
|
||||
)
|
||||
|
||||
@@ -92,3 +95,83 @@ func assertTimeWithinMinutes(t *testing.T, name string, got, want time.Time, lim
|
||||
t.Fatalf("%s = %s, want %s", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 四个地平入口须只求一次站心位置:参考实现按旧口径对同一时刻重复求值,逐位对照。
|
||||
func TestHorizontalEntriesMatchDuplicatedEvaluation(t *testing.T) {
|
||||
type site struct {
|
||||
name string
|
||||
lon, lat float64
|
||||
}
|
||||
sites := []site{
|
||||
{"shanghai", 121.4737, 31.2304},
|
||||
{"sydney", 151.2093, -33.8688},
|
||||
{"north-pole", 0, 89.9999},
|
||||
{"south-pole", 0, -89.9999},
|
||||
{"dateline-west", -179.99, 12},
|
||||
{"dateline-east", 179.99, -12},
|
||||
{"equator", 0, 0},
|
||||
}
|
||||
zones := []*time.Location{
|
||||
time.UTC,
|
||||
time.FixedZone("CST", 8*3600),
|
||||
time.FixedZone("EST", -5*3600),
|
||||
time.FixedZone("LMT", -7*3600-52*60-58),
|
||||
}
|
||||
years := []int{-500, 1000, 1582, 2025, 2100, 3000, 4000}
|
||||
hours := []int{0, 5, 12, 20, 23}
|
||||
|
||||
type sample struct {
|
||||
label string
|
||||
date time.Time
|
||||
lon float64
|
||||
lat float64
|
||||
}
|
||||
samples := make([]sample, 0, len(sites)*len(zones)*len(years)*len(hours)+200)
|
||||
for _, st := range sites {
|
||||
for _, zone := range zones {
|
||||
for _, year := range years {
|
||||
for _, hour := range hours {
|
||||
samples = append(samples, sample{
|
||||
label: st.name + "/" + zone.String(),
|
||||
date: time.Date(year, 3, 17, hour, 43, 21, 123456789, zone),
|
||||
lon: st.lon,
|
||||
lat: st.lat,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rng := rand.New(rand.NewSource(20260915))
|
||||
for i := 0; i < 200; i++ {
|
||||
zone := time.FixedZone("random", (rng.Intn(97)-48)*1800)
|
||||
samples = append(samples, sample{
|
||||
label: "random",
|
||||
date: time.Date(
|
||||
rng.Intn(8000)-2000, time.Month(1+rng.Intn(12)), 1+rng.Intn(28),
|
||||
rng.Intn(24), rng.Intn(60), rng.Intn(60), rng.Intn(1000000000), zone,
|
||||
),
|
||||
lon: rng.Float64()*360 - 180,
|
||||
lat: rng.Float64()*179.8 - 89.9,
|
||||
})
|
||||
}
|
||||
|
||||
for _, s := range samples {
|
||||
jd := basic.Date2JDE(s.date.UTC())
|
||||
altitude, azimuth, hourAngle := lite.HorizontalCoordinates(ApparentRa(s.date, s.lon, s.lat), ApparentDec(s.date, s.lon, s.lat), jd, s.lon, s.lat)
|
||||
checks := []struct {
|
||||
name string
|
||||
got float64
|
||||
want float64
|
||||
}{
|
||||
{"HourAngle", HourAngle(s.date, s.lon, s.lat), hourAngle},
|
||||
{"Azimuth", Azimuth(s.date, s.lon, s.lat), azimuth},
|
||||
{"Altitude", Altitude(s.date, s.lon, s.lat), altitude},
|
||||
{"Zenith", Zenith(s.date, s.lon, s.lat), 90 - altitude},
|
||||
}
|
||||
for _, check := range checks {
|
||||
if check.got != check.want {
|
||||
t.Fatalf("%s %s %s: got %.17g want %.17g", check.name, s.label, s.date.Format(time.RFC3339Nano), check.got, check.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package moon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/basic"
|
||||
lite "b612.me/astro/lite/internal"
|
||||
)
|
||||
|
||||
var benchmarkHorizontalSink float64
|
||||
|
||||
func benchmarkHorizontalInputs() (time.Time, float64, float64) {
|
||||
return time.Date(2026, 1, 1, 0, 0, 0, 0, time.FixedZone("CST", 8*3600)), 121.4737, 31.2304
|
||||
}
|
||||
|
||||
func BenchmarkMoonHourAngle(b *testing.B) {
|
||||
date, lon, lat := benchmarkHorizontalInputs()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
sink := 0.0
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = HourAngle(date, lon, lat)
|
||||
}
|
||||
benchmarkHorizontalSink = sink
|
||||
}
|
||||
|
||||
func BenchmarkMoonAzimuth(b *testing.B) {
|
||||
date, lon, lat := benchmarkHorizontalInputs()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
sink := 0.0
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = Azimuth(date, lon, lat)
|
||||
}
|
||||
benchmarkHorizontalSink = sink
|
||||
}
|
||||
|
||||
func BenchmarkMoonAltitude(b *testing.B) {
|
||||
date, lon, lat := benchmarkHorizontalInputs()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
sink := 0.0
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = Altitude(date, lon, lat)
|
||||
}
|
||||
benchmarkHorizontalSink = sink
|
||||
}
|
||||
|
||||
func BenchmarkMoonZenith(b *testing.B) {
|
||||
date, lon, lat := benchmarkHorizontalInputs()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
sink := 0.0
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = Zenith(date, lon, lat)
|
||||
}
|
||||
benchmarkHorizontalSink = sink
|
||||
}
|
||||
|
||||
func BenchmarkMoonHorizontalLegacy(b *testing.B) {
|
||||
date, lon, lat := benchmarkHorizontalInputs()
|
||||
jd := basic.Date2JDE(date.UTC())
|
||||
entries := []struct {
|
||||
name string
|
||||
pick func(altitude, azimuth, hourAngle float64) float64
|
||||
}{
|
||||
{"HourAngle", func(_, _, hourAngle float64) float64 { return hourAngle }},
|
||||
{"Azimuth", func(_, azimuth, _ float64) float64 { return azimuth }},
|
||||
{"Altitude", func(altitude, _, _ float64) float64 { return altitude }},
|
||||
{"Zenith", func(altitude, _, _ float64) float64 { return 90 - altitude }},
|
||||
}
|
||||
for _, entry := range entries {
|
||||
b.Run(entry.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
sink := 0.0
|
||||
for i := 0; i < b.N; i++ {
|
||||
sink = entry.pick(lite.HorizontalCoordinates(ApparentRa(date, lon, lat), ApparentDec(date, lon, lat), jd, lon, lat))
|
||||
}
|
||||
benchmarkHorizontalSink = sink
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// Package sun 轻量太阳链路:简化的太阳真黄经与视黄经、轻量赤道坐标和升落搜索,角度单位为度。
|
||||
// Package sun is the lightweight solar chain: simplified true and apparent solar longitude, light equatorial conversion and rise/set search; angles are degrees.
|
||||
package sun
|
||||
Reference in New Issue
Block a user