2 Commits

Author SHA1 Message Date
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00
b612 1f31a9b5b5 docs: 修正README中月球公式计算口径表述笔误 2026-09-17 21:33:28 +08:00
503 changed files with 33290 additions and 9471 deletions
+81 -2411
View File
File diff suppressed because it is too large Load Diff
+72 -2384
View File
File diff suppressed because it is too large Load Diff
+79 -2
View File
@@ -1,17 +1,94 @@
// Package astro 进程级 ΔT 模型的读取、替换与恢复默认:DeltaT、SetDeltaT、DefaultDeltaT。 // Package astro 提供进程级时标配置与时标换算 / process-wide time-scale configuration and conversions.
// Package astro exposes the process-wide DeltaT model: read the current function, install a custom one, or restore the built-in default.
package astro package astro
import "b612.me/astro/basic" import "b612.me/astro/basic"
// DeltaT 返回当前的 TT−UT1(ΔT)模型 / the active TT−UT1 (DeltaT) model.
func DeltaT() func(float64, bool) float64 { func DeltaT() func(float64, bool) float64 {
return basic.GetDeltaTFn() return basic.GetDeltaTFn()
} }
// SetDeltaT 替换 ΔT 模型(入参为数值与“该数值是否为儒略日”,为假时是十进制年),传 nil 恢复默认 / replaces the DeltaT model; nil restores the default.
func SetDeltaT(deltaT func(float64, bool) float64) { func SetDeltaT(deltaT func(float64, bool) float64) {
basic.SetDeltaTFn(deltaT) basic.SetDeltaTFn(deltaT)
} }
// DefaultDeltaT 返回内置默认 ΔT 模型 / the built-in default DeltaT model.
func DefaultDeltaT() func(float64, bool) float64 { func DefaultDeltaT() func(float64, bool) float64 {
return basic.DefaultDeltaTv2 return basic.DefaultDeltaTv2
} }
// TTMinusUTC 返回当前 TT−UTC 覆盖函数,未设置时为 nil / current TT−UTC override, or nil.
func TTMinusUTC() func(float64) float64 {
return basic.GetTTMinusUTCFn()
}
// SetTTMinusUTC 替换 TT−UTC 模型,nil 恢复内置表与政策 / overrides TT−UTC; nil restores the built-in table and policy.
func SetTTMinusUTC(ttMinusUTC func(float64) float64) {
basic.SetTTMinusUTCFn(ttMinusUTC)
}
// DefaultTTMinusUTC 返回内置闰秒表模型,忽略覆盖与未来政策 / built-in leap-table model, ignoring overrides and future policy.
func DefaultTTMinusUTC() func(float64) float64 {
return basic.TTMinusUTCSecondsDefault
}
// TimeScaleFuturePolicy 民用时标换算政策,与输出用的 TimeScale 无关 / civil-time conversion policy, separate from output TimeScale.
type TimeScaleFuturePolicy = basic.TimeScaleFuturePolicy
const (
// TimeScaleLeapSecond 默认按 ΔT 越限施加整数秒校正,不代表闰秒公告 / default integer-second corrections driven by extrapolated ΔT, not announcements.
TimeScaleLeapSecond = basic.TimeScaleLeapSecond
// TimeScaleAssumeUT1Tracking 窗口外固定末端 DUT1,平滑跟随 UT1 / holds the last observed DUT1 beyond the window.
TimeScaleAssumeUT1Tracking = basic.TimeScaleAssumeUT1Tracking
// TimeScaleFreezeUTCOffset 假设 UTC 偏移冻结在精确窗口末端 / freezes the UTC offset at the window end.
TimeScaleFreezeUTCOffset = basic.TimeScaleFreezeUTCOffset
// TimeScaleLeapHour 按 ΔT 越限施加整小时校正,仅作情景演算 / applies hour-sized corrections as a scenario assumption.
TimeScaleLeapHour = basic.TimeScaleLeapHour
// TimeScaleUT1Civil 全时轴民用时标等同 UT1,TT−UTC 覆盖仍优先 / uses UT1 as civil time everywhere unless TT−UTC is overridden.
TimeScaleUT1Civil = basic.TimeScaleUT1Civil
)
// SetTimeScaleFuturePolicy 设置与 basic 共享的民用时标换算政策 / sets the civil-time conversion policy shared with basic.
func SetTimeScaleFuturePolicy(policy TimeScaleFuturePolicy) {
basic.SetTimeScaleFuturePolicy(policy)
}
// GetTimeScaleFuturePolicy 返回当前民用时标换算政策 / current civil-time conversion policy.
func GetTimeScaleFuturePolicy() TimeScaleFuturePolicy {
return basic.GetTimeScaleFuturePolicy()
}
// DeltaTModel 标识可选的已发布 ΔT 模型 / a selectable published ΔT model.
type DeltaTModel = basic.DeltaTModel
const (
// DeltaTModelDefault 内置默认:SMH2016 + Morrison 2021 / the built-in SMH2016 + Morrison 2021 model.
DeltaTModelDefault = basic.DeltaTModelDefault
// DeltaTModelSMH2016 显式选择内置模型 / selects the built-in model explicitly.
DeltaTModelSMH2016 = basic.DeltaTModelSMH2016
// DeltaTModelMS2004 Morrison & Stephenson 2004 长期抛物线 / the M&S2004 long-term parabola.
DeltaTModelMS2004 = basic.DeltaTModelMS2004
// DeltaTModelEspenakMeeus2006 Espenak & Meeus 2006 分段多项式 / the Espenak & Meeus 2006 piecewise polynomial.
DeltaTModelEspenakMeeus2006 = basic.DeltaTModelEspenakMeeus2006
// DeltaTModelNASACanon2006 上式再加配对修正 c,即 NASA 五千年目录的印刷口径 / plus the pairing term c, the NASA canon convention.
DeltaTModelNASACanon2006 = basic.DeltaTModelNASACanon2006
// DeltaTModelManual 表示当前生效的是 SetDeltaT 注入的任意函数 / an arbitrary function injected with SetDeltaT.
DeltaTModelManual = basic.DeltaTModelManual
)
// SetDeltaTModel 安装命名 ΔT 模型;keepObserved 为真时逐月实测段优先(推荐),未知模型返回 false 且不改动现状。
// SetDeltaTModel installs a named ΔT model; with keepObserved the monthly observed span wins wherever it covers the instant.
func SetDeltaTModel(model DeltaTModel, keepObserved bool) bool {
return basic.SetDeltaTModel(model, keepObserved)
}
// GetDeltaTModel 返回当前生效的模型与是否保留实测段 / the active model and whether the observed span is kept.
func GetDeltaTModel() (DeltaTModel, bool) {
return basic.GetDeltaTModel()
}
// DeltaTModelSeconds 按命名模型求 ΔT(秒,入参为 UT 儒略日),不改变进程状态,便于对照 / evaluates a named model without touching process state.
func DeltaTModelSeconds(model DeltaTModel, jd float64, keepObserved bool) float64 {
return basic.DeltaTModelSeconds(model, jd, keepObserved)
}
+69
View File
@@ -0,0 +1,69 @@
package astro
import (
"math"
"testing"
"time"
"b612.me/astro/basic"
)
// time.Time 往返经 JD 舍入,容差为 0.2 ms。
func TestBarycentricTimeFacade(t *testing.T) {
tt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
if got := TCGMinusTT(tt); math.Abs(got-1.0777) > 5e-3 {
t.Errorf("TCG−TT=%v 秒, want ≈1.078", got)
}
if got := TCBMinusTT(tt); math.Abs(got-23.9757) > 5e-3 {
t.Errorf("TCB−TT=%v 秒, want ≈23.976", got)
}
if got := TDBMinusTT(tt); math.Abs(got) > 2e-3 {
t.Errorf("TDB−TT=%v 秒, want |·| ≤ 1.7 ms", got)
}
for _, tc := range []struct {
name string
fwd func(time.Time) time.Time
back func(time.Time) time.Time
}{
{"TCG", TCGFromTT, TTFromTCG},
{"TCB", TCBFromTT, TTFromTCB},
{"TDB", TDBFromTT, TTFromTDB},
} {
round := tc.back(tc.fwd(tt))
if delta := math.Abs(round.Sub(tt).Seconds()); delta > 2e-4 {
t.Errorf("%s 往返差 %.6f 秒", tc.name, delta)
}
if forward := tc.fwd(tt); !forward.After(tt.Add(-time.Second)) || !forward.Before(tt.Add(time.Minute)) {
t.Errorf("%s 结果 %v 离开合理区间", tc.name, forward)
}
}
tcb := TCBFromTT(tt)
if diff := tcb.Sub(TDBFromTCB(tcb)).Seconds(); math.Abs(diff-23.9757) > 0.1 {
t.Errorf("TCB−TDB=%v 秒, want ≈23.976", diff)
}
if delta := math.Abs(TTFromTCB(tcb).Sub(tt).Seconds()); delta > 2e-4 {
t.Errorf("TT→TCB 往返差 %.6f 秒", delta)
}
}
func TestBarycentricTimeFacadePreservesScaleAndInstant(t *testing.T) {
tt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for _, date := range []time.Time{tt, tt.In(time.FixedZone("UTC+08", 8*3600))} {
jd := basic.Date2JD(date.UTC())
for name, pair := range map[string][2]float64{
"TCG": {TCGMinusTT(date), basic.TCGMinusTTSeconds(jd)},
"TCB": {TCBMinusTT(date), basic.TCBMinusTTSeconds(jd)},
"TDB": {TDBMinusTT(date), basic.TDBMinusTTSeconds(jd)},
} {
if pair[0] != pair[1] {
t.Errorf("%s facade=%v, basic=%v", name, pair[0], pair[1])
}
}
if direct, composed := TDBFromTT(date), TDBFromTCB(TCBFromTT(date)); math.Abs(direct.Sub(composed).Seconds()) > 1e-4 {
t.Errorf("direct TDB=%v, composed=%v", direct, composed)
}
if got := TCGFromTT(date); got.Location() != time.UTC || !got.Equal(TCGFromTT(tt)) {
t.Errorf("TCG depends on input Location: %v", got)
}
}
}
+3 -3
View File
@@ -9,11 +9,11 @@ import (
const ancientStationTolerance = 10.0 / 1440.0 const ancientStationTolerance = 10.0 / 1440.0
func ancientStationTT(year int, month time.Month, day int) float64 { func ancientStationTT(year int, month time.Month, day int) float64 {
return TD2UT(Date2JDE(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)), true) return UTC2TT(Date2JD(time.Date(year, month, day, 0, 0, 0, 0, time.UTC)))
} }
func ancientStationUT(year int, month time.Month, day, hour, minute int) float64 { func ancientStationUT(year int, month time.Month, day, hour, minute int) float64 {
return Date2JDE(time.Date(year, month, day, hour, minute, 0, 0, time.UTC)) return Date2JD(time.Date(year, month, day, hour, minute, 0, 0, time.UTC))
} }
func assertAncientNextStation(t *testing.T, queryTT, gotUT, wantUT float64) { func assertAncientNextStation(t *testing.T, queryTT, gotUT, wantUT float64) {
@@ -23,7 +23,7 @@ func assertAncientNextStation(t *testing.T, queryTT, gotUT, wantUT float64) {
} }
if diff := math.Abs(gotUT - wantUT); diff > ancientStationTolerance { if diff := math.Abs(gotUT - wantUT); diff > ancientStationTolerance {
t.Fatalf("station differs by %.3f minutes: got %s, want %s", diff*1440, t.Fatalf("station differs by %.3f minutes: got %s, want %s", diff*1440,
JDE2DateByZone(gotUT, time.UTC, false), JDE2DateByZone(wantUT, time.UTC, false)) JD2DateByZone(gotUT, time.UTC, false), JD2DateByZone(wantUT, time.UTC, false))
} }
} }
+9 -9
View File
@@ -32,8 +32,8 @@ const (
// ApsisEvent 轨道极值事件 / orbital distance extremum event. // ApsisEvent 轨道极值事件 / orbital distance extremum event.
type ApsisEvent struct { type ApsisEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day. // JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64 JD float64
// Distance 是极值距离;地球相关事件单位 AU,月球相关事件单位 km / extremum distance. // Distance 是极值距离;地球相关事件单位 AU,月球相关事件单位 km / extremum distance.
Distance float64 Distance float64
} }
@@ -79,7 +79,7 @@ func earthApsis(year int, aphelion bool) ApsisEvent {
} }
eventTT, distanceAU := refineDistanceExtremum(seedTT, cfg, EarthAway) eventTT, distanceAU := refineDistanceExtremum(seedTT, cfg, EarthAway)
return ApsisEvent{ return ApsisEvent{
JDE: TD2UT(eventTT, false), JD: TT2UTC(eventTT),
Distance: distanceAU, Distance: distanceAU,
} }
} }
@@ -87,8 +87,8 @@ func earthApsis(year int, aphelion bool) ApsisEvent {
func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent { func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0) endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true) startTT := UTC2TT(Date2JD(startUTC))
endTT := TD2UT(Date2JDE(endUTC), true) endTT := UTC2TT(Date2JD(endUTC))
kStart := int(math.Floor((startTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) - 1 kStart := int(math.Floor((startTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) - 1
kEnd := int(math.Ceil((endTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) + 1 kEnd := int(math.Ceil((endTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) + 1
@@ -110,19 +110,19 @@ func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
for k := kStart; k <= kEnd; k++ { for k := kStart; k <= kEnd; k++ {
seedTT := moonApsisSeedTT(float64(k) + phase) seedTT := moonApsisSeedTT(float64(k) + phase)
eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, HMoonAway) eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, HMoonAway)
eventUT := TD2UT(eventTT, false) eventUT := TT2UTC(eventTT)
eventTimeUTC := JDE2DateByZone(eventUT, time.UTC, false) eventTimeUTC := JD2DateByZone(eventUT, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) { if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue continue
} }
events = append(events, ApsisEvent{ events = append(events, ApsisEvent{
JDE: eventUT, JD: eventUT,
Distance: distanceKM, Distance: distanceKM,
}) })
} }
sort.Slice(events, func(i, j int) bool { sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE return events[i].JD < events[j].JD
}) })
return events return events
} }
+2 -2
View File
@@ -62,7 +62,7 @@ func TestEarthApsisMatchesHorizonsBaseline(t *testing.T) {
t.Fatalf("unknown earth apsis kind %q", sample.Kind) t.Fatalf("unknown earth apsis kind %q", sample.Kind)
} }
gotTime := JDE2DateByZone(got.JDE, time.UTC, false) gotTime := JD2DateByZone(got.JD, time.UTC, false)
timeDiff := gotTime.Sub(wantTime) timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 { if timeDiff < 0 {
timeDiff = -timeDiff timeDiff = -timeDiff
@@ -139,7 +139,7 @@ func TestMoonApsisMatchesHorizonsBaseline(t *testing.T) {
t.Fatalf("unknown moon apsis kind %q", sample.Kind) t.Fatalf("unknown moon apsis kind %q", sample.Kind)
} }
gotTime := JDE2DateByZone(got.JDE, time.UTC, false) gotTime := JD2DateByZone(got.JD, time.UTC, false)
timeDiff := gotTime.Sub(wantTime) timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 { if timeDiff < 0 {
timeDiff = -timeDiff timeDiff = -timeDiff
+137
View File
@@ -0,0 +1,137 @@
package basic
import "math"
const (
// 线性时标关系的参考历元,不表示各时标读数在此相等。
barycentricT0JDE = 2443144.5003725
lgRate = 6.969290134e-10
lbRate = 1.550519768e-8
tdb0Seconds = -65.5e-6
)
// tdbPeriodicGroups 按 t 的幂次分组的 TDB−TT 周期项(振幅秒、频率 rad/儒略千年、相位 rad)。
var tdbPeriodicGroups = [5][][3]float64{
{
{0.001656674564, 6283.075849991, 6.240054195},
{2.2417471e-05, 5753.384884897, 4.296977442},
{1.3839792e-05, 12566.151699983, 6.19690441},
{4.770086e-06, 529.690965095, 0.444401603},
{4.67674e-06, 6069.776754553, 4.021195093},
{2.256707e-06, 213.299095438, 5.543113262},
{1.694205e-06, -3.523118349, 5.025132748},
{1.554905e-06, 77713.77146792, 5.19846709},
{1.276839e-06, 7860.419392439, 5.988822341},
{1.193379e-06, 5223.693919802, 3.64982373},
{1.115322e-06, 3930.20969622, 1.422745069},
{7.94185e-07, 11506.769769794, 2.322313077},
{4.47061e-07, 26.2983198, 3.615796498},
{4.35206e-07, -398.149003408, 4.349338347},
{6.00309e-07, 1577.343542448, 2.678271909},
{4.96817e-07, 6208.294251424, 5.696701824},
{4.86306e-07, 5884.926846583, 0.520007179},
{4.32392e-07, 74.781598567, 2.435898309},
{4.68597e-07, 6244.942814354, 5.866398759},
{3.7551e-07, 5507.553238667, 4.103476804},
{2.43085e-07, -775.522611324, 3.651837925},
{1.73435e-07, 18849.227549974, 6.153743485},
{2.30685e-07, 5856.477659115, 4.773852582},
{2.03747e-07, 12036.460734888, 4.333987818},
},
{
{0.000102156724, 6283.075849991, 4.249032005},
{1.706807e-06, 12566.151699983, 4.205904248},
{2.69668e-07, 213.299095438, 3.400290479},
{2.65919e-07, 529.690965095, 5.836047367},
{2.10568e-07, -3.523118349, 6.262738348},
{7.7996e-08, 5223.693919802, 4.670344204},
{5.4764e-08, 1577.343542448, 4.53480017},
{5.9146e-08, 26.2983198, 1.083044735},
},
{
{4.32299e-06, 6283.075849991, 2.642893748},
{4.06495e-07, 0.0, 4.71238898},
{1.22605e-07, 12566.151699983, 2.438140634},
{1.9476e-08, 213.299095438, 1.642186981},
},
{
{1.43388e-07, 6283.075849991, 1.131453581},
{6.671e-09, 12566.151699983, 0.775148887},
},
{
{3.826e-09, 6283.075849991, 5.705257275},
{3.03e-10, 12566.151699983, 5.407132842},
},
}
// 地心近似,不包含观测者位置引起的日周项。
func tdbPeriodicSeconds(jd float64) float64 {
t := (jd - 2451545.0) / 365250.0
sum := 0.0
for i := len(tdbPeriodicGroups) - 1; i >= 0; i-- {
group := 0.0
for _, term := range tdbPeriodicGroups[i] {
group += term[0] * math.Sin(term[1]*t+term[2])
}
sum = sum*t + group
}
return sum + 0.00065e-6*math.Sin(6069.776754*t+4.021194) +
0.00033e-6*math.Sin(213.299095*t+5.543132) -
0.00196e-6*math.Sin(6208.294251*t+5.696701) -
0.00173e-6*math.Sin(74.781599*t+2.435900) +
0.03638e-6*t*t
}
// TCGMinusTTSeconds 返回 TT 时刻的 TCG−TT(秒)/ TCG−TT in seconds at a TT instant.
func TCGMinusTTSeconds(jd float64) float64 {
return lgRate / (1 - lgRate) * (jd - barycentricT0JDE) * 86400
}
// TCBMinusTTSeconds 返回 TT 时刻的地心 TCB−TT 近似值(秒)/ geocentric TCB−TT approximation in seconds at a TT instant.
func TCBMinusTTSeconds(jd float64) float64 {
return (lbRate*(jd-barycentricT0JDE)*86400 + tdbPeriodicSeconds(jd) - tdb0Seconds) / (1 - lbRate)
}
// TDBMinusTTSeconds 返回 TT 时刻的地心 TDB−TT 近似值(秒)/ geocentric TDB−TT approximation in seconds at a TT instant.
func TDBMinusTTSeconds(jd float64) float64 { return tdbPeriodicSeconds(jd) }
// TT2TCG 地球时转地心坐标时 / converts TT to TCG.
func TT2TCG(ttJDE float64) float64 { return ttJDE + TCGMinusTTSeconds(ttJDE)/86400 }
// TCG2TT 地心坐标时转地球时 / converts TCG to TT.
func TCG2TT(tcgJDE float64) float64 { return tcgJDE - lgRate*(tcgJDE-barycentricT0JDE) }
// TT2TCB 地球时转太阳系质心坐标时,采用地心近似 / converts TT to TCB using a geocentric approximation.
func TT2TCB(ttJDE float64) float64 { return ttJDE + TCBMinusTTSeconds(ttJDE)/86400 }
// TCB2TT 是 TT2TCB 的逆 / inverts TT2TCB.
func TCB2TT(tcbJDE float64) float64 {
// TDB 与 TT 相差不超过 2 ms,拿 TDB 读数当初值可少迭代两轮。
tt := TCB2TDB(tcbJDE)
for i := 0; i < 3; i++ {
tt = tcbJDE - TCBMinusTTSeconds(tt)/86400
}
return tt
}
// TT2TDB 地球时转太阳系质心力学时,采用地心近似 / converts TT to TDB using a geocentric approximation.
func TT2TDB(ttJDE float64) float64 { return ttJDE + TDBMinusTTSeconds(ttJDE)/86400 }
// TDB2TT 是 TT2TDB 的逆 / inverts TT2TDB.
func TDB2TT(tdbJDE float64) float64 {
tt := tdbJDE - TDBMinusTTSeconds(tdbJDE)/86400
for i := 0; i < 3; i++ {
tt = tdbJDE - TDBMinusTTSeconds(tt)/86400
}
return tt
}
// TCB2TDB 太阳系质心坐标时转质心力学时 / converts TCB to TDB.
func TCB2TDB(tcbJDE float64) float64 {
return tcbJDE + (tdb0Seconds/86400 - lbRate*(tcbJDE-barycentricT0JDE))
}
// TDB2TCB 太阳系质心力学时转质心坐标时 / converts TDB to TCB.
func TDB2TCB(tdbJDE float64) float64 {
return tdbJDE + (lbRate*(tdbJDE-barycentricT0JDE)-tdb0Seconds/86400)/(1-lbRate)
}
+128
View File
@@ -0,0 +1,128 @@
package basic
import (
"math"
"testing"
)
func TestBarycentricScaleDefinitions(t *testing.T) {
for _, jd := range []float64{JDCalc(-3000, 1, 1), barycentricT0JDE, JDCalc(2026, 1, 1), JDCalc(6000, 1, 1)} {
tcgOffset := TCGMinusTTSeconds(jd)
if diff := tcgOffset*(1-6.969290134e-10) - 6.969290134e-10*(jd-2443144.5003725)*86400; math.Abs(diff) > 1e-12 {
t.Errorf("jd=%v TCG defining relation residual=%g s", jd, diff)
}
tcbOffset := TCBMinusTTSeconds(jd)
if diff := tcbOffset*(1-1.550519768e-8) - 1.550519768e-8*(jd-2443144.5003725)*86400 - TDBMinusTTSeconds(jd) - 65.5e-6; math.Abs(diff) > 1e-12 {
t.Errorf("jd=%v TCB defining relation residual=%g s", jd, diff)
}
}
if got := TCGMinusTTSeconds(barycentricT0JDE); got != 0 {
t.Errorf("TCG-TT at T0=%v, want 0", got)
}
if got, want := TCB2TDB(barycentricT0JDE), barycentricT0JDE-65.5e-6/86400; got != want {
t.Errorf("TDB at TCB T0=%.12f, want %.12f", got, want)
}
}
func TestBarycentricLinearConversionAnchors(t *testing.T) {
for _, tc := range []struct {
name string
fn func(float64) float64
jd float64
want float64
}{
{"TCB2TDB", TCB2TDB, 2453750.5 + 0.893019599, 2453750.5 + 0.8928551362746343397},
{"TDB2TCB", TDB2TCB, 2453750.5 + 0.892855137, 2453750.5 + 0.8930195997253656716},
{"TCG2TT", TCG2TT, 2453750.5 + 0.892862531, 2453750.5 + 0.8928551387488816828},
{"TT2TCG", TT2TCG, 2453750.5 + 0.892482639, 2453750.5 + 0.8924900312508587113},
} {
ulp := math.Nextafter(tc.want, math.Inf(1)) - tc.want
if got := tc.fn(tc.jd); math.Abs(got-tc.want) > ulp {
t.Errorf("%s=%.12f, want %.12f", tc.name, got, tc.want)
}
}
}
func TestBarycentricPeriodicTermsMatchFullSeries(t *testing.T) {
for _, tc := range []struct {
jd float64
want float64
}{
{625307.5, 0.001558488628712037},
{766574.5, -0.000924800103328196},
{1721057.5, 0.0008624527814561311},
{2433282.5, -7.069829559472634e-05},
{2443144.5, -6.551401857064118e-05},
{2451544.5, -0.0001137630988927298},
{2461041.5, -8.20152430051247e-05},
{2461222.5, 0.0001186656922342686},
{2469807.5, -8.01882947792431e-05},
{3182029.5, -0.0009618304620565844},
{3912514.5, -0.00138271667055309},
} {
if got := TDBMinusTTSeconds(tc.jd); math.Abs(got-tc.want) > 2.1e-6 {
t.Errorf("jd=%v TDB-TT=%.9f us, want %.9f us", tc.jd, got*1e6, tc.want*1e6)
}
}
}
func TestBarycentricScaleRoundTrips(t *testing.T) {
for _, jd := range []float64{
JDCalc(-3000, 1, 1), JDCalc(0, 1, 1), JDCalc(1900, 1, 1), JDCalc(1950, 6, 1),
barycentricT0JDE, JDCalc(2000, 1, 1), JDCalc(2026, 4, 1), JDCalc(2050, 7, 1),
JDCalc(2100, 1, 1), JDCalc(6000, 1, 1),
} {
ulp := math.Nextafter(jd, math.Inf(1)) - jd
for _, tc := range []struct {
name string
fwd func(float64) float64
back func(float64) float64
}{
{"TT-TCG", TT2TCG, TCG2TT},
{"TT-TCB", TT2TCB, TCB2TT},
{"TT-TDB", TT2TDB, TDB2TT},
{"TCB-TDB", TCB2TDB, TDB2TCB},
} {
if got := tc.back(tc.fwd(jd)); math.Abs(got-jd) > ulp {
t.Errorf("jd=%v %s round-trip error=%g s", jd, tc.name, (got-jd)*86400)
}
if got := tc.fwd(tc.back(jd)); math.Abs(got-jd) > ulp {
t.Errorf("jd=%v %s inverse round-trip error=%g s", jd, tc.name, (got-jd)*86400)
}
}
// 组合路径分别舍入,允许两次 JD 舍入误差。
if diff := TCB2TDB(TT2TCB(jd)) - TT2TDB(jd); math.Abs(diff) > 2*ulp {
t.Errorf("jd=%v TT-TCB-TDB differs from TT-TDB by %g s", jd, diff*86400)
}
if diff := (TT2TDB(jd)-jd)*86400 - TDBMinusTTSeconds(jd); math.Abs(diff) > ulp*86400 {
t.Errorf("jd=%v TDB seconds and JD conversions differ by %g s", jd, diff)
}
}
}
func TestBarycentricScaleMagnitudes(t *testing.T) {
jd := JDCalc(2026, 1, 1)
if got := TCGMinusTTSeconds(jd); math.Abs(got-1.0777) > 5e-3 {
t.Errorf("TCG-TT=%v s, want about 1.078", got)
}
if got := TCBMinusTTSeconds(jd); math.Abs(got-23.9757) > 5e-3 {
t.Errorf("TCB-TT=%v s, want about 23.976", got)
}
if got := TCGMinusTTSeconds(barycentricT0JDE+365.25) - TCGMinusTTSeconds(barycentricT0JDE); math.Abs(got-0.02204) > 5e-5 {
t.Errorf("TCG-TT annual increase=%v s, want about 0.02204", got)
}
if got := TCBMinusTTSeconds(barycentricT0JDE+365.25) - TCBMinusTTSeconds(barycentricT0JDE); math.Abs(got-0.48934) > 5e-4 {
t.Errorf("TCB-TT annual increase=%v s, want about 0.48934", got)
}
if got := TDBMinusTTSeconds(JDCalc(2100, 1, 1)) - TDBMinusTTSeconds(JDCalc(2000, 1, 1)); math.Abs(got) > 2e-4 {
t.Errorf("TDB-TT century difference=%v s, want within 0.2 ms", got)
}
min, max := math.Inf(1), math.Inf(-1)
for month := 1; month <= 12; month++ {
value := TDBMinusTTSeconds(JDCalc(2026, month, 1))
min, max = math.Min(min, value), math.Max(max, value)
}
if amplitude := max - min; amplitude < 3.0e-3 || amplitude > 3.6e-3 {
t.Errorf("TDB-TT annual range=%v s, want 3.0-3.6 ms", amplitude)
}
}
+2 -2
View File
@@ -26,7 +26,7 @@ func TestGetJQTime(t *testing.T) {
month -= 12 month -= 12
} }
jd := JDECalc(year, int(month), float64(day)) jd := JDCalc(year, int(month), float64(day))
if angle == 0 { if angle == 0 {
angle = 360 angle = 360
} }
@@ -43,7 +43,7 @@ func TestGetJQTime(t *testing.T) {
jd -= 0.001 jd -= 0.001
} }
jd += 0.001 jd += 0.001
return TD2UT(jd, false) return TT2UTC(jd)
} }
testCases := []struct { testCases := []struct {
+1 -1
View File
@@ -65,7 +65,7 @@ func TestConstellationNameLookups(t *testing.T) {
} }
func TestConstellationNameEN(t *testing.T) { func TestConstellationNameEN(t *testing.T) {
jde := Date2JDE(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) jde := Date2JD(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
if en := ConstellationNameEN(88.792939, 7.407064, jde); en != "Orion" { if en := ConstellationNameEN(88.792939, 7.407064, jde); en != "Orion" {
t.Fatalf("ConstellationNameEN() = %q, want %q", en, "Orion") t.Fatalf("ConstellationNameEN() = %q, want %q", en, "Orion")
} }
+3 -3
View File
@@ -5,7 +5,7 @@ import (
) )
func TestConstellationNameZH(t *testing.T) { func TestConstellationNameZH(t *testing.T) {
now := GetNowJDE() now := GetNowJD()
//finish on 30s //finish on 30s
for i := 0.00; i <= 360.00; i += 0.5 { for i := 0.00; i <= 360.00; i += 0.5 {
for j := -90.00; j <= 90.00; j += 0.5 { for j := -90.00; j <= 90.00; j += 0.5 {
@@ -15,9 +15,9 @@ func TestConstellationNameZH(t *testing.T) {
} }
func BenchmarkConstellationNameZH(b *testing.B) { func BenchmarkConstellationNameZH(b *testing.B) {
jde := GetNowJDE() jde := GetNowJD()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
//GetNowJDE() //GetNowJD()
ConstellationNameZH(11.11, 12.12, jde) ConstellationNameZH(11.11, 12.12, jde)
} }
+24 -26
View File
@@ -86,7 +86,7 @@ func psini(lat, h float64) float64 {
} }
func TopocentricRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) { func TopocentricRaDec(ra, dec, lat, lon, jd, au, h float64) (float64, float64) {
return topocentricRaDecWithSidereal(ra, dec, lat, lon, ApparentSiderealTime(jd)*15, au, h) return topocentricRaDecWithSidereal(ra, dec, lat, lon, ApparentSiderealTime(UTC2UT1(jd))*15, au, h)
} }
func topocentricRaDecWithSidereal(ra, dec, lat, lon, siderealDegrees, au, h float64) (float64, float64) { func topocentricRaDecWithSidereal(ra, dec, lat, lon, siderealDegrees, au, h float64) (float64, float64) {
@@ -109,43 +109,41 @@ func TopocentricDec(ra, dec, lat, lon, jd, au, h float64) float64 { //jd为格
return topocentricDec return topocentricDec
} }
func TopocentricLo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时 // TopocentricLoBo 一次求值给出站心黄经与黄纬 / topocentric ecliptic longitude and latitude in one solve.
c := pcosi(lat, h) //
s := psini(lat, h) // 先解站心赤道坐标再转黄道:黄道版公式的分母在黄经 90°–270° 时变号,直接 atan2 会切到对顶象限。
sinpi := Sin(0.0024427777777) / au func TopocentricLoBo(lo, bo, lat, lon, jde, au, h float64) (float64, float64) {
ra := LoToRa(jd, lo, bo) ra, dec := LoBoToRaDec(jde, lo, bo)
tH := Limit360(ApparentSiderealTime(jd)*15 + lon - ra) topRA, topDec := TopocentricRaDec(ra, dec, lat, lon, jde, au, h)
n := Cos(lo)*Cos(bo) - c*sinpi*Cos(tH) return RaDecToLoBo(jde, topRA, topDec)
nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(s*Sin(TrueObliquity(jd))+c*Cos(TrueObliquity(jd))*Sin(tH)), n) * 180 / math.Pi }
// TopocentricLo 站心黄经 / topocentric ecliptic longitude.
func TopocentricLo(lo, bo, lat, lon, jde, au, h float64) float64 {
nlo, _ := TopocentricLoBo(lo, bo, lat, lon, jde, au, h)
return nlo return nlo
} }
func TopocentricBo(lo, bo, lat, lon, jd, au, h float64) float64 { //jd为格林尼治标准时 // TopocentricBo 站心黄纬 / topocentric ecliptic latitude.
c := pcosi(lat, h) func TopocentricBo(lo, bo, lat, lon, jde, au, h float64) float64 {
s := psini(lat, h) _, nbo := TopocentricLoBo(lo, bo, lat, lon, jde, au, h)
sinpi := Sin(0.0024427777777) / au
ra := LoToRa(jd, lo, bo)
tH := Limit360(ApparentSiderealTime(jd)*15 + lon - ra)
n := Cos(lo)*Cos(bo) - c*sinpi*Cos(tH)
nlo := math.Atan2(Sin(lo)*Cos(bo)-sinpi*(s*Sin(TrueObliquity(jd))+c*Cos(TrueObliquity(jd))*Sin(tH)), n) * 180 / math.Pi
nbo := math.Atan2(Cos(nlo)*(Sin(bo)-sinpi*(s*Cos(TrueObliquity(jd))-c*Sin(TrueObliquity(jd))*Sin(tH))), n) * 180 / math.Pi
return nbo return nbo
} }
func GXCLo(lo, bo, jd float64) float64 { //光行差修正 func GXCLo(lo, bo, jde float64) float64 { //光行差修正
k := 20.49552 k := 20.49552
sunlo := SunTrueLo(jd) sunlo := SunTrueLo(jde)
e := Earthe(jd) e := Earthe(jde)
epi := EarthPI(jd) epi := EarthPI(jde)
tmp := (-k*Cos(sunlo-lo) + e*k*Cos(epi-lo)) / Cos(bo) tmp := (-k*Cos(sunlo-lo) + e*k*Cos(epi-lo)) / Cos(bo)
return tmp return tmp
} }
func GXCBo(lo, bo, jd float64) float64 { func GXCBo(lo, bo, jde float64) float64 {
k := 20.49552 k := 20.49552
sunlo := SunTrueLo(jd) sunlo := SunTrueLo(jde)
e := Earthe(jd) e := Earthe(jde)
epi := EarthPI(jd) epi := EarthPI(jde)
tmp := -k * Sin(bo) * (Sin(sunlo-lo) - e*Sin(epi-lo)) tmp := -k * Sin(bo) * (Sin(sunlo-lo) - e*Sin(epi-lo))
return tmp return tmp
} }
+10 -9
View File
@@ -9,27 +9,28 @@ import (
) )
func TestTopocentricRaDecUsesUTJulianDateForSiderealTime(t *testing.T) { func TestTopocentricRaDecUsesUTJulianDateForSiderealTime(t *testing.T) {
ut := Date2JDE(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC)) ut := Date2JD(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC))
ra := 189.527817246 ra := 189.527817246
dec := -5.973400893 dec := -5.973400893
lat := 6.79657 lat := 6.79657
lon := 121.55381 lon := 121.55381
distanceAU := HMoonAwayN(TD2UT(ut, true), -1) / 149597870.7 distanceAU := HMoonAwayN(UTC2TT(ut), -1) / 149597870.7
gotRA, gotDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0) gotRA, gotDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
wantRA, wantDec := independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0) wantRA, wantDec := independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
if delta := angularDistanceArcsec(gotRA, gotDec, wantRA, wantDec); delta > 1e-6 { // 逐分量比较:零距离处 acos 度规的病态下限会把 1 ulp 放大成毫角秒。
t.Fatalf("TopocentricRaDec differs from independent formula by %.9f arcsec", delta) if deltaRA, deltaDec := math.Abs(gotRA-wantRA), math.Abs(gotDec-wantDec); deltaRA > 1e-12 || deltaDec > 1e-12 {
t.Fatalf("TopocentricRaDec differs from independent formula: dRA=%.3g dDec=%.3g deg", deltaRA, deltaDec)
} }
} }
func TestTopocentricRaAndDecMatchCombinedResult(t *testing.T) { func TestTopocentricRaAndDecMatchCombinedResult(t *testing.T) {
ut := Date2JDE(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC)) ut := Date2JD(time.Date(2025, 6, 5, 12, 2, 7, 700000000, time.UTC))
ra := 189.527817246 ra := 189.527817246
dec := -5.973400893 dec := -5.973400893
lat := 6.79657 lat := 6.79657
lon := 121.55381 lon := 121.55381
distanceAU := HMoonAwayN(TD2UT(ut, true), -1) / 149597870.7 distanceAU := HMoonAwayN(UTC2TT(ut), -1) / 149597870.7
wantRA, wantDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0) wantRA, wantDec := TopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, 0)
if got := TopocentricRa(ra, dec, lat, lon, ut, distanceAU, 0); got != wantRA { if got := TopocentricRa(ra, dec, lat, lon, ut, distanceAU, 0); got != wantRA {
@@ -41,11 +42,11 @@ func TestTopocentricRaAndDecMatchCombinedResult(t *testing.T) {
} }
func TestHMoonHeightUsesUTForTopocentricCorrection(t *testing.T) { func TestHMoonHeightUsesUTForTopocentricCorrection(t *testing.T) {
ut := Date2JDE(time.Date(2026, 4, 28, 16, 1, 30, 0, time.UTC)) ut := Date2JD(time.Date(2026, 4, 28, 16, 1, 30, 0, time.UTC))
longitude := 0.0 longitude := 0.0
latitude := 51.4779 latitude := 51.4779
ra, dec := HMoonApparentRaDecN(ut, longitude, latitude, 0, -1) ra, dec := HMoonApparentRaDecN(ut, longitude, latitude, 0, -1)
hourAngle := Limit360(ApparentSiderealTime(ut)*15 + longitude - ra) hourAngle := Limit360(ApparentSiderealTime(UTC2UT1(ut))*15 + longitude - ra)
want := ArcSin(Sin(latitude)*Sin(dec) + Cos(dec)*Cos(latitude)*Cos(hourAngle)) want := ArcSin(Sin(latitude)*Sin(dec) + Cos(dec)*Cos(latitude)*Cos(hourAngle))
got := HMoonHeightN(ut, longitude, latitude, 0, -1) got := HMoonHeightN(ut, longitude, latitude, 0, -1)
if difference := math.Abs(got - want); difference > 1e-10 { if difference := math.Abs(got - want); difference > 1e-10 {
@@ -62,7 +63,7 @@ func independentTopocentricRaDec(ra, dec, lat, lon, ut, distanceAU, height float
rhoCos := math.Cos(u) + height/6378140.0*Cos(lat) rhoCos := math.Cos(u) + height/6378140.0*Cos(lat)
rhoSin := polarRadiusKM/equatorialRadiusKM*math.Sin(u) + height/6378140.0*Sin(lat) rhoSin := polarRadiusKM/equatorialRadiusKM*math.Sin(u) + height/6378140.0*Sin(lat)
sinParallax := Sin(0.0024427777777) / distanceAU sinParallax := Sin(0.0024427777777) / distanceAU
hourAngle := Limit360(ApparentSiderealTime(ut)*15 + lon - ra) hourAngle := Limit360(ApparentSiderealTime(UTC2UT1(ut))*15 + lon - ra)
deltaRA := math.Atan2( deltaRA := math.Atan2(
-rhoCos*sinParallax*Sin(hourAngle), -rhoCos*sinParallax*Sin(hourAngle),
Cos(dec)-rhoCos*sinParallax*Cos(hourAngle), Cos(dec)-rhoCos*sinParallax*Cos(hourAngle),
+6 -6
View File
@@ -10,7 +10,7 @@ import (
// 调用方按各自约定补偿后,两者都必须落在同一个本地民用日,并接近当日本地日的真实中天。 // 调用方按各自约定补偿后,两者都必须落在同一个本地民用日,并接近当日本地日的真实中天。
// //
// 框架约定(由各自调用方固定下来): // 框架约定(由各自调用方固定下来):
// - SunHeight/HMoonHeight 的 jd 是本地民用时框架,本地民用日从 Date2JDE(本地 0 时) 起算; // - SunHeight/HMoonHeight 的 jd 是本地民用时框架,本地民用日从 Date2JD(本地 0 时) 起算;
// - basic.CulminationTime 的返回值是本地民用时框架,sun/sun.go 减 tz/24 得到 UT; // - basic.CulminationTime 的返回值是本地民用时框架,sun/sun.go 减 tz/24 得到 UT;
// - basic.MoonCulminationTime 的返回值就是本地民用时框架,moon/moon.go 按 byZone=true 使用。 // - basic.MoonCulminationTime 的返回值就是本地民用时框架,moon/moon.go 按 byZone=true 使用。
@@ -33,7 +33,7 @@ var culminationAnchorSites = []culminationAnchorSite{
func culminationAnchorLocalMidnight(site culminationAnchorSite, timestamp time.Time) (time.Time, float64) { func culminationAnchorLocalMidnight(site culminationAnchorSite, timestamp time.Time) (time.Time, float64) {
location := time.FixedZone("anchor", int(site.tz*3600)) location := time.FixedZone("anchor", int(site.tz*3600))
local := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, location) local := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, location)
return local, Date2JDE(local) return local, Date2JD(local)
} }
func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) { func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) {
@@ -42,7 +42,7 @@ func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) {
local, midnightJD := culminationAnchorLocalMidnight(site, timestamp) local, midnightJD := culminationAnchorLocalMidnight(site, timestamp)
location := local.Location() location := local.Location()
// sun/sun.go 的补偿:本地 0 时 JD 再加半天,使 floor 落在同一本地日;随后减 tz/24 得 UT。 // sun/sun.go 的补偿:本地 0 时 JD 再加半天,使 floor 落在同一本地日;随后减 tz/24 得 UT。
got := JDE2DateByZone(CulminationTime(midnightJD+0.5, site.lon, site.tz)-site.tz/24, location, false) got := JD2DateByZone(CulminationTime(midnightJD+0.5, site.lon, site.tz)-site.tz/24, location, false)
truthJD, truthAltitude := 0.0, -999.0 truthJD, truthAltitude := 0.0, -999.0
for minute := 0; minute <= 24*60; minute++ { for minute := 0; minute <= 24*60; minute++ {
jd := midnightJD + float64(minute)/1440.0 jd := midnightJD + float64(minute)/1440.0
@@ -50,7 +50,7 @@ func TestSunCulminationAnchorKeepsLocalDay(t *testing.T) {
truthAltitude, truthJD = altitude, jd truthAltitude, truthJD = altitude, jd
} }
} }
truth := JDE2DateByZone(truthJD-site.tz/24, location, false) truth := JD2DateByZone(truthJD-site.tz/24, location, false)
if got.Day() != local.Day() || got.Month() != local.Month() { if got.Day() != local.Day() || got.Month() != local.Month() {
t.Fatalf("%s: sun culmination = %s, want local day %s", site.name, got.Format("2006-01-02 15:04"), local.Format("2006-01-02")) t.Fatalf("%s: sun culmination = %s, want local day %s", site.name, got.Format("2006-01-02 15:04"), local.Format("2006-01-02"))
} }
@@ -65,7 +65,7 @@ func TestMoonCulminationAnchorKeepsLocalDay(t *testing.T) {
for _, site := range culminationAnchorSites { for _, site := range culminationAnchorSites {
local, midnightJD := culminationAnchorLocalMidnight(site, timestamp) local, midnightJD := culminationAnchorLocalMidnight(site, timestamp)
location := local.Location() location := local.Location()
got := JDE2DateByZone(MoonCulminationTime(midnightJD, site.lon, site.lat, site.tz), location, true) got := JD2DateByZone(MoonCulminationTime(midnightJD, site.lon, site.lat, site.tz), location, true)
truthJD, truthAltitude := 0.0, -999.0 truthJD, truthAltitude := 0.0, -999.0
for minute := 0; minute <= 24*60; minute++ { for minute := 0; minute <= 24*60; minute++ {
jd := midnightJD + float64(minute)/1440.0 jd := midnightJD + float64(minute)/1440.0
@@ -73,7 +73,7 @@ func TestMoonCulminationAnchorKeepsLocalDay(t *testing.T) {
truthAltitude, truthJD = altitude, jd truthAltitude, truthJD = altitude, jd
} }
} }
truth := JDE2DateByZone(truthJD, location, true) truth := JD2DateByZone(truthJD, location, true)
if got.Day() != local.Day() || got.Month() != local.Month() { if got.Day() != local.Day() || got.Month() != local.Month() {
t.Fatalf("%s: moon culmination = %s, want local day %s", site.name, got.Format("2006-01-02 15:04"), local.Format("2006-01-02")) t.Fatalf("%s: moon culmination = %s, want local day %s", site.name, got.Format("2006-01-02 15:04"), local.Format("2006-01-02"))
} }
-203
View File
@@ -1,203 +0,0 @@
package basic
import (
"math"
"sync"
)
var defDeltaTFn = DefaultDeltaTv2
var deltaTFnMu sync.RWMutex
// deltaTGeneration 随每次 ΔT 覆盖递增,供依赖 ΔT 的只读记忆表判断自身是否过期。
// 起始为 1,使零值缓存条目(世代 0)天然视为未命中。
// deltaTGeneration increments on every ΔT override so ΔT-dependent memo tables can detect
// staleness. It starts at 1 so a zero-valued cache entry (generation 0) is never a hit.
var deltaTGeneration uint64 = 1
func DeltaT(date float64, isJDE bool) float64 {
deltaTFnMu.RLock()
fn := defDeltaTFn
deltaTFnMu.RUnlock()
return fn(date, isJDE)
}
func SetDeltaTFn(fn func(float64, bool) float64) {
if fn != nil {
deltaTFnMu.Lock()
defDeltaTFn = fn
deltaTGeneration++
deltaTFnMu.Unlock()
}
}
// deltaTGenerationValue 返回当前 ΔT 世代,用于让只读记忆表在 ΔT 改变后整体失效。
// deltaTGenerationValue returns the current ΔT generation so memo tables can be invalidated.
func deltaTGenerationValue() uint64 {
deltaTFnMu.RLock()
value := deltaTGeneration
deltaTFnMu.RUnlock()
return value
}
func GetDeltaTFn() func(float64, bool) float64 {
deltaTFnMu.RLock()
fn := defDeltaTFn
deltaTFnMu.RUnlock()
return fn
}
func DefaultDeltaTv2(date float64, isJd bool) float64 { //传入年或儒略日,传出为秒
if math.IsNaN(date) || math.IsInf(date, 0) {
return math.NaN()
}
if !isJd {
year := math.Floor(date)
start := JDECalc(int(year), 1, 1)
end := JDECalc(int(year)+1, 1, 1)
date = start + (date-year)*(end-start)
}
return DeltaTv2(date)
}
// 使用Stephenson等人(2016)和Morrison等人(2021)的拟合和外推公式计算Delta T
// http://astro.ukho.gov.uk/nao/lvm/
// 2010年后的系数已修改以包含2019年后的数据
// 返回Delta T,单位为秒
func DeltaTSplineY(y float64) float64 {
if math.IsNaN(y) || math.IsInf(y, 0) {
return math.NaN()
}
// 积分lod(平均太阳日偏离86400秒的偏差)方程:
// 来自 http://astro.ukho.gov.uk/nao/lvm/:
// lod = 1.72 t − 3.5 sin(2*pi*(t+0.75)/14) 单位ms/day,其中 t = (y - 1825)/100
// 是从1825年开始的世纪数
// 使用 1ms = 1e-3s 和 1儒略年 = 365.25天,
// lod = 6.2823e-3 * Delta y - 1.278375*sin(2*pi/14*(Delta y /100 + 0.75) 单位s/year
// 其中 Delta y = y - 1825。积分该方程得到
// Integrate[lod, y] = 3.14115e-3*(Delta y)^2 + 894.8625/pi*cos(2*pi/14*(Delta y /100 + 0.75)
// 单位为秒。积分常数设为0。
integratedLod := func(x float64) float64 {
u := x - 1825
return 3.14115e-3*u*u + 284.8435805251424*math.Cos(0.4487989505128276*(0.01*u+0.75))
}
if y < -720 {
// 使用积分lod + 常数
const c = 1.007739546148514
return integratedLod(y) + c
}
if y > 2025 {
// 使用积分lod + 常数
const c = -150.56787057979514
return integratedLod(y) + c
}
// 使用三次样条拟合
y0 := []float64{-720, -100, 400, 1000, 1150, 1300, 1500, 1600, 1650, 1720, 1800, 1810, 1820, 1830, 1840, 1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900, 1905, 1910, 1915, 1920, 1925, 1930, 1935, 1940, 1945, 1950, 1953, 1956, 1959, 1962, 1965, 1968, 1971, 1974, 1977, 1980, 1983, 1986, 1989, 1992, 1995, 1998, 2001, 2004, 2007, 2010, 2013, 2016, 2019, 2022}
y1 := []float64{-100, 400, 1000, 1150, 1300, 1500, 1600, 1650, 1720, 1800, 1810, 1820, 1830, 1840, 1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900, 1905, 1910, 1915, 1920, 1925, 1930, 1935, 1940, 1945, 1950, 1953, 1956, 1959, 1962, 1965, 1968, 1971, 1974, 1977, 1980, 1983, 1986, 1989, 1992, 1995, 1998, 2001, 2004, 2007, 2010, 2013, 2016, 2019, 2022, 2025}
a0 := []float64{20371.848, 11557.668, 6535.116, 1650.393, 1056.647, 681.149, 292.343, 109.127, 43.952, 12.068, 18.367, 15.678, 16.516, 10.804, 7.634, 9.338, 10.357, 9.04, 8.255, 2.371, -1.126, -3.21, -4.388, -3.884, -5.017, -1.977, 4.923, 11.142, 17.479, 21.617, 23.789, 24.418, 24.164, 24.426, 27.05, 28.932, 30.002, 30.76, 32.652, 33.621, 35.093, 37.956, 40.951, 44.244, 47.291, 50.361, 52.936, 54.984, 56.373, 58.453, 60.678, 62.898, 64.083, 64.553, 65.197, 66.061, 66.919, 68.130, 69.250, 69.296}
a1 := []float64{-9999.586, -5822.27, -5671.519, -753.21, -459.628, -421.345, -192.841, -78.697, -68.089, 2.507, -3.481, 0.021, -2.157, -6.018, -0.416, 1.642, -0.486, -0.591, -3.456, -5.593, -2.314, -1.893, 0.101, -0.531, 0.134, 5.715, 6.828, 6.33, 5.518, 3.02, 1.333, 0.052, -0.419, 1.645, 2.499, 1.127, 0.737, 1.409, 1.577, 0.868, 2.275, 3.035, 3.157, 3.199, 3.069, 2.878, 2.354, 1.577, 1.648, 2.235, 2.324, 1.804, 0.674, 0.466, 0.804, 0.839, 1.005, 1.348, 0.594, -0.227}
a2 := []float64{776.247, 1303.151, -298.291, 184.811, 108.771, 61.953, -6.572, 10.505, 38.333, 41.731, -1.126, 4.629, -6.806, 2.944, 2.658, 0.261, -2.389, 2.284, -5.148, 3.011, 0.269, 0.152, 1.842, -2.474, 3.138, 2.443, -1.329, 0.831, -1.643, -0.856, -0.831, -0.449, -0.022, 2.086, -1.232, 0.22, -0.61, 1.282, -1.115, 0.406, 1.002, -0.242, 0.364, -0.323, 0.193, -0.384, -0.14, -0.637, 0.708, -0.121, 0.21, -0.729, -0.402, 0.194, 0.144, -0.109, 0.275, 0.068, -0.822, 0.001}
a3 := []float64{409.16, -503.433, 1085.087, -25.346, -24.641, -29.414, 16.197, 3.018, -2.127, -37.939, 1.918, -3.812, 3.25, -0.096, -0.539, -0.883, 1.558, -2.477, 2.72, -0.914, -0.039, 0.563, -1.438, 1.871, -0.232, -1.257, 0.72, -0.825, 0.262, 0.008, 0.127, 0.142, 0.702, -1.106, 0.614, -0.277, 0.631, -0.799, 0.507, 0.199, -0.414, 0.202, -0.229, 0.172, -0.192, 0.081, -0.165, 0.448, -0.276, 0.11, -0.313, 0.109, 0.199, -0.017, -0.084, 0.128, -0.069, -0.297, 0.274, 0.086}
n := len(y0)
var i int
for i = n - 1; i >= 0; i-- {
if y >= y0[i] {
break
}
}
t := (y - y0[i]) / (y1[i] - y0[i])
dT := a0[i] + t*(a1[i]+t*(a2[i]+t*a3[i]))
return dT
}
func DeltaTv2(jd float64) float64 {
if math.IsNaN(jd) || math.IsInf(jd, 0) {
return math.NaN()
}
if jd > 2461041.5 || jd < 2441317.5 {
var y float64
if jd >= 2299160.5 {
y = (jd-2451544.5)/365.2425 + 2000
} else {
y = (jd+0.5)/365.25 - 4712
}
return DeltaTSplineY(y)
}
// 闰秒JD值
jdLeaps := []float64{2457754.5, 2457204.5, 2456109.5, 2454832.5,
2453736.5, 2451179.5, 2450630.5, 2450083.5,
2449534.5, 2449169.5, 2448804.5, 2448257.5,
2447892.5, 2447161.5, 2446247.5, 2445516.5,
2445151.5, 2444786.5, 2444239.5, 2443874.5,
2443509.5, 2443144.5, 2442778.5, 2442413.5,
2442048.5, 2441683.5, 2441499.5, 2441133.5}
n := len(jdLeaps)
deltaTSeconds := 42.184
for i := 0; i < n; i++ {
if jd >= jdLeaps[i] {
deltaTSeconds += float64(n - i - 1)
break
}
}
return deltaTSeconds
}
// DeltaTSecondsAt 返回某个 TT 时刻实际使用的 ΔT(秒):overrideSeconds 是有限值时直接采用
// (含 0,可显式要求 ΔT=0),为 NaN/±Inf 时改用进程级模型。模型按 UT 键控,因此这里先解
// TT−ΔT(UT) 再求值,不把 TT 直接当作 UT 送进模型(差约 2e-6 s)。
// DeltaTSecondsAt returns the ΔT in seconds used at one TT instant: a finite override wins
// (including 0, which requests ΔT = 0 explicitly), while NaN or ±Inf selects the process-wide
// model. The model is keyed by UT, so the equation TT - ΔT(UT) is solved instead of feeding TT.
func DeltaTSecondsAt(jdeTT, overrideSeconds float64) float64 {
if !math.IsNaN(overrideSeconds) && !math.IsInf(overrideSeconds, 0) {
return overrideSeconds
}
return deltaTModelSecondsAtTT(jdeTT)
}
func deltaTModelSecondsAtTT(jdeTT float64) float64 {
ut := jdeTT - DeltaT(jdeTT, true)/86400.0
for iteration := 0; iteration < 4; iteration++ {
next := jdeTT - DeltaT(ut, true)/86400.0
if next == ut {
break
}
ut = next
}
return DeltaT(ut, true)
}
// DeltaTGroundShiftKM 把 ΔT 误差换算为站点相对影子的地面横移距离(千米)。
// 地球赤道自转线速度 465.1 m/s,因此 ΔT 相差 Δ 秒时,地面点相对影子横移
// 0.4651·|Δ|·cos(纬度) 千米;±400 年跨度上 ΔT 外推差几百到几千秒,足以挪动本影
// 边界数百千米,调用方可用本函数把外部给出的 ΔT 不确定度换算成几何不确定度。
// DeltaTGroundShiftKM converts a ΔT error into the ground displacement of a station
// relative to the shadow, in kilometres: 0.4651 * |ΔT| * cos(latitude).
func DeltaTGroundShiftKM(deltaTSeconds, latitudeDeg float64) float64 {
if math.IsNaN(deltaTSeconds) || math.IsInf(deltaTSeconds, 0) ||
math.IsNaN(latitudeDeg) || math.IsInf(latitudeDeg, 0) {
return math.NaN()
}
return solarEclipseEarthEquatorialRotationKMPerSecond * math.Abs(deltaTSeconds) * math.Cos(latitudeDeg*rad)
}
func TD2UT(jde float64, utToTD bool) float64 { // true 世界时转力学时CC,false 力学时转世界时VV
deltaTSeconds := DeltaT(jde, true)
if utToTD {
return jde + deltaTSeconds/3600/24
}
// Delta T is evaluated at UT in the forward conversion. Solve the same
// equation in reverse so distant-epoch contact times survive a round trip.
ut := jde - deltaTSeconds/3600/24
for iteration := 0; iteration < 4; iteration++ {
next := jde - DeltaT(ut, true)/3600/24
if next == ut {
break
}
ut = next
}
return ut
}
+186
View File
@@ -0,0 +1,186 @@
package basic
import "math"
// DeltaTModel 标识一个可选的已发布 ΔT 模型 / a selectable published ΔT model.
type DeltaTModel string
const (
// DeltaTModelDefault 内置默认模型:Stephenson–Morrison–Hohenkerk 2016 + Morrison 2021 的样条与积分 lod。
// DeltaTModelDefault is the built-in model: the SMH2016 + Morrison 2021 spline and integrated lod.
DeltaTModelDefault DeltaTModel = ""
// DeltaTModelSMH2016 显式选择内置的 SMH2016 + Morrison 2021 模型。
// DeltaTModelSMH2016 selects the built-in SMH2016 + Morrison 2021 model explicitly.
DeltaTModelSMH2016 DeltaTModel = "smh2016"
// DeltaTModelMS2004 Morrison & Stephenson 2004 的长期抛物线 ΔT = −20 + 32u²(u = (y−1820)/100),单位秒。
// DeltaTModelMS2004 is the Morrison & Stephenson 2004 long-term parabola in seconds.
DeltaTModelMS2004 DeltaTModel = "ms2004"
// DeltaTModelEspenakMeeus2006 Espenak & Meeus 2006 的分段多项式,基于 M&S2004,未加配对修正 c。
// DeltaTModelEspenakMeeus2006 is the Espenak & Meeus 2006 piecewise polynomial without the pairing term c.
DeltaTModelEspenakMeeus2006 DeltaTModel = "espenak_meeus_2006"
// DeltaTModelNASACanon2006 是 Espenak–Meeus 再加配对修正 c = −0.000012932(y−1955)²(y < 1955),
// 即 NASA 五千年目录实际印刷的 ΔT 口径,配对的是 ELP2000/82 的 −25.858″/cy²。
// DeltaTModelNASACanon2006 adds the pairing term c for y < 1955, the convention printed by NASA's canon.
DeltaTModelNASACanon2006 DeltaTModel = "nasa_canon_2006"
// DeltaTModelManual 表示当前生效的是 SetDeltaTFn 注入的任意函数,不是命名模型。
// DeltaTModelManual reports that an arbitrary function injected with SetDeltaTFn is active.
DeltaTModelManual DeltaTModel = "manual"
)
// deltaTModelFunction 把命名模型包成 ΔT 钩子的签名;isJulianDay 为假时入参是十进制年。
func deltaTModelFunction(model DeltaTModel, keepObserved bool) func(float64, bool) float64 {
return func(date float64, isJulianDay bool) float64 {
if math.IsNaN(date) || math.IsInf(date, 0) {
return math.NaN()
}
if isJulianDay {
return DeltaTModelSeconds(model, date, keepObserved)
}
if keepObserved {
year := math.Floor(date)
start := JDCalc(int(year), 1, 1)
end := JDCalc(int(year)+1, 1, 1)
jd := start + (date-year)*(end-start)
if seconds, ok := deltaTMonthlyAt(jd); ok {
return seconds
}
}
return deltaTModelSecondsAtYear(model, date)
}
}
// DeltaTModelSeconds 按命名模型求 ΔT(秒),入参为 UT 儒略日。
//
// keepObserved 为真时,落在逐月实测表覆盖段内的时刻一律返回实测值,模型只接管表外;
// 这是"实测优先"的推荐用法,也是内置默认模型的结构。为假时全程使用模型,仅用于口径对照。
// DeltaTModelSeconds evaluates a named model. With keepObserved the monthly observed table wins
// wherever it covers the instant and the model only takes over outside it.
func DeltaTModelSeconds(model DeltaTModel, jd float64, keepObserved bool) float64 {
if math.IsNaN(jd) || math.IsInf(jd, 0) {
return math.NaN()
}
switch model {
case DeltaTModelDefault, DeltaTModelSMH2016:
if keepObserved {
// 与内置默认逐位一致(含实测段与样条表尾的常值锚定)。
return deltaTModelSecondsAtUT(jd)
}
return deltaTSplineAtJDE(jd)
}
if keepObserved {
if seconds, ok := deltaTMonthlyAt(jd); ok {
return seconds
}
}
return deltaTModelSecondsAtYear(model, deltaTYearAtJDE(jd))
}
// SetDeltaTModel 安装命名模型;keepObserved 为真时保留实测表覆盖段(推荐),未知模型返回 false 且不改动现状。
// SetDeltaTModel installs a named model, keeping the observed span when keepObserved is true;
// an unknown model returns false and leaves the current model untouched.
func SetDeltaTModel(model DeltaTModel, keepObserved bool) bool {
if !knownDeltaTModel(model) {
return false
}
if model == DeltaTModelDefault || model == DeltaTModelSMH2016 {
if keepObserved {
// 内置模型本身就是"实测优先",直接用它可以避免表尾锚定出现差异。
installDeltaTFn(DefaultDeltaTv2, model, true)
return true
}
installDeltaTFn(deltaTModelFunction(model, false), model, false)
return true
}
installDeltaTFn(deltaTModelFunction(model, keepObserved), model, keepObserved)
return true
}
// GetDeltaTModel 返回当前生效的模型与是否保留实测段;SetDeltaTFn 注入的任意函数报 DeltaTModelManual。
// GetDeltaTModel returns the active model and whether the observed span is kept; an arbitrary
// function injected with SetDeltaTFn reports DeltaTModelManual.
func GetDeltaTModel() (DeltaTModel, bool) {
deltaTFnMu.RLock()
defer deltaTFnMu.RUnlock()
return activeDeltaTModel, activeDeltaTKeepObserved
}
func knownDeltaTModel(model DeltaTModel) bool {
switch model {
case DeltaTModelDefault, DeltaTModelSMH2016, DeltaTModelMS2004,
DeltaTModelEspenakMeeus2006, DeltaTModelNASACanon2006:
return true
}
return false
}
// deltaTModelSecondsAtYear 按十进制年求模型值;实测段是否介入由调用方决定。
func deltaTModelSecondsAtYear(model DeltaTModel, year float64) float64 {
switch model {
case DeltaTModelMS2004:
u := (year - 1820) / 100
return -20 + 32*u*u
case DeltaTModelEspenakMeeus2006:
return espenakMeeus2006Seconds(year)
case DeltaTModelNASACanon2006:
value := espenakMeeus2006Seconds(year)
if year < 1955 {
// 配对修正:把 −26.0″/cy² 口径的 ΔT 换到 −25.858″/cy²(1955 起原子时使其与月球历表无关)。
d := year - 1955
value += -0.000012932 * d * d
}
return value
}
return DeltaTSplineY(year)
}
// espenakMeeus2006Seconds 是 Espenak & Meeus 2006 对 −1999..+3000 的分段多项式(秒),
// 自变量 y = year + (month−0.5)/12;分段边界处两段取值一致(−500 处由 17203.7 强制连续)。
func espenakMeeus2006Seconds(y float64) float64 {
switch {
case y < -500:
u := (y - 1820) / 100
return -20 + 32*u*u
case y < 500:
u := y / 100
return 10583.6 + u*(-1014.41+u*(33.78311+u*(-5.952053+u*(-0.1798452+u*(0.022174192+u*0.0090316521)))))
case y < 1600:
u := (y - 1000) / 100
return 1574.2 + u*(-556.01+u*(71.23472+u*(0.319781+u*(-0.8503463+u*(-0.005050998+u*0.0083572073)))))
case y < 1700:
t := y - 1600
return 120 - 0.9808*t - 0.01532*t*t + t*t*t/7129
case y < 1800:
t := y - 1700
return 8.83 + t*(0.1603+t*(-0.0059285+t*(0.00013336-t/1174000)))
case y < 1860:
t := y - 1800
return 13.72 + t*(-0.332447+t*(0.0068612+t*(0.0041116+t*(-0.00037436+
t*(0.0000121272+t*(-0.0000001699+t*0.000000000875))))))
case y < 1900:
t := y - 1860
return 7.62 + t*(0.5737+t*(-0.251754+t*(0.01680668+t*(-0.0004473624+t/233174))))
case y < 1920:
t := y - 1900
return -2.79 + t*(1.494119+t*(-0.0598939+t*(0.0061966-t*0.000197)))
case y < 1941:
t := y - 1920
return 21.20 + t*(0.84493+t*(-0.076100+t*0.0020936))
case y < 1961:
t := y - 1950
return 29.07 + t*(0.407-t/233+t*t/2547)
case y < 1986:
t := y - 1975
return 45.45 + t*(1.067-t/260-t*t/718)
case y < 2005:
t := y - 2000
return 63.86 + t*(0.3345+t*(-0.060374+t*(0.0017275+t*(0.000651814+t*0.00002373599))))
case y < 2050:
t := y - 2000
return 62.92 + t*(0.32217+t*0.005589)
case y < 2150:
u := (y - 1820) / 100
return -20 + 32*u*u - 0.5628*(2150-y)
}
u := (y - 1820) / 100
return -20 + 32*u*u
}
+94
View File
@@ -0,0 +1,94 @@
package basic
import (
"math"
"testing"
)
// ΔT 命名模型的契约:已发表锚点、分段连续性、实测段优先的混合语义与模型标记。
func TestDeltaTModelPublishedAnchors(t *testing.T) {
resetTimeScaleState(t)
check := func(name string, year, want, tolerance float64) {
t.Helper()
if got := DeltaT(year, false); math.Abs(got-want) > tolerance {
t.Fatalf("%s ΔT(%.5f)=%.6f, want %.6f±%g", name, year, got, want, tolerance)
}
}
if !SetDeltaTModel(DeltaTModelEspenakMeeus2006, false) {
t.Fatal("Espenak–Meeus 模型应可安装")
}
check("E-M", 2017.64, 70.34, 0.01)
check("E-M", 2024.27, 74.03, 0.01)
check("E-M", 2100, 202.7, 0.05)
if !SetDeltaTModel(DeltaTModelMS2004, false) {
t.Fatal("M&S2004 模型应可安装")
}
check("M&S2004", -500, 17203.7, 0.05)
check("M&S2004", 2100, 230.9, 0.05)
if !SetDeltaTModel(DeltaTModelEspenakMeeus2006, false) {
t.Fatal("Espenak–Meeus 模型应可安装")
}
// 分段边界:原表各段是独立拟合的,边界处本身留有台阶(实测最大 ≤0.3 s,1600 处约 0.25 s),
// 这里只守住这个量级,用来抓 Horner 展开之类的结构性错误。
for _, knot := range []float64{-500, 500, 1600, 1700, 1800, 1860, 1900, 1920, 1941, 1961, 1986, 2005, 2050, 2150} {
before, after := DeltaT(knot-1e-6, false), DeltaT(knot+1e-6, false)
if diff := math.Abs(after - before); diff > 0.3 {
t.Errorf("分段边界 %.0f 台阶过大:%.6f vs %.6f(差 %g)", knot, before, after, diff)
}
}
}
func TestDeltaTModelKeepsObservedSpan(t *testing.T) {
resetTimeScaleState(t)
jd := JDCalc(2026, 3, 1)
observed := DeltaT(jd, true)
if !SetDeltaTModel(DeltaTModelEspenakMeeus2006, true) {
t.Fatal("Espenak–Meeus 模型应可安装")
}
if got := DeltaT(jd, true); math.Abs(got-observed) > 1e-12 {
t.Fatalf("实测段应优先:got %.9f, want %.9f", got, observed)
}
pure := DeltaTModelSeconds(DeltaTModelEspenakMeeus2006, jd, false)
if math.Abs(pure-observed) < 5 {
t.Fatalf("纯模型值 %.3f 应与实测 %.3f 明显不同(否则测不到混合语义)", pure, observed)
}
if model, keep := GetDeltaTModel(); model != DeltaTModelEspenakMeeus2006 || !keep {
t.Fatalf("模型标记 = (%q, %v)", model, keep)
}
future := JDCalc(2100, 1, 1)
if got, want := DeltaT(future, true), DeltaTModelSeconds(DeltaTModelEspenakMeeus2006, future, false); math.Abs(got-want) > 1e-9 {
t.Fatalf("表外应走模型:got %.9f, want %.9f", got, want)
}
// SMH2016 混合档必须与内置默认逐位一致。
if !SetDeltaTModel(DeltaTModelSMH2016, true) {
t.Fatal("SMH2016 模型应可安装")
}
if got := DeltaT(jd, true); got != observed {
t.Fatalf("SMH2016 混合档 %.9f 与内置默认 %.9f 不一致", got, observed)
}
if got := DeltaT(future, true); got != DefaultDeltaTv2(future, true) {
t.Fatalf("SMH2016 混合档表外 %.9f 与内置默认 %.9f 不一致", got, DefaultDeltaTv2(future, true))
}
// 手工注入的任意函数标记为 manual;恢复 nil 回到内置默认。
SetDeltaTFn(func(float64, bool) float64 { return 0 })
if model, keep := GetDeltaTModel(); model != DeltaTModelManual || keep {
t.Fatalf("手工注入标记 = (%q, %v)", model, keep)
}
SetDeltaTFn(nil)
if model, keep := GetDeltaTModel(); model != DeltaTModelDefault || !keep {
t.Fatalf("恢复默认标记 = (%q, %v)", model, keep)
}
if SetDeltaTModel(DeltaTModel("nope"), true) {
t.Fatal("未知模型应被拒绝")
}
if model, _ := GetDeltaTModel(); model != DeltaTModelDefault {
t.Fatalf("被拒绝后模型不应改动,got %q", model)
}
}
+24 -18
View File
@@ -8,8 +8,8 @@ import (
func TestDefaultDeltaTFractionalYear(t *testing.T) { func TestDefaultDeltaTFractionalYear(t *testing.T) {
for _, year := range []float64{-700.5, -0.5, 0, 1000.5, 1582.75, 1800.5, 2000.5, 2026.5} { for _, year := range []float64{-700.5, -0.5, 0, 1000.5, 1582.75, 1800.5, 2000.5, 2026.5} {
whole := math.Floor(year) whole := math.Floor(year)
start := JDECalc(int(whole), 1, 1) start := JDCalc(int(whole), 1, 1)
end := JDECalc(int(whole)+1, 1, 1) end := JDCalc(int(whole)+1, 1, 1)
want := DefaultDeltaTv2(start+(year-whole)*(end-start), true) want := DefaultDeltaTv2(start+(year-whole)*(end-start), true)
got := DefaultDeltaTv2(year, false) got := DefaultDeltaTv2(year, false)
if math.IsNaN(got) || math.Abs(got-want) > 1e-10 { if math.IsNaN(got) || math.Abs(got-want) > 1e-10 {
@@ -28,36 +28,42 @@ func TestDefaultDeltaTInvalidInput(t *testing.T) {
} }
} }
func TestDeltaTLeapSecondBoundary(t *testing.T) { // 闰秒只改 TT−UTC,不改 TT−UT1:ΔT 在闰秒两侧连续,TT−UTC 才跳 1 秒。
func TestDeltaTStaysContinuousAcrossLeapSecond(t *testing.T) {
boundary := 2457754.5 boundary := 2457754.5
if got := DeltaTv2(boundary); got != 69.184 { before := DeltaTv2(boundary - 1e-6)
t.Fatalf("DeltaT at leap-second boundary=%v, want 69.184", got) after := DeltaTv2(boundary + 1e-6)
if math.Abs(after-before) > 1e-3 {
t.Fatalf("ΔT should stay continuous at a leap second: %v → %v", before, after)
}
if step := TTMinusUTCSeconds(boundary+1e-6) - TTMinusUTCSeconds(boundary-1e-6); math.Abs(step-1) > 1e-9 {
t.Fatalf("TT−UTC should step by one second: %v", step)
} }
if got := DeltaTSplineY(math.NaN()); !math.IsNaN(got) { if got := DeltaTSplineY(math.NaN()); !math.IsNaN(got) {
t.Fatalf("DeltaTSplineY(NaN)=%v, want NaN", got) t.Fatalf("DeltaTSplineY(NaN)=%v, want NaN", got)
} }
} }
func TestTD2UTRoundTrip(t *testing.T) { func TestUTC2TTRoundTrip(t *testing.T) {
for _, year := range []int{-2000, -720, 0, 26, 1426, 2025, 2027, 3627, 4026, 5000} { for _, year := range []int{-2000, -720, 0, 26, 1426, 2025, 2027, 3627, 4026, 5000} {
ut := JDECalc(year, 9, 20.123456) utc := JDCalc(year, 9, 20.123456)
tt := TD2UT(ut, true) tt := UTC2TT(utc)
if got := TD2UT(tt, false); math.Abs(got-ut) > math.Nextafter(ut, math.Inf(1))-ut { if got := TT2UTC(tt); math.Abs(got-utc) > math.Nextafter(utc, math.Inf(1))-utc {
t.Errorf("year=%d UT round trip differs by %.9f seconds", year, (got-ut)*86400) t.Errorf("year=%d UTC round trip differs by %.9f seconds", year, (got-utc)*86400)
} }
if got := TD2UT(TD2UT(tt, false), true); got != tt { if got := UTC2TT(TT2UTC(tt)); got != tt {
t.Errorf("year=%d TT round trip differs by %.9f seconds", year, (got-tt)*86400) t.Errorf("year=%d TT round trip differs by %.9f seconds", year, (got-tt)*86400)
} }
} }
for _, seconds := range []float64{-70, -1, -0.1, 0, 0.1, 1, 70} { for _, seconds := range []float64{-70, -1, -0.1, 0, 0.1, 1, 70} {
ut := 2457754.5 + seconds/86400 utc := 2457754.5 + seconds/86400
if got := TD2UT(TD2UT(ut, true), false); got != ut { if got := TT2UTC(UTC2TT(utc)); got != utc {
t.Errorf("leap second offset=%g round trip differs by %.9f seconds", seconds, (got-ut)*86400) t.Errorf("leap second offset=%g round trip differs by %.9f seconds", seconds, (got-utc)*86400)
} }
} }
} }
func TestTD2UTCustomDeltaT(t *testing.T) { func TestUTC2TTRoundTripUnderCustomDeltaT(t *testing.T) {
original := GetDeltaTFn() original := GetDeltaTFn()
t.Cleanup(func() { SetDeltaTFn(original) }) t.Cleanup(func() { SetDeltaTFn(original) })
for _, slope := range []float64{0, 0.01} { for _, slope := range []float64{0, 0.01} {
@@ -67,9 +73,9 @@ func TestTD2UTCustomDeltaT(t *testing.T) {
} }
return 10000 + slope*(jd-2451545) return 10000 + slope*(jd-2451545)
}) })
ut := 3000000.123456 utc := 3000000.123456
if got := TD2UT(TD2UT(ut, true), false); got != ut { if got := TT2UTC(UTC2TT(utc)); got != utc {
t.Errorf("custom slope=%g round trip differs by %.9f seconds", slope, (got-ut)*86400) t.Errorf("custom slope=%g round trip differs by %.9f seconds", slope, (got-utc)*86400)
} }
} }
} }
+72 -72
View File
@@ -26,181 +26,181 @@ func angularSemidiameterFromAU(radiusKM, distanceAU float64) float64 {
} }
// SunSemidiameter 太阳视半径,单位角秒 / apparent solar semidiameter in arcseconds. // SunSemidiameter 太阳视半径,单位角秒 / apparent solar semidiameter in arcseconds.
func SunSemidiameter(jd float64) float64 { func SunSemidiameter(jde float64) float64 {
return SunSemidiameterN(jd, -1) return SunSemidiameterN(jde, -1)
} }
// SunSemidiameterN 太阳视半径(截断版),单位角秒 / truncated apparent solar semidiameter in arcseconds. // SunSemidiameterN 太阳视半径(截断版),单位角秒 / truncated apparent solar semidiameter in arcseconds.
func SunSemidiameterN(jd float64, n int) float64 { func SunSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jd, n)) return angularSemidiameterFromAU(sunEquatorialRadiusKM, EarthAwayN(jde, n))
} }
// SunDiameter 太阳视直径,单位角秒 / apparent solar diameter in arcseconds. // SunDiameter 太阳视直径,单位角秒 / apparent solar diameter in arcseconds.
func SunDiameter(jd float64) float64 { func SunDiameter(jde float64) float64 {
return SunDiameterN(jd, -1) return SunDiameterN(jde, -1)
} }
// SunDiameterN 太阳视直径(截断版),单位角秒 / truncated apparent solar diameter in arcseconds. // SunDiameterN 太阳视直径(截断版),单位角秒 / truncated apparent solar diameter in arcseconds.
func SunDiameterN(jd float64, n int) float64 { func SunDiameterN(jde float64, n int) float64 {
return 2 * SunSemidiameterN(jd, n) return 2 * SunSemidiameterN(jde, n)
} }
// MoonSemidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds. // MoonSemidiameter 月亮视半径,单位角秒 / apparent lunar semidiameter in arcseconds.
func MoonSemidiameter(jd float64) float64 { func MoonSemidiameter(jde float64) float64 {
return MoonSemidiameterN(jd, -1) return MoonSemidiameterN(jde, -1)
} }
// MoonSemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds. // MoonSemidiameterN 月亮视半径(截断版),单位角秒 / truncated apparent lunar semidiameter in arcseconds.
func MoonSemidiameterN(jd float64, n int) float64 { func MoonSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jd, n)) return angularSemidiameterArcsec(moonEquatorialRadiusKM, HMoonAwayN(jde, n))
} }
// MoonDiameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds. // MoonDiameter 月亮视直径,单位角秒 / apparent lunar diameter in arcseconds.
func MoonDiameter(jd float64) float64 { func MoonDiameter(jde float64) float64 {
return MoonDiameterN(jd, -1) return MoonDiameterN(jde, -1)
} }
// MoonDiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds. // MoonDiameterN 月亮视直径(截断版),单位角秒 / truncated apparent lunar diameter in arcseconds.
func MoonDiameterN(jd float64, n int) float64 { func MoonDiameterN(jde float64, n int) float64 {
return 2 * MoonSemidiameterN(jd, n) return 2 * MoonSemidiameterN(jde, n)
} }
// MercurySemidiameter 水星视半径,单位角秒 / apparent Mercury semidiameter in arcseconds. // MercurySemidiameter 水星视半径,单位角秒 / apparent Mercury semidiameter in arcseconds.
func MercurySemidiameter(jd float64) float64 { func MercurySemidiameter(jde float64) float64 {
return MercurySemidiameterN(jd, -1) return MercurySemidiameterN(jde, -1)
} }
// MercurySemidiameterN 水星视半径(截断版),单位角秒 / truncated apparent Mercury semidiameter in arcseconds. // MercurySemidiameterN 水星视半径(截断版),单位角秒 / truncated apparent Mercury semidiameter in arcseconds.
func MercurySemidiameterN(jd float64, n int) float64 { func MercurySemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jd, n)) return angularSemidiameterFromAU(mercuryEquatorialRadiusKM, EarthMercuryAwayN(jde, n))
} }
// MercuryDiameter 水星视直径,单位角秒 / apparent Mercury diameter in arcseconds. // MercuryDiameter 水星视直径,单位角秒 / apparent Mercury diameter in arcseconds.
func MercuryDiameter(jd float64) float64 { func MercuryDiameter(jde float64) float64 {
return MercuryDiameterN(jd, -1) return MercuryDiameterN(jde, -1)
} }
// MercuryDiameterN 水星视直径(截断版),单位角秒 / truncated apparent Mercury diameter in arcseconds. // MercuryDiameterN 水星视直径(截断版),单位角秒 / truncated apparent Mercury diameter in arcseconds.
func MercuryDiameterN(jd float64, n int) float64 { func MercuryDiameterN(jde float64, n int) float64 {
return 2 * MercurySemidiameterN(jd, n) return 2 * MercurySemidiameterN(jde, n)
} }
// VenusSemidiameter 金星视半径,单位角秒 / apparent Venus semidiameter in arcseconds. // VenusSemidiameter 金星视半径,单位角秒 / apparent Venus semidiameter in arcseconds.
func VenusSemidiameter(jd float64) float64 { func VenusSemidiameter(jde float64) float64 {
return VenusSemidiameterN(jd, -1) return VenusSemidiameterN(jde, -1)
} }
// VenusSemidiameterN 金星视半径(截断版),单位角秒 / truncated apparent Venus semidiameter in arcseconds. // VenusSemidiameterN 金星视半径(截断版),单位角秒 / truncated apparent Venus semidiameter in arcseconds.
func VenusSemidiameterN(jd float64, n int) float64 { func VenusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jd, n)) return angularSemidiameterFromAU(venusEquatorialRadiusKM, EarthVenusAwayN(jde, n))
} }
// VenusDiameter 金星视直径,单位角秒 / apparent Venus diameter in arcseconds. // VenusDiameter 金星视直径,单位角秒 / apparent Venus diameter in arcseconds.
func VenusDiameter(jd float64) float64 { func VenusDiameter(jde float64) float64 {
return VenusDiameterN(jd, -1) return VenusDiameterN(jde, -1)
} }
// VenusDiameterN 金星视直径(截断版),单位角秒 / truncated apparent Venus diameter in arcseconds. // VenusDiameterN 金星视直径(截断版),单位角秒 / truncated apparent Venus diameter in arcseconds.
func VenusDiameterN(jd float64, n int) float64 { func VenusDiameterN(jde float64, n int) float64 {
return 2 * VenusSemidiameterN(jd, n) return 2 * VenusSemidiameterN(jde, n)
} }
// MarsSemidiameter 火星视半径,单位角秒 / apparent Mars semidiameter in arcseconds. // MarsSemidiameter 火星视半径,单位角秒 / apparent Mars semidiameter in arcseconds.
func MarsSemidiameter(jd float64) float64 { func MarsSemidiameter(jde float64) float64 {
return MarsSemidiameterN(jd, -1) return MarsSemidiameterN(jde, -1)
} }
// MarsSemidiameterN 火星视半径(截断版),单位角秒 / truncated apparent Mars semidiameter in arcseconds. // MarsSemidiameterN 火星视半径(截断版),单位角秒 / truncated apparent Mars semidiameter in arcseconds.
func MarsSemidiameterN(jd float64, n int) float64 { func MarsSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jd, n)) return angularSemidiameterFromAU(marsEquatorialRadiusKM, EarthMarsAwayN(jde, n))
} }
// MarsDiameter 火星视直径,单位角秒 / apparent Mars diameter in arcseconds. // MarsDiameter 火星视直径,单位角秒 / apparent Mars diameter in arcseconds.
func MarsDiameter(jd float64) float64 { func MarsDiameter(jde float64) float64 {
return MarsDiameterN(jd, -1) return MarsDiameterN(jde, -1)
} }
// MarsDiameterN 火星视直径(截断版),单位角秒 / truncated apparent Mars diameter in arcseconds. // MarsDiameterN 火星视直径(截断版),单位角秒 / truncated apparent Mars diameter in arcseconds.
func MarsDiameterN(jd float64, n int) float64 { func MarsDiameterN(jde float64, n int) float64 {
return 2 * MarsSemidiameterN(jd, n) return 2 * MarsSemidiameterN(jde, n)
} }
// JupiterSemidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds. // JupiterSemidiameter 木星视半径,单位角秒 / apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameter(jd float64) float64 { func JupiterSemidiameter(jde float64) float64 {
return JupiterSemidiameterN(jd, -1) return JupiterSemidiameterN(jde, -1)
} }
// JupiterSemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds. // JupiterSemidiameterN 木星视半径(截断版),单位角秒 / truncated apparent Jupiter semidiameter in arcseconds.
func JupiterSemidiameterN(jd float64, n int) float64 { func JupiterSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jd, n)) return angularSemidiameterFromAU(jupiterEquatorialRadiusKM, EarthJupiterAwayN(jde, n))
} }
// JupiterDiameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds. // JupiterDiameter 木星视直径,单位角秒 / apparent Jupiter diameter in arcseconds.
func JupiterDiameter(jd float64) float64 { func JupiterDiameter(jde float64) float64 {
return JupiterDiameterN(jd, -1) return JupiterDiameterN(jde, -1)
} }
// JupiterDiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds. // JupiterDiameterN 木星视直径(截断版),单位角秒 / truncated apparent Jupiter diameter in arcseconds.
func JupiterDiameterN(jd float64, n int) float64 { func JupiterDiameterN(jde float64, n int) float64 {
return 2 * JupiterSemidiameterN(jd, n) return 2 * JupiterSemidiameterN(jde, n)
} }
// SaturnSemidiameter 土星视半径,单位角秒 / apparent Saturn semidiameter in arcseconds. // SaturnSemidiameter 土星视半径,单位角秒 / apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameter(jd float64) float64 { func SaturnSemidiameter(jde float64) float64 {
return SaturnSemidiameterN(jd, -1) return SaturnSemidiameterN(jde, -1)
} }
// SaturnSemidiameterN 土星视半径(截断版),单位角秒 / truncated apparent Saturn semidiameter in arcseconds. // SaturnSemidiameterN 土星视半径(截断版),单位角秒 / truncated apparent Saturn semidiameter in arcseconds.
func SaturnSemidiameterN(jd float64, n int) float64 { func SaturnSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jd, n)) return angularSemidiameterFromAU(saturnEquatorialRadiusKM, EarthSaturnAwayN(jde, n))
} }
// SaturnDiameter 土星视直径,单位角秒 / apparent Saturn diameter in arcseconds. // SaturnDiameter 土星视直径,单位角秒 / apparent Saturn diameter in arcseconds.
func SaturnDiameter(jd float64) float64 { func SaturnDiameter(jde float64) float64 {
return SaturnDiameterN(jd, -1) return SaturnDiameterN(jde, -1)
} }
// SaturnDiameterN 土星视直径(截断版),单位角秒 / truncated apparent Saturn diameter in arcseconds. // SaturnDiameterN 土星视直径(截断版),单位角秒 / truncated apparent Saturn diameter in arcseconds.
func SaturnDiameterN(jd float64, n int) float64 { func SaturnDiameterN(jde float64, n int) float64 {
return 2 * SaturnSemidiameterN(jd, n) return 2 * SaturnSemidiameterN(jde, n)
} }
// UranusSemidiameter 天王星视半径,单位角秒 / apparent Uranus semidiameter in arcseconds. // UranusSemidiameter 天王星视半径,单位角秒 / apparent Uranus semidiameter in arcseconds.
func UranusSemidiameter(jd float64) float64 { func UranusSemidiameter(jde float64) float64 {
return UranusSemidiameterN(jd, -1) return UranusSemidiameterN(jde, -1)
} }
// UranusSemidiameterN 天王星视半径(截断版),单位角秒 / truncated apparent Uranus semidiameter in arcseconds. // UranusSemidiameterN 天王星视半径(截断版),单位角秒 / truncated apparent Uranus semidiameter in arcseconds.
func UranusSemidiameterN(jd float64, n int) float64 { func UranusSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jd, n)) return angularSemidiameterFromAU(uranusEquatorialRadiusKM, EarthUranusAwayN(jde, n))
} }
// UranusDiameter 天王星视直径,单位角秒 / apparent Uranus diameter in arcseconds. // UranusDiameter 天王星视直径,单位角秒 / apparent Uranus diameter in arcseconds.
func UranusDiameter(jd float64) float64 { func UranusDiameter(jde float64) float64 {
return UranusDiameterN(jd, -1) return UranusDiameterN(jde, -1)
} }
// UranusDiameterN 天王星视直径(截断版),单位角秒 / truncated apparent Uranus diameter in arcseconds. // UranusDiameterN 天王星视直径(截断版),单位角秒 / truncated apparent Uranus diameter in arcseconds.
func UranusDiameterN(jd float64, n int) float64 { func UranusDiameterN(jde float64, n int) float64 {
return 2 * UranusSemidiameterN(jd, n) return 2 * UranusSemidiameterN(jde, n)
} }
// NeptuneSemidiameter 海王星视半径,单位角秒 / apparent Neptune semidiameter in arcseconds. // NeptuneSemidiameter 海王星视半径,单位角秒 / apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameter(jd float64) float64 { func NeptuneSemidiameter(jde float64) float64 {
return NeptuneSemidiameterN(jd, -1) return NeptuneSemidiameterN(jde, -1)
} }
// NeptuneSemidiameterN 海王星视半径(截断版),单位角秒 / truncated apparent Neptune semidiameter in arcseconds. // NeptuneSemidiameterN 海王星视半径(截断版),单位角秒 / truncated apparent Neptune semidiameter in arcseconds.
func NeptuneSemidiameterN(jd float64, n int) float64 { func NeptuneSemidiameterN(jde float64, n int) float64 {
return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jd, n)) return angularSemidiameterFromAU(neptuneEquatorialRadiusKM, EarthNeptuneAwayN(jde, n))
} }
// NeptuneDiameter 海王星视直径,单位角秒 / apparent Neptune diameter in arcseconds. // NeptuneDiameter 海王星视直径,单位角秒 / apparent Neptune diameter in arcseconds.
func NeptuneDiameter(jd float64) float64 { func NeptuneDiameter(jde float64) float64 {
return NeptuneDiameterN(jd, -1) return NeptuneDiameterN(jde, -1)
} }
// NeptuneDiameterN 海王星视直径(截断版),单位角秒 / truncated apparent Neptune diameter in arcseconds. // NeptuneDiameterN 海王星视直径(截断版),单位角秒 / truncated apparent Neptune diameter in arcseconds.
func NeptuneDiameterN(jd float64, n int) float64 { func NeptuneDiameterN(jde float64, n int) float64 {
return 2 * NeptuneSemidiameterN(jd, n) return 2 * NeptuneSemidiameterN(jde, n)
} }
+2 -2
View File
@@ -51,7 +51,7 @@ func TestAngularDiametersMatchHorizonsBaseline(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err) t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
} }
jd := TD2UT(Date2JDE(date.UTC()), true) jd := UTC2TT(Date2JD(date.UTC()))
for _, tc := range cases { for _, tc := range cases {
want := sample.Values[tc.baselineKey] want := sample.Values[tc.baselineKey]
got := tc.diameter(jd) got := tc.diameter(jd)
@@ -75,7 +75,7 @@ func TestAngularDiametersMatchHorizonsBaseline(t *testing.T) {
} }
func TestAngularDiameterNFullMatchesDefault(t *testing.T) { func TestAngularDiameterNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true) jd := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
cases := []struct { cases := []struct {
name string name string
+2 -2
View File
@@ -7,7 +7,7 @@ import (
) )
func TestLunarEclipseDiagramIncludesContacts(t *testing.T) { func TestLunarEclipseDiagramIncludesContacts(t *testing.T) {
diagram := LunarEclipseDiagram(JDECalc(2026, 3, 3), LunarEclipseDiagramOptions{StepDays: 10.0 / 1440.0}) diagram := LunarEclipseDiagram(JDCalc(2026, 3, 3), LunarEclipseDiagramOptions{StepDays: 10.0 / 1440.0})
if diagram.Eclipse.Type != LunarEclipseTotal { if diagram.Eclipse.Type != LunarEclipseTotal {
t.Fatalf("unexpected eclipse type: got %s want %s", diagram.Eclipse.Type, LunarEclipseTotal) t.Fatalf("unexpected eclipse type: got %s want %s", diagram.Eclipse.Type, LunarEclipseTotal)
} }
@@ -38,7 +38,7 @@ func TestLunarEclipseDiagramIncludesContacts(t *testing.T) {
func TestLocalSolarEclipseDiagramIncludesContacts(t *testing.T) { func TestLocalSolarEclipseDiagramIncludesContacts(t *testing.T) {
diagram := LocalSolarEclipseDiagram( diagram := LocalSolarEclipseDiagram(
TD2UT(Date2JDE(time.Date(2024, 4, 8, 12, 0, 0, 0, time.UTC)), true), UTC2TT(Date2JD(time.Date(2024, 4, 8, 12, 0, 0, 0, time.UTC))),
-96.7970, -96.7970,
32.7767, 32.7767,
0, 0,
+2 -2
View File
@@ -94,12 +94,12 @@ func greatestTimeContourAlignedLevels(startTT, endTT float64, step time.Duration
// greatestTimeContourTTToUTC 把 TT 儒略日换成对应的 UTC 时刻。 // greatestTimeContourTTToUTC 把 TT 儒略日换成对应的 UTC 时刻。
func greatestTimeContourTTToUTC(tt float64) time.Time { func greatestTimeContourTTToUTC(tt float64) time.Time {
return JDE2DateByZone(TD2UT(tt, false), time.UTC, false) return JD2DateByZone(TT2UTC(tt), time.UTC, false)
} }
// greatestTimeContourUTCToTT 把 UTC 时刻换成对应的 TT 儒略日。 // greatestTimeContourUTCToTT 把 UTC 时刻换成对应的 TT 儒略日。
func greatestTimeContourUTCToTT(value time.Time) float64 { func greatestTimeContourUTCToTT(value time.Time) float64 {
return TD2UT(Date2JDE(value.UTC()), true) return UTC2TT(Date2JD(value.UTC()))
} }
// greatestTimeContourPointSegmentKM 返回点到折线段的距离,单位 km。 // greatestTimeContourPointSegmentKM 返回点到折线段的距离,单位 km。
+1 -1
View File
@@ -8,7 +8,7 @@ import (
// 对齐网格是导出行为(等时线按整刻度取值)的契约,取值本身写成字面量以免与实际生成函数互相自证。 // 对齐网格是导出行为(等时线按整刻度取值)的契约,取值本身写成字面量以免与实际生成函数互相自证。
func TestGreatestTimeContourAlignedLevelsGridContracts(t *testing.T) { func TestGreatestTimeContourAlignedLevelsGridContracts(t *testing.T) {
tt := func(hour, minute int) float64 { tt := func(hour, minute int) float64 {
return TD2UT(Date2JDE(time.Date(2024, 1, 1, hour, minute, 0, 0, time.UTC)), true) return UTC2TT(Date2JD(time.Date(2024, 1, 1, hour, minute, 0, 0, time.UTC)))
} }
cases := []struct { cases := []struct {
name string name string
+3 -3
View File
@@ -9,7 +9,7 @@ const (
) )
func eventQueryTTAsUT(queryTT float64) float64 { func eventQueryTTAsUT(queryTT float64) float64 {
return TD2UT(queryTT, false) return TT2UTC(queryTT)
} }
func eventUTQueryTTDelta(eventUT, queryTT float64) float64 { func eventUTQueryTTDelta(eventUT, queryTT float64) float64 {
@@ -25,11 +25,11 @@ func eventUTQueryAfterOrEqual(eventUT, queryTT float64) bool {
} }
func eventUTNextQueryTT(eventUT float64) float64 { func eventUTNextQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) + 1.0 return UTC2TT(eventUT) + 1.0
} }
func eventUTLastQueryTT(eventUT float64) float64 { func eventUTLastQueryTT(eventUT float64) float64 {
return TD2UT(eventUT, true) - 1.0 return UTC2TT(eventUT) - 1.0
} }
func innerNextCycleOffset(delta, period float64) float64 { func innerNextCycleOffset(delta, period float64) float64 {
+3 -3
View File
@@ -34,7 +34,7 @@ func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true) queryTT := UTC2TT(tc.seed)
last := tc.lastFn(queryTT) last := tc.lastFn(queryTT)
next := tc.nextFn(queryTT) next := tc.nextFn(queryTT)
if !sameEventJD(last, tc.seed) { if !sameEventJD(last, tc.seed) {
@@ -64,7 +64,7 @@ func TestInnerPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
first := tc.next(tc.seed) first := tc.next(tc.seed)
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true) query := UTC2TT(Date2JD(JD2DateByZone(first, time.UTC, false).Add(time.Second)))
next := tc.next(query) next := tc.next(query)
if !eventUTQueryAfterOrEqual(next, query) { if !eventUTQueryAfterOrEqual(next, query) {
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next) t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
@@ -91,7 +91,7 @@ func TestInnerPlanetTypedConjunctionExactBoundaryIncludesCurrent(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
queryTT := TD2UT(tc.seed, true) queryTT := UTC2TT(tc.seed)
last := tc.last(queryTT) last := tc.last(queryTT)
next := tc.next(queryTT) next := tc.next(queryTT)
if !sameEventJD(last, tc.seed) { if !sameEventJD(last, tc.seed) {
+2 -2
View File
@@ -152,8 +152,8 @@ func assertInnerBaselineEvent(t *testing.T, event innerBaselineEvent, lastFn, ne
when := parseInnerBaselineTime(t, event.VerifiedJST) when := parseInnerBaselineTime(t, event.VerifiedJST)
before := when.Add(-24 * time.Hour) before := when.Add(-24 * time.Hour)
after := when.Add(24 * time.Hour) after := when.Add(24 * time.Hour)
next := JDE2DateByZone(nextFn(toUTJD(before)), when.Location(), false) next := JD2DateByZone(nextFn(toUTJD(before)), when.Location(), false)
last := JDE2DateByZone(lastFn(toUTJD(after)), when.Location(), false) last := JD2DateByZone(lastFn(toUTJD(after)), when.Location(), false)
tolerance := innerBaselineTolerance(event) tolerance := innerBaselineTolerance(event)
if diff := next.Sub(when); diff < -tolerance || diff > tolerance { if diff := next.Sub(when); diff < -tolerance || diff > tolerance {
+25 -21
View File
@@ -9,10 +9,10 @@ import (
var ErrInvalidCivilDate = errors.New("invalid civil date") var ErrInvalidCivilDate = errors.New("invalid civil date")
var timeNow = time.Now var timeNow = time.Now
// Date2JDE 日期转儒略日 // Date2JD 日期转儒略日
func Date2JDE(date time.Time) float64 { func Date2JD(date time.Time) float64 {
day := float64(date.Day()) + float64(date.Hour())/24.0 + float64(date.Minute())/24.0/60.0 + float64(date.Second())/24.0/3600.0 + float64(date.Nanosecond())/1000000000.0/3600.0/24.0 day := float64(date.Day()) + float64(date.Hour())/24.0 + float64(date.Minute())/24.0/60.0 + float64(date.Second())/24.0/3600.0 + float64(date.Nanosecond())/1000000000.0/3600.0/24.0
return JDECalc(date.Year(), int(date.Month()), day) return JDCalc(date.Year(), int(date.Month()), day)
} }
func ValidateCivilDate(year, month int, day float64) error { func ValidateCivilDate(year, month int, day float64) error {
@@ -68,12 +68,10 @@ func isCivilLeapYear(year, month int, day float64) bool {
return year%4 == 0 return year%4 == 0
} }
/* // JDCalc 由公历年月日(day 可含小数)计算儒略日 / Julian day from a civil year, month and fractional day.
@name: 儒略日计算 // 1582 年 10 月 15 日起按格里高利历,之前按儒略历;日期非法时返回 NaN。
@dec: 计算给定时间的儒略日,1582年改力后为格里高利历,之前为儒略历 // Gregorian from 1582-10-15 onward and Julian before it; an invalid civil date yields NaN.
@ 请注意,传入的时间在天文计算中一般为力学时,应当注意和世界时的转化 func JDCalc(year, month int, day float64) float64 {
*/
func JDECalc(year, month int, day float64) float64 {
if err := ValidateCivilDate(year, month, day); err != nil { if err := ValidateCivilDate(year, month, day); err != nil {
return math.NaN() return math.NaN()
} }
@@ -92,17 +90,20 @@ func JDECalc(year, month int, day float64) float64 {
return (math.Floor(365.25*(float64(year)+4716.0)) + math.Floor(30.6001*float64(month+1)) + day + float64(gregorianCorrection) - 1524.5) return (math.Floor(365.25*(float64(year)+4716.0)) + math.Floor(30.6001*float64(month+1)) + day + float64(gregorianCorrection) - 1524.5)
} }
/* // GetNowJD 按当前时区的日历字段取儒略日 / Julian day from the current clock's calendar fields.
@name: 获得当前儒略日时间:当地世界时,非格林尼治时间 // 读的是当前时区的年月日时分秒,不做时区归算。
*/ // It reads the calendar fields in the current location without any zone conversion.
func GetNowJDE() (nowJDE float64) { func GetNowJD() (nowJD float64) {
now := timeNow() now := timeNow()
dayFraction := float64(now.Second())/3600.0/24.0 + float64(now.Minute())/60.0/24.0 + float64(now.Hour())/24.0 dayFraction := float64(now.Second())/3600.0/24.0 + float64(now.Minute())/60.0/24.0 + float64(now.Hour())/24.0
nowJDE = JDECalc(now.Year(), int(now.Month()), float64(now.Day())+dayFraction) nowJD = JDCalc(now.Year(), int(now.Month()), float64(now.Day())+dayFraction)
return return
} }
func JDE2Date(jd float64) time.Time { // JD2Date 儒略日转 Local 时区的时刻 / Local-zone instant from a Julian day.
// 儒略日的日历字段按 Local 时区解释,1582 年 10 月 15 日之前按儒略历。
// The calendar fields are read in the Local zone, and dates before 1582-10-15 are read in the Julian calendar.
func JD2Date(jd float64) time.Time {
jd = jd + 0.5 jd = jd + 0.5
z := float64(int(jd)) z := float64(int(jd))
f := jd - z f := jd - z
@@ -137,12 +138,12 @@ func JDE2Date(jd float64) time.Time {
return time.Unix(dates.Unix()+int64(tms), int64((tms-math.Floor(tms))*1000000000)) return time.Unix(dates.Unix()+int64(tms), int64((tms-math.Floor(tms))*1000000000))
} }
// JDE2DateByZone JDE(儒略日)转日期 // JD2DateByZone 儒略日转日期
// jd: 儒略日 // jd: 儒略日
// tz: 目标时区 // tz: 目标时区
// byZone: (true: 传入的儒略日视为目标时区当地时间的儒略日,false: 传入的儒略日视为UTC时间的儒略日) // byZone: (true: 传入的儒略日视为目标时区当地时间的儒略日,false: 传入的儒略日视为UTC时间的儒略日)
// 回参:转换后的日期,时区始终为目标时区 // 回参:转换后的日期,时区始终为目标时区
func JDE2DateByZone(jd float64, tz *time.Location, byZone bool) time.Time { func JD2DateByZone(jd float64, tz *time.Location, byZone bool) time.Time {
jd = jd + 0.5 jd = jd + 0.5
z := float64(int(jd)) z := float64(int(jd))
f := jd - z f := jd - z
@@ -172,10 +173,13 @@ func JDE2DateByZone(jd float64, tz *time.Location, byZone bool) time.Time {
} }
tms := (days - math.Floor(days)) * 24 * 3600 tms := (days - math.Floor(days)) * 24 * 3600
days = math.Floor(days) days = math.Floor(days)
var transTz = tz date := time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, time.UTC).
Add(time.Duration(int64(1000000000 * tms)))
if !byZone { if !byZone {
transTz = time.UTC return date.In(tz)
} }
return time.Date(int(years), time.Month(int(months)), int(days), 0, 0, 0, 0, transTz). // 当地 JD 的小数部分是钟表读数,不能按夏令时午夜后的实际时长累加。
Add(time.Duration(int64(1000000000 * tms))).In(tz) year, month, day := date.Date()
hour, minute, second := date.Clock()
return time.Date(year, month, day, hour, minute, second, date.Nanosecond(), tz)
} }
+5 -5
View File
@@ -6,7 +6,7 @@ import (
"time" "time"
) )
func TestGetNowJDEUsesSingleTimestamp(t *testing.T) { func TestGetNowJDUsesSingleTimestamp(t *testing.T) {
oldTimeNow := timeNow oldTimeNow := timeNow
defer func() { defer func() {
timeNow = oldTimeNow timeNow = oldTimeNow
@@ -23,12 +23,12 @@ func TestGetNowJDEUsesSingleTimestamp(t *testing.T) {
return second return second
} }
got := GetNowJDE() got := GetNowJD()
want := Date2JDE(first) want := Date2JD(first)
if calls != 1 { if calls != 1 {
t.Fatalf("GetNowJDE should read current time once, got %d calls", calls) t.Fatalf("GetNowJD should read current time once, got %d calls", calls)
} }
if math.Float64bits(got) != math.Float64bits(want) { if math.Float64bits(got) != math.Float64bits(want) {
t.Fatalf("GetNowJDE mismatch: got %.15f want %.15f", got, want) t.Fatalf("GetNowJD mismatch: got %.15f want %.15f", got, want)
} }
} }
+28
View File
@@ -0,0 +1,28 @@
package basic
import (
"testing"
"time"
)
func TestLocalJulianDayAcrossDST(t *testing.T) {
for _, name := range []string{"America/New_York", "Australia/Lord_Howe"} {
loc, err := time.LoadLocation(name)
if err != nil {
t.Fatal(err)
}
for _, md := range [][2]int{{3, 8}, {11, 1}, {4, 5}, {10, 4}} {
for _, hour := range []int{0, 3, 12, 23} {
want := time.Date(2026, time.Month(md[0]), md[1], hour, 17, 23, 125000000, loc)
got := JD2DateByZone(Date2JD(want), loc, true)
if delta := got.Sub(want); delta < -100*time.Microsecond || delta > 100*time.Microsecond {
t.Errorf("local %s: got %s, delta=%s", want, got, delta)
}
got = JD2DateByZone(Date2JD(want.UTC()), loc, false)
if delta := got.Sub(want); delta < -100*time.Microsecond || delta > 100*time.Microsecond {
t.Errorf("UTC %s: got %s, delta=%s", want, got, delta)
}
}
}
}
}
+63 -63
View File
@@ -7,79 +7,79 @@ import (
. "b612.me/astro/tools" . "b612.me/astro/tools"
) )
func JupiterL(jd float64) float64 { func JupiterL(jde float64) float64 {
return planet.WherePlanet(4, 0, jd) return planet.WherePlanet(4, 0, jde)
} }
func JupiterB(jd float64) float64 { func JupiterB(jde float64) float64 {
return planet.WherePlanet(4, 1, jd) return planet.WherePlanet(4, 1, jde)
} }
func JupiterR(jd float64) float64 { func JupiterR(jde float64) float64 {
return planet.WherePlanet(4, 2, jd) return planet.WherePlanet(4, 2, jde)
} }
func AJupiterX(jd float64) float64 { func AJupiterX(jde float64) float64 {
l := JupiterL(jd) l := JupiterL(jde)
b := JupiterB(jd) b := JupiterB(jde)
r := JupiterR(jd) r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x return x
} }
func AJupiterY(jd float64) float64 { func AJupiterY(jde float64) float64 {
l := JupiterL(jd) l := JupiterL(jde)
b := JupiterB(jd) b := JupiterB(jde)
r := JupiterR(jd) r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y return y
} }
func AJupiterZ(jd float64) float64 { func AJupiterZ(jde float64) float64 {
//l := JupiterL(jd) //l := JupiterL(jde)
b := JupiterB(jd) b := JupiterB(jde)
r := JupiterR(jd) r := JupiterR(jde)
// el := planet.WherePlanet(-1, 0, jd) // el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return z return z
} }
func AJupiterXYZ(jd float64) (float64, float64, float64) { func AJupiterXYZ(jde float64) (float64, float64, float64) {
l := JupiterL(jd) l := JupiterL(jde)
b := JupiterB(jd) b := JupiterB(jde)
r := JupiterR(jd) r := JupiterR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return x, y, z return x, y, z
} }
func JupiterApparentRa(jd float64) float64 { func JupiterApparentRa(jde float64) float64 {
lo, bo := JupiterApparentLoBo(jd) lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo)) ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi ra = ra * 180 / math.Pi
return Limit360(ra) return Limit360(ra)
} }
func JupiterApparentDec(jd float64) float64 { func JupiterApparentDec(jde float64) float64 {
lo, bo := JupiterApparentLoBo(jd) lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec return dec
} }
func JupiterApparentRaDec(jd float64) (float64, float64) { func JupiterApparentRaDec(jde float64) (float64, float64) {
lo, bo := JupiterApparentLoBo(jd) lo, bo := JupiterApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo)) ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
@@ -105,22 +105,22 @@ func JupiterApparentLoBo(jd float64) (float64, float64) {
return geo.lo, geo.bo return geo.lo, geo.bo
} }
func JupiterMag(jd float64) float64 { func JupiterMag(jde float64) float64 {
sunDistance := JupiterR(jd) sunDistance := JupiterR(jde)
earthDistance := EarthJupiterAway(jd) earthDistance := EarthJupiterAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jd) earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance) i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i) i = ArcCos(i)
mag := -9.40 + 5*math.Log10(sunDistance*earthDistance) + 0.0005*i mag := -9.40 + 5*math.Log10(sunDistance*earthDistance) + 0.0005*i
return FloatRound(mag, 2) return FloatRound(mag, 2)
} }
func JupiterHeight(jde, lon, lat, timezone float64) float64 { func JupiterHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := JupiterApparentRaDec(TD2UT(utcJde, true)) ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式 // 高度角、时角与天球座标三角转换公式
@@ -129,12 +129,12 @@ func JupiterHeight(jde, lon, lat, timezone float64) float64 {
return ArcSin(sinHeight) return ArcSin(sinHeight)
} }
func JupiterAzimuth(jde, lon, lat, timezone float64) float64 { func JupiterAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := JupiterApparentRaDec(TD2UT(utcJde, true)) ra, dec := JupiterApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 三角转换公式 // 三角转换公式
@@ -153,21 +153,21 @@ func JupiterAzimuth(jde, lon, lat, timezone float64) float64 {
} }
func JupiterHourAngle(jd, lon, timezone float64) float64 { func JupiterHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon) siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - JupiterApparentRa(TD2UT(jd-timezone/24.0, true)) hourAngle := siderealLongitude - JupiterApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 { if hourAngle < 0 {
hourAngle += 360 hourAngle += 360
} }
return hourAngle return hourAngle
} }
func JupiterCulminationTime(jde, lon, timezone float64) float64 { func JupiterCulminationTime(localJD, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时 // localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标 //ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5 localJD = math.Floor(localJD) + 0.5
estimateJD := jde + Limit360(360-JupiterHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851 estimateJD := localJD + Limit360(360-JupiterHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 { normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := JupiterHourAngle(jde, lon, timezone) currentHourAngle := JupiterHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 { if currentHourAngle < 180 {
currentHourAngle += 360 currentHourAngle += 360
} }
+27 -27
View File
@@ -74,15 +74,15 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
} else { } else {
jde += daysPerDegree * currentDelta jde += daysPerDegree * currentDelta
} }
estimateJD := jde estimateJDE := jde
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true) longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -90,7 +90,7 @@ func jupiterConjunctionFull(jde, degree float64, next uint8) float64 {
if !converged { if !converged {
return math.NaN() return math.NaN()
} }
return TD2UT(estimateJD, false) return TT2UTC(estimateJDE)
} }
func jupiterConjunction(jde, degree float64, next uint8) float64 { func jupiterConjunction(jde, degree float64, next uint8) float64 {
@@ -105,15 +105,15 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
} else { } else {
jde += daysPerDegree * currentDelta jde += daysPerDegree * currentDelta
} }
estimateJD := jde estimateJDE := jde
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDeltaN(prevJD, degree, true, jupiterEventSearchN) longitudeDelta := jupiterSunLongitudeDeltaN(prevJDE, degree, true, jupiterEventSearchN)
longitudeSlope := (jupiterSunLongitudeDeltaN(prevJD+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJD-0.000005, degree, true, jupiterEventSearchN)) / 0.00001 longitudeSlope := (jupiterSunLongitudeDeltaN(prevJDE+0.000005, degree, true, jupiterEventSearchN) - jupiterSunLongitudeDeltaN(prevJDE-0.000005, degree, true, jupiterEventSearchN)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= jupiterPhaseCoarseTolerance { if math.Abs(nextJD-prevJDE) <= jupiterPhaseCoarseTolerance {
converged = true converged = true
break break
} }
@@ -123,12 +123,12 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
} }
converged = false converged = false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := jupiterSunLongitudeDelta(prevJD, degree, true) longitudeDelta := jupiterSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (jupiterSunLongitudeDelta(prevJD+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 longitudeSlope := (jupiterSunLongitudeDelta(prevJDE+0.000005, degree, true) - jupiterSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -136,7 +136,7 @@ func jupiterConjunction(jde, degree float64, next uint8) float64 {
if !converged { if !converged {
return math.NaN() return math.NaN()
} }
return TD2UT(estimateJD, false) return TT2UTC(estimateJDE)
} }
func LastJupiterConjunction(jde float64) float64 { func LastJupiterConjunction(jde float64) float64 {
@@ -175,22 +175,22 @@ func jupiterRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposit
if !isFiniteFloat(oppositionJD) { if !isFiniteFloat(oppositionJD) {
return math.NaN() return math.NaN()
} }
oppositionTT := TD2UT(oppositionJD, true) oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT startTT := oppositionTT
endTT := oppositionTT endTT := oppositionTT
if searchBeforeOpposition { if searchBeforeOpposition {
easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0) easternQuadratureUT := jupiterConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true) startTT = UTC2TT(easternQuadratureUT)
} else { } else {
westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1) westernQuadratureUT := jupiterConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true) endTT = UTC2TT(westernQuadratureUT)
} }
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 { bestJDE := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return jupiterRADerivativeN(jd, stationDerivativeStepDay, jupiterEventSearchN) return jupiterRADerivativeN(jd, stationDerivativeStepDay, jupiterEventSearchN)
}, func(jd float64) float64 { }, func(jd float64) float64 {
return jupiterRADerivative(jd, stationDerivativeStepDay) return jupiterRADerivative(jd, stationDerivativeStepDay)
}) })
return TD2UT(bestJD, false) return TT2UTC(bestJDE)
} }
func NextJupiterRetrogradeToPrograde(jde float64) float64 { func NextJupiterRetrogradeToPrograde(jde float64) float64 {
+18 -18
View File
@@ -25,14 +25,14 @@ type JupiterCentralMeridianInfo struct {
} }
// JupiterCentralMeridians 木星 System I/II/III 中央经线 / Jupiter System I/II/III central meridians. // JupiterCentralMeridians 木星 System I/II/III 中央经线 / Jupiter System I/II/III central meridians.
func JupiterCentralMeridians(jd float64) JupiterCentralMeridianInfo { func JupiterCentralMeridians(jde float64) JupiterCentralMeridianInfo {
return JupiterCentralMeridiansN(jd, -1) return JupiterCentralMeridiansN(jde, -1)
} }
// JupiterCentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians. // JupiterCentralMeridiansN 木星 System I/II/III 中央经线(截断版) / truncated Jupiter System I/II/III central meridians.
func JupiterCentralMeridiansN(jd float64, n int) JupiterCentralMeridianInfo { func JupiterCentralMeridiansN(jde float64, n int) JupiterCentralMeridianInfo {
observations := jupiterPhysicalObservationsN(jd, n) observations := jupiterPhysicalObservationsN(jde, n)
physical := JupiterPhysicalN(jd, n) physical := JupiterPhysicalN(jde, n)
return JupiterCentralMeridianInfo{ return JupiterCentralMeridianInfo{
SystemI: observations.SystemI, SystemI: observations.SystemI,
SystemII: observations.SystemII, SystemII: observations.SystemII,
@@ -41,18 +41,18 @@ func JupiterCentralMeridiansN(jd float64, n int) JupiterCentralMeridianInfo {
} }
// JupiterDSDE 木星 DS/DE 行星中心赤纬 / Jupiter planetocentric declinations of Sun and Earth. // JupiterDSDE 木星 DS/DE 行星中心赤纬 / Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDE(jd float64) (ds, de float64) { func JupiterDSDE(jde float64) (ds, de float64) {
return JupiterDSDEN(jd, -1) return JupiterDSDEN(jde, -1)
} }
// JupiterDSDEN 木星 DS/DE 行星中心赤纬(截断版) / truncated Jupiter planetocentric declinations of Sun and Earth. // JupiterDSDEN 木星 DS/DE 行星中心赤纬(截断版) / truncated Jupiter planetocentric declinations of Sun and Earth.
func JupiterDSDEN(jd float64, n int) (ds, de float64) { func JupiterDSDEN(jde float64, n int) (ds, de float64) {
observations := jupiterPhysicalObservationsN(jd, n) observations := jupiterPhysicalObservationsN(jde, n)
return observations.DS, observations.DE return observations.DS, observations.DE
} }
func jupiterPhysicalObservationsN(jd float64, n int) jupiterPhysicalObservationInfo { func jupiterPhysicalObservationsN(jde float64, n int) jupiterPhysicalObservationInfo {
days := jd - 2433282.5 days := jde - 2433282.5
julianCentury := days / 36525.0 julianCentury := days / 36525.0
poleRA := (268.0 + 0.1061*julianCentury) * rad poleRA := (268.0 + 0.1061*julianCentury) * rad
@@ -60,9 +60,9 @@ func jupiterPhysicalObservationsN(jd float64, n int) jupiterPhysicalObservationI
w1 := (17.71 + 877.90003539*days) * rad w1 := (17.71 + 877.90003539*days) * rad
w2 := (16.838 + 870.27003539*days) * rad w2 := (16.838 + 870.27003539*days) * rad
earthLon := planet.WherePlanetN(-1, 0, jd, n) earthLon := planet.WherePlanetN(-1, 0, jde, n)
earthLat := planet.WherePlanetN(-1, 1, jd, n) earthLat := planet.WherePlanetN(-1, 1, jde, n)
earthRadius := planet.WherePlanetN(-1, 2, jd, n) earthRadius := planet.WherePlanetN(-1, 2, jde, n)
delta := 4.0 delta := 4.0
var jupiterLon float64 var jupiterLon float64
@@ -73,16 +73,16 @@ func jupiterPhysicalObservationsN(jd float64, n int) jupiterPhysicalObservationI
var z float64 var z float64
for i := 0; i < 2; i++ { for i := 0; i < 2; i++ {
lightTimeDays := astronomicalUnitLightTimeDays * delta lightTimeDays := astronomicalUnitLightTimeDays * delta
jupiterLon = planet.WherePlanetN(4, 0, jd-lightTimeDays, n) jupiterLon = planet.WherePlanetN(4, 0, jde-lightTimeDays, n)
jupiterLat = planet.WherePlanetN(4, 1, jd-lightTimeDays, n) jupiterLat = planet.WherePlanetN(4, 1, jde-lightTimeDays, n)
jupiterRadius = planet.WherePlanetN(4, 2, jd-lightTimeDays, n) jupiterRadius = planet.WherePlanetN(4, 2, jde-lightTimeDays, n)
x = jupiterRadius*Cos(jupiterLat)*Cos(jupiterLon) - earthRadius*Cos(earthLat)*Cos(earthLon) x = jupiterRadius*Cos(jupiterLat)*Cos(jupiterLon) - earthRadius*Cos(earthLat)*Cos(earthLon)
y = jupiterRadius*Cos(jupiterLat)*Sin(jupiterLon) - earthRadius*Cos(earthLat)*Sin(earthLon) y = jupiterRadius*Cos(jupiterLat)*Sin(jupiterLon) - earthRadius*Cos(earthLat)*Sin(earthLon)
z = jupiterRadius*Sin(jupiterLat) - earthRadius*Sin(earthLat) z = jupiterRadius*Sin(jupiterLat) - earthRadius*Sin(earthLat)
delta = math.Sqrt(x*x + y*y + z*z) delta = math.Sqrt(x*x + y*y + z*z)
} }
meanObliquity := EclipticObliquity(jd, false) meanObliquity := EclipticObliquity(jde, false)
sinMeanObliquity, cosMeanObliquity := math.Sincos(meanObliquity) sinMeanObliquity, cosMeanObliquity := math.Sincos(meanObliquity)
sinJupiterLat, cosJupiterLat := math.Sincos(jupiterLat * rad) sinJupiterLat, cosJupiterLat := math.Sincos(jupiterLat * rad)
sinJupiterLon, cosJupiterLon := math.Sincos(jupiterLon * rad) sinJupiterLon, cosJupiterLon := math.Sincos(jupiterLon * rad)
+3 -3
View File
@@ -37,14 +37,14 @@ func TestJupiterCentralMeridianSystemIIIMatchesHorizonsBaseline(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err) t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
} }
jd := TD2UT(Date2JDE(date.UTC()), true) jd := UTC2TT(Date2JD(date.UTC()))
got := JupiterCentralMeridians(jd) got := JupiterCentralMeridians(jd)
assertPlanetPhaseClose(t, "Jupiter."+sample.InputUTC+".CMIII", got.SystemIII, sample.SubEarthLongitude, 0.02) assertPlanetPhaseClose(t, "Jupiter."+sample.InputUTC+".CMIII", got.SystemIII, sample.SubEarthLongitude, 0.02)
} }
} }
func TestJupiterCentralMeridiansNFullMatchesDefault(t *testing.T) { func TestJupiterCentralMeridiansNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true) jd := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
got := JupiterCentralMeridians(jd) got := JupiterCentralMeridians(jd)
gotN := JupiterCentralMeridiansN(jd, -1) gotN := JupiterCentralMeridiansN(jd, -1)
ds, de := JupiterDSDE(jd) ds, de := JupiterDSDE(jd)
@@ -76,7 +76,7 @@ func TestJupiterCentralMeridianAndDSDESampleSweepFiniteAndInRange(t *testing.T)
} }
for _, date := range dates { for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true) jd := UTC2TT(Date2JD(date.UTC()))
meridians := JupiterCentralMeridians(jd) meridians := JupiterCentralMeridians(jd)
ds, de := JupiterDSDE(jd) ds, de := JupiterDSDE(jd)
physical := JupiterPhysical(jd) physical := JupiterPhysical(jd)
+6 -6
View File
@@ -464,8 +464,8 @@ func jupiterGalileanContactGeometryAt(
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) { if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanContactGeometry{}, false return jupiterGalileanContactGeometry{}, false
} }
evaluationJD := TD2UT(jd, true) evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJD) context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 { if context.jupiterDistance == 0 {
return jupiterGalileanContactGeometry{}, false return jupiterGalileanContactGeometry{}, false
} }
@@ -527,8 +527,8 @@ func jupiterGalileanEclipseSignedDistanceAt(jd float64, satellite int, penumbra
if !isFinite(jd) || satellite < 1 || satellite > 4 { if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false return math.NaN(), false
} }
evaluationJD := TD2UT(jd, true) evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJD) context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 { if context.jupiterDistance == 0 {
return math.NaN(), false return math.NaN(), false
} }
@@ -590,8 +590,8 @@ func jupiterGalileanShadowLimbMetricAt(jd float64, satellite int, penumbra bool)
if !isFinite(jd) || satellite < 1 || satellite > 4 { if !isFinite(jd) || satellite < 1 || satellite > 4 {
return math.NaN(), false return math.NaN(), false
} }
evaluationJD := TD2UT(jd, true) evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJD) context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 { if context.jupiterDistance == 0 {
return math.NaN(), false return math.NaN(), false
} }
@@ -18,13 +18,13 @@ func TestJupiterGalileanPhenomenonContactEventsAgainstIMCCEBaseline(t *testing.T
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2) queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type) phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
event := ClosestJupiterGalileanPhenomenonContactEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType) event := ClosestJupiterGalileanPhenomenonContactEvent(Date2JD(queryMid.UTC()), record.Satellite, phenomenonType)
if !event.Valid { if !event.Valid {
t.Fatalf("%s invalid contact event", record.Label) t.Fatalf("%s invalid contact event", record.Label)
} }
gotStart := JDE2DateByZone(event.Disappearance.Start, time.UTC, false) gotStart := JD2DateByZone(event.Disappearance.Start, time.UTC, false)
gotEnd := JDE2DateByZone(event.Reappearance.Start, time.UTC, false) gotEnd := JD2DateByZone(event.Reappearance.Start, time.UTC, false)
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds()) startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds()) endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
startDurationDiff := math.Abs((event.Disappearance.End-event.Disappearance.Start)*86400 - record.StartDurationMinutes*60) startDurationDiff := math.Abs((event.Disappearance.End-event.Disappearance.Start)*86400 - record.StartDurationMinutes*60)
@@ -61,7 +61,7 @@ func TestJupiterGalileanPhenomenonContactEventsAgainstIMCCEBaseline(t *testing.T
if !(event.Reappearance.Start <= event.Reappearance.ModelCrossing && event.Reappearance.ModelCrossing <= event.Reappearance.End) { if !(event.Reappearance.Start <= event.Reappearance.ModelCrossing && event.Reappearance.ModelCrossing <= event.Reappearance.End) {
t.Fatalf("%s reappearance ordering invalid", record.Label) t.Fatalf("%s reappearance ordering invalid", record.Label)
} }
fullEvent := ClosestJupiterGalileanPhenomenonEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType) fullEvent := ClosestJupiterGalileanPhenomenonEvent(Date2JD(queryMid.UTC()), record.Satellite, phenomenonType)
if phenomenonType != JupiterGalileanShadowTransit { if phenomenonType != JupiterGalileanShadowTransit {
if math.Abs(event.Disappearance.ModelCrossing-fullEvent.Start)*86400 > 2 { if math.Abs(event.Disappearance.ModelCrossing-fullEvent.Start)*86400 > 2 {
t.Fatalf("%s disappearance model crossing mismatch", record.Label) t.Fatalf("%s disappearance model crossing mismatch", record.Label)
+5 -5
View File
@@ -58,7 +58,7 @@ type jupiterGalileanShadowPoint struct {
// LastJupiterGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event. // LastJupiterGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event.
// //
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。 // jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent { func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true) event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
return event return event
@@ -66,7 +66,7 @@ func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonTyp
// NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event. // NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
// //
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。 // jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent { func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false) event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
return event return event
@@ -74,7 +74,7 @@ func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonTyp
// ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event. // ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
// //
// jd 与返回时刻都是 UTC/UT 儒略日;需要瞬时状态时先用 TD2UT(jd, true) 换成 TT。 // jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func ClosestJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent { func ClosestJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true) last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false) next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
@@ -428,8 +428,8 @@ func jupiterGalileanPhenomenonMetricAt(
} }
} }
evaluationJD := TD2UT(jd, true) evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJD) context := newJupiterGalileanObservationContext(evaluationJDE)
if context.jupiterDistance == 0 { if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{ return jupiterGalileanMetricSample{
metric: math.Inf(1), metric: math.Inf(1),
+5 -5
View File
@@ -33,9 +33,9 @@ func TestJupiterGalileanPhenomenonEventsAgainstIMCCEBaseline(t *testing.T) {
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2) queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type) phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
next := NextJupiterGalileanPhenomenonEvent(Date2JDE(queryBefore.UTC()), record.Satellite, phenomenonType) next := NextJupiterGalileanPhenomenonEvent(Date2JD(queryBefore.UTC()), record.Satellite, phenomenonType)
last := LastJupiterGalileanPhenomenonEvent(Date2JDE(queryAfter.UTC()), record.Satellite, phenomenonType) last := LastJupiterGalileanPhenomenonEvent(Date2JD(queryAfter.UTC()), record.Satellite, phenomenonType)
closest := ClosestJupiterGalileanPhenomenonEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType) closest := ClosestJupiterGalileanPhenomenonEvent(Date2JD(queryMid.UTC()), record.Satellite, phenomenonType)
assertGalileanEventMatchesBaseline(t, record.Label+" next", next, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff) assertGalileanEventMatchesBaseline(t, record.Label+" next", next, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
assertGalileanEventMatchesBaseline(t, record.Label+" last", last, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff) assertGalileanEventMatchesBaseline(t, record.Label+" last", last, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
@@ -62,8 +62,8 @@ func assertGalileanEventMatchesBaseline(
if string(event.Type) != record.Type { if string(event.Type) != record.Type {
t.Fatalf("%s type mismatch: got %q want %q", name, event.Type, record.Type) t.Fatalf("%s type mismatch: got %q want %q", name, event.Type, record.Type)
} }
gotStart := JDE2DateByZone(event.Start, time.UTC, false) gotStart := JD2DateByZone(event.Start, time.UTC, false)
gotEnd := JDE2DateByZone(event.End, time.UTC, false) gotEnd := JD2DateByZone(event.End, time.UTC, false)
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds()) startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds()) endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
if startDiff > *maxStartDiff { if startDiff > *maxStartDiff {
+5 -5
View File
@@ -22,18 +22,18 @@ type JupiterGalileanPhenomenon struct {
} }
// JupiterGalileanSatellitePhenomenon 单颗伽利略卫星瞬时现象 / instantaneous phenomena of one Galilean satellite. // JupiterGalileanSatellitePhenomenon 单颗伽利略卫星瞬时现象 / instantaneous phenomena of one Galilean satellite.
func JupiterGalileanSatellitePhenomenon(jd float64, satellite int) JupiterGalileanPhenomenon { func JupiterGalileanSatellitePhenomenon(jde float64, satellite int) JupiterGalileanPhenomenon {
if satellite < 1 || satellite > 4 || !isFinite(jd) { if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanPhenomenon() return invalidJupiterGalileanPhenomenon()
} }
context := newJupiterGalileanObservationContext(jd) context := newJupiterGalileanObservationContext(jde)
return context.phenomenonForSatellite(satellite - 1) return context.phenomenonForSatellite(satellite - 1)
} }
// JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites. // JupiterGalileanSatellitePhenomena 四颗伽利略卫星瞬时现象 / instantaneous phenomena of the four Galilean satellites.
func JupiterGalileanSatellitePhenomena(jd float64) [4]JupiterGalileanPhenomenon { func JupiterGalileanSatellitePhenomena(jde float64) [4]JupiterGalileanPhenomenon {
var phenomena [4]JupiterGalileanPhenomenon var phenomena [4]JupiterGalileanPhenomenon
context := newJupiterGalileanObservationContext(jd) context := newJupiterGalileanObservationContext(jde)
for i := range phenomena { for i := range phenomena {
phenomena[i] = context.phenomenonForSatellite(i) phenomena[i] = context.phenomenonForSatellite(i)
} }
+20 -20
View File
@@ -35,7 +35,7 @@ type jupiterGalileanL1Term struct {
// JupiterGalileanState 木星伽利略卫星原始状态 / raw Galilean-satellite state. // JupiterGalileanState 木星伽利略卫星原始状态 / raw Galilean-satellite state.
// //
// 输入 jd 使用 TT/TDB 对应的儒略日;返回值为 IMCCE L1 理论的木心 J2000 平赤道直角坐标与速度,单位 AU / AU/day。 // 输入 jd 使用 TT/TDB 对应的儒略日;返回值为 IMCCE L1 理论的木心 J2000 平赤道直角坐标与速度,单位 AU / AU/day。
// The input jd is a TT/TDB Julian day (convert a UT query with TD2UT(jd, true)). Returned coordinates are Jovicentric J2000 mean-equatorial position and velocity from the IMCCE L1 theory, in AU and AU/day. // The input jd is a TT/TDB Julian day (convert a civil query with UTC2TT(jd)). Returned coordinates are Jovicentric J2000 mean-equatorial position and velocity from the IMCCE L1 theory, in AU and AU/day.
type JupiterGalileanState struct { type JupiterGalileanState struct {
X float64 X float64
Y float64 Y float64
@@ -97,11 +97,11 @@ func JupiterGalileanSatelliteStates(jd float64) [4]JupiterGalileanState {
// //
// jd 为 TT/TDB 对应儒略日;返回卫星的天球视赤道坐标,以及相对木星中心的东/北平面偏移。 // jd 为 TT/TDB 对应儒略日;返回卫星的天球视赤道坐标,以及相对木星中心的东/北平面偏移。
// jd is a TT/TDB Julian day. The result contains the satellite's astrometric equatorial coordinates and its east/north sky-plane offsets relative to Jupiter's center. // jd is a TT/TDB Julian day. The result contains the satellite's astrometric equatorial coordinates and its east/north sky-plane offsets relative to Jupiter's center.
func JupiterGalileanSatelliteObservation(jd float64, satellite int) JupiterGalileanObservation { func JupiterGalileanSatelliteObservation(jde float64, satellite int) JupiterGalileanObservation {
if satellite < 1 || satellite > 4 || !isFinite(jd) { if satellite < 1 || satellite > 4 || !isFinite(jde) {
return invalidJupiterGalileanObservation() return invalidJupiterGalileanObservation()
} }
context := newJupiterGalileanObservationContext(jd) context := newJupiterGalileanObservationContext(jde)
return context.observationForSatellite(satellite - 1) return context.observationForSatellite(satellite - 1)
} }
@@ -109,9 +109,9 @@ func JupiterGalileanSatelliteObservation(jd float64, satellite int) JupiterGalil
// //
// 返回次序固定为 Io、Europa、Ganymede、Callisto。 // 返回次序固定为 Io、Europa、Ganymede、Callisto。
// The returned order is Io, Europa, Ganymede, Callisto. // The returned order is Io, Europa, Ganymede, Callisto.
func JupiterGalileanSatelliteObservations(jd float64) [4]JupiterGalileanObservation { func JupiterGalileanSatelliteObservations(jde float64) [4]JupiterGalileanObservation {
var observations [4]JupiterGalileanObservation var observations [4]JupiterGalileanObservation
context := newJupiterGalileanObservationContext(jd) context := newJupiterGalileanObservationContext(jde)
for i := range observations { for i := range observations {
observations[i] = context.observationForSatellite(i) observations[i] = context.observationForSatellite(i)
} }
@@ -141,14 +141,14 @@ type jupiterGalileanObservationContext struct {
bodyZ Vector3 bodyZ Vector3
} }
func newJupiterGalileanObservationContext(jd float64) jupiterGalileanObservationContext { func newJupiterGalileanObservationContext(jde float64) jupiterGalileanObservationContext {
context := jupiterGalileanObservationContext{jd: jd} context := jupiterGalileanObservationContext{jd: jde}
if !isFinite(jd) { if !isFinite(jde) {
return context return context
} }
context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jd), orbitJ2000Obliquity) context.earthHelioJ2000 = rotateEclipticToEquatorial(earthHeliocentricVectorJ2000(jde), orbitJ2000Obliquity)
context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jd, context.earthHelioJ2000) context.jupiterGeoJ2000, context.jupiterLightTime = jupiterAstrometricGeocentricVectorJ2000(jde, context.earthHelioJ2000)
context.targetJD = jd - context.jupiterLightTime context.targetJD = jde - context.jupiterLightTime
context.jupiterDistance = vectorMagnitude(context.jupiterGeoJ2000) context.jupiterDistance = vectorMagnitude(context.jupiterGeoJ2000)
if context.jupiterDistance == 0 { if context.jupiterDistance == 0 {
return context return context
@@ -220,9 +220,9 @@ func jupiterGalileanSatelliteAstrometricGeocentric(index int, jd, initialLightTi
result := Vector3{} result := Vector3{}
includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd) includeSolarLongPeriod := jupiterGalileanUseSolarLongPeriod(jd)
for i := 0; i < 8; i++ { for i := 0; i < 8; i++ {
targetJD := jd - lightTime targetJDE := jd - lightTime
jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJD), orbitJ2000Obliquity) jupiterHelio := rotateEclipticToEquatorial(jupiterHeliocentricVectorJ2000(targetJDE), orbitJ2000Obliquity)
state = jupiterGalileanSatelliteStateAtET(targetJD-jupiterGalileanReferenceJD, index, includeSolarLongPeriod) state = jupiterGalileanSatelliteStateAtET(targetJDE-jupiterGalileanReferenceJD, index, includeSolarLongPeriod)
result = Vector3{ result = Vector3{
jupiterHelio[0] + state.X - earthHelioJ2000[0], jupiterHelio[0] + state.X - earthHelioJ2000[0],
jupiterHelio[1] + state.Y - earthHelioJ2000[1], jupiterHelio[1] + state.Y - earthHelioJ2000[1],
@@ -256,14 +256,14 @@ func jupiterAstrometricGeocentricVectorJ2000(jd float64, earthHelioJ2000 Vector3
return result, lightTime return result, lightTime
} }
func jupiterHeliocentricVectorJ2000(jd float64) Vector3 { func jupiterHeliocentricVectorJ2000(jde float64) Vector3 {
return eclipticVectorAtReferenceEpoch( return eclipticVectorAtReferenceEpoch(
eclipticCartesian( eclipticCartesian(
planet.WherePlanet(4, 0, jd), planet.WherePlanet(4, 0, jde),
planet.WherePlanet(4, 1, jd), planet.WherePlanet(4, 1, jde),
planet.WherePlanet(4, 2, jd), planet.WherePlanet(4, 2, jde),
), ),
jd, jde,
orbitReferenceJD, orbitReferenceJD,
) )
} }
+24 -8
View File
@@ -71,7 +71,9 @@ func occultationPathVectorRaDec(vector occultationPathVector) (float64, float64,
if !finite(distance) || distance <= 0 { if !finite(distance) || distance <= 0 {
return 0, 0, 0, false return 0, 0, 0, false
} }
ra := math.Atan2(vector.y, vector.x) / rad // atan2 给 (−180,180],统一到 [0,360):与精确分支(LoBoToRaDec、starMeanToApparentRaDec)
// 和 occultationRiseSetBodyFromVector 的 normalizeRA 一致;下游只按周期量使用,数值不变。
ra := normalizeRA(math.Atan2(vector.y, vector.x) / rad)
dec := math.Asin(math.Max(-1, math.Min(1, vector.z/distance))) / rad dec := math.Asin(math.Max(-1, math.Min(1, vector.z/distance))) / rad
return ra, dec, distance, finite(ra) && finite(dec) return ra, dec, distance, finite(ra) && finite(dec)
} }
@@ -112,6 +114,9 @@ type starOccultationLocalEphemeris struct {
star StarCoordinate star StarCoordinate
nodes []localEphemerisVectorNode nodes []localEphemerisVectorNode
dense bool dense bool
// distanceKM 是中心时刻的当日距离;hasDistance 为假表示恒星距离未知,几何按无穷远处理。
distanceKM float64
hasDistance bool
} }
func newStarOccultationLocalEphemeris(center float64, star StarCoordinate) *starOccultationLocalEphemeris { func newStarOccultationLocalEphemeris(center float64, star StarCoordinate) *starOccultationLocalEphemeris {
@@ -133,7 +138,16 @@ func newStarOccultationLocalEphemeris(center float64, star StarCoordinate) *star
next: [3]float64{target.x, target.y, target.z}, next: [3]float64{target.x, target.y, target.z},
} }
} }
return &starOccultationLocalEphemeris{star: star, nodes: nodes} return newStarOccultationLocalEphemerisFromNodes(center, star, nodes, false)
}
// newStarOccultationLocalEphemerisFromNodes 装配局部星历:距离必须与节点同源,密集分支同样要带上。
func newStarOccultationLocalEphemerisFromNodes(center float64, star StarCoordinate, nodes []localEphemerisVectorNode, dense bool) *starOccultationLocalEphemeris {
_, _, distanceAU := starApparentRaDecDistanceGeocentric(center, star)
return &starOccultationLocalEphemeris{
star: star, nodes: nodes, dense: dense,
distanceKM: distanceAU * occultationPathAstronomicalUnitKM, hasDistance: distanceAU > 0,
}
} }
func (ephemeris *starOccultationLocalEphemeris) stateAt(tt float64) (starOccultationEphemerisState, bool) { func (ephemeris *starOccultationLocalEphemeris) stateAt(tt float64) (starOccultationEphemerisState, bool) {
@@ -144,15 +158,20 @@ func (ephemeris *starOccultationLocalEphemeris) stateAt(tt float64) (starOcculta
moonRA, moonDec, moonDistance, moonOK := occultationPathVectorRaDec(occultationPathVector{ moonRA, moonDec, moonDistance, moonOK := occultationPathVectorRaDec(occultationPathVector{
x: moonXYZ[0], y: moonXYZ[1], z: moonXYZ[2], x: moonXYZ[0], y: moonXYZ[1], z: moonXYZ[2],
}) })
targetRA, targetDec, _, targetOK := occultationPathVectorRaDec(occultationPathVector{ targetRA, targetDec, targetDistance, targetOK := occultationPathVectorRaDec(occultationPathVector{
x: targetXYZ[0], y: targetXYZ[1], z: targetXYZ[2], x: targetXYZ[0], y: targetXYZ[1], z: targetXYZ[2],
}) })
if !moonOK || !targetOK { if !moonOK || !targetOK {
return starOccultationEphemerisState{}, false return starOccultationEphemerisState{}, false
} }
// 距离取插值矢量自身的模长,才与同一次插值给出的方向同源;距离未知时按 0 上报。
starDistanceKM := 0.0
if ephemeris.hasDistance {
starDistanceKM = targetDistance
}
return starOccultationEphemerisState{ return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistance, moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistance,
starRA: targetRA, starDec: targetDec, starDistanceKM: ephemeris.starDistanceKM(), starRA: targetRA, starDec: targetDec, starDistanceKM: starDistanceKM,
valid: true, valid: true,
}, true }, true
} }
@@ -168,10 +187,7 @@ func (ephemeris *starOccultationLocalEphemeris) vectorsAt(tt float64) ([3]float6
} }
func (ephemeris *starOccultationLocalEphemeris) starDistanceKM() float64 { func (ephemeris *starOccultationLocalEphemeris) starDistanceKM() float64 {
if ephemeris.star.ParallaxMas <= 0 { return ephemeris.distanceKM
return 0
}
return 206264806.247 / ephemeris.star.ParallaxMas * occultationPathAstronomicalUnitKM
} }
type planetOccultationLocalEphemeris struct { type planetOccultationLocalEphemeris struct {
+27 -5
View File
@@ -11,11 +11,11 @@ func TestSolarEclipseLocalEphemerisBoundedError(t *testing.T) {
name string name string
seed float64 seed float64
}{ }{
{"2010-01-15", JDECalc(2010, 1, 15)}, {"2010-01-15", JDCalc(2010, 1, 15)},
{"2014-04-29", JDECalc(2014, 4, 29)}, {"2014-04-29", JDCalc(2014, 4, 29)},
{"2023-04-20", JDECalc(2023, 4, 20)}, {"2023-04-20", JDCalc(2023, 4, 20)},
{"2031-05-21", JDECalc(2031, 5, 21)}, {"2031-05-21", JDCalc(2031, 5, 21)},
{"2309-06-09", JDECalc(2309, 6, 9)}, {"2309-06-09", JDCalc(2309, 6, 9)},
} }
for _, event := range events { for _, event := range events {
t.Run(event.name, func(t *testing.T) { t.Run(event.name, func(t *testing.T) {
@@ -133,3 +133,25 @@ func TestOccultationLocalEphemerisFullWindowBoundedError(t *testing.T) {
func vectorDifferenceKM(a, b [3]float64) float64 { func vectorDifferenceKM(a, b [3]float64) float64 {
return math.Sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]) + (a[2]-b[2])*(a[2]-b[2])) return math.Sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]) + (a[2]-b[2])*(a[2]-b[2]))
} }
// 矢量回读的赤经必须落在 [0,360):atan2 给 (−180,180],而精确分支
// (LoBoToRaDec、starMeanToApparentRaDec)与 rise/set 的矢量回读都在 [0,360),
// 不归一会让同一时刻的密集/精确状态相差 360 度,误导比较与日志。
func TestOccultationPathVectorRaDecKeepsNormalizedRA(t *testing.T) {
for _, want := range []float64{0.5, 90, 189.1975, 270, 359.9} {
vector := occultationPathRaDecVector(want, -5.8, 2.5)
got, dec, distance, ok := occultationPathVectorRaDec(vector)
if !ok {
t.Fatalf("RA %.4f 的回读失败", want)
}
if math.Abs(got-want) > 1e-9 {
t.Fatalf("RA %.4f 回读为 %.4f,want [0,360) 内的同一读数", want, got)
}
if math.Abs(dec+5.8) > 1e-9 || math.Abs(distance-2.5) > 1e-12 {
t.Fatalf("RA %.4f 的赤纬/距离 = %.6f / %.6f,want -5.8 / 2.5", want, dec, distance)
}
}
if _, _, _, ok := occultationPathVectorRaDec(occultationPathVector{}); ok {
t.Fatal("零矢量应判为无效")
}
}
+2 -3
View File
@@ -91,10 +91,9 @@ const (
// 沿用月食常量: // 沿用月食常量:
// - 0.2725076 用于月亮视半径和半影几何 // - 0.2725076 用于月亮视半径和半影几何
// - 959.63 / 8.794 分别为太阳视半径与太阳视差的常用角秒常量 // - 太阳视半径与日食共用标准档常量,太阳视差用常用角秒常量 8.794
lunarMoonRadiusRatio = 0.2725076 lunarMoonRadiusRatio = 0.2725076
lunarMoonRadiusScale = lunarMoonRadiusRatio * lunarEarthEquatorialRadiusKM * 1.0000036 lunarMoonRadiusScale = lunarMoonRadiusRatio * lunarEarthEquatorialRadiusKM * 1.0000036
lunarSolarRadiusArcsec = 959.63
lunarSolarParallaxArcsec = 8.794 lunarSolarParallaxArcsec = 8.794
lunarLongitudeAberration = -3.4e-6 lunarLongitudeAberration = -3.4e-6
lunarFiniteDifferenceStep = 60.0 / 86400.0 lunarFiniteDifferenceStep = 60.0 / 86400.0
@@ -336,7 +335,7 @@ func computeLunarShadowState(jde float64, shadowModel lunarEclipseShadowModel) l
moonRadiusArcsec := lunarMoonRadiusScale * lunarArcsecPerRadian / moonDistanceKM moonRadiusArcsec := lunarMoonRadiusScale * lunarArcsecPerRadian / moonDistanceKM
earthParallaxArcsec := lunarEarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian earthParallaxArcsec := lunarEarthEquatorialRadiusKM / moonDistanceKM * lunarArcsecPerRadian
solarRadiusArcsec := lunarSolarRadiusArcsec / sunDistanceAU solarRadiusArcsec := eclipseSunRadiusStandardArcsec / sunDistanceAU
solarParallaxArcsec := lunarSolarParallaxArcsec / sunDistanceAU solarParallaxArcsec := lunarSolarParallaxArcsec / sunDistanceAU
umbraRadiusArcsec, penumbraRadiusArcsec := lunarEclipseShadowRadiiArcsec( umbraRadiusArcsec, penumbraRadiusArcsec := lunarEclipseShadowRadiiArcsec(
earthParallaxArcsec, earthParallaxArcsec,
+3 -3
View File
@@ -38,7 +38,7 @@ func TestLunarEclipseAbsentPhasesAreNaN(t *testing.T) {
func TestLunarEclipseWithoutEclipseHasNoContacts(t *testing.T) { func TestLunarEclipseWithoutEclipseHasNoContacts(t *testing.T) {
// 2025-01-13 的望月没有月食(半影食分 < 0)。 // 2025-01-13 的望月没有月食(半影食分 < 0)。
eclipse := LunarEclipse(JDECalc(2025, 1, 13)) eclipse := LunarEclipse(JDCalc(2025, 1, 13))
if eclipse.Type != LunarEclipseNone { if eclipse.Type != LunarEclipseNone {
t.Fatalf("type = %s, want %s", eclipse.Type, LunarEclipseNone) t.Fatalf("type = %s, want %s", eclipse.Type, LunarEclipseNone)
} }
@@ -63,7 +63,7 @@ func TestLunarEclipseWithoutEclipseHasNoContacts(t *testing.T) {
} }
func TestLunarEclipseTotalKeepsAllContactsFinite(t *testing.T) { func TestLunarEclipseTotalKeepsAllContactsFinite(t *testing.T) {
eclipse := LunarEclipse(JDECalc(2025, 3, 14)) eclipse := LunarEclipse(JDCalc(2025, 3, 14))
if eclipse.Type != LunarEclipseTotal || !eclipse.HasPenumbral || !eclipse.HasPartial || !eclipse.HasTotal { if eclipse.Type != LunarEclipseTotal || !eclipse.HasPenumbral || !eclipse.HasPartial || !eclipse.HasTotal {
t.Fatalf("unexpected result: %+v", eclipse) t.Fatalf("unexpected result: %+v", eclipse)
} }
@@ -92,7 +92,7 @@ func TestLunarEclipseDiagramSkipsAbsentPhases(t *testing.T) {
t.Fatalf("non-finite diagram point: %+v", point) t.Fatalf("non-finite diagram point: %+v", point)
} }
} }
if empty := LunarEclipseDiagram(JDECalc(2025, 1, 13), LunarEclipseDiagramOptions{}); len(empty.Points) != 0 { if empty := LunarEclipseDiagram(JDCalc(2025, 1, 13), LunarEclipseDiagramOptions{}); len(empty.Points) != 0 {
t.Fatalf("eclipse-free diagram should have no points, got %d", len(empty.Points)) t.Fatalf("eclipse-free diagram should have no points, got %d", len(empty.Points))
} }
} }
+5 -1
View File
@@ -10,11 +10,15 @@ import "math"
// NASA lunar-eclipse charts; multiply a radius by the Earth's parallax at the Moon to get Earth radii. // NASA lunar-eclipse charts; multiply a radius by the Earth's parallax at the Moon to get Earth radii.
type LunarEclipseShadowGeometry struct { type LunarEclipseShadowGeometry struct {
// Gamma 是月心到地影轴的最小距离,单位地球赤道半径。 // Gamma 是月心到地影轴的最小距离,单位地球赤道半径。
// Gamma is the least distance from the Moon's centre to the shadow axis, in Earth equatorial radii.
Gamma float64 Gamma float64
// PenumbralRadiusDegrees 与 UmbralRadiusDegrees 是半影、本影在地影轴垂直面上的角半径,单位度。 // PenumbralRadiusDegrees 与 UmbralRadiusDegrees 是半影、本影在地影轴垂直面上的角半径,单位度。
// PenumbralRadiusDegrees and UmbralRadiusDegrees are the penumbral and umbral angular radii on the
// plane perpendicular to the shadow axis, in degrees.
PenumbralRadiusDegrees float64 PenumbralRadiusDegrees float64
UmbralRadiusDegrees float64 UmbralRadiusDegrees float64
// MoonDistanceEarthRadii 是食甚时的地心月距。 // MoonDistanceEarthRadii 是食甚时的地心月距,单位地球赤道半径。
// MoonDistanceEarthRadii is the geocentric lunar distance at greatest eclipse, in Earth equatorial radii.
MoonDistanceEarthRadii float64 MoonDistanceEarthRadii float64
// AxisDegrees 是食甚时月心到地影轴的角距,即 NASA 月食图上那列 Axis。 // AxisDegrees 是食甚时月心到地影轴的角距,即 NASA 月食图上那列 Axis。
// 它与 Gamma 是同一个量的两种刻度:Gamma 除以月球处的地球视差就是它。 // 它与 Gamma 是同一个量的两种刻度:Gamma 除以月球处的地球视差就是它。
+2 -2
View File
@@ -13,7 +13,7 @@ func TestLunarEclipseShadowGeometryAxisMatchesGamma(t *testing.T) {
time.Date(2026, time.March, 3, 0, 0, 0, 0, time.UTC), time.Date(2026, time.March, 3, 0, 0, 0, 0, time.UTC),
time.Date(2028, time.July, 6, 0, 0, 0, 0, time.UTC), time.Date(2028, time.July, 6, 0, 0, 0, 0, time.UTC),
} { } {
result := LunarEclipse(TD2UT(Date2JDE(date), true)) result := LunarEclipse(UTC2TT(Date2JD(date)))
geometry := LunarEclipseShadowGeometryAt(result.Maximum) geometry := LunarEclipseShadowGeometryAt(result.Maximum)
// 影几何内部用的是小角近似的地球视差(R⊕/月距),这里必须用同一形式,否则二级差会露出来。 // 影几何内部用的是小角近似的地球视差(R⊕/月距),这里必须用同一形式,否则二级差会露出来。
earthParallaxDegrees := (1 / geometry.MoonDistanceEarthRadii) * 180 / math.Pi earthParallaxDegrees := (1 / geometry.MoonDistanceEarthRadii) * 180 / math.Pi
@@ -32,7 +32,7 @@ func TestLunarEclipseMagnitudeInvertsToAxis(t *testing.T) {
time.Date(2028, time.July, 6, 0, 0, 0, 0, time.UTC), time.Date(2028, time.July, 6, 0, 0, 0, 0, time.UTC),
time.Date(2020, time.November, 30, 0, 0, 0, 0, time.UTC), time.Date(2020, time.November, 30, 0, 0, 0, 0, time.UTC),
} { } {
result := LunarEclipse(TD2UT(Date2JDE(date), true)) result := LunarEclipse(UTC2TT(Date2JD(date)))
geometry := LunarEclipseShadowGeometryAt(result.Maximum) geometry := LunarEclipseShadowGeometryAt(result.Maximum)
moonSemidiameter := MoonSemidiameter(result.Maximum) / 3600 moonSemidiameter := MoonSemidiameter(result.Maximum) / 3600
fromUmbral := geometry.UmbralRadiusDegrees + moonSemidiameter - fromUmbral := geometry.UmbralRadiusDegrees + moonSemidiameter -
+17 -17
View File
@@ -27,7 +27,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
testCases := []lunarEclipseBaseline{ testCases := []lunarEclipseBaseline{
{ {
name: "2022-11-08 total", name: "2022-11-08 total",
jde: JDECalc(2022, 11, 8), jde: JDCalc(2022, 11, 8),
expectedType: LunarEclipseTotal, expectedType: LunarEclipseTotal,
expectedMax: 2459891.9585873615, expectedMax: 2459891.9585873615,
expectedMag: 1.3635170051692678, expectedMag: 1.3635170051692678,
@@ -40,14 +40,14 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
}, },
{ {
name: "2023-05-05 penumbral", name: "2023-05-05 penumbral",
jde: JDECalc(2023, 5, 5), jde: JDCalc(2023, 5, 5),
expectedType: LunarEclipsePenumbral, expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460070.1342392800, expectedPenumbralStart: 2460070.1342392800,
expectedPenumbralEnd: 2460070.3159191823, expectedPenumbralEnd: 2460070.3159191823,
}, },
{ {
name: "2023-10-28 partial", name: "2023-10-28 partial",
jde: JDECalc(2023, 10, 28), jde: JDCalc(2023, 10, 28),
expectedType: LunarEclipsePartial, expectedType: LunarEclipsePartial,
expectedMax: 2460246.3439460830, expectedMax: 2460246.3439460830,
expectedMag: 0.12723850274626405, expectedMag: 0.12723850274626405,
@@ -58,14 +58,14 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
}, },
{ {
name: "2024-03-25 penumbral", name: "2024-03-25 penumbral",
jde: JDECalc(2024, 3, 25), jde: JDCalc(2024, 3, 25),
expectedType: LunarEclipsePenumbral, expectedType: LunarEclipsePenumbral,
expectedPenumbralStart: 2460394.7028870000, expectedPenumbralStart: 2460394.7028870000,
expectedPenumbralEnd: 2460394.8999071894, expectedPenumbralEnd: 2460394.8999071894,
}, },
{ {
name: "2024-09-18 partial", name: "2024-09-18 partial",
jde: JDECalc(2024, 9, 18), jde: JDCalc(2024, 9, 18),
expectedType: LunarEclipsePartial, expectedType: LunarEclipsePartial,
expectedMax: 2460571.6148748010, expectedMax: 2460571.6148748010,
expectedMag: 0.09042791952817894, expectedMag: 0.09042791952817894,
@@ -76,7 +76,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
}, },
{ {
name: "2025-03-14 total", name: "2025-03-14 total",
jde: JDECalc(2025, 3, 14), jde: JDCalc(2025, 3, 14),
expectedType: LunarEclipseTotal, expectedType: LunarEclipseTotal,
expectedMax: 2460748.7916214615, expectedMax: 2460748.7916214615,
expectedMag: 1.1828107517800281, expectedMag: 1.1828107517800281,
@@ -89,7 +89,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
}, },
{ {
name: "2025-09-07 total", name: "2025-09-07 total",
jde: JDECalc(2025, 9, 7), jde: JDCalc(2025, 9, 7),
expectedType: LunarEclipseTotal, expectedType: LunarEclipseTotal,
expectedMax: 2460926.2590034613, expectedMax: 2460926.2590034613,
expectedMag: 1.3672329695760280, expectedMag: 1.3672329695760280,
@@ -102,7 +102,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
}, },
{ {
name: "2026-03-03 total", name: "2026-03-03 total",
jde: JDECalc(2026, 3, 3), jde: JDCalc(2026, 3, 3),
expectedType: LunarEclipseTotal, expectedType: LunarEclipseTotal,
expectedMax: 2461102.9825476190, expectedMax: 2461102.9825476190,
expectedMag: 1.1556387222746651, expectedMag: 1.1556387222746651,
@@ -154,7 +154,7 @@ func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
} }
func TestLunarEclipseDefaultUsesDanjon(t *testing.T) { func TestLunarEclipseDefaultUsesDanjon(t *testing.T) {
jde := JDECalc(2025, 3, 14) jde := JDCalc(2025, 3, 14)
defaultResult := LunarEclipse(jde) defaultResult := LunarEclipse(jde)
danjonResult := LunarEclipseDanjon(jde) danjonResult := LunarEclipseDanjon(jde)
chauvenetResult := LunarEclipseChauvenet(jde) chauvenetResult := LunarEclipseChauvenet(jde)
@@ -176,9 +176,9 @@ func TestPenumbralLunarEclipseKeepsNegativeUmbralMagnitude(t *testing.T) {
jde float64 jde float64
calc func(float64) LunarEclipseResult calc func(float64) LunarEclipseResult
}{ }{
{name: "default 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipse}, {name: "default 2024-03-25", jde: JDCalc(2024, 3, 25), calc: LunarEclipse},
{name: "danjon 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipseDanjon}, {name: "danjon 2024-03-25", jde: JDCalc(2024, 3, 25), calc: LunarEclipseDanjon},
{name: "chauvenet 2023-05-05", jde: JDECalc(2023, 5, 5), calc: LunarEclipseChauvenet}, {name: "chauvenet 2023-05-05", jde: JDCalc(2023, 5, 5), calc: LunarEclipseChauvenet},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -210,28 +210,28 @@ func TestLunarEclipseDanjonMagnitudesCloserToNASA(t *testing.T) {
}{ }{
{ {
name: "2023-10-28 partial", name: "2023-10-28 partial",
jde: JDECalc(2023, 10, 28), jde: JDCalc(2023, 10, 28),
expectedType: LunarEclipsePartial, expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.1181, nasaPenumbralMagnitude: 1.1181,
nasaUmbralMagnitude: 0.1220, nasaUmbralMagnitude: 0.1220,
}, },
{ {
name: "2025-03-14 total", name: "2025-03-14 total",
jde: JDECalc(2025, 3, 14), jde: JDCalc(2025, 3, 14),
expectedType: LunarEclipseTotal, expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.2595, nasaPenumbralMagnitude: 2.2595,
nasaUmbralMagnitude: 1.1784, nasaUmbralMagnitude: 1.1784,
}, },
{ {
name: "2026-03-03 total", name: "2026-03-03 total",
jde: JDECalc(2026, 3, 3), jde: JDCalc(2026, 3, 3),
expectedType: LunarEclipseTotal, expectedType: LunarEclipseTotal,
nasaPenumbralMagnitude: 2.1838, nasaPenumbralMagnitude: 2.1838,
nasaUmbralMagnitude: 1.1507, nasaUmbralMagnitude: 1.1507,
}, },
{ {
name: "2026-08-28 partial", name: "2026-08-28 partial",
jde: JDECalc(2026, 8, 28), jde: JDCalc(2026, 8, 28),
expectedType: LunarEclipsePartial, expectedType: LunarEclipsePartial,
nasaPenumbralMagnitude: 1.9645, nasaPenumbralMagnitude: 1.9645,
nasaUmbralMagnitude: 0.9299, nasaUmbralMagnitude: 0.9299,
@@ -277,7 +277,7 @@ func TestLunarEclipseNoEvent(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
result := tc.calc(JDECalc(2023, 6, 4)) result := tc.calc(JDCalc(2023, 6, 4))
if result.Type != LunarEclipseNone { if result.Type != LunarEclipseNone {
t.Fatalf("Type mismatch: got %s want %s", result.Type, LunarEclipseNone) t.Fatalf("Type mismatch: got %s want %s", result.Type, LunarEclipseNone)
} }
+3 -5
View File
@@ -3,16 +3,14 @@ package basic
import "math" import "math"
func GetLunar(year, month, day int, tz float64) (lyear, lmonth, lday int, leap bool, result string) { func GetLunar(year, month, day int, tz float64) (lyear, lmonth, lday int, leap bool, result string) {
julianDayEpoch := JDECalc(year, month, float64(day)) julianDayEpoch := JDCalc(year, month, float64(day))
// 确定农历年份 // 确定农历年份
lyear = year lyear = year
adjustedYear := year adjustedYear := year
if month == 11 || month == 12 { if month == 11 || month == 12 {
winterSolsticeDay := GetJQTime(year, 270) + tz winterSolsticeDay := GetJQTime(year, 270) + tz
//firstNewMoonDay := TD2UT(CalcMoonS(float64(year)+11.0/12.0+5.0/30.0/12.0, 0), true) + tz firstNewMoonDay := TT2UTC(CalcMoonSHByJDE(winterSolsticeDay-16, 0)) + tz
//nextNewMoonDay := TD2UT(CalcMoonS(float64(year)+1.0, 0), true) + tz nextNewMoonDay := TT2UTC(CalcMoonSHByJDE(firstNewMoonDay+28, 0)) + tz
firstNewMoonDay := TD2UT(CalcMoonSHByJDE(winterSolsticeDay-16, 0), false) + tz
nextNewMoonDay := TD2UT(CalcMoonSHByJDE(firstNewMoonDay+28, 0), false) + tz
firstNewMoonDay = normalizeTimePoint(firstNewMoonDay) firstNewMoonDay = normalizeTimePoint(firstNewMoonDay)
nextNewMoonDay = normalizeTimePoint(nextNewMoonDay) nextNewMoonDay = normalizeTimePoint(nextNewMoonDay)
+63 -63
View File
@@ -7,79 +7,79 @@ import (
. "b612.me/astro/tools" . "b612.me/astro/tools"
) )
func MarsL(jd float64) float64 { func MarsL(jde float64) float64 {
return planet.WherePlanet(3, 0, jd) return planet.WherePlanet(3, 0, jde)
} }
func MarsB(jd float64) float64 { func MarsB(jde float64) float64 {
return planet.WherePlanet(3, 1, jd) return planet.WherePlanet(3, 1, jde)
} }
func MarsR(jd float64) float64 { func MarsR(jde float64) float64 {
return planet.WherePlanet(3, 2, jd) return planet.WherePlanet(3, 2, jde)
} }
func AMarsX(jd float64) float64 { func AMarsX(jde float64) float64 {
l := MarsL(jd) l := MarsL(jde)
b := MarsB(jd) b := MarsB(jde)
r := MarsR(jd) r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x return x
} }
func AMarsY(jd float64) float64 { func AMarsY(jde float64) float64 {
l := MarsL(jd) l := MarsL(jde)
b := MarsB(jd) b := MarsB(jde)
r := MarsR(jd) r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y return y
} }
func AMarsZ(jd float64) float64 { func AMarsZ(jde float64) float64 {
//l := MarsL(jd) //l := MarsL(jde)
b := MarsB(jd) b := MarsB(jde)
r := MarsR(jd) r := MarsR(jde)
// el := planet.WherePlanet(-1, 0, jd) // el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return z return z
} }
func AMarsXYZ(jd float64) (float64, float64, float64) { func AMarsXYZ(jde float64) (float64, float64, float64) {
l := MarsL(jd) l := MarsL(jde)
b := MarsB(jd) b := MarsB(jde)
r := MarsR(jd) r := MarsR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return x, y, z return x, y, z
} }
func MarsApparentRa(jd float64) float64 { func MarsApparentRa(jde float64) float64 {
lo, bo := MarsApparentLoBo(jd) lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo)) ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi ra = ra * 180 / math.Pi
return Limit360(ra) return Limit360(ra)
} }
func MarsApparentDec(jd float64) float64 { func MarsApparentDec(jde float64) float64 {
lo, bo := MarsApparentLoBo(jd) lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec return dec
} }
func MarsApparentRaDec(jd float64) (float64, float64) { func MarsApparentRaDec(jde float64) (float64, float64) {
lo, bo := MarsApparentLoBo(jd) lo, bo := MarsApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo)) ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
@@ -115,22 +115,22 @@ func MarsTrueLo(jd float64) float64 {
return geo.lo return geo.lo
} }
func MarsMag(jd float64) float64 { func MarsMag(jde float64) float64 {
sunDistance := MarsR(jd) sunDistance := MarsR(jde)
earthDistance := EarthMarsAway(jd) earthDistance := EarthMarsAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jd) earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance) i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i) i = ArcCos(i)
mag := -1.52 + 5*math.Log10(sunDistance*earthDistance) + 0.016*i mag := -1.52 + 5*math.Log10(sunDistance*earthDistance) + 0.016*i
return FloatRound(mag, 2) return FloatRound(mag, 2)
} }
func MarsHeight(jde, lon, lat, timezone float64) float64 { func MarsHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := MarsApparentRaDec(TD2UT(utcJde, true)) ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式 // 高度角、时角与天球座标三角转换公式
@@ -139,12 +139,12 @@ func MarsHeight(jde, lon, lat, timezone float64) float64 {
return ArcSin(sinHeight) return ArcSin(sinHeight)
} }
func MarsAzimuth(jde, lon, lat, timezone float64) float64 { func MarsAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := MarsApparentRaDec(TD2UT(utcJde, true)) ra, dec := MarsApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 三角转换公式 // 三角转换公式
@@ -163,21 +163,21 @@ func MarsAzimuth(jde, lon, lat, timezone float64) float64 {
} }
func MarsHourAngle(jd, lon, timezone float64) float64 { func MarsHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon) siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MarsApparentRa(TD2UT(jd-timezone/24.0, true)) hourAngle := siderealLongitude - MarsApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 { if hourAngle < 0 {
hourAngle += 360 hourAngle += 360
} }
return hourAngle return hourAngle
} }
func MarsCulminationTime(jde, lon, timezone float64) float64 { func MarsCulminationTime(localJD, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时 // localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标 //ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5 localJD = math.Floor(localJD) + 0.5
estimateJD := jde + Limit360(360-MarsHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851 estimateJD := localJD + Limit360(360-MarsHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 { normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MarsHourAngle(jde, lon, timezone) currentHourAngle := MarsHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 { if currentHourAngle < 180 {
currentHourAngle += 360 currentHourAngle += 360
} }
+1 -1
View File
@@ -28,7 +28,7 @@ func marsEventSamples() []time.Time {
} }
func marsEventSampleTTJD(date time.Time) float64 { func marsEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true) return UTC2TT(Date2JD(date.UTC()))
} }
func marsEventCases() []marsEventCase { func marsEventCases() []marsEventCase {
+27 -27
View File
@@ -77,15 +77,15 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 {
} else { } else {
jde += daysPerDegree * currentDelta jde += daysPerDegree * currentDelta
} }
estimateJD := jde estimateJDE := jde
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true) longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -93,7 +93,7 @@ func marsConjunctionFull(jde, degree float64, next uint8) float64 {
if !converged { if !converged {
return math.NaN() return math.NaN()
} }
return TD2UT(estimateJD, false) return TT2UTC(estimateJDE)
} }
func marsConjunction(jde, degree float64, next uint8) float64 { func marsConjunction(jde, degree float64, next uint8) float64 {
@@ -108,15 +108,15 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
} else { } else {
jde += daysPerDegree * currentDelta jde += daysPerDegree * currentDelta
} }
estimateJD := jde estimateJDE := jde
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDeltaN(prevJD, degree, true, marsEventSearchN) longitudeDelta := marsSunLongitudeDeltaN(prevJDE, degree, true, marsEventSearchN)
longitudeSlope := (marsSunLongitudeDeltaN(prevJD+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJD-0.000005, degree, true, marsEventSearchN)) / 0.00001 longitudeSlope := (marsSunLongitudeDeltaN(prevJDE+0.000005, degree, true, marsEventSearchN) - marsSunLongitudeDeltaN(prevJDE-0.000005, degree, true, marsEventSearchN)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= marsPhaseCoarseTolerance { if math.Abs(nextJD-prevJDE) <= marsPhaseCoarseTolerance {
converged = true converged = true
break break
} }
@@ -126,12 +126,12 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
} }
converged = false converged = false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := marsSunLongitudeDelta(prevJD, degree, true) longitudeDelta := marsSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (marsSunLongitudeDelta(prevJD+0.000005, degree, true) - marsSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 longitudeSlope := (marsSunLongitudeDelta(prevJDE+0.000005, degree, true) - marsSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -139,7 +139,7 @@ func marsConjunction(jde, degree float64, next uint8) float64 {
if !converged { if !converged {
return math.NaN() return math.NaN()
} }
return TD2UT(estimateJD, false) return TT2UTC(estimateJDE)
} }
func LastMarsConjunction(jde float64) float64 { func LastMarsConjunction(jde float64) float64 {
@@ -178,22 +178,22 @@ func marsRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposition
if !isFiniteFloat(oppositionJD) { if !isFiniteFloat(oppositionJD) {
return math.NaN() return math.NaN()
} }
oppositionTT := TD2UT(oppositionJD, true) oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT startTT := oppositionTT
endTT := oppositionTT endTT := oppositionTT
if searchBeforeOpposition { if searchBeforeOpposition {
easternQuadratureUT := marsConjunction(oppositionTT, 90, 0) easternQuadratureUT := marsConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true) startTT = UTC2TT(easternQuadratureUT)
} else { } else {
westernQuadratureUT := marsConjunction(oppositionTT, 270, 1) westernQuadratureUT := marsConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true) endTT = UTC2TT(westernQuadratureUT)
} }
bestJD := zeroEventInWindow(startTT, endTT, marsStationCoarseStepDay, marsStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 { bestJDE := zeroEventInWindow(startTT, endTT, marsStationCoarseStepDay, marsStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 {
return marsRADerivativeN(jd, marsStationDerivativeStepDay, marsEventSearchN) return marsRADerivativeN(jd, marsStationDerivativeStepDay, marsEventSearchN)
}, func(jd float64) float64 { }, func(jd float64) float64 {
return marsRADerivative(jd, marsStationDerivativeStepDay) return marsRADerivative(jd, marsStationDerivativeStepDay)
}) })
return TD2UT(bestJD, false) return TT2UTC(bestJDE)
} }
func NextMarsRetrogradeToPrograde(jde float64) float64 { func NextMarsRetrogradeToPrograde(jde float64) float64 {
+1 -1
View File
@@ -27,7 +27,7 @@ func BenchmarkTrueObliquity(b *testing.B) {
var benchMoonRise, benchMoonSet float64 var benchMoonRise, benchMoonSet float64
func BenchmarkMoonRiseSetChain(b *testing.B) { func BenchmarkMoonRiseSetChain(b *testing.B) {
jd := JDECalc(2023, 6, 21) jd := JDCalc(2023, 6, 21)
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
benchMoonRise, _ = GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0) benchMoonRise, _ = GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0)
+63 -63
View File
@@ -7,76 +7,76 @@ import (
. "b612.me/astro/tools" . "b612.me/astro/tools"
) )
func MercuryL(jd float64) float64 { func MercuryL(jde float64) float64 {
return planet.WherePlanet(1, 0, jd) return planet.WherePlanet(1, 0, jde)
} }
func MercuryB(jd float64) float64 { func MercuryB(jde float64) float64 {
return planet.WherePlanet(1, 1, jd) return planet.WherePlanet(1, 1, jde)
} }
func MercuryR(jd float64) float64 { func MercuryR(jde float64) float64 {
return planet.WherePlanet(1, 2, jd) return planet.WherePlanet(1, 2, jde)
} }
func AMercuryX(jd float64) float64 { func AMercuryX(jde float64) float64 {
l := MercuryL(jd) l := MercuryL(jde)
b := MercuryB(jd) b := MercuryB(jde)
r := MercuryR(jd) r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x return x
} }
func AMercuryY(jd float64) float64 { func AMercuryY(jde float64) float64 {
l := MercuryL(jd) l := MercuryL(jde)
b := MercuryB(jd) b := MercuryB(jde)
r := MercuryR(jd) r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y return y
} }
func AMercuryZ(jd float64) float64 { func AMercuryZ(jde float64) float64 {
//l := MercuryL(jd) //l := MercuryL(jde)
b := MercuryB(jd) b := MercuryB(jde)
r := MercuryR(jd) r := MercuryR(jde)
// el := planet.WherePlanet(-1, 0, jd) // el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return z return z
} }
func AMercuryXYZ(jd float64) (float64, float64, float64) { func AMercuryXYZ(jde float64) (float64, float64, float64) {
l := MercuryL(jd) l := MercuryL(jde)
b := MercuryB(jd) b := MercuryB(jde)
r := MercuryR(jd) r := MercuryR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return x, y, z return x, y, z
} }
func MercuryApparentRa(jd float64) float64 { func MercuryApparentRa(jde float64) float64 {
lo, bo := MercuryApparentLoBo(jd) lo, bo := MercuryApparentLoBo(jde)
return LoToRa(jd, lo, bo) return LoToRa(jde, lo, bo)
} }
func MercuryApparentDec(jd float64) float64 { func MercuryApparentDec(jde float64) float64 {
lo, bo := MercuryApparentLoBo(jd) lo, bo := MercuryApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec return dec
} }
func MercuryApparentRaDec(jd float64) (float64, float64) { func MercuryApparentRaDec(jde float64) (float64, float64) {
lo, bo := MercuryApparentLoBo(jd) lo, bo := MercuryApparentLoBo(jde)
return LoBoToRaDec(jd, lo, bo) return LoBoToRaDec(jde, lo, bo)
} }
func EarthMercuryAway(jd float64) float64 { func EarthMercuryAway(jd float64) float64 {
@@ -98,22 +98,22 @@ func MercuryApparentLoBo(jd float64) (float64, float64) {
return geo.lo, geo.bo return geo.lo, geo.bo
} }
func MercuryMag(jd float64) float64 { func MercuryMag(jde float64) float64 {
sunDistance := MercuryR(jd) sunDistance := MercuryR(jde)
earthDistance := EarthMercuryAway(jd) earthDistance := EarthMercuryAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jd) earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance) i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i) i = ArcCos(i)
mag := -0.42 + 5*math.Log10(sunDistance*earthDistance) + 0.0380*i - 0.000273*i*i + 0.000002*i*i*i mag := -0.42 + 5*math.Log10(sunDistance*earthDistance) + 0.0380*i - 0.000273*i*i + 0.000002*i*i*i
return FloatRound(mag, 2) return FloatRound(mag, 2)
} }
func MercuryHeight(jde, lon, lat, timezone float64) float64 { func MercuryHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := MercuryApparentRaDec(TD2UT(utcJde, true)) ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式 // 高度角、时角与天球座标三角转换公式
@@ -122,12 +122,12 @@ func MercuryHeight(jde, lon, lat, timezone float64) float64 {
return ArcSin(sinHeight) return ArcSin(sinHeight)
} }
func MercuryAzimuth(jde, lon, lat, timezone float64) float64 { func MercuryAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := MercuryApparentRaDec(TD2UT(utcJde, true)) ra, dec := MercuryApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 三角转换公式 // 三角转换公式
@@ -146,21 +146,21 @@ func MercuryAzimuth(jde, lon, lat, timezone float64) float64 {
} }
func MercuryHourAngle(jd, lon, timezone float64) float64 { func MercuryHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon) siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - MercuryApparentRa(TD2UT(jd-timezone/24.0, true)) hourAngle := siderealLongitude - MercuryApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 { if hourAngle < 0 {
hourAngle += 360 hourAngle += 360
} }
return hourAngle return hourAngle
} }
func MercuryCulminationTime(jde, lon, timezone float64) float64 { func MercuryCulminationTime(localJD, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时 // localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标 //ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5 localJD = math.Floor(localJD) + 0.5
estimateJD := jde + Limit360(360-MercuryHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851 estimateJD := localJD + Limit360(360-MercuryHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 { normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := MercuryHourAngle(jde, lon, timezone) currentHourAngle := MercuryHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 { if currentHourAngle < 180 {
currentHourAngle += 360 currentHourAngle += 360
} }
+1 -1
View File
@@ -31,7 +31,7 @@ func mercuryEventSamples() []time.Time {
} }
func mercuryEventSampleTTJD(date time.Time) float64 { func mercuryEventSampleTTJD(date time.Time) float64 {
return TD2UT(Date2JDE(date.UTC()), true) return UTC2TT(Date2JD(date.UTC()))
} }
func mercuryEventCases() []mercuryEventCase { func mercuryEventCases() []mercuryEventCase {
+56 -56
View File
@@ -51,11 +51,11 @@ type mercuryConjunctionResult struct {
geoLightDays float64 geoLightDays float64
} }
func mercuryHelioN(planetIndex int, jd float64, n int) mercuryConjunctionLBR { func mercuryHelioN(planetIndex int, jde float64, n int) mercuryConjunctionLBR {
return mercuryConjunctionLBR{ return mercuryConjunctionLBR{
lo: planet.WherePlanetN(planetIndex, 0, jd, n), lo: planet.WherePlanetN(planetIndex, 0, jde, n),
bo: planet.WherePlanetN(planetIndex, 1, jd, n), bo: planet.WherePlanetN(planetIndex, 1, jde, n),
r: planet.WherePlanetN(planetIndex, 2, jd, n), r: planet.WherePlanetN(planetIndex, 2, jde, n),
} }
} }
@@ -82,9 +82,9 @@ func mercuryConjunctionAngleDelta(diff float64) float64 {
return diff return diff
} }
func mercuryConjunctionHeliocentricDelta(jd, targetDeg float64, n int) float64 { func mercuryConjunctionHeliocentricDelta(jde, targetDeg float64, n int) float64 {
planetLo := planet.WherePlanetN(1, 0, jd, n) planetLo := planet.WherePlanetN(1, 0, jde, n)
earthLo := planet.WherePlanetN(-1, 0, jd, n) earthLo := planet.WherePlanetN(-1, 0, jde, n)
return mercuryConjunctionAngleDelta(planetLo - earthLo - targetDeg) return mercuryConjunctionAngleDelta(planetLo - earthLo - targetDeg)
} }
@@ -101,8 +101,8 @@ func mercuryConjunctionDifference(jd float64, n int, targetDeg, sunLightDays, ge
} }
} }
func mercuryConjunctionExactDelta(jd float64) float64 { func mercuryConjunctionExactDelta(jde float64) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLo(jd) - HSunApparentLo(jd)) return mercuryConjunctionAngleDelta(MercuryApparentLo(jde) - HSunApparentLo(jde))
} }
func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 { func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
@@ -110,32 +110,32 @@ func mercuryConjunctionApproxTT(seed float64, inferior bool) float64 {
if inferior { if inferior {
heliocentricTarget = 0 heliocentricTarget = 0
} }
jd := seed jde := seed
for i := 0; i < 6; i++ { for i := 0; i < 6; i++ {
jd -= mercuryConjunctionHeliocentricDelta(jd, heliocentricTarget, 8) / (360.0 / MERCURY_S_PERIOD) jde -= mercuryConjunctionHeliocentricDelta(jde, heliocentricTarget, 8) / (360.0 / MERCURY_S_PERIOD)
} }
startSample := mercuryConjunctionDifference(jd, 8, 0, 0, 0) startSample := mercuryConjunctionDifference(jde, 8, 0, 0, 0)
nextSample := mercuryConjunctionDifference(jd+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0) nextSample := mercuryConjunctionDifference(jde+mercuryConjunctionDerivativeStepDay, 8, 0, 0, 0)
diffSlope := mercuryConjunctionAngleDelta(nextSample.diff-startSample.diff) / mercuryConjunctionDerivativeStepDay diffSlope := mercuryConjunctionAngleDelta(nextSample.diff-startSample.diff) / mercuryConjunctionDerivativeStepDay
refined := mercuryConjunctionDifference(jd, 40, 0, startSample.sunLightDays, startSample.geoLightDays) refined := mercuryConjunctionDifference(jde, 40, 0, startSample.sunLightDays, startSample.geoLightDays)
jd -= refined.diff / diffSlope jde -= refined.diff / diffSlope
final := mercuryConjunctionDifference(jd, -1, 0, refined.sunLightDays, refined.geoLightDays) final := mercuryConjunctionDifference(jde, -1, 0, refined.sunLightDays, refined.geoLightDays)
jd -= final.diff / diffSlope jde -= final.diff / diffSlope
return jd return jde
} }
func mercuryConjunctionExactTT(seed float64, inferior bool) float64 { func mercuryConjunctionExactTT(seed float64, inferior bool) float64 {
estimateJD := mercuryConjunctionApproxTT(seed, inferior) estimateJD := mercuryConjunctionApproxTT(seed, inferior)
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJD
longitudeDelta := mercuryConjunctionExactDelta(prevJD) longitudeDelta := mercuryConjunctionExactDelta(prevJDE)
longitudeSlope := (mercuryConjunctionExactDelta(prevJD+0.000005) - mercuryConjunctionExactDelta(prevJD-0.000005)) / 0.00001 longitudeSlope := (mercuryConjunctionExactDelta(prevJDE+0.000005) - mercuryConjunctionExactDelta(prevJDE-0.000005)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJD = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -161,7 +161,7 @@ func mercuryConjunction(jde float64, next uint8) float64 {
if math.Abs(mercuryConjunctionExactDelta(jde)) <= mercuryConjunctionSameInstantDegrees { if math.Abs(mercuryConjunctionExactDelta(jde)) <= mercuryConjunctionSameInstantDegrees {
best := math.NaN() best := math.NaN()
consider := func(inferior bool) { consider := func(inferior bool) {
eventUT := TD2UT(mercuryConjunctionExactTT(jde, inferior), false) eventUT := TT2UTC(mercuryConjunctionExactTT(jde, inferior))
if !isFiniteFloat(eventUT) { if !isFiniteFloat(eventUT) {
return return
} }
@@ -186,80 +186,80 @@ func mercuryConjunction(jde float64, next uint8) float64 {
if next == 0 { if next == 0 {
direction = -1 direction = -1
} }
leftJD := jde leftJDE := jde
leftValue := mercuryConjunctionDeltaN(leftJD, mercuryEventSearchN) leftValue := mercuryConjunctionDeltaN(leftJDE, mercuryEventSearchN)
if !isFiniteFloat(leftValue) { if !isFiniteFloat(leftValue) {
return math.NaN() return math.NaN()
} }
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ { for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJD := jde + direction*mercuryConjunctionScanStepDay*float64(i+1) rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionDeltaN(rightJD, mercuryEventSearchN) rightValue := mercuryConjunctionDeltaN(rightJDE, mercuryEventSearchN)
if !isFiniteFloat(rightValue) { if !isFiniteFloat(rightValue) {
return math.NaN() return math.NaN()
} }
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 { if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
return mercuryConjunctionPolish(jde, leftJD, rightJD, direction) return mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction)
} }
leftJD, leftValue = rightJD, rightValue leftJDE, leftValue = rightJDE, rightValue
} }
return math.NaN() return math.NaN()
} }
// mercuryConjunctionDeltaN 截断级数下的水星-太阳视黄经差(度,[-180,180]),用于方向性括号扫描。 // mercuryConjunctionDeltaN 截断级数下的水星-太阳视黄经差(度,[-180,180]),用于方向性括号扫描。
func mercuryConjunctionDeltaN(jd float64, n int) float64 { func mercuryConjunctionDeltaN(jde float64, n int) float64 {
return mercuryConjunctionAngleDelta(MercuryApparentLoN(jd, n) - HSunApparentLoN(jd, n)) return mercuryConjunctionAngleDelta(MercuryApparentLoN(jde, n) - HSunApparentLoN(jde, n))
} }
// mercuryConjunctionPolish 用全项级数在截断级数给出的括号内抛光。 // mercuryConjunctionPolish 用全项级数在截断级数给出的括号内抛光。
// 截断误差可能让括号两端在全项函数上同号(罕见),此时沿扫描方向再扩一两个扫描步; // 截断误差可能让括号两端在全项函数上同号(罕见),此时沿扫描方向再扩一两个扫描步;
// 若仍未被确认(典型情形:查询几乎正好落在合上,截断级数在根两侧的符号与全项不一致), // 若仍未被确认(典型情形:查询几乎正好落在合上,截断级数在根两侧的符号与全项不一致),
// 退回全项级数的方向扫描,保证有界且不返回 NaN。 // 退回全项级数的方向扫描,保证有界且不返回 NaN。
func mercuryConjunctionPolish(jde, leftJD, rightJD, direction float64) float64 { func mercuryConjunctionPolish(jde, leftJDE, rightJDE, direction float64) float64 {
for attempt := 0; attempt < 3; attempt++ { for attempt := 0; attempt < 3; attempt++ {
leftValue := mercuryConjunctionExactDelta(leftJD) leftValue := mercuryConjunctionExactDelta(leftJDE)
rightValue := mercuryConjunctionExactDelta(rightJD) rightValue := mercuryConjunctionExactDelta(rightJDE)
if !isFiniteFloat(leftValue) || !isFiniteFloat(rightValue) { if !isFiniteFloat(leftValue) || !isFiniteFloat(rightValue) {
return math.NaN() return math.NaN()
} }
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 { if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJD, rightJD, leftValue, rightValue, root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta) mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
if !ok { if !ok {
return math.NaN() return math.NaN()
} }
return TD2UT(root, false) return TT2UTC(root)
} }
if direction > 0 { if direction > 0 {
rightJD += mercuryConjunctionScanStepDay rightJDE += mercuryConjunctionScanStepDay
continue continue
} }
leftJD -= mercuryConjunctionScanStepDay leftJDE -= mercuryConjunctionScanStepDay
} }
return mercuryConjunctionFullDirectionalScan(jde, direction) return mercuryConjunctionFullDirectionalScan(jde, direction)
} }
// mercuryConjunctionFullDirectionalScan 全项级数的方向扫描(截断括号未被确认时的兜底)。 // mercuryConjunctionFullDirectionalScan 全项级数的方向扫描(截断括号未被确认时的兜底)。
func mercuryConjunctionFullDirectionalScan(jde, direction float64) float64 { func mercuryConjunctionFullDirectionalScan(jde, direction float64) float64 {
leftJD := jde leftJDE := jde
leftValue := mercuryConjunctionExactDelta(leftJD) leftValue := mercuryConjunctionExactDelta(leftJDE)
if !isFiniteFloat(leftValue) { if !isFiniteFloat(leftValue) {
return math.NaN() return math.NaN()
} }
for i := 0; i < mercuryConjunctionScanMaxSteps; i++ { for i := 0; i < mercuryConjunctionScanMaxSteps; i++ {
rightJD := jde + direction*mercuryConjunctionScanStepDay*float64(i+1) rightJDE := jde + direction*mercuryConjunctionScanStepDay*float64(i+1)
rightValue := mercuryConjunctionExactDelta(rightJD) rightValue := mercuryConjunctionExactDelta(rightJDE)
if !isFiniteFloat(rightValue) { if !isFiniteFloat(rightValue) {
return math.NaN() return math.NaN()
} }
if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 { if leftValue == 0 || rightValue == 0 || leftValue*rightValue < 0 {
root, ok := eventBracketSecantRoot(leftJD, rightJD, leftValue, rightValue, root, ok := eventBracketSecantRoot(leftJDE, rightJDE, leftValue, rightValue,
mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta) mercuryConjunctionPolishToleranceDay, mercuryConjunctionExactDelta)
if !ok { if !ok {
return math.NaN() return math.NaN()
} }
return TD2UT(root, false) return TT2UTC(root)
} }
leftJD, leftValue = rightJD, rightValue leftJDE, leftValue = rightJDE, rightValue
} }
return math.NaN() return math.NaN()
} }
@@ -335,12 +335,12 @@ func mercuryRADerivativeN(jde, delta float64, n int) float64 {
} }
func mercuryStationInWindow(startTT, endTT float64) float64 { func mercuryStationInWindow(startTT, endTT float64) float64 {
bestJD := zeroEventInWindow(startTT, endTT, mercuryStationCoarseStepDay, mercuryStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 { bestJDE := zeroEventInWindow(startTT, endTT, mercuryStationCoarseStepDay, mercuryStationHalfWindowDay, 30.0/86400.0, func(jd float64) float64 {
return mercuryRADerivativeN(jd, mercuryStationDerivativeStepDay, mercuryEventSearchN) return mercuryRADerivativeN(jd, mercuryStationDerivativeStepDay, mercuryEventSearchN)
}, func(jd float64) float64 { }, func(jd float64) float64 {
return mercuryRADerivative(jd, mercuryStationDerivativeStepDay) return mercuryRADerivative(jd, mercuryStationDerivativeStepDay)
}) })
return TD2UT(bestJD, false) return TT2UTC(bestJDE)
} }
func mercuryStationBetween(startTT, endTT float64) bool { func mercuryStationBetween(startTT, endTT float64) bool {
@@ -377,12 +377,12 @@ func mercuryStationBetween(startTT, endTT float64) bool {
} }
func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64) float64 { func mercuryProgradeToRetrogradeAroundInferior(inferiorUT float64) float64 {
inferiorTT := TD2UT(inferiorUT, true) inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT) return mercuryStationInWindow(inferiorTT-mercuryStationWindowDays, inferiorTT)
} }
func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64) float64 { func mercuryRetrogradeToProgradeAroundInferior(inferiorUT float64) float64 {
inferiorTT := TD2UT(inferiorUT, true) inferiorTT := UTC2TT(inferiorUT)
return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays) return mercuryStationInWindow(inferiorTT, inferiorTT+mercuryStationWindowDays)
} }
@@ -488,7 +488,7 @@ func NextMercuryRetrograde(jde float64) float64 {
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay) motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
if motion > mercuryStationMotionTolerance { if motion > mercuryStationMotionTolerance {
p2r := NextMercuryProgradeToRetrograde(jde) p2r := NextMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(jde, TD2UT(p2r, true)) { if isFiniteFloat(p2r) && !mercuryStationBetween(jde, UTC2TT(p2r)) {
return p2r return p2r
} }
best := earliestFiniteEventUT(p2r, NextMercuryRetrogradeToPrograde(jde)) best := earliestFiniteEventUT(p2r, NextMercuryRetrogradeToPrograde(jde))
@@ -499,7 +499,7 @@ func NextMercuryRetrograde(jde float64) float64 {
} }
if motion < -mercuryStationMotionTolerance { if motion < -mercuryStationMotionTolerance {
r2p := NextMercuryRetrogradeToPrograde(jde) r2p := NextMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(jde, TD2UT(r2p, true)) { if isFiniteFloat(r2p) && !mercuryStationBetween(jde, UTC2TT(r2p)) {
return r2p return r2p
} }
best := earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), r2p) best := earliestFiniteEventUT(NextMercuryProgradeToRetrograde(jde), r2p)
@@ -522,7 +522,7 @@ func LastMercuryRetrograde(jde float64) float64 {
motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay) motion := mercuryRADerivative(jde, mercuryStationDerivativeStepDay)
if motion > mercuryStationMotionTolerance { if motion > mercuryStationMotionTolerance {
r2p := LastMercuryRetrogradeToPrograde(jde) r2p := LastMercuryRetrogradeToPrograde(jde)
if isFiniteFloat(r2p) && !mercuryStationBetween(TD2UT(r2p, true), jde) { if isFiniteFloat(r2p) && !mercuryStationBetween(UTC2TT(r2p), jde) {
return r2p return r2p
} }
best := latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), r2p) best := latestFiniteEventUT(LastMercuryProgradeToRetrograde(jde), r2p)
@@ -533,7 +533,7 @@ func LastMercuryRetrograde(jde float64) float64 {
} }
if motion < -mercuryStationMotionTolerance { if motion < -mercuryStationMotionTolerance {
p2r := LastMercuryProgradeToRetrograde(jde) p2r := LastMercuryProgradeToRetrograde(jde)
if isFiniteFloat(p2r) && !mercuryStationBetween(TD2UT(p2r, true), jde) { if isFiniteFloat(p2r) && !mercuryStationBetween(UTC2TT(p2r), jde) {
return p2r return p2r
} }
best := latestFiniteEventUT(p2r, LastMercuryRetrogradeToPrograde(jde)) best := latestFiniteEventUT(p2r, LastMercuryRetrogradeToPrograde(jde))
@@ -572,9 +572,9 @@ func mercurySunElongationN(jde float64, n int) float64 {
// 窗口两端是世界时,目标函数收力学时,因此逐次换算。 // 窗口两端是世界时,目标函数收力学时,因此逐次换算。
func mercuryGreatestElongationInWindow(start, end float64) float64 { func mercuryGreatestElongationInWindow(start, end float64) float64 {
return maximizeInWindow(start, end, 2.0, func(utJD float64) float64 { return maximizeInWindow(start, end, 2.0, func(utJD float64) float64 {
return mercurySunElongationN(TD2UT(utJD, true), mercuryEventSearchN) return mercurySunElongationN(UTC2TT(utJD), mercuryEventSearchN)
}, func(utJD float64) float64 { }, func(utJD float64) float64 {
return MercurySunElongation(TD2UT(utJD, true)) return MercurySunElongation(UTC2TT(utJD))
}) })
} }
@@ -13,7 +13,7 @@ func TestMercuryRetrogradeFastPathKeepsTypedCandidateOrder(t *testing.T) {
1644733.927538287, 1644733.927538287,
1645082.416782375, 1645082.416782375,
} { } {
queryTT := TD2UT(queryUT, true) queryTT := UTC2TT(queryUT)
nextP2R := NextMercuryProgradeToRetrograde(queryTT) nextP2R := NextMercuryProgradeToRetrograde(queryTT)
nextR2P := NextMercuryRetrogradeToPrograde(queryTT) nextR2P := NextMercuryRetrogradeToPrograde(queryTT)
wantNext := math.Min(nextP2R, nextR2P) wantNext := math.Min(nextP2R, nextR2P)
+5 -5
View File
@@ -8,7 +8,7 @@ import (
func mercuryTTJDJST(year int, month time.Month, day, hour, minute, second int) float64 { func mercuryTTJDJST(year int, month time.Month, day, hour, minute, second int) float64 {
loc := time.FixedZone("JST", 9*3600) loc := time.FixedZone("JST", 9*3600)
return TD2UT(Date2JDE(time.Date(year, month, day, hour, minute, second, 0, loc).UTC()), true) return UTC2TT(Date2JD(time.Date(year, month, day, hour, minute, second, 0, loc).UTC()))
} }
func TestMercuryTypedStationRegression1929(t *testing.T) { func TestMercuryTypedStationRegression1929(t *testing.T) {
@@ -22,19 +22,19 @@ func TestMercuryTypedStationRegression1929(t *testing.T) {
nextP2R := NextMercuryProgradeToRetrograde(query) nextP2R := NextMercuryProgradeToRetrograde(query)
nextR2P := NextMercuryRetrogradeToPrograde(query) nextR2P := NextMercuryRetrogradeToPrograde(query)
if math.Abs(nextP2R-wantP2R) > tolerance { if math.Abs(nextP2R-wantP2R) > tolerance {
t.Fatalf("next P2R mismatch: got %s want %s", JDE2DateByZone(nextP2R, loc, false), JDE2DateByZone(wantP2R, loc, false)) t.Fatalf("next P2R mismatch: got %s want %s", JD2DateByZone(nextP2R, loc, false), JD2DateByZone(wantP2R, loc, false))
} }
if math.Abs(nextR2P-wantR2P) > tolerance { if math.Abs(nextR2P-wantR2P) > tolerance {
t.Fatalf("next R2P mismatch: got %s want %s", JDE2DateByZone(nextR2P, loc, false), JDE2DateByZone(wantR2P, loc, false)) t.Fatalf("next R2P mismatch: got %s want %s", JD2DateByZone(nextR2P, loc, false), JD2DateByZone(wantR2P, loc, false))
} }
query = mercuryTTJDJST(1929, time.October, 20, 0, 0, 0) query = mercuryTTJDJST(1929, time.October, 20, 0, 0, 0)
lastP2R := LastMercuryProgradeToRetrograde(query) lastP2R := LastMercuryProgradeToRetrograde(query)
lastR2P := LastMercuryRetrogradeToPrograde(query) lastR2P := LastMercuryRetrogradeToPrograde(query)
if math.Abs(lastP2R-wantP2R) > tolerance { if math.Abs(lastP2R-wantP2R) > tolerance {
t.Fatalf("last P2R mismatch: got %s want %s", JDE2DateByZone(lastP2R, loc, false), JDE2DateByZone(wantP2R, loc, false)) t.Fatalf("last P2R mismatch: got %s want %s", JD2DateByZone(lastP2R, loc, false), JD2DateByZone(wantP2R, loc, false))
} }
if math.Abs(lastR2P-wantR2P) > tolerance { if math.Abs(lastR2P-wantR2P) > tolerance {
t.Fatalf("last R2P mismatch: got %s want %s", JDE2DateByZone(lastR2P, loc, false), JDE2DateByZone(wantR2P, loc, false)) t.Fatalf("last R2P mismatch: got %s want %s", JD2DateByZone(lastR2P, loc, false), JD2DateByZone(wantR2P, loc, false))
} }
} }
+40 -40
View File
@@ -5,58 +5,58 @@ import (
. "b612.me/astro/tools" . "b612.me/astro/tools"
) )
func MoonLo(jd float64) float64 { //'月球平黄经 func MoonLo(jde float64) float64 { //'月球平黄经
return planet.MoonLo(jd) return planet.MoonLo(jde)
} }
func SunMoonAngle(jd float64) float64 { // '月日距角 func SunMoonAngle(jde float64) float64 { // '月日距角
return planet.SunMoonAngle(jd) return planet.SunMoonAngle(jde)
} }
func MoonM(jd float64) float64 { // '月平近点角 func MoonM(jde float64) float64 { // '月平近点角
return planet.MoonM(jd) return planet.MoonM(jde)
} }
func MoonLonX(jd float64) float64 { // As Double '月球经度参数(到升交点的平角距离) func MoonLonX(jde float64) float64 { // As Double '月球经度参数(到升交点的平角距离)
return planet.MoonLonX(jd) return planet.MoonLonX(jde)
} }
func MoonI(jd float64) float64 { func MoonI(jde float64) float64 {
return planet.MoonI(jd) return planet.MoonI(jde)
} }
func MoonR(jd float64) float64 { func MoonR(jde float64) float64 {
return planet.MoonR(jd) return planet.MoonR(jde)
} }
func MoonB(jd float64) float64 { func MoonB(jde float64) float64 {
return planet.MoonB(jd) return planet.MoonB(jde)
} }
func MoonTrueLo(jd float64) float64 { func MoonTrueLo(jde float64) float64 {
return planet.MoonTrueLo(jd) return planet.MoonTrueLo(jde)
} }
func MoonTrueBo(jd float64) float64 { func MoonTrueBo(jde float64) float64 {
return planet.MoonTrueBo(jd) return planet.MoonTrueBo(jde)
} }
func MoonAway(jd float64) float64 { //'月地距离 func MoonAway(jde float64) float64 { //'月地距离
return planet.MoonAway(jd) return planet.MoonAway(jde)
} }
/* /*
* @name 月球视黄经 * @name 月球视黄经
*/ */
func MoonApparentLo(jd float64) float64 { func MoonApparentLo(jde float64) float64 {
return MoonTrueLo(jd) + Nutation2000Bi(jd) return MoonTrueLo(jde) + Nutation2000Bi(jde)
} }
/* /*
* 月球真赤纬 * 月球真赤纬
*/ */
func MoonTrueDec(jd float64) float64 { func MoonTrueDec(jde float64) float64 {
moonLo := MoonApparentLo(jd) moonLo := MoonApparentLo(jde)
moonBo := MoonTrueBo(jd) moonBo := MoonTrueBo(jde)
tmp := Sin(moonBo)*Cos(TrueObliquity(jd)) + Cos(moonBo)*Sin(TrueObliquity(jd))*Sin(moonLo) tmp := Sin(moonBo)*Cos(TrueObliquity(jde)) + Cos(moonBo)*Sin(TrueObliquity(jde))*Sin(moonLo)
res := ArcSin(tmp) res := ArcSin(tmp)
return res return res
} }
@@ -64,31 +64,31 @@ func MoonTrueDec(jd float64) float64 {
/* /*
* 月球真赤经 * 月球真赤经
*/ */
func MoonTrueRa(jd float64) float64 { func MoonTrueRa(jde float64) float64 {
return LoToRa(jd, MoonApparentLo(jd), MoonTrueBo(jd)) return LoToRa(jde, MoonApparentLo(jde), MoonTrueBo(jde))
} }
func MoonTrueRaDec(jd float64) (float64, float64) { func MoonTrueRaDec(jde float64) (float64, float64) {
return LoBoToRaDec(jd, MoonApparentLo(jd), MoonTrueBo(jd)) return LoBoToRaDec(jde, MoonApparentLo(jde), MoonTrueBo(jde))
} }
// MoonApparentRa 站心视赤经;jd 为当地时儒略日,tz 为时区小时数 / topocentric apparent right ascension; jd is local civil time and tz is the zone offset in hours. // MoonApparentRa 站心视赤经;jd 为当地时儒略日,tz 为时区小时数 / topocentric apparent right ascension; jd is local civil time and tz is the zone offset in hours.
func MoonApparentRa(jd, lon, lat float64, tz int) float64 { func MoonApparentRa(jd, lon, lat float64, tz int) float64 {
jde := TD2UT(jd, true) // 本地时刻加 ΔT 再减时区偏移即该地时刻的 TT(加法可交换),故 jde 就是 TT。
utcJD := jde - float64(tz)/24.000 jde := UTC2TT(jd) - float64(tz)/24.000
ra := MoonTrueRa(utcJD) ra := MoonTrueRa(jde)
dec := MoonTrueDec(utcJD) dec := MoonTrueDec(jde)
away := MoonAway(utcJD) / 149597870.7 away := MoonAway(jde) / 149597870.7
topoRA := TopocentricRa(ra, dec, lat, lon, jd-float64(tz)/24.000, away, 0) topoRA := TopocentricRa(ra, dec, lat, lon, jd-float64(tz)/24.000, away, 0)
return topoRA return topoRA
} }
func MoonApparentDec(jd, lon, lat, tz float64) float64 { func MoonApparentDec(jd, lon, lat, tz float64) float64 {
jde := TD2UT(jd, true) // 同上:jde 是当地时刻的 TT。
utcJD := jde - tz/24 jde := UTC2TT(jd) - tz/24
ra := MoonTrueRa(utcJD) ra := MoonTrueRa(jde)
dec := MoonTrueDec(utcJD) dec := MoonTrueDec(jde)
away := MoonAway(utcJD) / 149597870.7 away := MoonAway(jde) / 149597870.7
topoDec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0) topoDec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
return topoDec return topoDec
} }
+18 -18
View File
@@ -3,39 +3,39 @@ package basic
import . "b612.me/astro/tools" import . "b612.me/astro/tools"
// MoonBrightLimbPositionAngle 月亮明亮边缘位置角 / position angle of the Moon's bright limb. // MoonBrightLimbPositionAngle 月亮明亮边缘位置角 / position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngle(jd float64) float64 { func MoonBrightLimbPositionAngle(jde float64) float64 {
return MoonBrightLimbPositionAngleN(jd, -1) return MoonBrightLimbPositionAngleN(jde, -1)
} }
// MoonBrightLimbPositionAngleN 月亮明亮边缘位置角(截断版) / truncated position angle of the Moon's bright limb. // MoonBrightLimbPositionAngleN 月亮明亮边缘位置角(截断版) / truncated position angle of the Moon's bright limb.
func MoonBrightLimbPositionAngleN(jd float64, n int) float64 { func MoonBrightLimbPositionAngleN(jde float64, n int) float64 {
sunRA, sunDec := HSunApparentRaDecN(jd, n) sunRA, sunDec := HSunApparentRaDecN(jde, n)
moonRA, moonDec := HMoonTrueRaDecN(jd, n) moonRA, moonDec := HMoonTrueRaDecN(jde, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec) return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
} }
// MoonTopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角 / topocentric position angle of the Moon's bright limb. // MoonTopocentricBrightLimbPositionAngle 月亮站心明亮边缘位置角 / topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngle(jd, observerLon, observerLat, height float64) float64 { func MoonTopocentricBrightLimbPositionAngle(jde, observerLon, observerLat, height float64) float64 {
return MoonTopocentricBrightLimbPositionAngleN(jd, observerLon, observerLat, height, -1) return MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height, -1)
} }
// MoonTopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版) / truncated topocentric position angle of the Moon's bright limb. // MoonTopocentricBrightLimbPositionAngleN 月亮站心明亮边缘位置角(截断版) / truncated topocentric position angle of the Moon's bright limb.
func MoonTopocentricBrightLimbPositionAngleN(jd, observerLon, observerLat, height float64, n int) float64 { func MoonTopocentricBrightLimbPositionAngleN(jde, observerLon, observerLat, height float64, n int) float64 {
sunRA, sunDec := sunTopocentricApparentRaDecN(jd, observerLon, observerLat, height, n) sunRA, sunDec := sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
moonRA, moonDec := moonTopocentricApparentRaDecN(jd, observerLon, observerLat, height, n) moonRA, moonDec := moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height, n)
return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec) return brightLimbPositionAngleFromRaDec(sunRA, sunDec, moonRA, moonDec)
} }
func moonTopocentricApparentRaDecN(jd, observerLon, observerLat, height float64, n int) (float64, float64) { func moonTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA := HMoonTrueRaN(jd, n) geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jd, n) geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jd, n) / moonPhysicalAstronomicalUnitKM distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TD2UT(jd, false), distanceAU, height) return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), distanceAU, height)
} }
func sunTopocentricApparentRaDecN(jd, observerLon, observerLat, height float64, n int) (float64, float64) { func sunTopocentricApparentRaDecN(jde, observerLon, observerLat, height float64, n int) (float64, float64) {
geocentricRA, geocentricDec := HSunApparentRaDecN(jd, n) geocentricRA, geocentricDec := HSunApparentRaDecN(jde, n)
return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TD2UT(jd, false), EarthAwayN(jd, n), height) return TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, TT2UTC(jde), EarthAwayN(jde, n), height)
} }
func brightLimbPositionAngleFromRaDec(sunRA, sunDec, bodyRA, bodyDec float64) float64 { func brightLimbPositionAngleFromRaDec(sunRA, sunDec, bodyRA, bodyDec float64) float64 {
+2 -2
View File
@@ -11,7 +11,7 @@ func TestMoonBrightLimbPositionAngleMeeusExample(t *testing.T) {
} }
func TestMoonBrightLimbPositionAngleNFullMatchesDefault(t *testing.T) { func TestMoonBrightLimbPositionAngleNFullMatchesDefault(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true) jd := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
got := MoonBrightLimbPositionAngle(jd) got := MoonBrightLimbPositionAngle(jd)
gotN := MoonBrightLimbPositionAngleN(jd, -1) gotN := MoonBrightLimbPositionAngleN(jd, -1)
@@ -21,7 +21,7 @@ func TestMoonBrightLimbPositionAngleNFullMatchesDefault(t *testing.T) {
} }
func TestMoonTopocentricBrightLimbPositionAngleSampleFiniteAndInRange(t *testing.T) { func TestMoonTopocentricBrightLimbPositionAngleSampleFiniteAndInRange(t *testing.T) {
jd := TD2UT(Date2JDE(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)), true) jd := UTC2TT(Date2JD(time.Date(2026, 4, 28, 9, 30, 45, 0, time.UTC)))
got := MoonTopocentricBrightLimbPositionAngle(jd, 121.4737, 31.2304, 4) got := MoonTopocentricBrightLimbPositionAngle(jd, 121.4737, 31.2304, 4)
assertFiniteRange(t, "MoonTopocentricBrightLimbPositionAngle", got, 0, 360, true) assertFiniteRange(t, "MoonTopocentricBrightLimbPositionAngle", got, 0, 360, true)
@@ -34,7 +34,7 @@ func TestMoonGeocentricApparentCoordinatesMatchHorizonsBaseline(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("parse sample time %q: %v", sample.InputUTC, err) t.Fatalf("parse sample time %q: %v", sample.InputUTC, err)
} }
jd := TD2UT(Date2JDE(date.UTC()), true) jd := UTC2TT(Date2JD(date.UTC()))
prefix := "moon." + sample.InputUTC prefix := "moon." + sample.InputUTC
assertPlanetApparentAngleClose(t, prefix+".RightAscension", HMoonGeocentricApparentRa(jd), sample.RightAscension, 0.001) assertPlanetApparentAngleClose(t, prefix+".RightAscension", HMoonGeocentricApparentRa(jd), sample.RightAscension, 0.001)
@@ -54,7 +54,7 @@ func TestMoonGeocentricTrueCoordinatesFollowDefinition(t *testing.T) {
} }
for _, sample := range samples { for _, sample := range samples {
jd := TD2UT(Date2JDE(sample.UTC()), true) jd := UTC2TT(Date2JD(sample.UTC()))
wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd)) wantRA, wantDec := LoBoToRaDec(jd, HMoonTrueLo(jd), HMoonTrueBo(jd))
gotRA, gotDec := HMoonGeocentricTrueRaDec(jd) gotRA, gotDec := HMoonGeocentricTrueRaDec(jd)
+2 -2
View File
@@ -6,7 +6,7 @@ import (
) )
func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) { func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) {
jd := TD2UT(JDECalc(2026, 1, 1.25), true) jd := UTC2TT(JDCalc(2026, 1, 1.25))
ra, dec := HMoonGeocentricApparentRaDec(jd) ra, dec := HMoonGeocentricApparentRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricApparentRa(jd)); diff > 1e-12 { if diff := math.Abs(ra - HMoonGeocentricApparentRa(jd)); diff > 1e-12 {
@@ -18,7 +18,7 @@ func TestHMoonGeocentricApparentRaDecComponentsMatch(t *testing.T) {
} }
func TestHMoonGeocentricTrueRaDecComponentsMatch(t *testing.T) { func TestHMoonGeocentricTrueRaDecComponentsMatch(t *testing.T) {
jd := TD2UT(JDECalc(2026, 1, 1.25), true) jd := UTC2TT(JDCalc(2026, 1, 1.25))
ra, dec := HMoonGeocentricTrueRaDec(jd) ra, dec := HMoonGeocentricTrueRaDec(jd)
if diff := math.Abs(ra - HMoonGeocentricTrueRa(jd)); diff > 1e-12 { if diff := math.Abs(ra - HMoonGeocentricTrueRa(jd)); diff > 1e-12 {
+5 -48
View File
@@ -1,58 +1,15 @@
package basic package basic
import "math" // MoonHorizon 返回 UTC 儒略日下海平面几何月球中心地平圈(月球恰好在地平线上的观测者轨迹,
// MoonHorizon 返回 UT 儒略日下海平面几何月球中心地平圈(月球恰好在地平线上的观测者轨迹,
// 即月下点周围的地平圈)的 [经度, 纬度] 顶点,单位为度,不重复首点。视差与椭球口径同 // 即月下点周围的地平圈)的 [经度, 纬度] 顶点,单位为度,不重复首点。视差与椭球口径同
// HMoonHeight,不含折射;与 HMoonHeight(经, 纬, ..., 0) 配合时该圈上的点高度角为 0。 // HMoonHeight,不含折射;与 HMoonHeight(经, 纬, ..., 0) 配合时该圈上的点高度角为 0。
// samples<=0 取 360,其余夹到 [12, 1440]。 // samples<=0 取 360,其余夹到 [12, 1440]。
// MoonHorizon returns sea-level geometric Moon-centre horizon vertices in degrees for a UT Julian // MoonHorizon returns sea-level geometric Moon-centre horizon vertices in degrees for a UTC Julian
// day: the locus of observers that see the Moon exactly on the horizon. Parallax and the observer // day: the locus of observers that see the Moon exactly on the horizon. Parallax and the observer
// ellipsoid match HMoonHeight; refraction is excluded. // ellipsoid match HMoonHeight; refraction is excluded.
func MoonHorizon(jdUT float64, samples int) [][2]float64 { func MoonHorizon(jdUTC float64, samples int) [][2]float64 {
if !finite(jdUT) { if !finite(jdUTC) {
return nil return nil
} }
if samples <= 0 { return MoonStateAt(jdUTC).MoonHorizon(samples)
samples = 360
}
if samples < 12 {
samples = 12
} else if samples > 1440 {
samples = 1440
}
tt := TD2UT(jdUT, true)
ra, dec := HMoonTrueRaDec(tt)
distanceAU := HMoonAway(tt) / angularDiameterAstronomicalUnitKM
parallax := math.Sin(0.0024427777777*rad) / distanceAU
longitude := (ra - ApparentSiderealTime(jdUT)*15) * rad
latitude := dec * rad
if !finite(parallax) || parallax <= 0 || parallax >= 1 || !finite(longitude) || !finite(latitude) {
return nil
}
center := [3]float64{math.Cos(latitude) * math.Cos(longitude), math.Cos(latitude) * math.Sin(longitude), math.Sin(latitude)}
north := [3]float64{-math.Sin(latitude) * math.Cos(longitude), -math.Sin(latitude) * math.Sin(longitude), math.Cos(latitude)}
east := [3]float64{-math.Sin(longitude), math.Cos(longitude), 0}
points := make([][2]float64, samples)
for index := range points {
bearing := 2 * math.Pi * float64(index) / float64(samples)
radius := math.Acos(parallax)
var point [3]float64
for iteration := 0; iteration < 8; iteration++ {
for axis := range point {
point[axis] = center[axis]*math.Cos(radius) +
(north[axis]*math.Cos(bearing)+east[axis]*math.Sin(bearing))*math.Sin(radius)
}
lat := math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad
// The topocentric direction is horizontal when its dot product
// with the geodetic zenith vanishes: cos(radius)=observer/range.
next := math.Acos(parallax * (pcosi(lat, 0)*math.Cos(lat*rad) + psini(lat, 0)*math.Sin(lat*rad)))
if math.Abs(next-radius) < 1e-14 {
break
}
radius = next
}
points[index] = [2]float64{math.Atan2(point[1], point[0]) / rad, math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad}
}
return points
} }
+92 -1
View File
@@ -3,10 +3,11 @@ package basic
import ( import (
"math" "math"
"testing" "testing"
"time"
) )
func TestMoonHorizonMatchesTopocentricAltitude(t *testing.T) { func TestMoonHorizonMatchesTopocentricAltitude(t *testing.T) {
for _, jd := range []float64{JDECalc(2026, 3, 3), JDECalc(2025, 9, 7), JDECalc(2024, 12, 15)} { for _, jd := range []float64{JDCalc(2026, 3, 3), JDCalc(2025, 9, 7), JDCalc(2024, 12, 15)} {
points := MoonHorizon(jd, 360) points := MoonHorizon(jd, 360)
if len(points) != 360 { if len(points) != 360 {
t.Fatalf("horizon points=%d", len(points)) t.Fatalf("horizon points=%d", len(points))
@@ -21,3 +22,93 @@ func TestMoonHorizonMatchesTopocentricAltitude(t *testing.T) {
t.Fatal("invalid JD accepted") t.Fatal("invalid JD accepted")
} }
} }
// TestMoonHorizonUsesUTCInput 固定地平圈的时标口径:入参是 UTC 儒略日,站心恒星时按 UTC→UT1 换算。
// 取 DUT1 明显的两个时刻,确认圈上点只在 UTC 口径下高度角为零,且两种口径在本地可区分。
func TestMoonHorizonUsesUTCInput(t *testing.T) {
for _, at := range []time.Time{
time.Date(1980, 3, 15, 18, 0, 0, 0, time.UTC),
time.Date(2035, 3, 15, 18, 0, 0, 0, time.UTC),
} {
jdUTC := Date2JD(at)
dut1Seconds := (UTC2UT1(jdUTC) - jdUTC) * 86400
if math.Abs(dut1Seconds) < 0.3 {
t.Fatalf("%s DUT1=%.3f s 太小,区分不出两种口径", at.Format("2006-01-02"), dut1Seconds)
}
jdAsUT1 := jdUTC + dut1Seconds/86400
distinguishable := false
for _, point := range MoonHorizon(jdUTC, 360) {
if altitude := HMoonHeight(jdUTC, point[0], point[1], 0); math.Abs(altitude) > 1e-9 {
t.Fatalf("%s UTC 口径下圈上点高度角=%g,应为零", at.Format("2006-01-02"), altitude)
}
if altitude := HMoonHeight(jdAsUT1, point[0], point[1], 0); math.Abs(altitude) > 1e-4 {
distinguishable = true
}
}
if !distinguishable {
t.Fatalf("%s 两种口径不可区分,用例失去意义", at.Format("2006-01-02"))
}
if MoonHorizon(jdAsUT1, 360)[0] == MoonHorizon(jdUTC, 360)[0] {
t.Fatalf("%s UTC 与 UT1 两种读法给出了同一条圈", at.Format("2006-01-02"))
}
}
}
// TestMoonStateHorizonMatchesPackageHorizon 固定 MoonState 派生量与包级函数逐位一致,以及采样数与非法入参的兜底。
func TestMoonStateHorizonMatchesPackageHorizon(t *testing.T) {
jd := JDCalc(2026, 3, 3)
state := MoonStateAt(jd)
for _, samples := range []int{0, 1, 12, 360, 5000} {
got := state.MoonHorizon(samples)
want := MoonHorizon(jd, samples)
if len(got) != len(want) {
t.Fatalf("samples=%d 点数 %d,包级 %d", samples, len(got), len(want))
}
for index := range got {
if got[index] != want[index] {
t.Fatalf("samples=%d 第 %d 点 %v,包级 %v", samples, index, got[index], want[index])
}
}
}
for _, invalid := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
if points := MoonStateAt(invalid).MoonHorizon(360); points != nil {
t.Fatalf("非有限 UTC 儒略日 %v 仍给出地平圈", invalid)
}
if points := MoonHorizon(invalid, 360); points != nil {
t.Fatalf("非有限 UTC 儒略日 %v 仍给出地平圈", invalid)
}
}
if altitude := MoonStateAt(math.NaN()).HMoonHeight(0, 0); !math.IsNaN(altitude) {
t.Fatalf("非有限状态的高度角=%v,期望 NaN", altitude)
}
}
// TestHMoonHeightUsesLocalCivilFrame 固定 HMoonHeight 的时标框架:jd 是当地民用时(墙上时刻),
// tz 是时区偏移小时数。同一物理时刻写成「当地民用时 + tz」与「UTC 数值 + tz=0」必须一致;
// 把 UTC 数值再配非零 tz 会多减一次时区,必须能区分。
func TestHMoonHeightUsesLocalCivilFrame(t *testing.T) {
utc := time.Date(2029, 1, 1, 16, 0, 0, 0, time.UTC)
zone := time.FixedZone("CST", 8*3600)
utcJD := Date2JD(utc)
localJD := Date2JD(utc.In(zone))
// JD 约 2.5e6 天,双精度下 8 小时之差本身带 ~1e-8 h 的表示误差。
if offset := (localJD - utcJD) * 24; math.Abs(offset-8) > 1e-6 {
t.Fatalf("当地民用时框架与 UTC 相差 %.9f h,期望 8 h", offset)
}
for _, site := range []struct{ lon, lat float64 }{{108.729, -59.937}, {0, 0}, {-70, 45}} {
local := HMoonHeight(localJD, site.lon, site.lat, 8)
reference := HMoonHeight(utcJD, site.lon, site.lat, 0)
if math.Abs(local-reference) > 1e-5 {
t.Fatalf("(%.3f,%.3f) 当地民用时 %.9f,UTC 数值配 tz=0 %.9f", site.lon, site.lat, local, reference)
}
}
state := MoonStateAt(utcJD)
for _, site := range []struct{ lon, lat float64 }{{108.729, -59.937}, {0, 0}, {-70, 45}} {
if state.HMoonHeight(site.lon, site.lat) != HMoonHeight(utcJD, site.lon, site.lat, 0) {
t.Fatalf("(%.3f,%.3f) MoonState 与 HMoonHeight 不一致", site.lon, site.lat)
}
}
if mixed := HMoonHeight(utcJD, 0, 0, 8) - HMoonHeight(utcJD, 0, 0, 0); math.Abs(mixed) < 0.1 {
t.Fatalf("UTC 数值配非零 tz 的差异只有 %.6f°,用例失去区分度", mixed)
}
}
+16 -16
View File
@@ -16,8 +16,8 @@ const (
// DeclinationEvent 赤纬极值事件 / declination extremum event. // DeclinationEvent 赤纬极值事件 / declination extremum event.
type DeclinationEvent struct { type DeclinationEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day. // JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64 JD float64
// Declination 是该时刻月心地心赤纬,单位度 / geocentric lunar declination at the event, in degrees. // Declination 是该时刻月心地心赤纬,单位度 / geocentric lunar declination at the event, in degrees.
Declination float64 Declination float64
} }
@@ -124,8 +124,8 @@ func ClosestMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients) []DeclinationEvent { func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients) []DeclinationEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC) startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0) endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true) startTT := UTC2TT(Date2JD(startUTC))
endTT := TD2UT(Date2JDE(endUTC), true) endTT := UTC2TT(Date2JD(endUTC))
kStart := int(math.Floor((startTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) - 1 kStart := int(math.Floor((startTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) - 1
kEnd := int(math.Ceil((endTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) + 1 kEnd := int(math.Ceil((endTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) + 1
@@ -142,7 +142,7 @@ func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDe
events := make([]DeclinationEvent, 0, 2) events := make([]DeclinationEvent, 0, 2)
for k := kStart; k <= kEnd; k++ { for k := kStart; k <= kEnd; k++ {
event := moonMaximumDeclinationEvent(k, coeffs, cfg) event := moonMaximumDeclinationEvent(k, coeffs, cfg)
eventTimeUTC := JDE2DateByZone(event.JDE, time.UTC, false) eventTimeUTC := JD2DateByZone(event.JD, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) { if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue continue
} }
@@ -150,7 +150,7 @@ func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDe
} }
sort.Slice(events, func(i, j int) bool { sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE return events[i].JD < events[j].JD
}) })
return events return events
} }
@@ -161,7 +161,7 @@ func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, c
return HMoonTrueDecN(sampleTT, -1) return HMoonTrueDecN(sampleTT, -1)
}) })
return DeclinationEvent{ return DeclinationEvent{
JDE: TD2UT(eventTT, false), JD: TT2UTC(eventTT),
Declination: declination, Declination: declination,
} }
} }
@@ -180,10 +180,10 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
event := moonMaximumDeclinationEvent(centerK, coeffs, cfg) event := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
// 事件时刻随周期序号单调递增:该方向上最近的候选就是 centerK 沿该方向第一个满足方向不变量 // 事件时刻随周期序号单调递增:该方向上最近的候选就是 centerK 沿该方向第一个满足方向不变量
// 的周期;命中后只需再回退检查更早的周期是否同样满足,可达范围仍旧是 ±3 个周期。 // 的周期;命中后只需再回退检查更早的周期是否同样满足,可达范围仍旧是 ±3 个周期。
if !moonMaximumDeclinationMatchesDirection(event.JDE-jd, direction, includeCurrent) { if !moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ { for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg) event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
if moonMaximumDeclinationMatchesDirection(event.JDE-jd, direction, includeCurrent) { if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event return event
} }
} }
@@ -191,7 +191,7 @@ func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficie
} }
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ { for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg) previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg)
if !moonMaximumDeclinationMatchesDirection(previous.JDE-jd, direction, includeCurrent) { if !moonMaximumDeclinationMatchesDirection(previous.JD-jd, direction, includeCurrent) {
break break
} }
event = previous event = previous
@@ -207,15 +207,15 @@ func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoeffici
centerK := moonMaximumDeclinationOffset(jd, coeffs) centerK := moonMaximumDeclinationOffset(jd, coeffs)
center := moonMaximumDeclinationEvent(centerK, coeffs, cfg) center := moonMaximumDeclinationEvent(centerK, coeffs, cfg)
var last, next DeclinationEvent var last, next DeclinationEvent
if center.JDE <= jd { if center.JD <= jd {
last = center last = center
next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false) next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false)
} else { } else {
next = center next = center
last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true) last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true)
} }
lastDistance := math.Abs(jd - last.JDE) lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JDE - jd) nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance { if lastDistance <= nextDistance {
return last return last
} }
@@ -231,7 +231,7 @@ func moonMaximumDeclinationAround(jd float64, coeffs moonMaxDeclinationCoefficie
} }
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ { for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg) event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg)
if moonMaximumDeclinationMatchesDirection(event.JDE-jd, direction, includeCurrent) { if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event return event
} }
} }
@@ -251,7 +251,7 @@ func moonMaximumDeclinationSearchConfig(coeffs moonMaxDeclinationCoefficients) a
// moonMaximumDeclinationOffset 查询时刻落在哪个平均周期(种子多项式的中心序号)。 // moonMaximumDeclinationOffset 查询时刻落在哪个平均周期(种子多项式的中心序号)。
func moonMaximumDeclinationOffset(jd float64, coeffs moonMaxDeclinationCoefficients) int { func moonMaximumDeclinationOffset(jd float64, coeffs moonMaxDeclinationCoefficients) int {
return int(math.Round((TD2UT(jd, true) - coeffs.JDE0) / moonMaxDeclinationMeanMonthDays)) return int(math.Round((UTC2TT(jd) - coeffs.JDE0) / moonMaxDeclinationMeanMonthDays))
} }
func moonMaximumDeclinationMatchesDirection(delta float64, direction int, includeCurrent bool) bool { func moonMaximumDeclinationMatchesDirection(delta float64, direction int, includeCurrent bool) bool {
@@ -272,7 +272,7 @@ func moonMaximumDeclinationMatchesDirection(delta float64, direction int, includ
} }
func moonMaximumDeclinationEarlier(a, b DeclinationEvent) bool { func moonMaximumDeclinationEarlier(a, b DeclinationEvent) bool {
return a.JDE < b.JDE return a.JD < b.JD
} }
func moonMaximumDeclinationSeedTT(k int, coeffs moonMaxDeclinationCoefficients) float64 { func moonMaximumDeclinationSeedTT(k int, coeffs moonMaxDeclinationCoefficients) float64 {
+5 -5
View File
@@ -18,7 +18,7 @@ func moonMaximumDeclinationReferenceSearch(jd float64, coeffs moonMaxDeclination
var best DeclinationEvent var best DeclinationEvent
for offset := -moonMaxDeclinationSearchSpan; offset <= moonMaxDeclinationSearchSpan; offset++ { for offset := -moonMaxDeclinationSearchSpan; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+offset, coeffs, cfg) event := moonMaximumDeclinationEvent(centerK+offset, coeffs, cfg)
delta := event.JDE - jd delta := event.JD - jd
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) { if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue continue
} }
@@ -38,8 +38,8 @@ func moonMaximumDeclinationReferenceClosest(jd float64, coeffs moonMaxDeclinatio
} }
last := moonMaximumDeclinationReferenceSearch(jd, coeffs, -1, true) last := moonMaximumDeclinationReferenceSearch(jd, coeffs, -1, true)
next := moonMaximumDeclinationReferenceSearch(jd, coeffs, 1, false) next := moonMaximumDeclinationReferenceSearch(jd, coeffs, 1, false)
lastDistance := math.Abs(jd - last.JDE) lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JDE - jd) nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance { if lastDistance <= nextDistance {
return last return last
} }
@@ -48,7 +48,7 @@ func moonMaximumDeclinationReferenceClosest(jd float64, coeffs moonMaxDeclinatio
func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) { func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) {
for _, coeffs := range []moonMaxDeclinationCoefficients{moonMaxDeclinationNorthCoefficients, moonMaxDeclinationSouthCoefficients} { for _, coeffs := range []moonMaxDeclinationCoefficients{moonMaxDeclinationNorthCoefficients, moonMaxDeclinationSouthCoefficients} {
for jd := JDECalc(2024, 1, 1); jd <= JDECalc(2025, 6, 1); jd += 2.5 { for jd := JDCalc(2024, 1, 1); jd <= JDCalc(2025, 6, 1); jd += 2.5 {
for _, direction := range []struct { for _, direction := range []struct {
name string name string
dir int dir int
@@ -77,7 +77,7 @@ func TestMoonMaximumDeclinationPruningMatchesFullSearch(t *testing.T) {
} }
func BenchmarkMoonMaximumDeclinationFullSearchReference(b *testing.B) { func BenchmarkMoonMaximumDeclinationFullSearchReference(b *testing.B) {
jd := JDECalc(2025, 3, 1) jd := JDCalc(2025, 3, 1)
b.Run("Next", func(b *testing.B) { b.Run("Next", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_ = moonMaximumDeclinationReferenceSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false) _ = moonMaximumDeclinationReferenceSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false)
+25 -25
View File
@@ -79,7 +79,7 @@ func TestMoonMaximumDeclinationsMatchHorizonsBaseline(t *testing.T) {
t.Fatalf("unknown declination kind %q", sample.Kind) t.Fatalf("unknown declination kind %q", sample.Kind)
} }
gotTime := JDE2DateByZone(got.JDE, time.UTC, false) gotTime := JD2DateByZone(got.JD, time.UTC, false)
timeDiff := gotTime.Sub(wantTime) timeDiff := gotTime.Sub(wantTime)
if timeDiff < 0 { if timeDiff < 0 {
timeDiff = -timeDiff timeDiff = -timeDiff
@@ -124,23 +124,23 @@ func TestMoonMaximumDeclinationSignsAndOrder(t *testing.T) {
if event.Declination <= 0 { if event.Declination <= 0 {
t.Fatalf("north event #%d should be positive, got %.8f", i+1, event.Declination) t.Fatalf("north event #%d should be positive, got %.8f", i+1, event.Declination)
} }
if i > 0 && !(north[i-1].JDE < event.JDE) { if i > 0 && !(north[i-1].JD < event.JD) {
t.Fatalf("north events not strictly increasing: %.12f then %.12f", north[i-1].JDE, event.JDE) t.Fatalf("north events not strictly increasing: %.12f then %.12f", north[i-1].JD, event.JD)
} }
} }
for i, event := range south { for i, event := range south {
if event.Declination >= 0 { if event.Declination >= 0 {
t.Fatalf("south event #%d should be negative, got %.8f", i+1, event.Declination) t.Fatalf("south event #%d should be negative, got %.8f", i+1, event.Declination)
} }
if i > 0 && !(south[i-1].JDE < event.JDE) { if i > 0 && !(south[i-1].JD < event.JD) {
t.Fatalf("south events not strictly increasing: %.12f then %.12f", south[i-1].JDE, event.JDE) t.Fatalf("south events not strictly increasing: %.12f then %.12f", south[i-1].JD, event.JD)
} }
} }
} }
func TestMoonMaximumDeclinationSearchMatchesMonthlyEvents(t *testing.T) { func TestMoonMaximumDeclinationSearchMatchesMonthlyEvents(t *testing.T) {
query := time.Date(2026, time.January, 10, 0, 0, 0, 0, time.UTC) query := time.Date(2026, time.January, 10, 0, 0, 0, 0, time.UTC)
queryJDE := Date2JDE(query) queryJD := Date2JD(query)
northEvents := append([]DeclinationEvent{}, MoonMaximumNorthDeclinations(2025, time.December)...) northEvents := append([]DeclinationEvent{}, MoonMaximumNorthDeclinations(2025, time.December)...)
northEvents = append(northEvents, MoonMaximumNorthDeclinations(2026, time.January)...) northEvents = append(northEvents, MoonMaximumNorthDeclinations(2026, time.January)...)
@@ -150,13 +150,13 @@ func TestMoonMaximumDeclinationSearchMatchesMonthlyEvents(t *testing.T) {
southEvents = append(southEvents, MoonMaximumSouthDeclinations(2026, time.January)...) southEvents = append(southEvents, MoonMaximumSouthDeclinations(2026, time.January)...)
southEvents = append(southEvents, MoonMaximumSouthDeclinations(2026, time.February)...) southEvents = append(southEvents, MoonMaximumSouthDeclinations(2026, time.February)...)
assertSameDeclinationEvent(t, "last north", LastMoonMaximumNorthDeclination(queryJDE), expectedDirectionalDeclinationEvent(northEvents, queryJDE, -1, true)) assertSameDeclinationEvent(t, "last north", LastMoonMaximumNorthDeclination(queryJD), expectedDirectionalDeclinationEvent(northEvents, queryJD, -1, true))
assertSameDeclinationEvent(t, "next north", NextMoonMaximumNorthDeclination(queryJDE), expectedDirectionalDeclinationEvent(northEvents, queryJDE, 1, false)) assertSameDeclinationEvent(t, "next north", NextMoonMaximumNorthDeclination(queryJD), expectedDirectionalDeclinationEvent(northEvents, queryJD, 1, false))
assertSameDeclinationEvent(t, "closest north", ClosestMoonMaximumNorthDeclination(queryJDE), expectedClosestDeclinationEvent(northEvents, queryJDE)) assertSameDeclinationEvent(t, "closest north", ClosestMoonMaximumNorthDeclination(queryJD), expectedClosestDeclinationEvent(northEvents, queryJD))
assertSameDeclinationEvent(t, "last south", LastMoonMaximumSouthDeclination(queryJDE), expectedDirectionalDeclinationEvent(southEvents, queryJDE, -1, true)) assertSameDeclinationEvent(t, "last south", LastMoonMaximumSouthDeclination(queryJD), expectedDirectionalDeclinationEvent(southEvents, queryJD, -1, true))
assertSameDeclinationEvent(t, "next south", NextMoonMaximumSouthDeclination(queryJDE), expectedDirectionalDeclinationEvent(southEvents, queryJDE, 1, false)) assertSameDeclinationEvent(t, "next south", NextMoonMaximumSouthDeclination(queryJD), expectedDirectionalDeclinationEvent(southEvents, queryJD, 1, false))
assertSameDeclinationEvent(t, "closest south", ClosestMoonMaximumSouthDeclination(queryJDE), expectedClosestDeclinationEvent(southEvents, queryJDE)) assertSameDeclinationEvent(t, "closest south", ClosestMoonMaximumSouthDeclination(queryJD), expectedClosestDeclinationEvent(southEvents, queryJD))
} }
func TestMoonMaximumDeclinationSearchAtExactEventTime(t *testing.T) { func TestMoonMaximumDeclinationSearchAtExactEventTime(t *testing.T) {
@@ -165,29 +165,29 @@ func TestMoonMaximumDeclinationSearchAtExactEventTime(t *testing.T) {
t.Fatalf("expected at least two north events spanning Jan 2026 search window, got %d", len(north)) t.Fatalf("expected at least two north events spanning Jan 2026 search window, got %d", len(north))
} }
exactJDE := north[0].JDE exactJD := north[0].JD
assertSameDeclinationEvent(t, "exact last north", LastMoonMaximumNorthDeclination(exactJDE), north[0]) assertSameDeclinationEvent(t, "exact last north", LastMoonMaximumNorthDeclination(exactJD), north[0])
assertSameDeclinationEvent(t, "exact closest north", ClosestMoonMaximumNorthDeclination(exactJDE), north[0]) assertSameDeclinationEvent(t, "exact closest north", ClosestMoonMaximumNorthDeclination(exactJD), north[0])
assertSameDeclinationEvent(t, "exact next north", NextMoonMaximumNorthDeclination(exactJDE), north[1]) assertSameDeclinationEvent(t, "exact next north", NextMoonMaximumNorthDeclination(exactJD), north[1])
} }
func assertSameDeclinationEvent(t *testing.T, name string, got, want DeclinationEvent) { func assertSameDeclinationEvent(t *testing.T, name string, got, want DeclinationEvent) {
t.Helper() t.Helper()
if math.Abs(got.JDE-want.JDE) > 1e-12 { if math.Abs(got.JD-want.JD) > 1e-12 {
t.Fatalf("%s JDE mismatch: got %.12f want %.12f", name, got.JDE, want.JDE) t.Fatalf("%s JD mismatch: got %.12f want %.12f", name, got.JD, want.JD)
} }
if math.Float64bits(got.Declination) != math.Float64bits(want.Declination) { if math.Float64bits(got.Declination) != math.Float64bits(want.Declination) {
t.Fatalf("%s declination mismatch: got %.12f want %.12f", name, got.Declination, want.Declination) t.Fatalf("%s declination mismatch: got %.12f want %.12f", name, got.Declination, want.Declination)
} }
} }
func expectedDirectionalDeclinationEvent(events []DeclinationEvent, queryJDE float64, direction int, includeCurrent bool) DeclinationEvent { func expectedDirectionalDeclinationEvent(events []DeclinationEvent, queryJD float64, direction int, includeCurrent bool) DeclinationEvent {
var ( var (
found bool found bool
best DeclinationEvent best DeclinationEvent
) )
for _, event := range events { for _, event := range events {
delta := event.JDE - queryJDE delta := event.JD - queryJD
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) { if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue continue
} }
@@ -196,17 +196,17 @@ func expectedDirectionalDeclinationEvent(events []DeclinationEvent, queryJDE flo
found = true found = true
continue continue
} }
if math.Abs(delta) < math.Abs(best.JDE-queryJDE) || (math.Abs(delta) == math.Abs(best.JDE-queryJDE) && event.JDE < best.JDE) { if math.Abs(delta) < math.Abs(best.JD-queryJD) || (math.Abs(delta) == math.Abs(best.JD-queryJD) && event.JD < best.JD) {
best = event best = event
} }
} }
return best return best
} }
func expectedClosestDeclinationEvent(events []DeclinationEvent, queryJDE float64) DeclinationEvent { func expectedClosestDeclinationEvent(events []DeclinationEvent, queryJD float64) DeclinationEvent {
last := expectedDirectionalDeclinationEvent(events, queryJDE, -1, true) last := expectedDirectionalDeclinationEvent(events, queryJD, -1, true)
next := expectedDirectionalDeclinationEvent(events, queryJDE, 1, false) next := expectedDirectionalDeclinationEvent(events, queryJD, 1, false)
if math.Abs(queryJDE-last.JDE) <= math.Abs(next.JDE-queryJDE) { if math.Abs(queryJD-last.JD) <= math.Abs(next.JD-queryJD) {
return last return last
} }
return next return next
+122 -34
View File
@@ -12,14 +12,14 @@ import (
func MoonAzimuth(jd, lon, lat, tz float64) float64 { func MoonAzimuth(jd, lon, lat, tz float64) float64 {
//tmp := (tz*15 - lon) * 4 / 60 //tmp := (tz*15 - lon) * 4 / 60
calcjd := TD2UT(jd-tz/24, true) jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(calcjd) ra := MoonTrueRa(jde)
dec := MoonTrueDec(calcjd) dec := MoonTrueDec(jde)
away := MoonAway(calcjd) / 149597870.7 away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0) ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0) nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24 jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra) hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat)) tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2) azimuth := ArcTan(tmp2)
@@ -42,14 +42,14 @@ func MoonAzimuth(jd, lon, lat, tz float64) float64 {
func MoonHeight(jd, lon, lat, tz float64) float64 { func MoonHeight(jd, lon, lat, tz float64) float64 {
// tmp := (tz*15 - lon) * 4 / 60 // tmp := (tz*15 - lon) * 4 / 60
//truejd=jd-tmp/24; //truejd=jd-tmp/24;
calcjd := TD2UT(jd-tz/24, true) jde := UTC2TT(jd - tz/24)
ra := MoonTrueRa(calcjd) ra := MoonTrueRa(jde)
dec := MoonTrueDec(calcjd) dec := MoonTrueDec(jde)
away := MoonAway(calcjd) / 149597870.7 away := MoonAway(jde) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0) ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0) nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24 jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra) hourAngle := Limit360(st - nra)
tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle) tmp2 := Sin(lat)*Sin(ndec) + Cos(ndec)*Cos(lat)*Cos(hourAngle)
return ArcSin(tmp2) return ArcSin(tmp2)
@@ -60,14 +60,14 @@ func HMoonAzimuth(jd, lon, lat, tz float64) float64 {
} }
func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 { func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 {
calcjd := TD2UT(jd-tz/24, true) jde := UTC2TT(jd - tz/24)
ra := HMoonTrueRaN(calcjd, n) ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(calcjd, n) dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(calcjd, n) / 149597870.7 away := HMoonAwayN(jde, n) / 149597870.7
ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0) ndec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0) nra := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
calcjd = jd - tz/24 jdUT := jd - tz/24
st := Limit360(ApparentSiderealTime(calcjd)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(jdUT))*15 + lon)
hourAngle := Limit360(st - nra) hourAngle := Limit360(st - nra)
tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat)) tmp2 := Sin(hourAngle) / (Cos(hourAngle)*Sin(lat) - Tan(ndec)*Cos(lat))
azimuth := ArcTan(tmp2) azimuth := ArcTan(tmp2)
@@ -85,6 +85,14 @@ func HMoonAzimuthN(jd, lon, lat, tz float64, n int) float64 {
} }
} }
} }
// HMoonHeight 当地民用时儒略日下的月心几何高度角(度,不含折射)/ geometric Moon-centre altitude in degrees for a local civil Julian day.
//
// jd 是该时区的当地民用时(墙上时刻)儒略日,tz 是时区偏移小时数,库内按 jd−tz/24 换成 UTC。
// 只有 tz 给 0 时 jd 才是 UTC 儒略日;不要拿 UTC 数值再配非零 tz,那会多减一次时区。
// jd is that zone's local civil (wall-clock) Julian day and tz is the zone offset in hours,
// converted internally as jd-tz/24. Only tz 0 makes jd a UTC Julian day: pairing a UTC value with a
// non-zero tz subtracts the offset twice.
func HMoonHeight(jd, lon, lat, tz float64) float64 { func HMoonHeight(jd, lon, lat, tz float64) float64 {
return HMoonHeightN(jd, lon, lat, tz, -1) return HMoonHeightN(jd, lon, lat, tz, -1)
} }
@@ -95,12 +103,12 @@ type moonObservationState struct {
} }
func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObservationState { func hMoonObservationStateN(jd, lon, lat, tz, height float64, n int) moonObservationState {
calculationJD := TD2UT(jd-tz/24, true) calculationJDE := UTC2TT(jd - tz/24)
ra, dec := HMoonTrueRaDecN(calculationJD, n) ra, dec := HMoonTrueRaDecN(calculationJDE, n)
distanceKM := HMoonAwayN(calculationJD, n) distanceKM := HMoonAwayN(calculationJDE, n)
distanceAU := distanceKM / angularDiameterAstronomicalUnitKM distanceAU := distanceKM / angularDiameterAstronomicalUnitKM
topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, distanceAU, height) topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, lat, lon, jd-tz/24, distanceAU, height)
siderealTime := Limit360(ApparentSiderealTime(jd-tz/24)*15 + lon) siderealTime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
hourAngle := Limit360(siderealTime - topocentricRA) hourAngle := Limit360(siderealTime - topocentricRA)
altitudeSine := Sin(lat)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(lat)*Cos(hourAngle) altitudeSine := Sin(lat)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(lat)*Cos(hourAngle)
return moonObservationState{ return moonObservationState{
@@ -113,6 +121,89 @@ func HMoonHeightN(jd, lon, lat, tz float64, n int) float64 {
return hMoonObservationStateN(jd, lon, lat, tz, 0, n).altitude return hMoonObservationStateN(jd, lon, lat, tz, 0, n).altitude
} }
// MoonState 同一瞬间可对任意观测点复用的月球位置与恒星时 / one instant's lunar position and sidereal time, reusable across observers.
type MoonState struct {
rightAscension float64
declination float64
distanceAU float64
siderealTime float64
}
// MoonStateAt 由 UTC 儒略日构造该瞬间的可复用月球状态 / builds the reusable state for one UTC Julian day.
func MoonStateAt(utcJD float64) MoonState {
jde := UTC2TT(utcJD)
rightAscension, declination := HMoonTrueRaDec(jde)
return MoonState{
rightAscension: rightAscension,
declination: declination,
distanceAU: HMoonAway(jde) / angularDiameterAstronomicalUnitKM,
siderealTime: ApparentSiderealTime(UTC2UT1(utcJD)) * 15,
}
}
func (state MoonState) finite() bool {
return finite(state.rightAscension) && finite(state.declination) &&
finite(state.distanceAU) && finite(state.siderealTime)
}
// HMoonHeight 给定观测者经度、纬度(度,椭球高 0)的月心几何高度角,等于 HMoonHeight(构造本状态时的 UTC 儒略日, 经, 纬, 0)。
// HMoonHeight returns the geometric Moon-centre altitude for one observer, equal to HMoonHeight(the UTC Julian day given to MoonStateAt, lon, lat, 0).
func (state MoonState) HMoonHeight(longitude, latitude float64) float64 {
// 本状态固定是 UTC 瞬间、椭球高 0,因此只对应包级 tz=0、height=0 的用法。
// 恒星时已在状态里算好,这里不再走会重算恒星时与时标换算的 TopocentricRaDec。
topocentricRA, topocentricDec := topocentricRaDecWithSidereal(
state.rightAscension, state.declination, latitude, longitude, state.siderealTime, state.distanceAU, 0,
)
hourAngle := Limit360(Limit360(state.siderealTime+longitude) - topocentricRA)
return ArcSin(Sin(latitude)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(latitude)*Cos(hourAngle))
}
// MoonHorizon 用本状态生成海平面几何月心地平圈,口径同包级 MoonHorizon / sea-level geometric Moon-centre horizon ring from this state.
func (state MoonState) MoonHorizon(samples int) [][2]float64 {
if !state.finite() {
return nil
}
if samples <= 0 {
samples = 360
}
if samples < 12 {
samples = 12
} else if samples > 1440 {
samples = 1440
}
parallax := math.Sin(0.0024427777777*rad) / state.distanceAU
longitude := (state.rightAscension - state.siderealTime) * rad
latitude := state.declination * rad
if !finite(parallax) || parallax <= 0 || parallax >= 1 || !finite(longitude) || !finite(latitude) {
return nil
}
center := [3]float64{math.Cos(latitude) * math.Cos(longitude), math.Cos(latitude) * math.Sin(longitude), math.Sin(latitude)}
north := [3]float64{-math.Sin(latitude) * math.Cos(longitude), -math.Sin(latitude) * math.Sin(longitude), math.Cos(latitude)}
east := [3]float64{-math.Sin(longitude), math.Cos(longitude), 0}
points := make([][2]float64, samples)
for index := range points {
bearing := 2 * math.Pi * float64(index) / float64(samples)
radius := math.Acos(parallax)
var point [3]float64
for iteration := 0; iteration < 8; iteration++ {
for axis := range point {
point[axis] = center[axis]*math.Cos(radius) +
(north[axis]*math.Cos(bearing)+east[axis]*math.Sin(bearing))*math.Sin(radius)
}
lat := math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad
// The topocentric direction is horizontal when its dot product
// with the geodetic zenith vanishes: cos(radius)=observer/range.
next := math.Acos(parallax * (pcosi(lat, 0)*math.Cos(lat*rad) + psini(lat, 0)*math.Sin(lat*rad)))
if math.Abs(next-radius) < 1e-14 {
break
}
radius = next
}
points[index] = [2]float64{math.Atan2(point[1], point[0]) / rad, math.Asin(math.Max(-1, math.Min(1, point[2]))) / rad}
}
return points
}
func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 { func moonRiseSetResidual(jd, longitude, latitude, timeZone, zenithShift, height float64, n int) float64 {
state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n) state := hMoonObservationStateN(jd, longitude, latitude, timeZone, height, n)
// 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon. // 相对观测者下沉地平线的视上缘高度角 / Apparent upper-limb altitude relative to the observer's depressed horizon.
@@ -157,13 +248,12 @@ func GetMoonTZTime(jd, lon, lat, tz float64) float64 { //实际中天时间{
return estimateJD return estimateJD
} }
func MoonCulminationTime(jde, lon, lat, timezone float64) float64 { func MoonCulminationTime(localJD, lon, lat, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时 // localJD 是本地民用日锚点(当地 0 时),不是力学时;ra/dec 为瞬时天球坐标,非 J2000 等固定历元。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标 localJD = math.Floor(localJD) + 0.5
jde = math.Floor(jde) + 0.5 estimateJD := localJD + Limit360(360-MoonTimeAngle(localJD, lon, lat, timezone))/15.0/24.0/0.9
estimateJD := jde + Limit360(360-MoonTimeAngle(jde, lon, lat, timezone))/15.0/24.0/0.9 limitHA := func(localJD, lon, timezone float64) float64 {
limitHA := func(jde, lon, timezone float64) float64 { ha := MoonTimeAngle(localJD, lon, lat, timezone)
ha := MoonTimeAngle(jde, lon, lat, timezone)
if ha < 180 { if ha < 180 {
ha += 360 ha += 360
} }
@@ -182,7 +272,7 @@ func MoonCulminationTime(jde, lon, lat, timezone float64) float64 {
} }
func MoonTimeAngle(jd, lon, lat, tz float64) float64 { func MoonTimeAngle(jd, lon, lat, tz float64) float64 {
startime := Limit360(ApparentSiderealTime(jd-tz/24)*15 + lon) startime := Limit360(ApparentSiderealTime(UTC2UT1(jd-tz/24))*15 + lon)
timeangle := startime - HMoonApparentRa(jd, lon, lat, tz) timeangle := startime - HMoonApparentRa(jd, lon, lat, tz)
if timeangle < 0 { if timeangle < 0 {
timeangle += 360 timeangle += 360
@@ -198,8 +288,7 @@ func GetMoonRiseTime(julianDay, longitude, latitude, timeZone, zenithShift, heig
timeZone = longitude / 15 timeZone = longitude / 15
var timeToMeridian float64 var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5 civilDayStart := math.Floor(julianDay) + 0.5
//julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE // 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
//fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错
julianDay = math.Floor(julianDay) + 0.5 julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1) moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
@@ -282,8 +371,7 @@ func GetMoonSetTime(julianDay, longitude, latitude, timeZone, zenithShift, heigh
timeZone = longitude / 15 timeZone = longitude / 15
var timeToMeridian float64 var timeToMeridian float64
civilDayStart := math.Floor(julianDay) + 0.5 civilDayStart := math.Floor(julianDay) + 0.5
//julianDay = math.Floor(julianDay) + 0.5 - originalTimeZone/24 + timeZone/24 // 求0时JDE // 时间分界线以传入的时区为准,不用当地时区,否则 0 时的判断会出错。
//fix:这里时间分界线应当以传入的时区为准,不应当使用当地时区,否则在0时的判断会出错
julianDay = math.Floor(julianDay) + 0.5 julianDay = math.Floor(julianDay) + 0.5
estimatedTime := julianDay estimatedTime := julianDay
moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1) moonResidual := moonRiseSetResidual(julianDay, longitude, latitude, originalTimeZone, zenithShift, height, -1)
+6 -8
View File
@@ -6,13 +6,13 @@ import (
. "b612.me/astro/tools" . "b612.me/astro/tools"
) )
func MoonPhase(jd float64) float64 { func MoonPhase(jde float64) float64 {
moonBo := HMoonTrueBo(jd) moonBo := HMoonTrueBo(jde)
sunLo := HSunApparentLo(jd) sunLo := HSunApparentLo(jde)
moonLo := HMoonApparentLo(jd) moonLo := HMoonApparentLo(jde)
tmp := Cos(moonBo) * Cos(sunLo-moonLo) tmp := Cos(moonBo) * Cos(sunLo-moonLo)
earthSunDistance := Distance(jd) * 149597870.691 earthSunDistance := Distance(jde) * 149597870.691
i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAway(jd) - earthSunDistance*tmp) i := earthSunDistance * Sin(ArcCos(tmp)) / (HMoonAway(jde) - earthSunDistance*tmp)
i = ArcTan(i) i = ArcTan(i)
if i < 0 { if i < 0 {
i += 180 i += 180
@@ -197,8 +197,6 @@ func CalcMoonX(year float64, quarterType int) float64 {
A14 := 331.55 + 3.592518*k A14 := 331.55 + 3.592518*k
planetaryCorrection := 325*Sin(A1) + 165*Sin(A2) + 164*Sin(A3) + 126*Sin(A4) + 110*Sin(A5) + 62*Sin(A6) + 60*Sin(A7) + 56*Sin(A8) + 47*Sin(A9) + 42*Sin(A10) + 40*Sin(A11) + 37*Sin(A12) + 35*Sin(A13) + 23*Sin(A14) planetaryCorrection := 325*Sin(A1) + 165*Sin(A2) + 164*Sin(A3) + 126*Sin(A4) + 110*Sin(A5) + 62*Sin(A6) + 60*Sin(A7) + 56*Sin(A8) + 47*Sin(A9) + 42*Sin(A10) + 40*Sin(A11) + 37*Sin(A12) + 35*Sin(A13) + 23*Sin(A14)
planetaryCorrection /= 1000000 planetaryCorrection /= 1000000
//die(tmp2);
//die(JDE." ".tmp." ".tmp2." ".W);
jde = jde + planetaryCorrection + correction jde = jde + planetaryCorrection + correction
if quarterType == 0 { if quarterType == 0 {
jde += W jde += W
+24 -24
View File
@@ -28,35 +28,35 @@ type MoonPhysicalInfo struct {
} }
// MoonPhysical 月球物理观测参数 / physical observing parameters of the Moon. // MoonPhysical 月球物理观测参数 / physical observing parameters of the Moon.
func MoonPhysical(jd float64) MoonPhysicalInfo { func MoonPhysical(jde float64) MoonPhysicalInfo {
return MoonPhysicalN(jd, -1) return MoonPhysicalN(jde, -1)
} }
// MoonPhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon. // MoonPhysicalN 月球物理观测参数(截断版) / truncated physical observing parameters of the Moon.
func MoonPhysicalN(jd float64, n int) MoonPhysicalInfo { func MoonPhysicalN(jde float64, n int) MoonPhysicalInfo {
return moonPhysicalNFromCoordinates(jd, n, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n), HMoonTrueRaN(jd, n)) return moonPhysicalNFromCoordinates(jde, n, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n), HMoonTrueRaN(jde, n))
} }
// MoonTopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon. // MoonTopocentricPhysical 月球站心物理观测参数 / topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysical(jd, observerLon, observerLat, height float64) MoonPhysicalInfo { func MoonTopocentricPhysical(jde, observerLon, observerLat, height float64) MoonPhysicalInfo {
return MoonTopocentricPhysicalN(jd, observerLon, observerLat, height, -1) return MoonTopocentricPhysicalN(jde, observerLon, observerLat, height, -1)
} }
// MoonTopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon. // MoonTopocentricPhysicalN 月球站心物理观测参数(截断版) / truncated topocentric physical observing parameters of the Moon.
func MoonTopocentricPhysicalN(jd, observerLon, observerLat, height float64, n int) MoonPhysicalInfo { func MoonTopocentricPhysicalN(jde, observerLon, observerLat, height float64, n int) MoonPhysicalInfo {
lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jd, observerLon, observerLat, height, n) lambda, beta, alpha := moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height, n)
return moonPhysicalNFromCoordinates(jd, n, lambda, beta, alpha) return moonPhysicalNFromCoordinates(jde, n, lambda, beta, alpha)
} }
func moonPhysicalNFromCoordinates(jd float64, n int, lambda, beta, alpha float64) MoonPhysicalInfo { func moonPhysicalNFromCoordinates(jde float64, n int, lambda, beta, alpha float64) MoonPhysicalInfo {
t := (jd - 2451545.0) / 36525.0 t := (jde - 2451545.0) / 36525.0
epsilon := TrueObliquity(jd) epsilon := TrueObliquity(jde)
deltaPsi := Nutation2000Bi(jd) deltaPsi := Nutation2000Bi(jde)
D := Limit360(SunMoonAngle(jd)) D := Limit360(SunMoonAngle(jde))
sunMeanAnomaly := Limit360(SunM(jd)) sunMeanAnomaly := Limit360(SunM(jde))
moonMeanAnomaly := Limit360(MoonM(jd)) moonMeanAnomaly := Limit360(MoonM(jde))
F := Limit360(MoonLonX(jd)) F := Limit360(MoonLonX(jde))
omega := moonPhysicalMeanAscendingNode(t) omega := moonPhysicalMeanAscendingNode(t)
E := 1 - 0.002516*t - 0.0000074*t*t E := 1 - 0.002516*t - 0.0000074*t*t
K1 := 119.75 + 131.849*t K1 := 119.75 + 131.849*t
@@ -91,15 +91,15 @@ func moonPhysicalNFromCoordinates(jd float64, n int, lambda, beta, alpha float64
} }
} }
func moonTopocentricPhysicalCoordinatesN(jd, observerLon, observerLat, height float64, n int) (lambda, beta, alpha float64) { func moonTopocentricPhysicalCoordinatesN(jde, observerLon, observerLat, height float64, n int) (lambda, beta, alpha float64) {
geocentricRA := HMoonTrueRaN(jd, n) geocentricRA := HMoonTrueRaN(jde, n)
geocentricDec := HMoonTrueDecN(jd, n) geocentricDec := HMoonTrueDecN(jde, n)
distanceAU := HMoonAwayN(jd, n) / moonPhysicalAstronomicalUnitKM distanceAU := HMoonAwayN(jde, n) / moonPhysicalAstronomicalUnitKM
utJD := TD2UT(jd, false) utcJD := TT2UTC(jde)
var topocentricDec float64 var topocentricDec float64
alpha, topocentricDec = TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, utJD, distanceAU, height) alpha, topocentricDec = TopocentricRaDec(geocentricRA, geocentricDec, observerLat, observerLon, utcJD, distanceAU, height)
lambda, beta = RaDecToLoBo(jd, alpha, topocentricDec) lambda, beta = RaDecToLoBo(jde, alpha, topocentricDec)
return return
} }
+1 -1
View File
@@ -43,7 +43,7 @@ func TestMoonPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
} }
for _, date := range dates { for _, date := range dates {
jd := TD2UT(Date2JDE(date.UTC()), true) jd := UTC2TT(Date2JD(date.UTC()))
info := MoonPhysical(jd) info := MoonPhysical(jd)
prefix := date.Format(time.RFC3339) prefix := date.Format(time.RFC3339)
+1 -1
View File
@@ -184,7 +184,7 @@ func moonPlanetConjunctionEventUT(leftTT, rightTT float64, planet MoonPlanetConj
if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance { if math.Abs(moonPlanetConjunctionDeltaAt(eventTT, planet, -1)) > moonPlanetConjunctionEventTolerance {
return math.NaN() return math.NaN()
} }
return TD2UT(eventTT, false) return TT2UTC(eventTT)
} }
func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalResult, queryTT, eventUT float64) { func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalResult, queryTT, eventUT float64) {
+33 -33
View File
@@ -92,9 +92,9 @@ func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err) t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
} }
queryTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true) queryTT := UTC2TT(Date2JD(wantTime.Add(-12 * time.Hour).UTC()))
gotUT := tc.next(queryTT, tc.planet) gotUT := tc.next(queryTT, tc.planet)
gotTime := JDE2DateByZone(gotUT, time.UTC, false) gotTime := JD2DateByZone(gotUT, time.UTC, false)
diff := gotTime.Sub(wantTime) diff := gotTime.Sub(wantTime)
if diff < 0 { if diff < 0 {
diff = -diff diff = -diff
@@ -106,7 +106,7 @@ func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance) t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance)
} }
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), tc.planet, -1)) delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), tc.planet, -1))
if delta > 0.01 { if delta > 0.01 {
t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta) t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta)
} }
@@ -144,11 +144,11 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T
if err != nil { if err != nil {
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err) t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
} }
seedTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true) seedTT := UTC2TT(Date2JD(wantTime.Add(-12 * time.Hour).UTC()))
eventUT := NextMoonPlanetConjunction(seedTT, planet) eventUT := NextMoonPlanetConjunction(seedTT, planet)
eventTime := JDE2DateByZone(eventUT, time.UTC, false) eventTime := JD2DateByZone(eventUT, time.UTC, false)
queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true) queryAtTT := UTC2TT(Date2JD(eventTime.UTC()))
queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true) queryAfterTT := UTC2TT(Date2JD(eventTime.Add(time.Hour).UTC()))
exactNext := NextMoonPlanetConjunction(queryAtTT, planet) exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet) exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
@@ -159,7 +159,7 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T
"exactClosest": exactClosest, "exactClosest": exactClosest,
"lastAfterEvent": exactLastAfter, "lastAfterEvent": exactLastAfter,
} { } {
gotTime := JDE2DateByZone(gotUT, time.UTC, false) gotTime := JD2DateByZone(gotUT, time.UTC, false)
if diff := math.Abs(gotUT - eventUT); diff > 1e-9 { if diff := math.Abs(gotUT - eventUT); diff > 1e-9 {
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400) t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400)
} }
@@ -169,25 +169,25 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T
func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) { func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) {
query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC) query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC)
queryTT := TD2UT(Date2JDE(query), true) queryTT := UTC2TT(Date2JD(query))
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 { if math.Abs(lastUT-Date2JD(query)) <= 5.0/86400.0 {
t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano)) t.Fatalf("last returned query time on branch jump: got %s", JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
} }
if math.Abs(nextUT-Date2JDE(query)) <= 5.0/86400.0 { if math.Abs(nextUT-Date2JD(query)) <= 5.0/86400.0 {
t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano)) t.Fatalf("next returned query time on branch jump: got %s", JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
} }
for name, gotUT := range map[string]float64{ for name, gotUT := range map[string]float64{
"last": lastUT, "last": lastUT,
"next": nextUT, "next": nextUT,
} { } {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1)) delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), MoonPlanetConjunctionSaturn, -1))
if delta > moonPlanetConjunctionEventTolerance { if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano)) t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JD2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
} }
} }
} }
@@ -208,7 +208,7 @@ func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
} }
for _, sample := range samples { for _, sample := range samples {
queryTT := TD2UT(Date2JDE(sample.query.UTC()), true) queryTT := UTC2TT(Date2JD(sample.query.UTC()))
lastUT := LastMoonPlanetConjunction(queryTT, sample.planet) lastUT := LastMoonPlanetConjunction(queryTT, sample.planet)
nextUT := NextMoonPlanetConjunction(queryTT, sample.planet) nextUT := NextMoonPlanetConjunction(queryTT, sample.planet)
closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet) closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet)
@@ -217,22 +217,22 @@ func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT) t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT)
} }
if !eventUTQueryBeforeOrEqual(lastUT, queryTT) { if !eventUTQueryBeforeOrEqual(lastUT, queryTT) {
t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano)) t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
} }
if !eventUTQueryAfterOrEqual(nextUT, queryTT) { if !eventUTQueryAfterOrEqual(nextUT, queryTT) {
t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano)) t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
} }
if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) { if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) {
t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano)) t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JD2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano))
} }
for name, gotUT := range map[string]float64{ for name, gotUT := range map[string]float64{
"last": lastUT, "last": lastUT,
"next": nextUT, "next": nextUT,
"closest": closestUT, "closest": closestUT,
} { } {
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), sample.planet, -1)) delta := math.Abs(moonPlanetConjunctionDeltaAt(UTC2TT(gotUT), sample.planet, -1))
if delta > moonPlanetConjunctionEventTolerance { if delta > moonPlanetConjunctionEventTolerance {
t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano)) t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JD2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
} }
} }
} }
@@ -240,7 +240,7 @@ func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) { func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC) query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)
queryTT := TD2UT(Date2JDE(query.UTC()), true) queryTT := UTC2TT(Date2JD(query.UTC()))
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn) nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
@@ -250,35 +250,35 @@ func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC) wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC)
const tolerance = 5.0 / 86400.0 const tolerance = 5.0 / 86400.0
if diff := math.Abs(lastUT - Date2JDE(wantLast)); diff > tolerance { if diff := math.Abs(lastUT - Date2JD(wantLast)); diff > tolerance {
t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400) t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400)
} }
if diff := math.Abs(nextUT - Date2JDE(wantNext)); diff > tolerance { if diff := math.Abs(nextUT - Date2JD(wantNext)); diff > tolerance {
t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400) t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400)
} }
if !sameEventJD(closestUT, lastUT) { if !sameEventJD(closestUT, lastUT) {
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano)) t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JD2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
} }
} }
func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) { func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
seed := TD2UT(Date2JDE(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)), true) seed := UTC2TT(Date2JD(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)))
eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury) eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second) query := JD2DateByZone(eventUT, time.UTC, false).Add(time.Second)
queryTT := TD2UT(Date2JDE(query.UTC()), true) queryTT := UTC2TT(Date2JD(query.UTC()))
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury) nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
if eventUTQueryTTDelta(nextUT, queryTT) <= 0 { if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs", t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
query.Format(time.RFC3339Nano), query.Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
eventUTQueryTTDelta(nextUT, queryTT)*86400, eventUTQueryTTDelta(nextUT, queryTT)*86400,
) )
} }
if sameEventJD(nextUT, eventUT) { if sameEventJD(nextUT, eventUT) {
t.Fatalf("next conjunction should advance to a later event: event=%s next=%s", t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), JD2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
) )
} }
} }
+59 -59
View File
@@ -134,8 +134,8 @@ func moonReducedLinearPhase(offset, rate, t, tLow float64) float64 {
return math.FMA(-turns, 2*math.Pi, product) + (roundoff - turns*twoPiLow) + offset return math.FMA(-turns, 2*math.Pi, product) + (roundoff - turns*twoPiLow) + offset
} }
func HMoonTrueLo(jd float64) float64 { //计算月亮 func HMoonTrueLo(jde float64) float64 { //计算月亮
return HMoonTrueLoN(jd, -1) return HMoonTrueLoN(jde, -1)
} }
func HMoonTrueLoN(jd float64, n int) float64 { //计算月亮 func HMoonTrueLoN(jd float64, n int) float64 { //计算月亮
@@ -143,8 +143,8 @@ func HMoonTrueLoN(jd float64, n int) float64 { //计算月亮
return Limit360(v) return Limit360(v)
} }
func HMoonTrueBo(jd float64) float64 { func HMoonTrueBo(jde float64) float64 {
return HMoonTrueBoN(jd, -1) return HMoonTrueBoN(jde, -1)
} }
func HMoonTrueBoN(jd float64, n int) float64 { func HMoonTrueBoN(jd float64, n int) float64 {
@@ -152,8 +152,8 @@ func HMoonTrueBoN(jd float64, n int) float64 {
return v return v
} }
func HMoonAway(jd float64) float64 { //'月地距离 func HMoonAway(jde float64) float64 { //'月地距离
return HMoonAwayN(jd, -1) return HMoonAwayN(jde, -1)
} }
func HMoonAwayN(jd float64, n int) float64 { //'月地距离 func HMoonAwayN(jd float64, n int) float64 { //'月地距离
@@ -163,95 +163,95 @@ func HMoonAwayN(jd float64, n int) float64 { //'月地距离
/* /*
* @name 月球视黄经 * @name 月球视黄经
*/ */
func HMoonApparentLo(jd float64) float64 { func HMoonApparentLo(jde float64) float64 {
return HMoonApparentLoN(jd, -1) return HMoonApparentLoN(jde, -1)
} }
func HMoonApparentLoN(jd float64, n int) float64 { func HMoonApparentLoN(jde float64, n int) float64 {
return HMoonTrueLoN(jd, n) + Nutation2000Bi(jd) return HMoonTrueLoN(jde, n) + Nutation2000Bi(jde)
} }
// HMoonGeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension of the Moon. // HMoonGeocentricApparentRa 月亮地心视赤经 / apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRa(jd float64) float64 { func HMoonGeocentricApparentRa(jde float64) float64 {
return HMoonGeocentricApparentRaN(jd, -1) return HMoonGeocentricApparentRaN(jde, -1)
} }
// HMoonGeocentricApparentRaN 月亮地心视赤经(截断版) / truncated apparent geocentric right ascension of the Moon. // HMoonGeocentricApparentRaN 月亮地心视赤经(截断版) / truncated apparent geocentric right ascension of the Moon.
func HMoonGeocentricApparentRaN(jd float64, n int) float64 { func HMoonGeocentricApparentRaN(jde float64, n int) float64 {
return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n)) return LoToRa(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
} }
// HMoonGeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination of the Moon. // HMoonGeocentricApparentDec 月亮地心视赤纬 / apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDec(jd float64) float64 { func HMoonGeocentricApparentDec(jde float64) float64 {
return HMoonGeocentricApparentDecN(jd, -1) return HMoonGeocentricApparentDecN(jde, -1)
} }
// HMoonGeocentricApparentDecN 月亮地心视赤纬(截断版) / truncated apparent geocentric declination of the Moon. // HMoonGeocentricApparentDecN 月亮地心视赤纬(截断版) / truncated apparent geocentric declination of the Moon.
func HMoonGeocentricApparentDecN(jd float64, n int) float64 { func HMoonGeocentricApparentDecN(jde float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) + return ArcSin(Sin(HMoonTrueBoN(jde, n))*Cos(TrueObliquity(jde)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonApparentLoN(jd, n))) Cos(HMoonTrueBoN(jde, n))*Sin(TrueObliquity(jde))*Sin(HMoonApparentLoN(jde, n)))
} }
// HMoonGeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination of the Moon. // HMoonGeocentricApparentRaDec 月亮地心视赤经、视赤纬 / apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDec(jd float64) (float64, float64) { func HMoonGeocentricApparentRaDec(jde float64) (float64, float64) {
return HMoonGeocentricApparentRaDecN(jd, -1) return HMoonGeocentricApparentRaDecN(jde, -1)
} }
// HMoonGeocentricApparentRaDecN 月亮地心视赤经、视赤纬(截断版) / truncated apparent geocentric right ascension and declination of the Moon. // HMoonGeocentricApparentRaDecN 月亮地心视赤经、视赤纬(截断版) / truncated apparent geocentric right ascension and declination of the Moon.
func HMoonGeocentricApparentRaDecN(jd float64, n int) (float64, float64) { func HMoonGeocentricApparentRaDecN(jde float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n)) return LoBoToRaDec(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
} }
// HMoonGeocentricTrueRa 月亮地心真赤经 / true geocentric right ascension of the Moon. // HMoonGeocentricTrueRa 月亮地心真赤经 / true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRa(jd float64) float64 { func HMoonGeocentricTrueRa(jde float64) float64 {
return HMoonGeocentricTrueRaN(jd, -1) return HMoonGeocentricTrueRaN(jde, -1)
} }
// HMoonGeocentricTrueRaN 月亮地心真赤经(截断版) / truncated true geocentric right ascension of the Moon. // HMoonGeocentricTrueRaN 月亮地心真赤经(截断版) / truncated true geocentric right ascension of the Moon.
func HMoonGeocentricTrueRaN(jd float64, n int) float64 { func HMoonGeocentricTrueRaN(jde float64, n int) float64 {
return LoToRa(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n)) return LoToRa(jde, HMoonTrueLoN(jde, n), HMoonTrueBoN(jde, n))
} }
// HMoonGeocentricTrueDec 月亮地心真赤纬 / true geocentric declination of the Moon. // HMoonGeocentricTrueDec 月亮地心真赤纬 / true geocentric declination of the Moon.
func HMoonGeocentricTrueDec(jd float64) float64 { func HMoonGeocentricTrueDec(jde float64) float64 {
return HMoonGeocentricTrueDecN(jd, -1) return HMoonGeocentricTrueDecN(jde, -1)
} }
// HMoonGeocentricTrueDecN 月亮地心真赤纬(截断版) / truncated true geocentric declination of the Moon. // HMoonGeocentricTrueDecN 月亮地心真赤纬(截断版) / truncated true geocentric declination of the Moon.
func HMoonGeocentricTrueDecN(jd float64, n int) float64 { func HMoonGeocentricTrueDecN(jde float64, n int) float64 {
return ArcSin(Sin(HMoonTrueBoN(jd, n))*Cos(TrueObliquity(jd)) + return ArcSin(Sin(HMoonTrueBoN(jde, n))*Cos(TrueObliquity(jde)) +
Cos(HMoonTrueBoN(jd, n))*Sin(TrueObliquity(jd))*Sin(HMoonTrueLoN(jd, n))) Cos(HMoonTrueBoN(jde, n))*Sin(TrueObliquity(jde))*Sin(HMoonTrueLoN(jde, n)))
} }
// HMoonGeocentricTrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination of the Moon. // HMoonGeocentricTrueRaDec 月亮地心真赤经、真赤纬 / true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDec(jd float64) (float64, float64) { func HMoonGeocentricTrueRaDec(jde float64) (float64, float64) {
return HMoonGeocentricTrueRaDecN(jd, -1) return HMoonGeocentricTrueRaDecN(jde, -1)
} }
// HMoonGeocentricTrueRaDecN 月亮地心真赤经、真赤纬(截断版) / truncated true geocentric right ascension and declination of the Moon. // HMoonGeocentricTrueRaDecN 月亮地心真赤经、真赤纬(截断版) / truncated true geocentric right ascension and declination of the Moon.
func HMoonGeocentricTrueRaDecN(jd float64, n int) (float64, float64) { func HMoonGeocentricTrueRaDecN(jde float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonTrueLoN(jd, n), HMoonTrueBoN(jd, n)) return LoBoToRaDec(jde, HMoonTrueLoN(jde, n), HMoonTrueBoN(jde, n))
} }
func HMoonTrueRaDec(jd float64) (float64, float64) { func HMoonTrueRaDec(jde float64) (float64, float64) {
return HMoonTrueRaDecN(jd, -1) return HMoonTrueRaDecN(jde, -1)
} }
func HMoonTrueRaDecN(jd float64, n int) (float64, float64) { func HMoonTrueRaDecN(jde float64, n int) (float64, float64) {
return LoBoToRaDec(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n)) return LoBoToRaDec(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
} }
/* /*
* 月球真赤纬 * 月球真赤纬
*/ */
func HMoonTrueDec(jd float64) float64 { func HMoonTrueDec(jde float64) float64 {
return HMoonTrueDecN(jd, -1) return HMoonTrueDecN(jde, -1)
} }
func HMoonTrueDecN(jd float64, n int) float64 { func HMoonTrueDecN(jde float64, n int) float64 {
moonLo := HMoonApparentLoN(jd, n) moonLo := HMoonApparentLoN(jde, n)
moonBo := HMoonTrueBoN(jd, n) moonBo := HMoonTrueBoN(jde, n)
tmp := Sin(moonBo)*Cos(TrueObliquity(jd)) + Cos(moonBo)*Sin(TrueObliquity(jd))*Sin(moonLo) tmp := Sin(moonBo)*Cos(TrueObliquity(jde)) + Cos(moonBo)*Sin(TrueObliquity(jde))*Sin(moonLo)
res := ArcSin(tmp) res := ArcSin(tmp)
return res return res
} }
@@ -259,12 +259,12 @@ func HMoonTrueDecN(jd float64, n int) float64 {
/* /*
* 月球真赤经 * 月球真赤经
*/ */
func HMoonTrueRa(jd float64) float64 { func HMoonTrueRa(jde float64) float64 {
return HMoonTrueRaN(jd, -1) return HMoonTrueRaN(jde, -1)
} }
func HMoonTrueRaN(jd float64, n int) float64 { func HMoonTrueRaN(jde float64, n int) float64 {
return LoToRa(jd, HMoonApparentLoN(jd, n), HMoonTrueBoN(jd, n)) return LoToRa(jde, HMoonApparentLoN(jde, n), HMoonTrueBoN(jde, n))
} }
/* /*
@@ -275,7 +275,7 @@ func HMoonApparentRaDec(jd, lon, lat, tz float64) (float64, float64) {
} }
func HMoonApparentRaDecN(jd, lon, lat, tz float64, n int) (float64, float64) { func HMoonApparentRaDecN(jd, lon, lat, tz float64, n int) (float64, float64) {
jde := TD2UT(jd, true) jde := UTC2TT(jd)
ra := HMoonTrueRaN(jde-tz/24, n) ra := HMoonTrueRaN(jde-tz/24, n)
dec := HMoonTrueDecN(jde-tz/24, n) dec := HMoonTrueDecN(jde-tz/24, n)
away := HMoonAwayN(jde-tz/24, n) / 149597870.7 away := HMoonAwayN(jde-tz/24, n) / 149597870.7
@@ -288,10 +288,10 @@ func HMoonApparentRa(jd, lon, lat, tz float64) float64 {
} }
func HMoonApparentRaN(jd, lon, lat, tz float64, n int) float64 { func HMoonApparentRaN(jd, lon, lat, tz float64, n int) float64 {
jde := TD2UT(jd, true) jde := UTC2TT(jd) - tz/24
ra := HMoonTrueRaN(jde-tz/24, n) ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde-tz/24, n) dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde-tz/24, n) / 149597870.7 away := HMoonAwayN(jde, n) / 149597870.7
topoRA := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0) topoRA := TopocentricRa(ra, dec, lat, lon, jd-tz/24, away, 0)
return topoRA return topoRA
} }
@@ -300,10 +300,10 @@ func HMoonApparentDec(jd, lon, lat, tz float64) float64 {
} }
func HMoonApparentDecN(jd, lon, lat, tz float64, n int) float64 { func HMoonApparentDecN(jd, lon, lat, tz float64, n int) float64 {
jde := TD2UT(jd, true) jde := UTC2TT(jd) - tz/24
ra := HMoonTrueRaN(jde-tz/24, n) ra := HMoonTrueRaN(jde, n)
dec := HMoonTrueDecN(jde-tz/24, n) dec := HMoonTrueDecN(jde, n)
away := HMoonAwayN(jde-tz/24, n) / 149597870.7 away := HMoonAwayN(jde, n) / 149597870.7
topoDec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0) topoDec := TopocentricDec(ra, dec, lat, lon, jd-tz/24, away, 0)
return topoDec return topoDec
} }
+6 -6
View File
@@ -39,10 +39,10 @@ func TestMoonRiseSetMissingEventConvention(t *testing.T) {
lon, lat, tz float64 lon, lat, tz float64
riseErr, setErr error riseErr, setErr error
}{ }{
{"极昼:全天在地平线上", JDECalc(2023, 6, 21), 0, 85, 0, ErrNeverSet, ErrNeverSet}, {"极昼:全天在地平线上", JDCalc(2023, 6, 21), 0, 85, 0, ErrNeverSet, ErrNeverSet},
{"极夜:全天在地平线下", JDECalc(2023, 12, 22), 0, -85, 0, ErrNeverRise, ErrNeverRise}, {"极夜:全天在地平线下", JDCalc(2023, 12, 22), 0, -85, 0, ErrNeverRise, ErrNeverRise},
{"当日无升起但别日有", JDECalc(2024, 2, 29), 0, 60, 0, ErrNotOnThisDate, nil}, {"当日无升起但别日有", JDCalc(2024, 2, 29), 0, 60, 0, ErrNotOnThisDate, nil},
{"正常日两侧都有", JDECalc(2025, 6, 5), 116.4074, 39.9042, 8, nil, nil}, {"正常日两侧都有", JDCalc(2025, 6, 5), 116.4074, 39.9042, 8, nil, nil},
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -58,8 +58,8 @@ func TestMoonRiseSetMissingEventConvention(t *testing.T) {
func TestMoonRiseSetMissingEventMatchesDailyGeometry(t *testing.T) { func TestMoonRiseSetMissingEventMatchesDailyGeometry(t *testing.T) {
dates := []float64{ dates := []float64{
JDECalc(2023, 6, 21), JDECalc(2023, 12, 22), JDECalc(2024, 2, 29), JDCalc(2023, 6, 21), JDCalc(2023, 12, 22), JDCalc(2024, 2, 29),
JDECalc(2025, 6, 21), JDECalc(2025, 12, 22), JDECalc(2026, 3, 3), JDCalc(2025, 6, 21), JDCalc(2025, 12, 22), JDCalc(2026, 3, 3),
} }
latitudes := []float64{-89, -85, -75, -66, -60, 60, 66, 75, 85, 89} latitudes := []float64{-89, -85, -75, -66, -60, 60, 66, 75, 85, 89}
for _, jd := range dates { for _, jd := range dates {
+6 -6
View File
@@ -113,7 +113,7 @@ func TestMoonRiseSetMatchesExternalBaselines(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("parse %s date %q: %v", sample.Site, sample.DateUTC, err) t.Fatalf("parse %s date %q: %v", sample.Site, sample.DateUTC, err)
} }
jd := Date2JDE(day) jd := Date2JD(day)
currentRiseJD, err := GetMoonRiseTime(jd, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight) currentRiseJD, err := GetMoonRiseTime(jd, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight)
if err != nil { if err != nil {
t.Fatalf("%s current moonrise: %v", sample.Site, err) t.Fatalf("%s current moonrise: %v", sample.Site, err)
@@ -160,7 +160,7 @@ func TestMoonRiseSetLegacyComparatorMatchesPreFixSnapshot(t *testing.T) {
SetDeltaTFn(DefaultDeltaTv2) SetDeltaTFn(DefaultDeltaTv2)
defer SetDeltaTFn(previousDeltaT) defer SetDeltaTFn(previousDeltaT)
jd := JDECalc(2023, 1, 15) jd := JDCalc(2023, 1, 15)
currentRise, err := GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0) currentRise, err := GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0)
if err != nil { if err != nil {
t.Fatalf("current moonrise: %v", err) t.Fatalf("current moonrise: %v", err)
@@ -206,8 +206,8 @@ func compareMoonRiseSetEvent(t *testing.T, name string, currentJD, legacyJD floa
horizonsUTC, metUTC, imcceUTC string, tolerances moonRiseSetExternalTolerances, horizonsUTC, metUTC, imcceUTC string, tolerances moonRiseSetExternalTolerances,
stats *moonRiseSetComparisonStats) { stats *moonRiseSetComparisonStats) {
t.Helper() t.Helper()
current := JDE2DateByZone(currentJD, time.UTC, false) current := JD2DateByZone(currentJD, time.UTC, false)
legacy := JDE2DateByZone(legacyJD, time.UTC, false) legacy := JD2DateByZone(legacyJD, time.UTC, false)
horizons := parseMoonRiseSetExternalTime(t, name+".jpl", horizonsUTC) horizons := parseMoonRiseSetExternalTime(t, name+".jpl", horizonsUTC)
met := parseMoonRiseSetExternalTime(t, name+".met", metUTC) met := parseMoonRiseSetExternalTime(t, name+".met", metUTC)
imcce := parseMoonRiseSetExternalTime(t, name+".imcce", imcceUTC) imcce := parseMoonRiseSetExternalTime(t, name+".imcce", imcceUTC)
@@ -301,7 +301,7 @@ func legacyMoonRiseSetFromCurrent(currentJD, longitude, latitude, timeZone, zeni
} }
func legacyHMoonHeight(jd, longitude, latitude, timeZone float64) float64 { func legacyHMoonHeight(jd, longitude, latitude, timeZone float64) float64 {
calculationJD := TD2UT(jd-timeZone/24, true) calculationJD := UTC2TT(jd - timeZone/24)
ra, dec := HMoonTrueRaDecN(calculationJD, -1) ra, dec := HMoonTrueRaDecN(calculationJD, -1)
distanceAU := HMoonAwayN(calculationJD, -1) / 149597870.7 distanceAU := HMoonAwayN(calculationJD, -1) / 149597870.7
topocentricRA, topocentricDec := legacyTopocentricRaDec(ra, dec, latitude, longitude, calculationJD, distanceAU, 0) topocentricRA, topocentricDec := legacyTopocentricRaDec(ra, dec, latitude, longitude, calculationJD, distanceAU, 0)
@@ -316,7 +316,7 @@ func legacyTopocentricRaDec(ra, dec, latitude, longitude, jd, distanceAU, height
horizontalParallaxSine := tools.Sin(0.0024427777777) / distanceAU horizontalParallaxSine := tools.Sin(0.0024427777777) / distanceAU
observerCosine := pcosi(latitude, height) observerCosine := pcosi(latitude, height)
observerSine := psini(latitude, height) observerSine := psini(latitude, height)
hourAngle := tools.Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + longitude - ra) hourAngle := tools.Limit360(TT2UTC(ApparentSiderealTime(jd))*15 + longitude - ra)
raCorrection := math.Atan2(-observerCosine*horizontalParallaxSine*tools.Sin(hourAngle), raCorrection := math.Atan2(-observerCosine*horizontalParallaxSine*tools.Sin(hourAngle),
tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi
correctedDec := math.Atan2((tools.Sin(dec)-observerSine*horizontalParallaxSine)*tools.Cos(raCorrection), correctedDec := math.Atan2((tools.Sin(dec)-observerSine*horizontalParallaxSine)*tools.Cos(raCorrection),
+17
View File
@@ -0,0 +1,17 @@
package basic
import "testing"
// TestMoonStateMatchesHMoonHeight 固定 MoonState 与 HMoonHeight 逐位同口径。
func TestMoonStateMatchesHMoonHeight(t *testing.T) {
for _, jd := range []float64{2462502.5, 2416745.5, 2469807.75, 2378496.25} {
state := MoonStateAt(jd)
for lon := -180.0; lon < 180; lon += 23.5 {
for lat := -89.5; lat <= 89.5; lat += 7.5 {
if got, want := state.HMoonHeight(lon, lat), HMoonHeight(jd, lon, lat, 0); got != want {
t.Fatalf("jd=%v lon=%v lat=%v got %v want %v", jd, lon, lat, got, want)
}
}
}
}
}
+9 -9
View File
@@ -7,7 +7,7 @@ import (
) )
func Benchmark_MoonRiseBench(b *testing.B) { func Benchmark_MoonRiseBench(b *testing.B) {
jde := GetNowJDE() jde := GetNowJD()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
GetMoonRiseTime(jde, 105, 40, 8, 0, 10) GetMoonRiseTime(jde, 105, 40, 8, 0, 10)
} }
@@ -632,7 +632,7 @@ func TestMoonRiseSetRegression(t *testing.T) {
) )
for i, testCase := range moonRiseSetTestData { for i, testCase := range moonRiseSetTestData {
julianDay := JDECalc(testCase.Year, testCase.Month, testCase.Day) julianDay := JDCalc(testCase.Year, testCase.Month, testCase.Day)
// 测试月出时间 // 测试月出时间
@@ -646,7 +646,7 @@ func TestMoonRiseSetRegression(t *testing.T) {
} }
if !riseMatches { if !riseMatches {
t.Errorf("测试用例 %d 月出时间不匹配:\n"+ t.Errorf("测试用例 %d 月出时间不匹配:\n"+
" 日期: %d-%d-%.1f, 经纬度: (%.4f, %.4f), 时区: %.1f, 天顶修正: %.0f, 海拔: %.0f\n"+ " 日期: %d-%d-%.1f, 经纬度: (%.4f, %.4f), 时区: %.1f, 天顶修正: %.0f, 椭球高: %.0f\n"+
" 期望月出: %s, 实际月出: %.6f, 实际错误: %v, 差值: %.9f", " 期望月出: %s, 实际月出: %.6f, 实际错误: %v, 差值: %.9f",
i, testCase.Year, testCase.Month, testCase.Day, i, testCase.Year, testCase.Month, testCase.Day,
testCase.Longitude, testCase.Latitude, testCase.TimeZone, testCase.Longitude, testCase.Latitude, testCase.TimeZone,
@@ -665,7 +665,7 @@ func TestMoonRiseSetRegression(t *testing.T) {
} }
if !setMatches { if !setMatches {
t.Errorf("测试用例 %d 月落时间不匹配:\n"+ t.Errorf("测试用例 %d 月落时间不匹配:\n"+
" 日期: %d-%d-%.1f, 经纬度: (%.4f, %.4f), 时区: %.1f, 天顶修正: %.0f, 海拔: %.0f\n"+ " 日期: %d-%d-%.1f, 经纬度: (%.4f, %.4f), 时区: %.1f, 天顶修正: %.0f, 椭球高: %.0f\n"+
" 期望月落: %s, 实际月落: %.6f, 实际错误: %v, 差值: %.9f", " 期望月落: %s, 实际月落: %.6f, 实际错误: %v, 差值: %.9f",
i, testCase.Year, testCase.Month, testCase.Day, i, testCase.Year, testCase.Month, testCase.Day,
testCase.Longitude, testCase.Latitude, testCase.TimeZone, testCase.Longitude, testCase.Latitude, testCase.TimeZone,
@@ -715,7 +715,7 @@ func TestMoonRiseSetSpecialCases(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
julianDay := JDECalc(tc.year, tc.month, tc.day) julianDay := JDCalc(tc.year, tc.month, tc.day)
actualRise, riseErr := GetMoonRiseTime(julianDay, tc.longitude, tc.latitude, actualRise, riseErr := GetMoonRiseTime(julianDay, tc.longitude, tc.latitude,
tc.timeZone, tc.zenithShift, tc.height) tc.timeZone, tc.zenithShift, tc.height)
@@ -751,7 +751,7 @@ func moonRiseSetExpectation(code float64) string {
if err := moonRiseSetExpectedError(code); err != nil { if err := moonRiseSetExpectedError(code); err != nil {
return err.Error() return err.Error()
} }
return JDE2Date(code).String() return JD2Date(code).String()
} }
func moonRiseSetMatches(actual float64, err error, expected float64, tolerance float64) bool { func moonRiseSetMatches(actual float64, err error, expected float64, tolerance float64) bool {
@@ -791,7 +791,7 @@ func moonRiseSetDynamicResidualMatches(actual float64, testCase MoonRiseSetTestC
} }
func moonRiseSetNearCivilBoundary(jd float64, testCase MoonRiseSetTestCase, tolerance float64) bool { func moonRiseSetNearCivilBoundary(jd float64, testCase MoonRiseSetTestCase, tolerance float64) bool {
dayStart := math.Floor(JDECalc(testCase.Year, testCase.Month, testCase.Day)) + 0.5 dayStart := math.Floor(JDCalc(testCase.Year, testCase.Month, testCase.Day)) + 0.5
return math.Abs(jd-dayStart) <= tolerance || math.Abs(jd-(dayStart+1)) <= tolerance return math.Abs(jd-dayStart) <= tolerance || math.Abs(jd-(dayStart+1)) <= tolerance
} }
@@ -802,7 +802,7 @@ func floatEquals(a, b, tolerance float64) bool {
// BenchmarkMoonRiseTime 月出时间计算性能测试 // BenchmarkMoonRiseTime 月出时间计算性能测试
func BenchmarkMoonRiseTime(b *testing.B) { func BenchmarkMoonRiseTime(b *testing.B) {
julianDay := JDECalc(2023, 6, 21) julianDay := JDCalc(2023, 6, 21)
longitude, latitude := 116.4074, 39.9042 longitude, latitude := 116.4074, 39.9042
timeZone, zenithShift, height := 8.0, 1.0, 0.0 timeZone, zenithShift, height := 8.0, 1.0, 0.0
@@ -814,7 +814,7 @@ func BenchmarkMoonRiseTime(b *testing.B) {
// BenchmarkMoonSetTime 月落时间计算性能测试 // BenchmarkMoonSetTime 月落时间计算性能测试
func BenchmarkMoonSetTime(b *testing.B) { func BenchmarkMoonSetTime(b *testing.B) {
julianDay := JDECalc(2023, 6, 21) julianDay := JDCalc(2023, 6, 21)
longitude, latitude := 116.4074, 39.9042 longitude, latitude := 116.4074, 39.9042
timeZone, zenithShift, height := 8.0, 1.0, 0.0 timeZone, zenithShift, height := 8.0, 1.0, 0.0
+4 -4
View File
@@ -8,7 +8,7 @@ import (
) )
func TestMoonTopocentricPhysicalMatchesCorrectionMethod(t *testing.T) { func TestMoonTopocentricPhysicalMatchesCorrectionMethod(t *testing.T) {
jd := TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true) jd := UTC2TT(Date2JD(testTime(2026, 4, 28, 9, 30, 45)))
observerLon := 121.4737 observerLon := 121.4737
observerLat := 31.2304 observerLat := 31.2304
@@ -28,8 +28,8 @@ func TestMoonTopocentricPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
observerLat float64 observerLat float64
height float64 height float64
}{ }{
{"shanghai", TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true), 121.4737, 31.2304, 4}, {"shanghai", UTC2TT(Date2JD(testTime(2026, 4, 28, 9, 30, 45))), 121.4737, 31.2304, 4},
{"chicago", TD2UT(Date2JDE(testTime(2024, 3, 25, 7, 0, 0)), true), -87.65, 41.85, 180}, {"chicago", UTC2TT(Date2JD(testTime(2024, 3, 25, 7, 0, 0))), -87.65, 41.85, 180},
} }
for _, sample := range samples { for _, sample := range samples {
@@ -50,7 +50,7 @@ func moonTopocentricPhysicalByCorrection(jd, observerLon, observerLat float64) M
geocentric := MoonPhysical(jd) geocentric := MoonPhysical(jd)
moonRA := HMoonTrueRa(jd) moonRA := HMoonTrueRa(jd)
moonDec := HMoonTrueDec(jd) moonDec := HMoonTrueDec(jd)
hourAngle := StarHourAngle(TD2UT(jd, false), moonRA, observerLon, 0) hourAngle := StarHourAngle(TT2UTC(jd), moonRA, observerLon, 0)
horizontalParallax := ArcSin(6378.1366 / HMoonAway(jd)) horizontalParallax := ArcSin(6378.1366 / HMoonAway(jd))
Q := ArcTan2( Q := ArcTan2(
+63 -63
View File
@@ -7,79 +7,79 @@ import (
. "b612.me/astro/tools" . "b612.me/astro/tools"
) )
func NeptuneL(jd float64) float64 { func NeptuneL(jde float64) float64 {
return planet.WherePlanet(7, 0, jd) return planet.WherePlanet(7, 0, jde)
} }
func NeptuneB(jd float64) float64 { func NeptuneB(jde float64) float64 {
return planet.WherePlanet(7, 1, jd) return planet.WherePlanet(7, 1, jde)
} }
func NeptuneR(jd float64) float64 { func NeptuneR(jde float64) float64 {
return planet.WherePlanet(7, 2, jd) return planet.WherePlanet(7, 2, jde)
} }
func ANeptuneX(jd float64) float64 { func ANeptuneX(jde float64) float64 {
l := NeptuneL(jd) l := NeptuneL(jde)
b := NeptuneB(jd) b := NeptuneB(jde)
r := NeptuneR(jd) r := NeptuneR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
return x return x
} }
func ANeptuneY(jd float64) float64 { func ANeptuneY(jde float64) float64 {
l := NeptuneL(jd) l := NeptuneL(jde)
b := NeptuneB(jd) b := NeptuneB(jde)
r := NeptuneR(jd) r := NeptuneR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
return y return y
} }
func ANeptuneZ(jd float64) float64 { func ANeptuneZ(jde float64) float64 {
//l := NeptuneL(jd) //l := NeptuneL(jde)
b := NeptuneB(jd) b := NeptuneB(jde)
r := NeptuneR(jd) r := NeptuneR(jde)
// el := planet.WherePlanet(-1, 0, jd) // el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return z return z
} }
func ANeptuneXYZ(jd float64) (float64, float64, float64) { func ANeptuneXYZ(jde float64) (float64, float64, float64) {
l := NeptuneL(jd) l := NeptuneL(jde)
b := NeptuneB(jd) b := NeptuneB(jde)
r := NeptuneR(jd) r := NeptuneR(jde)
el := planet.WherePlanet(-1, 0, jd) el := planet.WherePlanet(-1, 0, jde)
eb := planet.WherePlanet(-1, 1, jd) eb := planet.WherePlanet(-1, 1, jde)
er := planet.WherePlanet(-1, 2, jd) er := planet.WherePlanet(-1, 2, jde)
x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el) x := r*Cos(b)*Cos(l) - er*Cos(eb)*Cos(el)
y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el) y := r*Cos(b)*Sin(l) - er*Cos(eb)*Sin(el)
z := r*Sin(b) - er*Sin(eb) z := r*Sin(b) - er*Sin(eb)
return x, y, z return x, y, z
} }
func NeptuneApparentRa(jd float64) float64 { func NeptuneApparentRa(jde float64) float64 {
lo, bo := NeptuneApparentLoBo(jd) lo, bo := NeptuneApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo)) ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi ra = ra * 180 / math.Pi
return Limit360(ra) return Limit360(ra)
} }
func NeptuneApparentDec(jd float64) float64 { func NeptuneApparentDec(jde float64) float64 {
lo, bo := NeptuneApparentLoBo(jd) lo, bo := NeptuneApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
return dec return dec
} }
func NeptuneApparentRaDec(jd float64) (float64, float64) { func NeptuneApparentRaDec(jde float64) (float64, float64) {
lo, bo := NeptuneApparentLoBo(jd) lo, bo := NeptuneApparentLoBo(jde)
eps := TrueObliquity(jd) eps := TrueObliquity(jde)
ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo)) ra := math.Atan2((Sin(lo)*Cos(eps) - Tan(bo)*Sin(eps)), Cos(lo))
ra = ra * 180 / math.Pi ra = ra * 180 / math.Pi
dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo)) dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
@@ -105,22 +105,22 @@ func NeptuneApparentLoBo(jd float64) (float64, float64) {
return geo.lo, geo.bo return geo.lo, geo.bo
} }
func NeptuneMag(jd float64) float64 { func NeptuneMag(jde float64) float64 {
sunDistance := NeptuneR(jd) sunDistance := NeptuneR(jde)
earthDistance := EarthNeptuneAway(jd) earthDistance := EarthNeptuneAway(jde)
earthSunDistance := planet.WherePlanet(-1, 2, jd) earthSunDistance := planet.WherePlanet(-1, 2, jde)
i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance) i := (sunDistance*sunDistance + earthDistance*earthDistance - earthSunDistance*earthSunDistance) / (2 * sunDistance * earthDistance)
i = ArcCos(i) i = ArcCos(i)
mag := -6.87 + 5*math.Log10(sunDistance*earthDistance) mag := -6.87 + 5*math.Log10(sunDistance*earthDistance)
return FloatRound(mag, 2) return FloatRound(mag, 2)
} }
func NeptuneHeight(jde, lon, lat, timezone float64) float64 { func NeptuneHeight(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := NeptuneApparentRaDec(TD2UT(utcJde, true)) ra, dec := NeptuneApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 高度角、时角与天球座标三角转换公式 // 高度角、时角与天球座标三角转换公式
@@ -129,12 +129,12 @@ func NeptuneHeight(jde, lon, lat, timezone float64) float64 {
return ArcSin(sinHeight) return ArcSin(sinHeight)
} }
func NeptuneAzimuth(jde, lon, lat, timezone float64) float64 { func NeptuneAzimuth(localJD, lon, lat, timezone float64) float64 {
// 转换为世界时 // 转换为世界时
utcJde := jde - timezone/24.0 utcJD := localJD - timezone/24.0
// 计算视恒星时 // 计算视恒星时
ra, dec := NeptuneApparentRaDec(TD2UT(utcJde, true)) ra, dec := NeptuneApparentRaDec(UTC2TT(utcJD))
st := Limit360(ApparentSiderealTime(utcJde)*15 + lon) st := Limit360(ApparentSiderealTime(UTC2UT1(utcJD))*15 + lon)
// 计算时角 // 计算时角
hourAngle := Limit360(st - ra) hourAngle := Limit360(st - ra)
// 三角转换公式 // 三角转换公式
@@ -153,21 +153,21 @@ func NeptuneAzimuth(jde, lon, lat, timezone float64) float64 {
} }
func NeptuneHourAngle(jd, lon, timezone float64) float64 { func NeptuneHourAngle(jd, lon, timezone float64) float64 {
siderealLongitude := Limit360(ApparentSiderealTime(jd-timezone/24)*15 + lon) siderealLongitude := Limit360(ApparentSiderealTime(UTC2UT1(jd-timezone/24))*15 + lon)
hourAngle := siderealLongitude - NeptuneApparentRa(TD2UT(jd-timezone/24.0, true)) hourAngle := siderealLongitude - NeptuneApparentRa(UTC2TT(jd-timezone/24.0))
if hourAngle < 0 { if hourAngle < 0 {
hourAngle += 360 hourAngle += 360
} }
return hourAngle return hourAngle
} }
func NeptuneCulminationTime(jde, lon, timezone float64) float64 { func NeptuneCulminationTime(localJD, lon, timezone float64) float64 {
//jde 世界时,非力学时,当地时区 0时,无需转换力学时 // localJD 是本地民用日锚点(当地 0 时),不是力学时。
//ra,dec 瞬时天球座标,非J2000等时间天球坐标 //ra,dec 瞬时天球座标,非J2000等时间天球坐标
jde = math.Floor(jde) + 0.5 localJD = math.Floor(localJD) + 0.5
estimateJD := jde + Limit360(360-NeptuneHourAngle(jde, lon, timezone))/15.0/24.0*0.99726851851851851851 estimateJD := localJD + Limit360(360-NeptuneHourAngle(localJD, lon, timezone))/15.0/24.0*0.99726851851851851851
normalizedHourAngle := func(jde, lon, timezone float64) float64 { normalizedHourAngle := func(localJD, lon, timezone float64) float64 {
currentHourAngle := NeptuneHourAngle(jde, lon, timezone) currentHourAngle := NeptuneHourAngle(localJD, lon, timezone)
if currentHourAngle < 180 { if currentHourAngle < 180 {
currentHourAngle += 360 currentHourAngle += 360
} }
+27 -27
View File
@@ -74,15 +74,15 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
} else { } else {
jde += daysPerDegree * currentDelta jde += daysPerDegree * currentDelta
} }
estimateJD := jde estimateJDE := jde
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true) longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -90,7 +90,7 @@ func neptuneConjunctionFull(jde, degree float64, next uint8) float64 {
if !converged { if !converged {
return math.NaN() return math.NaN()
} }
return TD2UT(estimateJD, false) return TT2UTC(estimateJDE)
} }
func neptuneConjunction(jde, degree float64, next uint8) float64 { func neptuneConjunction(jde, degree float64, next uint8) float64 {
@@ -105,15 +105,15 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
} else { } else {
jde += daysPerDegree * currentDelta jde += daysPerDegree * currentDelta
} }
estimateJD := jde estimateJDE := jde
converged := false converged := false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDeltaN(prevJD, degree, true, neptuneEventSearchN) longitudeDelta := neptuneSunLongitudeDeltaN(prevJDE, degree, true, neptuneEventSearchN)
longitudeSlope := (neptuneSunLongitudeDeltaN(prevJD+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJD-0.000005, degree, true, neptuneEventSearchN)) / 0.00001 longitudeSlope := (neptuneSunLongitudeDeltaN(prevJDE+0.000005, degree, true, neptuneEventSearchN) - neptuneSunLongitudeDeltaN(prevJDE-0.000005, degree, true, neptuneEventSearchN)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= neptunePhaseCoarseTolerance { if math.Abs(nextJD-prevJDE) <= neptunePhaseCoarseTolerance {
converged = true converged = true
break break
} }
@@ -123,12 +123,12 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
} }
converged = false converged = false
for i := 0; i < eventNewtonMaxIterations; i++ { for i := 0; i < eventNewtonMaxIterations; i++ {
prevJD := estimateJD prevJDE := estimateJDE
longitudeDelta := neptuneSunLongitudeDelta(prevJD, degree, true) longitudeDelta := neptuneSunLongitudeDelta(prevJDE, degree, true)
longitudeSlope := (neptuneSunLongitudeDelta(prevJD+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJD-0.000005, degree, true)) / 0.00001 longitudeSlope := (neptuneSunLongitudeDelta(prevJDE+0.000005, degree, true) - neptuneSunLongitudeDelta(prevJDE-0.000005, degree, true)) / 0.00001
nextJD := prevJD - longitudeDelta/longitudeSlope nextJD := prevJDE - longitudeDelta/longitudeSlope
estimateJD = nextJD estimateJDE = nextJD
if math.Abs(nextJD-prevJD) <= 0.00001 { if math.Abs(nextJD-prevJDE) <= 0.00001 {
converged = true converged = true
break break
} }
@@ -136,7 +136,7 @@ func neptuneConjunction(jde, degree float64, next uint8) float64 {
if !converged { if !converged {
return math.NaN() return math.NaN()
} }
return TD2UT(estimateJD, false) return TT2UTC(estimateJDE)
} }
func LastNeptuneConjunction(jde float64) float64 { func LastNeptuneConjunction(jde float64) float64 {
@@ -175,22 +175,22 @@ func neptuneRetrogradeAroundOpposition(oppositionJD float64, searchBeforeOpposit
if !isFiniteFloat(oppositionJD) { if !isFiniteFloat(oppositionJD) {
return math.NaN() return math.NaN()
} }
oppositionTT := TD2UT(oppositionJD, true) oppositionTT := UTC2TT(oppositionJD)
startTT := oppositionTT startTT := oppositionTT
endTT := oppositionTT endTT := oppositionTT
if searchBeforeOpposition { if searchBeforeOpposition {
easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0) easternQuadratureUT := neptuneConjunction(oppositionTT, 90, 0)
startTT = TD2UT(easternQuadratureUT, true) startTT = UTC2TT(easternQuadratureUT)
} else { } else {
westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1) westernQuadratureUT := neptuneConjunction(oppositionTT, 270, 1)
endTT = TD2UT(westernQuadratureUT, true) endTT = UTC2TT(westernQuadratureUT)
} }
bestJD := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 { bestJDE := zeroEventInWindow(startTT, endTT, 2.0, 2.0, 30.0/86400.0, func(jd float64) float64 {
return neptuneRADerivativeN(jd, stationDerivativeStepDay, neptuneEventSearchN) return neptuneRADerivativeN(jd, stationDerivativeStepDay, neptuneEventSearchN)
}, func(jd float64) float64 { }, func(jd float64) float64 {
return neptuneRADerivative(jd, stationDerivativeStepDay) return neptuneRADerivative(jd, stationDerivativeStepDay)
}) })
return TD2UT(bestJD, false) return TT2UTC(bestJDE)
} }
func NextNeptuneRetrogradeToPrograde(jde float64) float64 { func NextNeptuneRetrogradeToPrograde(jde float64) float64 {
+2 -2
View File
@@ -15,8 +15,8 @@ func EclipticObliquity(jde float64, nutation bool) float64 {
return eps return eps
} }
func TrueObliquity(JD float64) float64 { func TrueObliquity(JDE float64) float64 {
return EclipticObliquity(JD, true) return EclipticObliquity(JDE, true)
} }
// 黄经章动 1980 // 黄经章动 1980
+59 -8
View File
@@ -6,6 +6,8 @@ import (
"math" "math"
"strings" "strings"
"time" "time"
"b612.me/astro/tools"
) )
// ErrInvalidOccultationInput 表示月掩输入契约无效。 // ErrInvalidOccultationInput 表示月掩输入契约无效。
@@ -22,6 +24,8 @@ const (
occultationPathMinimumTargetSpacingKM = 1.0 occultationPathMinimumTargetSpacingKM = 1.0
occultationEventSelectionTolerance = 10 * time.Millisecond occultationEventSelectionTolerance = 10 * time.Millisecond
occultationEventSelectionToleranceDays = float64(occultationEventSelectionTolerance) / float64(24*time.Hour) occultationEventSelectionToleranceDays = float64(occultationEventSelectionTolerance) / float64(24*time.Hour)
// 银河系内恒星的径向速度上限,仅用于挡掉明显填错的输入。
starRadialVelocityLimitKmPerSecond = 1000.0
) )
// CoordinateFrame 标识恒星输入坐标使用的赤道坐标系。 // CoordinateFrame 标识恒星输入坐标使用的赤道坐标系。
@@ -155,7 +159,7 @@ func moonTopocentricSemidiameterN(tt float64, observer Observer, n int) float64
if !finite(moonRA) || !finite(moonDec) || !finite(moonDistanceKM) || moonDistanceKM <= 0 { if !finite(moonRA) || !finite(moonDec) || !finite(moonDistanceKM) || moonDistanceKM <= 0 {
return math.NaN() return math.NaN()
} }
distanceKM := topocentricDistanceKM(moonRA, moonDec, moonDistanceKM, observer, TD2UT(tt, false)) distanceKM := topocentricDistanceKM(moonRA, moonDec, moonDistanceKM, observer, TT2UTC(tt))
if !finite(distanceKM) || distanceKM <= 0 { if !finite(distanceKM) || distanceKM <= 0 {
return math.NaN() return math.NaN()
} }
@@ -167,7 +171,7 @@ func moonTopocentricSemidiameterN(tt float64, observer Observer, n int) float64
// topocentricDistanceKM uses the same WGS-84-style site factors as TopocentricRaDec. // topocentricDistanceKM uses the same WGS-84-style site factors as TopocentricRaDec.
// The target uses apparent equatorial coordinates, and the sidereal angle is based on UTC/UT. // The target uses apparent equatorial coordinates, and the sidereal angle is based on UTC/UT.
func topocentricDistanceKM(ra, dec, distanceKM float64, observer Observer, ut float64) float64 { func topocentricDistanceKM(ra, dec, distanceKM float64, observer Observer, ut float64) float64 {
return topocentricDistanceKMWithSidereal(ra, dec, distanceKM, observer, ApparentSiderealTime(ut)*15) return topocentricDistanceKMWithSidereal(ra, dec, distanceKM, observer, ApparentSiderealTime(UTC2UT1(ut))*15)
} }
func topocentricDistanceKMWithSidereal( func topocentricDistanceKMWithSidereal(
@@ -231,7 +235,18 @@ type StarCoordinate struct {
ProperMotionRACosDecMasPerYear float64 ProperMotionRACosDecMasPerYear float64
ProperMotionDecMasPerYear float64 ProperMotionDecMasPerYear float64
ParallaxMas float64 // ParallaxMas 为周年视差,单位毫角秒;0 表示未提供距离,此时退回二维自行传播。
// ParallaxMas is the annual parallax in milliarcseconds; 0 means no distance is given and the
// two-dimensional proper-motion path is used.
ParallaxMas float64
// DistanceLightYear 是 ParallaxMas 的替代输入,单位光年;仅当 ParallaxMas 为 0 时生效,0 表示未提供。
// DistanceLightYear is an alternative to ParallaxMas in light-years; it applies only when
// ParallaxMas is 0, and 0 means it was not given.
DistanceLightYear float64
// RadialVelocityKmPerSecond 单位千米/秒,0 合法,只在距离已知时参与三维空间运动。
// RadialVelocityKmPerSecond is in kilometres per second; 0 is valid and it enters the
// three-dimensional motion only when a distance is known.
RadialVelocityKmPerSecond float64
} }
// Validate 在构造目标前检查恒星坐标契约。 // Validate 在构造目标前检查恒星坐标契约。
@@ -255,9 +270,26 @@ func (s StarCoordinate) Validate() error {
if !finite(s.ParallaxMas) || s.ParallaxMas < 0 { if !finite(s.ParallaxMas) || s.ParallaxMas < 0 {
return fmt.Errorf("%w: star parallax must be finite and non-negative", ErrInvalidOccultationInput) return fmt.Errorf("%w: star parallax must be finite and non-negative", ErrInvalidOccultationInput)
} }
if !finite(s.DistanceLightYear) || s.DistanceLightYear < 0 {
return fmt.Errorf("%w: star distance in light-years must be finite and non-negative", ErrInvalidOccultationInput)
}
if !finite(s.RadialVelocityKmPerSecond) || math.Abs(s.RadialVelocityKmPerSecond) > starRadialVelocityLimitKmPerSecond {
return fmt.Errorf("%w: star radial velocity must be finite and within +/-%.0f km/s", ErrInvalidOccultationInput, starRadialVelocityLimitKmPerSecond)
}
return nil return nil
} }
// parallaxMas 返回生效的视差:显式视差优先,否则由光年距离折算。
func (s StarCoordinate) parallaxMas() float64 {
if s.ParallaxMas > 0 {
return s.ParallaxMas
}
if s.DistanceLightYear <= 0 {
return 0
}
return 1000 / tools.DistanceToParsecs(s.DistanceLightYear, tools.DistanceLightYear)
}
// StarCoordinateFromStarData 将一条内嵌星表记录转换为月掩搜索使用的 J2000 坐标契约。 // StarCoordinateFromStarData 将一条内嵌星表记录转换为月掩搜索使用的 J2000 坐标契约。
// 星表自行从角秒/年转换为毫角秒/年;正的秒差距距离转换为毫角秒年视差。本函数只转换传入值,不会加载星表。 // 星表自行从角秒/年转换为毫角秒/年;正的秒差距距离转换为毫角秒年视差。本函数只转换传入值,不会加载星表。
// StarCoordinateFromStarData converts one embedded-catalog entry into the J2000 coordinate contract used by lunar-occultation searches. // StarCoordinateFromStarData converts one embedded-catalog entry into the J2000 coordinate contract used by lunar-occultation searches.
@@ -284,6 +316,7 @@ func StarCoordinateFromStarData(star StarData) (StarCoordinate, error) {
ProperMotionRACosDecMasPerYear: star.PmRA * 1000, ProperMotionRACosDecMasPerYear: star.PmRA * 1000,
ProperMotionDecMasPerYear: star.PmDec * 1000, ProperMotionDecMasPerYear: star.PmDec * 1000,
ParallaxMas: parallaxMas, ParallaxMas: parallaxMas,
RadialVelocityKmPerSecond: star.RadVel,
} }
if err := coordinate.Validate(); err != nil { if err := coordinate.Validate(); err != nil {
return StarCoordinate{}, fmt.Errorf("convert star catalog coordinate: %w", err) return StarCoordinate{}, fmt.Errorf("convert star catalog coordinate: %w", err)
@@ -491,11 +524,25 @@ type PlanetOccultationInfo struct {
// LimitSeparationKM is non-zero only on northern/southern limit samples (total limits included) and is the same-instant ground distance to the opposite limit; it is constructed differently from WidthKM and must not be converted into it. // LimitSeparationKM is non-zero only on northern/southern limit samples (total limits included) and is the same-instant ground distance to the opposite limit; it is constructed differently from WidthKM and must not be converted into it.
// Base samples are solved directly; adaptive samples use width interpolation and five-meter error checks. // Base samples are solved directly; adaptive samples use width interpolation and five-meter error checks.
type OccultationPathPoint struct { type OccultationPathPoint struct {
Time time.Time // Time 是该点的时刻。
Longitude float64 // Time is the instant the point describes.
Latitude float64 Time time.Time
MoonAltitude float64 // Longitude 与 Latitude 是地面坐标,东经、北纬为正。
WidthKM float64 // Longitude and Latitude are ground coordinates, east and north positive.
Longitude float64
Latitude float64
// MoonAltitude 是月球几何高度角,单位度,不做蒙气差修正。
// MoonAltitude is the geometric lunar altitude in degrees, without refraction.
MoonAltitude float64
// WidthKM 是可见掩带在该点的地面宽度。掩星可见带可以宽达数千公里,与日食中心带宽度不是同一口径,不要互相比较。
// 擦边事件的路径可以没有中心线,此时它仍表示可见带宽度,与 LimitSeparationKM 口径不同。
// WidthKM is the ground width of the visible occultation band at the point. An occultation band
// can span thousands of kilometres and is not comparable to a solar central-path width. A grazing
// path can have no center line while this field still describes the visible band, which is a
// different construction from LimitSeparationKM.
WidthKM float64
// LimitSeparationKM 是该点南北限的地面间距,无论中心线是否存在都有定义。
// LimitSeparationKM is the ground separation of the two limits at the point, defined whether or not a center line exists.
LimitSeparationKM float64 LimitSeparationKM float64
} }
@@ -529,6 +576,8 @@ type StarOccultationPath struct {
// Complete is true when Start and End are the global outer-limb contacts rather than query-window clipping points. // Complete is true when Start and End are the global outer-limb contacts rather than query-window clipping points.
Complete bool Complete bool
// CenterLine 是掩星中心线;掠掩事件可以整条没有中心线(只有南北限),此时该切片为空而 Complete 仍可为真。
// CenterLine is the occultation center line; a grazing event can have no center line at all (limits only), leaving this slice empty while Complete can still be true.
CenterLine []OccultationPathPoint CenterLine []OccultationPathPoint
NorthernLimit []OccultationPathPoint NorthernLimit []OccultationPathPoint
SouthernLimit []OccultationPathPoint SouthernLimit []OccultationPathPoint
@@ -622,7 +671,9 @@ type PlanetOccultationPath struct {
Complete bool Complete bool
// CenterLine 是影轴与椭球交点的轨迹;非中心事件退化为最接近影轴的椭球点。 // CenterLine 是影轴与椭球交点的轨迹;非中心事件退化为最接近影轴的椭球点。
// 掠掩事件的路径可以整条没有中心线(只有南北限),此时该切片为空而 Complete 仍可为真。
// CenterLine is the track of the shadow-axis/ellipsoid intersection, degenerating to the ellipsoid point nearest the axis for non-central events. // CenterLine is the track of the shadow-axis/ellipsoid intersection, degenerating to the ellipsoid point nearest the axis for non-central events.
// A grazing event can have no center line at all (limits only); the slice is then empty while Complete can still be true.
CenterLine []OccultationPathPoint CenterLine []OccultationPathPoint
// NorthernLimit 和 SouthernLimit 是外接触锥的切点轨迹:掩星在该线上恰好退化为擦边,不是中心线的等距横向平移。 // NorthernLimit 和 SouthernLimit 是外接触锥的切点轨迹:掩星在该线上恰好退化为擦边,不是中心线的等距横向平移。
// NorthernLimit and SouthernLimit are the tangency tracks of the outer-contact cone, where the occultation degenerates to a graze, so they are not a constant-width offset of the center line. // NorthernLimit and SouthernLimit are the tangency tracks of the outer-contact cone, where the occultation degenerates to a graze, so they are not a constant-width offset of the center line.
+80
View File
@@ -0,0 +1,80 @@
package basic
import (
"math"
"testing"
"time"
)
// 解析速率与中心差分必须给出同一个接触度量导数:点源目标曾把零位置当站心位置,
// 减掉观测者速度后得到日尺度的假速率,掩带边界因此无法加密。
// The analytic contact rate must agree with the central difference for every target
// kind: the point-source branch once treated a zero position as a topocentric vector
// and produced a spurious diurnal rate that stopped the band boundary from refining.
func TestOccultationContactRateMatchesCentralDifference(t *testing.T) {
const tolerance = 0.01 // 度/日
star := hr4799OccultationCoordinateForTest()
infinite := star
infinite.ParallaxMas = 0
parallaxStar := star
parallaxStar.ParallaxMas = 10
planetConfig, _ := planetOccultationConfigFor(OccultationMercury)
planetCache := newPlanetOccultationEventCache(planetConfig)
starEvaluation := func(coordinate StarCoordinate) func(float64) occultationRiseSetEvaluation {
cache := newStarOccultationEventCache(coordinate)
return newOccultationRiseSetEvaluationCache(cache.riseSetContextAt).evaluation
}
cases := []struct {
name string
sampleTime time.Time
evaluation func(float64) occultationRiseSetEvaluation
}{
{"point-source star", time.Date(2025, 6, 5, 10, 0, 0, 0, time.UTC), starEvaluation(infinite)},
{"finite star", time.Date(2025, 6, 5, 10, 0, 0, 0, time.UTC), starEvaluation(parallaxStar)},
{"planet", time.Date(2024, 8, 21, 10, 0, 0, 0, time.UTC), newOccultationRiseSetEvaluationCache(planetCache.riseSetContextAt).evaluation},
}
sites := [][2]float64{{0, 0}, {115, 33}, {-74, 40}, {179, 80}, {-123, -64}}
for _, testCase := range cases {
tt := occultationTimeToTT(testCase.sampleTime)
evaluation := testCase.evaluation(tt)
for _, site := range sites {
analytic := evaluation.contactRateAt(site[0], site[1])
reference := evaluation.contactDerivative(site[0], site[1])
if !finite(analytic) || !finite(reference) {
t.Fatalf("%s lon=%g lat=%g: analytic=%g reference=%g", testCase.name, site[0], site[1], analytic, reference)
}
if math.Abs(analytic-reference) > tolerance {
t.Errorf("%s lon=%g lat=%g: analytic=%.6f reference=%.6f degrees/day",
testCase.name, site[0], site[1], analytic, reference)
}
}
}
}
// 无视差恒星的掩带边界必须加密到请求的地面间距:修复前相邻点可达 150 km。
func TestStarOccultationBandContourSpacing(t *testing.T) {
star := hr4799OccultationCoordinateForTest()
start := time.Date(2025, 6, 5, 0, 0, 0, 0, time.UTC)
paths, err := FindStarOccultationPaths(start, start.Add(24*time.Hour), star,
OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 30, DisableFootprints: true, DisableRiseSet: true})
if err != nil {
t.Fatal(err)
}
const limitKM = 45.0
sampled := 0
for _, path := range paths {
for branch, contour := range path.BandContours {
for index := 1; index < len(contour); index++ {
spacing := occultationPathDistanceKM(contour[index-1], contour[index])
sampled++
if spacing > limitKM {
t.Fatalf("branch %d sample %d: spacing %.3f km exceeds %.0f km", branch, index, spacing, limitKM)
}
}
}
}
if sampled == 0 {
t.Fatal("no band contour samples")
}
}
+14 -4
View File
@@ -9,7 +9,11 @@ import (
// StarOccultationInstant 包含指定时刻的点源恒星月掩可见足迹;若接触锥在该时刻未到达可见地球,Footprint 为 nil。 // StarOccultationInstant 包含指定时刻的点源恒星月掩可见足迹;若接触锥在该时刻未到达可见地球,Footprint 为 nil。
// StarOccultationInstant contains the visible point-source footprint at one requested instant; Footprint is nil when no part of the contact cone reaches the visible Earth. // StarOccultationInstant contains the visible point-source footprint at one requested instant; Footprint is nil when no part of the contact cone reaches the visible Earth.
type StarOccultationInstant struct { type StarOccultationInstant struct {
Time time.Time // Time 是本次查询的时刻。
// Time is the instant this result describes.
Time time.Time
// TargetID 是查询恒星的标识,与 StarCoordinate.ID 同值。
// TargetID identifies the queried star and equals StarCoordinate.ID.
TargetID string TargetID string
// DeltaTSeconds 本次实际使用的 ΔT / ΔT actually used. // DeltaTSeconds 本次实际使用的 ΔT / ΔT actually used.
DeltaTSeconds float64 DeltaTSeconds float64
@@ -22,8 +26,14 @@ type StarOccultationInstant struct {
// PlanetOccultationInstant 包含指定时刻的行星外接触和内接触可见足迹;相应接触锥未到达可见地球时,Partial 或 Total 为 nil。 // PlanetOccultationInstant 包含指定时刻的行星外接触和内接触可见足迹;相应接触锥未到达可见地球时,Partial 或 Total 为 nil。
// PlanetOccultationInstant contains the visible outer- and inner-contact footprints at one requested instant; Partial or Total is nil when that contact cone does not reach the visible Earth. // PlanetOccultationInstant contains the visible outer- and inner-contact footprints at one requested instant; Partial or Total is nil when that contact cone does not reach the visible Earth.
type PlanetOccultationInstant struct { type PlanetOccultationInstant struct {
Time time.Time // Time 是本次查询的时刻。
Planet OccultationPlanet // Time is the instant this result describes.
Time time.Time
// Planet 是本次查询的行星。
// Planet is the queried planet.
Planet OccultationPlanet
// TargetID 是行星标识,与 Planet.String() 同值。
// TargetID identifies the planet and equals Planet.String().
TargetID string TargetID string
// DeltaTSeconds 本次实际使用的 ΔT / ΔT actually used. // DeltaTSeconds 本次实际使用的 ΔT / ΔT actually used.
DeltaTSeconds float64 DeltaTSeconds float64
@@ -91,7 +101,7 @@ func occultationSublunarPoint(tt float64, frameAt occultationPathFrameFunc) (flo
} }
rightAscension := math.Atan2(frame.moon.y, frame.moon.x) * 180 / math.Pi rightAscension := math.Atan2(frame.moon.y, frame.moon.x) * 180 / math.Pi
declination := math.Asin(math.Max(-1, math.Min(1, frame.moon.z/distance))) * 180 / math.Pi declination := math.Asin(math.Max(-1, math.Min(1, frame.moon.z/distance))) * 180 / math.Pi
longitude := rightAscension - ApparentSiderealTime(TD2UT(tt, false))*15 longitude := rightAscension - ApparentSiderealTime(TT2UT1(tt))*15
for longitude > 180 { for longitude > 180 {
longitude -= 360 longitude -= 360
} }
+16 -1
View File
@@ -15,6 +15,11 @@ const (
occultationGreatestTimeContourCorrectionIterations = 12 occultationGreatestTimeContourCorrectionIterations = 12
occultationGreatestTimeContourGradientStepDegrees = 1e-4 occultationGreatestTimeContourGradientStepDegrees = 1e-4
occultationGreatestTimeContourLatitudeLimitDegrees = 88.0 occultationGreatestTimeContourLatitudeLimitDegrees = 88.0
// 残差量纲随判据口径变化:点源用角距平方(梯度约 1.4e-4/度),行星用外接触度量(约 1/度),
// 同一个绝对阈值在两种口径下相差四个量级——按角距平方给阈值会放过 78 km 的位置偏差。
// 收敛改按位置偏移判定:|残差| / |梯度| 即离零集的地面距离,1e-5 度约 1.1 m,
// 两种口径的残差噪声折算成位置抖动都不超过 2e-7 度。
occultationGreatestTimeContourPositionToleranceDegrees = 1e-5
) )
// occultationGreatestTimeArc 固定一个掩甚时刻后的等时线求根器。 // occultationGreatestTimeArc 固定一个掩甚时刻后的等时线求根器。
@@ -128,6 +133,9 @@ func (arc occultationGreatestTimeArc) correct(longitude, latitude float64) (floa
if denominator < 1e-18 || cosine < 1e-6 { if denominator < 1e-18 || cosine < 1e-6 {
return 0, 0, state, false return 0, 0, state, false
} }
if math.Abs(value) <= occultationGreatestTimeContourPositionToleranceDegrees*math.Sqrt(denominator) {
return longitude, latitude, state, true
}
// 完整牛顿步可能一步跨出可见域(掩带很窄,限界附近的种子尤其容易);逐步二分回退, // 完整牛顿步可能一步跨出可见域(掩带很窄,限界附近的种子尤其容易);逐步二分回退,
// 只要还有一步落在域内就继续投影。 // 只要还有一步落在域内就继续投影。
scale, advanced := 1.0, false scale, advanced := 1.0, false
@@ -150,7 +158,14 @@ func (arc occultationGreatestTimeArc) correct(longitude, latitude float64) (floa
} }
} }
value, current, ok := arc.sample(longitude, latitude) value, current, ok := arc.sample(longitude, latitude)
if !ok || math.Abs(value) > 1e-6 { if !ok {
return 0, 0, state, false
}
if math.Abs(value) <= greatestTimeContourResidualTolerance {
return longitude, latitude, current, true
}
east, north, gradientOK := arc.metricGradient(longitude, latitude)
if !gradientOK || math.Abs(value) > occultationGreatestTimeContourPositionToleranceDegrees*math.Hypot(east, north) {
return 0, 0, state, false return 0, 0, state, false
} }
return longitude, latitude, current, true return longitude, latitude, current, true
+85
View File
@@ -145,6 +145,91 @@ func TestPlanetOccultationGreatestTimeContoursMatchLocalGreatest(t *testing.T) {
} }
} }
// 只要存在落在可见域内的序列种子,就必须至少有一个被投影接受:全部被拒会让整条等时线退回 5° 粗扫,
// 产生相位错开的重复支路。投影判据只能按位置偏移给(残差量纲随口径变化),不能用绝对残差阈值。
func TestOccultationGreatestTimeContourSeedsConvergeInVisibleDomain(t *testing.T) {
star := occultationIsochroneTestStar()
starPaths, err := FindStarOccultationPaths(
time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC),
time.Date(2025, time.August, 24, 0, 0, 0, 0, time.UTC), star,
OccultationPathOptions{Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 15 * time.Minute},
)
if err != nil || len(starPaths) == 0 {
t.Fatalf("star: %v", err)
}
planetPaths, err := FindPlanetOccultationPaths(
time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC),
time.Date(2025, time.February, 2, 0, 0, 0, 0, time.UTC), OccultationSaturn,
OccultationPathOptions{Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 30 * time.Minute},
)
if err != nil || len(planetPaths) == 0 {
t.Fatalf("planet: %v", err)
}
config, _ := planetOccultationConfigFor(OccultationSaturn)
starCache := newStarOccultationEventCache(star)
planetCache := newPlanetOccultationEventCache(config)
cases := []struct {
name string
contours []OccultationGreatestTimeContour
series [][]OccultationPathPoint
arcAt func(level float64) occultationGreatestTimeArc
}{
{
name: "star",
contours: starPaths[0].GreatestTimeContours,
series: [][]OccultationPathPoint{
starPaths[0].CenterLine, starPaths[0].NorthernLimit, starPaths[0].SouthernLimit,
},
arcAt: func(level float64) occultationGreatestTimeArc {
return occultationGreatestTimeArc{
evaluation: starCache.riseSetCache.evaluation(level), location: time.UTC,
}
},
},
{
name: "planet",
contours: planetPaths[0].GreatestTimeContours,
series: [][]OccultationPathPoint{
planetPaths[0].CenterLine, planetPaths[0].NorthernLimit, planetPaths[0].SouthernLimit,
},
arcAt: func(level float64) occultationGreatestTimeArc {
return occultationGreatestTimeArc{
evaluation: planetCache.riseSetCache.evaluation(level),
location: time.UTC,
useContactMetric: true,
}
},
},
}
sampled := 0
for _, tc := range cases {
if len(tc.contours) == 0 {
t.Fatalf("%s: no contours", tc.name)
}
for _, contour := range tc.contours {
arc := tc.arcAt(contour.JDE)
inDomain, accepted := 0, 0
for _, seed := range occultationGreatestTimeContourSeeds(tc.series, contour.JDE) {
if _, _, ok := arc.sample(seed[0], seed[1]); !ok {
continue
}
inDomain++
if _, _, _, corrected := arc.correct(seed[0], seed[1]); corrected {
accepted++
}
}
sampled += inDomain
if inDomain > 0 && accepted == 0 {
t.Fatalf("%s contour %s: %d in-domain seeds, none accepted",
tc.name, contour.Time.Format("15:04"), inDomain)
}
}
}
if sampled == 0 {
t.Fatalf("no in-domain seeds sampled")
}
}
// 同一时刻取值的等时线必须是单条连通曲线:曾因"覆盖判据用点到顶点距离"而被重复延拓 // 同一时刻取值的等时线必须是单条连通曲线:曾因"覆盖判据用点到顶点距离"而被重复延拓
// (行星掩星每个取值 3 条重叠支路)。 // (行星掩星每个取值 3 条重叠支路)。
func TestOccultationGreatestTimeContoursHaveSingleBranchPerLevel(t *testing.T) { func TestOccultationGreatestTimeContoursHaveSingleBranchPerLevel(t *testing.T) {
@@ -16,6 +16,8 @@ const (
occultationLimitExactTolerance = 1e-9 occultationLimitExactTolerance = 1e-9
occultationLimitAnchorToleranceKM = 0.5 occultationLimitAnchorToleranceKM = 0.5
occultationLimitRatioTolerance = 0.002 occultationLimitRatioTolerance = 0.002
// 掠射端点锚点的容差:只钉住端点补采带来的百公里级缺口,不追求末位。
occultationLimitGrazingAnchorToleranceKM = 0.01
) )
type occultationLimitFixture struct { type occultationLimitFixture struct {
@@ -141,6 +143,11 @@ func occultationLimitIndependentGroundWidth(tt float64, frameAt occultationPathF
consider(vector) consider(vector)
} }
} }
// 掠射时横向极值恰好落在可见 θ 区间的相切端点上,端点判别式为 0:等差写法的末样本
// 与 interval.right 相差 1 ULP,足以把判别式推成负值而丢掉端点,端点必须按原值补采。
if vector, _, pointOK := occultationPathBoundaryVector(frame, interval.right); pointOK {
consider(vector)
}
} }
for index := 0; index < occultationPathBoundaryScanPoints; index++ { for index := 0; index < occultationPathBoundaryScanPoints; index++ {
if vector, _, pointOK := occultationPathBoundaryVector(frame, 2*math.Pi*float64(index)/float64(occultationPathBoundaryScanPoints)); pointOK { if vector, _, pointOK := occultationPathBoundaryVector(frame, 2*math.Pi*float64(index)/float64(occultationPathBoundaryScanPoints)); pointOK {
@@ -327,3 +334,70 @@ func TestStarOccultationLimitSeparationMatchesExportedLimits(t *testing.T) {
t.Fatalf("greatest limit separation/width=%.6f, want 1.023508 within %.4f", ratio, occultationLimitRatioTolerance) t.Fatalf("greatest limit separation/width=%.6f, want 1.023508 within %.4f", ratio, occultationLimitRatioTolerance)
} }
} }
// TestPlanetOccultationGrazingEndpointWidthAnchors 钉住两个掠射相切端点样本的带宽。
//
// 这两个时刻的横向极值恰好落在可见 θ 区间的相切端点上,只能靠端点原值取到:等差写法
// left+(right-left)*n/n 与端点相差 1 ULP,该 ULP 会把端点判别式推成负值而被判"无地面交点",
// 于是 Saturn 2024-08-21 的 CenterLine[160] 少 47.9 km(3810.7491 对 3858.7641)、
// Venus 1227-05-19 的 CenterLine[15] 少 158.6 km(6047.4858 对 6206.0535)。
func TestPlanetOccultationGrazingEndpointWidthAnchors(t *testing.T) {
testCases := []struct {
name string
planet OccultationPlanet
start time.Time
options OccultationPathOptions
tt float64
widthKM float64
}{
{
name: "Saturn 2024-08-21 center-line[160]", planet: OccultationSaturn,
start: time.Date(2024, time.August, 21, 0, 0, 0, 0, time.UTC),
tt: 2460543.661859489,
widthKM: 3858.7641,
},
{
name: "Venus 1227-05-19 center-line[15]", planet: OccultationVenus,
start: time.Date(1227, time.May, 18, 12, 0, 0, 0, time.UTC),
options: OccultationPathOptions{
Algorithm: OccultationPathAlgorithmExact, Step: 20 * time.Minute,
TargetSpacingKM: 900, DisableFootprints: true, RiseSetStep: time.Minute,
},
tt: 2169357.811307699,
widthKM: 6206.0535,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
paths, err := FindPlanetOccultationPaths(tc.start, tc.start.Add(24*time.Hour), tc.planet, tc.options)
if err != nil || len(paths) != 1 {
t.Fatalf("paths=%d err=%v, want one", len(paths), err)
}
path := paths[0]
frameAt := occultationLimitExactFrame(t, tc.planet, centerTimeTT(path.Greatest.Time))
matched := 0
for _, point := range append([]OccultationPathPoint{path.Greatest}, path.CenterLine...) {
tt := centerTimeTT(point.Time)
if math.Abs(tt-tc.tt) > 1e-6 {
continue
}
matched++
if difference := math.Abs(point.WidthKM - tc.widthKM); difference > occultationLimitGrazingAnchorToleranceKM {
t.Fatalf("width at %v = %.6f km, want %.4f within %.4f km",
point.Time, point.WidthKM, tc.widthKM, occultationLimitGrazingAnchorToleranceKM)
}
independent, ok := occultationLimitIndependentGroundWidth(tt, frameAt)
if !ok || independent <= 0 {
t.Fatalf("independent ground width at %v is unavailable", point.Time)
}
if difference := math.Abs(point.WidthKM - independent); difference > occultationLimitIndependentTolerance*independent {
t.Fatalf("width at %v field=%.9f independent=%.9f, want within %.3e relative",
point.Time, point.WidthKM, independent, occultationLimitIndependentTolerance)
}
}
if matched != 1 {
t.Fatalf("matched %d samples at tt=%.9f, want exactly one", matched, tc.tt)
}
})
}
}
+12 -9
View File
@@ -858,11 +858,9 @@ func starOccultationPathFrameAt(tt float64, star StarCoordinate) (occultationPat
func starOccultationEphemerisStateAt(tt float64, star StarCoordinate) starOccultationEphemerisState { func starOccultationEphemerisStateAt(tt float64, star StarCoordinate) starOccultationEphemerisState {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1) moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
moonDistanceKM := HMoonAwayN(tt, -1) moonDistanceKM := HMoonAwayN(tt, -1)
starRA, starDec := starApparentRaDecGeocentric(tt, star) // 方向与距离必须来自同一次三维推进,否则视差修正会退回历元距离。
starDistanceKM := 0.0 starRA, starDec, starDistanceAU := starApparentRaDecDistanceGeocentric(tt, star)
if star.ParallaxMas > 0 { starDistanceKM := starDistanceAU * occultationPathAstronomicalUnitKM
starDistanceKM = 206264806.247 / star.ParallaxMas * occultationPathAstronomicalUnitKM
}
return starOccultationEphemerisState{ return starOccultationEphemerisState{
moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistanceKM, moonRA: moonRA, moonDec: moonDec, moonDistanceKM: moonDistanceKM,
starRA: starRA, starDec: starDec, starDistanceKM: starDistanceKM, starRA: starRA, starDec: starDec, starDistanceKM: starDistanceKM,
@@ -994,6 +992,11 @@ func occultationPathScannedLimitsAtFrame(tt float64, frame occultationPathFrame)
consider(vector) consider(vector)
} }
} }
// 同 occultationPathFiniteCrossTrackExtrema:等差末样本与 rightTheta 差 1 ULP,
// 掠射时端点会被判"无地面交点"而丢掉极值,端点原值必须补采一次。
if vector, _, ok := occultationPathBoundaryVector(frame, rightTheta); ok {
consider(vector)
}
} }
} }
for i := 0; i < occultationPathBoundaryScanPoints; i++ { for i := 0; i < occultationPathBoundaryScanPoints; i++ {
@@ -1258,7 +1261,7 @@ func occultationPathPointFromVectorWithMoon(
location *time.Location, location *time.Location,
) OccultationPathPoint { ) OccultationPathPoint {
return occultationPathPointFromVectorWithMoonSidereal( return occultationPathPointFromVectorWithMoonSidereal(
tt, vector, width, moon, ApparentSiderealTime(TD2UT(tt, false))*15, location, tt, vector, width, moon, ApparentSiderealTime(TT2UT1(tt))*15, location,
) )
} }
@@ -1531,8 +1534,8 @@ func occultationEarthLineIntersectionWithTolerance(origin, direction occultation
} }
func occultationPathGeodetic(tt float64, vector occultationPathVector) (float64, float64) { func occultationPathGeodetic(tt float64, vector occultationPathVector) (float64, float64) {
ut := TD2UT(tt, false) ut1 := TT2UT1(tt)
return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut)*15) return occultationPathGeodeticWithSidereal(vector, ApparentSiderealTime(ut1)*15)
} }
func occultationPathGeodeticWithSidereal( func occultationPathGeodeticWithSidereal(
@@ -1560,7 +1563,7 @@ type occultationPathEarthRotation struct {
} }
func occultationPathEarthRotationAt(tt float64) occultationPathEarthRotation { func occultationPathEarthRotationAt(tt float64) occultationPathEarthRotation {
angle := ApparentSiderealTime(TD2UT(tt, false)) * 15 * math.Pi / 180 angle := ApparentSiderealTime(TT2UT1(tt)) * 15 * math.Pi / 180
return occultationPathEarthRotation{cosine: math.Cos(angle), sine: math.Sin(angle)} return occultationPathEarthRotation{cosine: math.Cos(angle), sine: math.Sin(angle)}
} }
+1 -1
View File
@@ -33,7 +33,7 @@ func (cache *starOccultationEventCache) preparePathEphemeris(center float64, alg
return [3]float64{moon.x, moon.y, moon.z}, [3]float64{target.x, target.y, target.z} return [3]float64{moon.x, moon.y, moon.z}, [3]float64{target.x, target.y, target.z}
}) })
if len(nodes) > 0 { if len(nodes) > 0 {
cache.local = &starOccultationLocalEphemeris{star: cache.star, nodes: nodes, dense: true} cache.local = newStarOccultationLocalEphemerisFromNodes(center, cache.star, nodes, true)
} }
} }
cache.prepareLocalEphemeris(center) cache.prepareLocalEphemeris(center)
+2 -2
View File
@@ -370,7 +370,7 @@ func planetMoonPositionAt(tt float64, config planetOccultationConfig, observer *
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n) moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, n)
planetRA, planetDec := config.apparentRaDecN(tt, n) planetRA, planetDec := config.apparentRaDecN(tt, n)
if observer != nil { if observer != nil {
ut := TD2UT(tt, false) ut := TT2UTC(tt)
moonDistanceAU := HMoonAwayN(tt, n) / angularDiameterAstronomicalUnitKM moonDistanceAU := HMoonAwayN(tt, n) / angularDiameterAstronomicalUnitKM
planetDistanceAU := config.earthDistanceN(tt, n) planetDistanceAU := config.earthDistanceN(tt, n)
moonRA, moonDec = TopocentricRaDec(moonRA, moonDec, observer.Latitude, observer.Longitude, ut, moonDistanceAU, observer.Height) moonRA, moonDec = TopocentricRaDec(moonRA, moonDec, observer.Latitude, observer.Longitude, ut, moonDistanceAU, observer.Height)
@@ -425,7 +425,7 @@ func planetTopocentricSemidiameterN(tt float64, config planetOccultationConfig,
if !finite(ra) || !finite(dec) || !finite(distanceKM) || distanceKM <= 0 { if !finite(ra) || !finite(dec) || !finite(distanceKM) || distanceKM <= 0 {
return math.NaN() return math.NaN()
} }
distanceKM = topocentricDistanceKM(ra, dec, distanceKM, observer, TD2UT(tt, false)) distanceKM = topocentricDistanceKM(ra, dec, distanceKM, observer, TT2UTC(tt))
if !finite(distanceKM) || distanceKM <= config.equatorialRadiusKM { if !finite(distanceKM) || distanceKM <= config.equatorialRadiusKM {
return math.NaN() return math.NaN()
} }
+2 -2
View File
@@ -440,7 +440,7 @@ func planetOccultationFootprintAtWithResolution(
if !ok { if !ok {
return PlanetOccultationFootprint{}, false return PlanetOccultationFootprint{}, false
} }
siderealDegrees := ApparentSiderealTime(TD2UT(tt, false)) * 15 siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
samples := make([]planetOccultationFootprintSample, boundaryPoints) samples := make([]planetOccultationFootprintSample, boundaryPoints)
for index := range samples { for index := range samples {
theta := 2 * math.Pi * float64(index) / float64(len(samples)) theta := 2 * math.Pi * float64(index) / float64(len(samples))
@@ -954,7 +954,7 @@ func planetOccultationHorizonCircle(
secondAxis := occultationPathCross(centerDirection, firstAxis) secondAxis := occultationPathCross(centerDirection, firstAxis)
centerDistance := 1 / distance centerDistance := 1 / distance
circleRadius := math.Sqrt(math.Max(0, 1-centerDistance*centerDistance)) circleRadius := math.Sqrt(math.Max(0, 1-centerDistance*centerDistance))
siderealDegrees := ApparentSiderealTime(TD2UT(tt, false)) * 15 siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
points := make([]OccultationPathPoint, count) points := make([]OccultationPathPoint, count)
for index := range points { for index := range points {
angle := 2 * math.Pi * float64(index) / float64(count) angle := 2 * math.Pi * float64(index) / float64(count)
+16 -1
View File
@@ -548,7 +548,7 @@ func occultationPathBoundarySamplesAtTimesForFrame(
first := make([]OccultationPathPoint, len(samples)) first := make([]OccultationPathPoint, len(samples))
second := make([]OccultationPathPoint, len(samples)) second := make([]OccultationPathPoint, len(samples))
for index, sample := range samples { for index, sample := range samples {
siderealDegrees := ApparentSiderealTime(TD2UT(sample.tt, false)) * 15 siderealDegrees := ApparentSiderealTime(TT2UT1(sample.tt)) * 15
first[index] = occultationPathPointFromVectorWithMoonSidereal( first[index] = occultationPathPointFromVectorWithMoonSidereal(
sample.tt, sample.first, 0, sample.moon, siderealDegrees, location, sample.tt, sample.first, 0, sample.moon, siderealDegrees, location,
) )
@@ -1532,6 +1532,11 @@ func occultationPathLimitsAndWidthForFrame(
consider(point) consider(point)
} }
} }
// 末样本的等差写法与 rightTheta 相差 1 ULP:掠射时该 ULP 会把端点的判别式推成负值,
// 端点处的横向极值就被当成"无地面交点"丢掉,故端点原值必须补采一次。
if point, _, pointOK := occultationPathBoundaryVector(frame, rightTheta); pointOK {
consider(point)
}
} }
} }
for i := 0; i < occultationPathBoundaryScanPoints; i++ { for i := 0; i < occultationPathBoundaryScanPoints; i++ {
@@ -1641,6 +1646,11 @@ func occultationPathFiniteCrossTrackExtrema(
consider(point) consider(point)
} }
} }
// 末样本的等差写法与 rightTheta 相差 1 ULP:掠射时该 ULP 会把端点的判别式推成负值,
// 端点处的极值就被当成"无地面交点"丢掉,故端点原值必须补采一次。
if point, _, pointOK := occultationPathBoundaryVector(frame, rightTheta); pointOK {
consider(point)
}
} }
} }
for _, interval := range occultationPathBoundaryThetaIntervals(frame) { for _, interval := range occultationPathBoundaryThetaIntervals(frame) {
@@ -1650,6 +1660,11 @@ func occultationPathFiniteCrossTrackExtrema(
consider(point) consider(point)
} }
} }
// 与 occultationPathLimitsAndWidthForFrame 同因:等差末样本与 interval.right 差 1 ULP,
// 掠射时会把端点判别式推成负值而丢掉该处极值,端点原值必须补采一次。
if point, _, pointOK := occultationPathBoundaryVector(frame, interval.right); pointOK {
consider(point)
}
} }
if !finite(minimumOffset) || !finite(maximumOffset) { if !finite(minimumOffset) || !finite(maximumOffset) {
return occultationPathVector{}, occultationPathVector{}, 0, false return occultationPathVector{}, occultationPathVector{}, 0, false
+3 -3
View File
@@ -7,7 +7,7 @@ import (
) )
func TestPlanetOccultationSupportsAllPlanetTargets(t *testing.T) { func TestPlanetOccultationSupportsAllPlanetTargets(t *testing.T) {
tt := TD2UT(Date2JDE(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)), true) tt := UTC2TT(Date2JD(time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC)))
tests := []struct { tests := []struct {
planet OccultationPlanet planet OccultationPlanet
name string name string
@@ -103,7 +103,7 @@ func TestPlanetOccultationUsesStationMoonDistanceForRadius(t *testing.T) {
t.Fatal("Saturn occultation config is unavailable") t.Fatal("Saturn occultation config is unavailable")
} }
moonRA, _ := HMoonGeocentricApparentRaDecN(tt, -1) moonRA, _ := HMoonGeocentricApparentRaDecN(tt, -1)
subMoonLongitude := normalizeLongitude180(moonRA - ApparentSiderealTime(TD2UT(tt, false))*15) subMoonLongitude := normalizeLongitude180(moonRA - ApparentSiderealTime(TT2UTC(tt))*15)
near := Observer{Longitude: subMoonLongitude, Latitude: 0} near := Observer{Longitude: subMoonLongitude, Latitude: 0}
far := Observer{Longitude: normalizeLongitude180(subMoonLongitude + 180), Latitude: 0} far := Observer{Longitude: normalizeLongitude180(subMoonLongitude + 180), Latitude: 0}
nearState := planetOccultationStateAt(tt, config, &near, -1) nearState := planetOccultationStateAt(tt, config, &near, -1)
@@ -128,7 +128,7 @@ func TestPlanetOccultationUsesStationPlanetDistanceForRadius(t *testing.T) {
t.Fatal("Mercury occultation config is unavailable") t.Fatal("Mercury occultation config is unavailable")
} }
planetRA, _ := config.apparentRaDecN(tt, -1) planetRA, _ := config.apparentRaDecN(tt, -1)
subPlanetLongitude := normalizeLongitude180(planetRA - ApparentSiderealTime(TD2UT(tt, false))*15) subPlanetLongitude := normalizeLongitude180(planetRA - ApparentSiderealTime(TT2UTC(tt))*15)
near := Observer{Longitude: subPlanetLongitude, Latitude: 0} near := Observer{Longitude: subPlanetLongitude, Latitude: 0}
far := Observer{Longitude: normalizeLongitude180(subPlanetLongitude + 180), Latitude: 0} far := Observer{Longitude: normalizeLongitude180(subPlanetLongitude + 180), Latitude: 0}
nearState := planetOccultationStateAt(tt, config, &near, -1) nearState := planetOccultationStateAt(tt, config, &near, -1)
+127 -3
View File
@@ -217,7 +217,7 @@ func newOccultationRiseSetContext(
target := newOccultationRiseSetBody(targetRA, targetDec, targetDistanceKM) target := newOccultationRiseSetBody(targetRA, targetDec, targetDistanceKM)
return occultationRiseSetContext{ return occultationRiseSetContext{
tt: tt, tt: tt,
siderealDegrees: ApparentSiderealTime(TD2UT(tt, false)) * 15, siderealDegrees: ApparentSiderealTime(TT2UT1(tt)) * 15,
moonRA: moonRA, moonRA: moonRA,
moonDec: moonDec, moonDec: moonDec,
moon: moon, moon: moon,
@@ -245,7 +245,7 @@ func newOccultationRiseSetContextFromVectors(
target, _, _, targetOK := occultationRiseSetBodyFromVector(targetXYZ, targetAtFiniteDistance) target, _, _, targetOK := occultationRiseSetBodyFromVector(targetXYZ, targetAtFiniteDistance)
return occultationRiseSetContext{ return occultationRiseSetContext{
tt: tt, tt: tt,
siderealDegrees: ApparentSiderealTime(TD2UT(tt, false)) * 15, siderealDegrees: ApparentSiderealTime(TT2UT1(tt)) * 15,
moonRA: moonRA, moonRA: moonRA,
moonDec: moonDec, moonDec: moonDec,
moon: moon, moon: moon,
@@ -635,7 +635,7 @@ func (evaluation occultationRiseSetEvaluation) pointsAt(
if !evaluation.center.valid || !evaluation.before.valid || !evaluation.after.valid { if !evaluation.center.valid || !evaluation.before.valid || !evaluation.after.valid {
return result return result
} }
gst := ApparentSiderealTime(TD2UT(evaluation.tt, false)) * 15 gst := ApparentSiderealTime(TT2UT1(evaluation.tt)) * 15
centerLongitude := normalizeLongitude(evaluation.center.moonRA - gst) centerLongitude := normalizeLongitude(evaluation.center.moonRA - gst)
centerLatitude := evaluation.center.moonDec centerLatitude := evaluation.center.moonDec
appendRoots := func(greatest bool) { appendRoots := func(greatest bool) {
@@ -776,6 +776,130 @@ func (evaluation occultationRiseSetEvaluation) classify(
}, occultationRiseSetCurveKey{phase: phase, direction: direction}, true }, occultationRiseSetCurveKey{phase: phase, direction: direction}, true
} }
// occultationSiderealRatePerDay 是视恒星时的角速率(弧度/日),用于观测者与天顶矢量的时间导数。
const occultationSiderealRatePerDay = 2 * math.Pi * 1.00273790935
// occultationRiseSetBodyVelocity 由前后时刻的体位置给出速度(千米/日);点源(距离为零)返回零。
func occultationRiseSetBodyVelocity(before, after occultationRiseSetBody, stepDays float64) occultationPathVector {
if stepDays <= 0 || before.distanceKM <= 0 || after.distanceKM <= 0 {
return occultationPathVector{}
}
return occultationPathScale(occultationPathSub(after.positionKM, before.positionKM), 1/(2*stepDays))
}
// occultationRiseSetDirectionRate 给出单位方向的时间导数 du/dt = (v − u(u·v))/|p|。
func occultationRiseSetDirectionRate(position, velocity, direction occultationPathVector) occultationPathVector {
norm := occultationPathNorm(position)
if norm <= 0 {
return occultationPathVector{}
}
radial := occultationPathScale(direction, occultationPathDot(direction, velocity))
return occultationPathScale(occultationPathSub(velocity, radial), 1/norm)
}
// occultationRiseSetRadiusRate 给出视半径 asin(R/d) 的解析时间导数(度/日),d 为站心距离。
func occultationRiseSetRadiusRate(
body occultationRiseSetBody,
observer, direction, velocity occultationPathVector,
radiusKM float64,
) (float64, float64, bool) {
if body.distanceKM <= 0 || radiusKM <= 0 {
return 0, 0, true
}
position := occultationPathSub(body.positionKM, observer)
distance := occultationPathNorm(position)
if distance <= radiusKM {
return 0, 0, false
}
ratio := radiusKM / distance
sinRadius := math.Max(-1, math.Min(1, ratio))
cosRadius := math.Sqrt(math.Max(0, 1-sinRadius*sinRadius))
if cosRadius <= 1e-12 {
return 0, 0, false
}
return math.Asin(sinRadius) / rad,
-(ratio / distance) * occultationPathDot(velocity, direction) / cosRadius / rad, true
}
// contactRateAt 用体位置的前后差分给出接触度量的解析时间导数(度/日):与 contactDerivative 的中心差分同口径,
// 但没有差分噪声,且不需要为前后时刻各求一次站心几何。
func (evaluation occultationRiseSetEvaluation) contactRateAt(longitude, latitude float64) float64 {
center := evaluation.center
observer, observerDistance, _ := occultationRiseSetObserverVectors(center.siderealDegrees, longitude, latitude)
observerVelocity := occultationPathCross(occultationPathVector{z: occultationSiderealRatePerDay}, observer)
moonPosition := occultationPathSub(center.moon.positionKM, observer)
targetPosition := occultationPathSub(center.target.positionKM, observer)
moonDirection := occultationRiseSetTopocentricDirection(center.moon, observer)
targetDirection := occultationRiseSetTopocentricDirection(center.target, observer)
moonVelocity := occultationPathSub(
occultationRiseSetBodyVelocity(evaluation.before.moon, evaluation.after.moon, occultationRiseSetDerivativeStepDays),
observerVelocity,
)
targetVelocity := occultationPathSub(
occultationRiseSetBodyVelocity(evaluation.before.target, evaluation.after.target, occultationRiseSetDerivativeStepDays),
observerVelocity,
)
if center.target.distanceKM <= 0 {
// 点源目标没有站心视差:视线方向就是地心视方向,速率取单位矢量的前后差分。
// 零位置减观测者会得到日尺度的虚假速率,掩带边界因此无法加密。
targetPosition = targetDirection
targetVelocity = occultationPathScale(
occultationPathSub(evaluation.after.target.direction, evaluation.before.target.direction),
1/(2*occultationRiseSetDerivativeStepDays),
)
}
moonDirectionRate := occultationRiseSetDirectionRate(moonPosition, moonVelocity, moonDirection)
targetDirectionRate := occultationRiseSetDirectionRate(targetPosition, targetVelocity, targetDirection)
cosSeparation := math.Max(-1, math.Min(1, occultationPathDot(moonDirection, targetDirection)))
sinSeparation := math.Sqrt(math.Max(0, 1-cosSeparation*cosSeparation))
if sinSeparation <= 1e-12 {
return math.NaN()
}
separationRate := -(occultationPathDot(moonDirectionRate, targetDirection) +
occultationPathDot(moonDirection, targetDirectionRate)) / sinSeparation / rad
if occultationRiseSetTopocentricDistance(center.moon, observerDistance) <= 0 {
return math.NaN()
}
moonRadius, moonRadiusRate, moonOK := occultationRiseSetRadiusRate(
center.moon, observer, moonDirection, moonVelocity, moonEquatorialRadiusKM,
)
targetRadius, targetRadiusRate, targetOK := occultationRiseSetRadiusRate(
center.target, observer, targetDirection, targetVelocity, center.targetRadiusKM,
)
if !moonOK || !targetOK {
return math.NaN()
}
if center.internalContact {
if moonRadius >= targetRadius {
return separationRate - (moonRadiusRate - targetRadiusRate)
}
return separationRate + (moonRadiusRate - targetRadiusRate)
}
return separationRate - moonRadiusRate - targetRadiusRate
}
// moonAltitudeRateAt 给出月球几何高度角的解析时间导数(度/日)。
func (evaluation occultationRiseSetEvaluation) moonAltitudeRateAt(longitude, latitude float64) float64 {
center := evaluation.center
observer, _, zenith := occultationRiseSetObserverVectors(center.siderealDegrees, longitude, latitude)
spin := occultationPathVector{z: occultationSiderealRatePerDay}
observerVelocity := occultationPathCross(spin, observer)
moonPosition := occultationPathSub(center.moon.positionKM, observer)
moonDirection := occultationRiseSetTopocentricDirection(center.moon, observer)
moonVelocity := occultationPathSub(
occultationRiseSetBodyVelocity(evaluation.before.moon, evaluation.after.moon, occultationRiseSetDerivativeStepDays),
observerVelocity,
)
moonDirectionRate := occultationRiseSetDirectionRate(moonPosition, moonVelocity, moonDirection)
sinAltitude := math.Max(-1, math.Min(1, occultationPathDot(moonDirection, zenith)))
cosAltitude := math.Sqrt(math.Max(0, 1-sinAltitude*sinAltitude))
if cosAltitude <= 1e-12 {
return math.NaN()
}
return (occultationPathDot(moonDirectionRate, zenith) +
occultationPathDot(moonDirection, occultationPathCross(spin, zenith))) / cosAltitude / rad
}
func (evaluation occultationRiseSetEvaluation) contactDerivative(longitude, latitude float64) float64 { func (evaluation occultationRiseSetEvaluation) contactDerivative(longitude, latitude float64) float64 {
before := evaluation.before.stateAt(longitude, latitude) before := evaluation.before.stateAt(longitude, latitude)
after := evaluation.after.stateAt(longitude, latitude) after := evaluation.after.stateAt(longitude, latitude)
+1 -1
View File
@@ -189,7 +189,7 @@ func legacyOccultationRiseSetStateAt(
targetRA, targetDec, targetDistanceKM, targetRadiusKM, targetRA, targetDec, targetDistanceKM, targetRadiusKM,
longitude, latitude float64, longitude, latitude float64,
) occultationRiseSetState { ) occultationRiseSetState {
siderealDegrees := ApparentSiderealTime(TD2UT(tt, false)) * 15 siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
observer := Observer{Longitude: longitude, Latitude: latitude} observer := Observer{Longitude: longitude, Latitude: latitude}
moonTopocentricRA, moonTopocentricDec := topocentricRaDecWithSidereal( moonTopocentricRA, moonTopocentricDec := topocentricRaDecWithSidereal(
moonRA, moonDec, latitude, longitude, siderealDegrees, moonRA, moonDec, latitude, longitude, siderealDegrees,
+79 -17
View File
@@ -273,7 +273,7 @@ func starOccultationMinimizeValue(left, right float64, value func(float64) float
func starOccultationGeocentricSeparationArcsec(tt float64, star StarCoordinate) float64 { func starOccultationGeocentricSeparationArcsec(tt float64, star StarCoordinate) float64 {
moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1) moonRA, moonDec := HMoonGeocentricApparentRaDecN(tt, -1)
starRA, starDec := starApparentRaDecGeocentric(tt, star) starRA, starDec, _ := starApparentRaDecDistanceGeocentric(tt, star)
return angularSeparationDegrees(moonRA, moonDec, starRA, starDec) * 3600 return angularSeparationDegrees(moonRA, moonDec, starRA, starDec) * 3600
} }
@@ -389,45 +389,107 @@ func starMoonPositionAt(tt float64, star StarCoordinate, observer Observer) star
func moonTopocentricApparentRaDec(tt float64, observer Observer, n int) (float64, float64) { func moonTopocentricApparentRaDec(tt float64, observer Observer, n int) (float64, float64) {
ra, dec := HMoonGeocentricApparentRaDecN(tt, n) ra, dec := HMoonGeocentricApparentRaDecN(tt, n)
ut := TD2UT(tt, false) ut := TT2UTC(tt)
distanceAU := HMoonAwayN(tt, n) / 149597870.7 distanceAU := HMoonAwayN(tt, n) / 149597870.7
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distanceAU, observer.Height) ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, ut, distanceAU, observer.Height)
return normalizeRA(ra), dec return normalizeRA(ra), dec
} }
func starApparentRaDec(tt float64, star StarCoordinate, observer Observer) (float64, float64) { func starApparentRaDec(tt float64, star StarCoordinate, observer Observer) (float64, float64) {
ra, dec := starApparentRaDecGeocentric(tt, star) ra, dec, distanceAU := starApparentRaDecDistanceGeocentric(tt, star)
if star.ParallaxMas > 0 { if distanceAU > 0 {
// 1 秒差距处 1 角秒对应 206264.806 AU。 ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, TT2UTC(tt), distanceAU, observer.Height)
// One arcsecond at 1 pc corresponds to 206264.806 AU.
distanceAU := 206264806.247 / star.ParallaxMas
ra, dec = TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, TD2UT(tt, false), distanceAU, observer.Height)
ra = normalizeRA(ra) ra = normalizeRA(ra)
} }
return ra, dec return ra, dec
} }
func starApparentRaDecGeocentric(tt float64, star StarCoordinate) (float64, float64) { func starApparentRaDecGeocentric(tt float64, star StarCoordinate) (float64, float64) {
ra, dec, _ := starApparentRaDecDistanceGeocentric(tt, star)
return ra, dec
}
// starApparentRaDecDistanceGeocentric 同时给出视位置与推进后的距离(天文单位,0 表示距离未知)。
func starApparentRaDecDistanceGeocentric(tt float64, star StarCoordinate) (float64, float64, float64) {
epochJD := occultationTimeToTT(star.Epoch) epochJD := occultationTimeToTT(star.Epoch)
years := (tt - epochJD) / 365.25 parallaxMas := star.parallaxMas()
ra := star.RA ra := star.RA
dec := star.Dec dec := star.Dec
precessionEpoch := 2451545.0 precessionEpoch := 2451545.0
if star.Frame == CoordinateFrameICRS { if star.Frame == CoordinateFrameICRS {
ra, dec = starICRSToMeanJ2000RaDec(ra, dec) ra, dec = starICRSToMeanJ2000RaDec(ra, dec)
} else if star.Frame == CoordinateFrameApparentOfDate { } else if star.Frame == CoordinateFrameApparentOfDate {
ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, star.ParallaxMas) ra, dec = starApparentToMeanRaDec(epochJD, ra, dec, parallaxMas)
precessionEpoch = epochJD precessionEpoch = epochJD
} }
epochDistanceAU := starEpochDistanceAU(parallaxMas)
ra, dec, distanceAU := starProperMotionRaDec(tt, star, ra, dec, epochDistanceAU)
ra, dec = Precess(ra, dec, precessionEpoch, tt)
ra, dec = starMeanToApparentRaDec(tt, ra, dec, parallaxMasAtDistance(parallaxMas, distanceAU))
return ra, dec, distanceAU
}
// starEpochDistanceAU 由历元视差给出距离,非正表示距离未知。
func starEpochDistanceAU(parallaxMas float64) float64 {
if parallaxMas <= 0 {
return 0
}
return 206264806.247 / parallaxMas
}
// starPropagatedPositionAU 把历元位置矢量按三维匀速直线运动推进一个历元差。
// 切向速度取自行乘历元距离,视向分量取径向速度;返回推进后的矢量,其模长即当日距离。
func starPropagatedPositionAU(star StarCoordinate, ra, dec, epochDistanceAU, years float64) ([3]float64, float64) {
if epochDistanceAU <= 0 {
return [3]float64{}, 0
}
raRad := ra * math.Pi / 180
decRad := dec * math.Pi / 180
cosDec, sinDec := math.Cos(decRad), math.Sin(decRad)
cosRA, sinRA := math.Cos(raRad), math.Sin(raRad)
pmRA := star.ProperMotionRACosDecMasPerYear / 1000 * math.Pi / (180 * 3600) * epochDistanceAU
pmDec := star.ProperMotionDecMasPerYear / 1000 * math.Pi / (180 * 3600) * epochDistanceAU
radial := star.RadialVelocityKmPerSecond * 365.25 * 86400 / 149597870.7
position := [3]float64{
epochDistanceAU*cosDec*cosRA + years*(pmDec*(-sinDec*cosRA)-pmRA*sinRA+radial*cosDec*cosRA),
epochDistanceAU*cosDec*sinRA + years*(pmDec*(-sinDec*sinRA)+pmRA*cosRA+radial*cosDec*sinRA),
epochDistanceAU*sinDec + years*(pmDec*cosDec+radial*sinDec),
}
norm := math.Sqrt(position[0]*position[0] + position[1]*position[1] + position[2]*position[2])
return position, norm
}
// parallaxMasAtDistance 把推进后的距离折回周年视差,视差修正必须跟着距离一起变。
func parallaxMasAtDistance(parallaxMas, distanceAU float64) float64 {
if parallaxMas <= 0 || distanceAU <= 0 {
return 0
}
return 206264806.247 / distanceAU
}
// starProperMotionRaDec 把历元输入坐标推进到 tt,并给出推进后的距离。
// 距离已知走三维、否则只推进两个角分量(此时距离返回 0)。
func starProperMotionRaDec(tt float64, star StarCoordinate, ra, dec, epochDistanceAU float64) (float64, float64, float64) {
years := (tt - occultationTimeToTT(star.Epoch)) / 365.25
if epochDistanceAU > 0 {
return starProperMotionRaDec3D(ra, dec, years, epochDistanceAU, star)
}
cosDec := math.Cos(dec * math.Pi / 180) cosDec := math.Cos(dec * math.Pi / 180)
if math.Abs(cosDec) > 1e-12 { if math.Abs(cosDec) > 1e-12 {
ra += years * star.ProperMotionRACosDecMasPerYear / (3600000.0 * cosDec) ra += years * star.ProperMotionRACosDecMasPerYear / (3600000.0 * cosDec)
} }
dec += years * star.ProperMotionDecMasPerYear / 3600000.0 dec += years * star.ProperMotionDecMasPerYear / 3600000.0
dec = math.Max(-90, math.Min(90, dec)) return ra, math.Max(-90, math.Min(90, dec)), 0
}
ra, dec = Precess(ra, dec, precessionEpoch, tt) // starProperMotionRaDec3D 按三维匀速直线运动推进:赤经赤纬只是位置矢量的方向。
return starMeanToApparentRaDec(tt, ra, dec, star.ParallaxMas) func starProperMotionRaDec3D(ra, dec, years, epochDistanceAU float64, star StarCoordinate) (float64, float64, float64) {
position, norm := starPropagatedPositionAU(star, ra, dec, epochDistanceAU, years)
outRA := math.Atan2(position[1], position[0]) * 180 / math.Pi
if outRA < 0 {
outRA += 360
}
return outRA, math.Asin(position[2]/norm) * 180 / math.Pi, norm
} }
func starICRSToMeanJ2000RaDec(ra, dec float64) (float64, float64) { func starICRSToMeanJ2000RaDec(ra, dec float64) (float64, float64) {
@@ -628,7 +690,7 @@ func occultationPositionAngle(moonRA, moonDec, starRA, starDec float64) float64
func occultationAltitude(tt float64, observer Observer, ra, dec float64) float64 { func occultationAltitude(tt float64, observer Observer, ra, dec float64) float64 {
return occultationAltitudeWithSidereal( return occultationAltitudeWithSidereal(
ApparentSiderealTime(TD2UT(tt, false))*15, observer, ra, dec, ApparentSiderealTime(TT2UT1(tt))*15, observer, ra, dec,
) )
} }
@@ -641,7 +703,7 @@ func occultationAltitudeWithSidereal(siderealDegrees float64, observer Observer,
} }
func occultationAzimuth(tt float64, observer Observer, ra, dec float64) float64 { func occultationAzimuth(tt float64, observer Observer, ra, dec float64) float64 {
hourAngle := signedAngleDifference(ApparentSiderealTime(TD2UT(tt, false))*15+observer.Longitude, ra) * math.Pi / 180 hourAngle := signedAngleDifference(ApparentSiderealTime(TT2UT1(tt))*15+observer.Longitude, ra) * math.Pi / 180
lat := observer.Latitude * math.Pi / 180 lat := observer.Latitude * math.Pi / 180
declination := dec * math.Pi / 180 declination := dec * math.Pi / 180
y := math.Sin(hourAngle) y := math.Sin(hourAngle)
@@ -666,12 +728,12 @@ func normalizeRA(ra float64) float64 {
} }
func occultationTimeToTT(value time.Time) float64 { func occultationTimeToTT(value time.Time) float64 {
return TD2UT(Date2JDE(value.UTC()), true) return UTC2TT(Date2JD(value.UTC()))
} }
func occultationTTToLocation(tt float64, location *time.Location) time.Time { func occultationTTToLocation(tt float64, location *time.Location) time.Time {
if location == nil { if location == nil {
location = time.UTC location = time.UTC
} }
return JDE2DateByZone(TD2UT(tt, false), location, false) return JD2DateByZone(TT2UTC(tt), location, false)
} }
+339
View File
@@ -0,0 +1,339 @@
package basic
import (
"fmt"
"math"
"testing"
"time"
"b612.me/astro/tools"
)
// 本文件锁定掩星恒星坐标的距离契约:无距离走二维、给光年或视差走三维。
func occStar3DTestCoordinate(ra, dec float64) StarCoordinate {
return StarCoordinate{
ID: "3D contract",
RA: ra,
Dec: dec,
Epoch: time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC),
Frame: CoordinateFrameJ2000,
}
}
func TestStarCoordinateDistanceGateKeepsTwoDimensions(t *testing.T) {
star := occStar3DTestCoordinate(189.1975, -5.831944444444)
star.ProperMotionRACosDecMasPerYear = -28
star.ProperMotionDecMasPerYear = -18
star.RadialVelocityKmPerSecond = -45
if got := star.parallaxMas(); got != 0 {
t.Fatalf("parallaxMas() = %v, want 0 without any distance", got)
}
tt := occultationTimeToTT(time.Date(2050, 1, 1, 0, 0, 0, 0, time.UTC))
gotRA, gotDec, gotDistance := starProperMotionRaDec(tt, star, star.RA, star.Dec, starEpochDistanceAU(star.parallaxMas()))
if gotDistance != 0 {
t.Fatalf("no-distance propagation reported distance %v, want 0", gotDistance)
}
cosDec := math.Cos(star.Dec * math.Pi / 180)
years := (tt - occultationTimeToTT(star.Epoch)) / 365.25
wantRA := star.RA + years*star.ProperMotionRACosDecMasPerYear/(3600000*cosDec)
wantDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000
if gotRA != wantRA || gotDec != wantDec {
t.Fatalf("no-distance propagation = %.12f %.12f, want 2D %.12f %.12f", gotRA, gotDec, wantRA, wantDec)
}
}
func TestStarCoordinateLightYearDistanceMatchesParallax(t *testing.T) {
const lightYears = 10
byLightYear := func() StarCoordinate {
star := occStar3DTestCoordinate(224.366667, -21.415556)
star.ProperMotionRACosDecMasPerYear = 1045
star.ProperMotionDecMasPerYear = -1729
star.RadialVelocityKmPerSecond = 20
star.DistanceLightYear = lightYears
return star
}()
byParallax := byLightYear
byParallax.DistanceLightYear = 0
byParallax.ParallaxMas = byLightYear.parallaxMas()
if byParallax.ParallaxMas <= 0 || byParallax.ParallaxMas > 400 {
t.Fatalf("derived parallax = %v mas, want a positive sub-arcsecond value", byParallax.ParallaxMas)
}
tt := occultationTimeToTT(time.Date(2050, 1, 1, 0, 0, 0, 0, time.UTC))
lightRA, lightDec := starApparentRaDecGeocentric(tt, byLightYear)
parallaxRA, parallaxDec := starApparentRaDecGeocentric(tt, byParallax)
if lightRA != parallaxRA || lightDec != parallaxDec {
t.Fatalf("light-year path = %.12f %.12f, want bit-identical to parallax path %.12f %.12f",
lightRA, lightDec, parallaxRA, parallaxDec)
}
if separation := starSepArcsec(lightRA, lightDec, byLightYear.RA, byLightYear.Dec); separation < 60 {
t.Fatalf("proper motion over 50 years moved the star only %.6f arcsec", separation)
}
}
func TestStarCoordinateParallaxTakesPriorityOverLightYear(t *testing.T) {
star := occStar3DTestCoordinate(120, 15)
star.DistanceLightYear = 10
if got, want := star.parallaxMas(), 1000/tools.DistanceToParsecs(10, tools.DistanceLightYear); math.Abs(got-want) > 1e-9 {
t.Fatalf("parallaxMas() = %.12f, want %.12f derived from light-years", got, want)
}
star.ParallaxMas = 25
if got := star.parallaxMas(); got != 25 {
t.Fatalf("parallaxMas() = %v, want the explicit 25 mas to win", got)
}
}
func TestStarCoordinateRadialVelocityContract(t *testing.T) {
base := occStar3DTestCoordinate(120, 15)
for _, velocity := range []float64{0, -500, 500, starRadialVelocityLimitKmPerSecond} {
star := base
star.RadialVelocityKmPerSecond = velocity
if err := star.Validate(); err != nil {
t.Fatalf("radial velocity %v rejected: %v", velocity, err)
}
}
for _, velocity := range []float64{-5000, 5000, math.NaN(), math.Inf(1), math.Inf(-1)} {
star := base
star.RadialVelocityKmPerSecond = velocity
if err := star.Validate(); err == nil {
t.Fatalf("radial velocity %v accepted, want a contract error", velocity)
}
}
for _, distance := range []float64{-1, math.NaN(), math.Inf(1)} {
star := base
star.DistanceLightYear = distance
if err := star.Validate(); err == nil {
t.Fatalf("light-year distance %v accepted, want a contract error", distance)
}
}
}
func TestStarCoordinateThreeDimensionsShiftsOccultationTimingWithinBudget(t *testing.T) {
// HR 5568 于 2026-01-13 的一次真实全掩;同一站、同一窗口下比较三种口径的真实接触时刻。
star := occStar3DTestCoordinate(224.366667, -21.415556)
star.ProperMotionRACosDecMasPerYear = 1045
star.ProperMotionDecMasPerYear = -1729
star.ParallaxMas = 173
star.RadialVelocityKmPerSecond = 20
noRadial := star
noRadial.RadialVelocityKmPerSecond = 0
noDistance := star
noDistance.ParallaxMas = 0
const longitude, latitude = 97.471, -45.720
start := time.Date(2026, 1, 12, 18, 0, 0, 0, time.UTC)
end := time.Date(2026, 1, 13, 6, 0, 0, 0, time.UTC)
event := func(c StarCoordinate) StarOccultationInfo {
t.Helper()
events, err := FindStarOccultations(start, end, c, longitude, latitude, 0, OccultationSearchOptions{})
if err != nil {
t.Fatalf("FindStarOccultations: %v", err)
}
if len(events) != 1 {
t.Fatalf("event count = %d, want 1", len(events))
}
return events[0]
}
// 无距离确实走的是二维分支,否则下面的对照没有意义。
if noDistance.parallaxMas() != 0 || star.parallaxMas() != 173 {
t.Fatalf("parallax gate = %v / %v, want 0 for the 2D branch and 173 for the 3D branch",
noDistance.parallaxMas(), star.parallaxMas())
}
unlimited := event(star)
limited := event(noDistance)
if unlimited.Type != OccultationTotal || limited.Type != OccultationTotal {
t.Fatalf("event types = %v / %v, want both total", unlimited.Type, limited.Type)
}
milliseconds := func(a, b time.Time) float64 { return math.Abs(a.Sub(b).Seconds()) * 1000 }
// 纯空间运动项:只把径向速度清零,几何仍为三维。这是本轮三维改动引入的那一项。
// 实测约 14 ms(路径距离也随径向项变化后由 8.6 ms 升到 14 ms),阈值留约 1.8 倍余量,
// 目的是抓口径回退,不是卡精度指标。
if got := milliseconds(unlimited.Immersion, event(noRadial).Immersion); got > 25 {
t.Fatalf("space-motion term shifts immersion by %.1f ms, want the 25 ms budget respected", got)
}
// 距离有无会额外启用站心视差修正,属于既有几何而非本轮改动,量级单独记录。
fmt.Printf("空间运动项 %.1f ms;距离项 %.1f ms\n",
milliseconds(unlimited.Immersion, event(noRadial).Immersion),
milliseconds(unlimited.Immersion, limited.Immersion))
}
func TestStarCoordinateTwoAndThreeDimensionsAreDistinctGeometries(t *testing.T) {
// 径向速度反号必须让三维结果分居两侧,且二维分支确实给出不同的角距,
// 否则"三维对二维"的对照就是空保证。
star := occStar3DTestCoordinate(224.366667, -21.415556)
star.ProperMotionRACosDecMasPerYear = 1045
star.ProperMotionDecMasPerYear = -1729
star.ParallaxMas = 173
star.RadialVelocityKmPerSecond = -100
away := star
away.RadialVelocityKmPerSecond = 100
noDistance := star
noDistance.ParallaxMas = 0
tt := occultationTimeToTT(time.Date(2026, 1, 13, 0, 14, 7, 0, time.UTC))
observer := Observer{Longitude: 97.471, Latitude: -45.720}
approaching := starMoonSeparationArcsec(tt, star, observer)
receding := starMoonSeparationArcsec(tt, away, observer)
twoDimension := starMoonSeparationArcsec(tt, noDistance, observer)
if math.Abs(approaching-receding) < 0.005 {
t.Fatalf("opposite radial velocities separate by only %.6f arcsec, want a measurable space-motion term",
math.Abs(approaching-receding))
}
if math.Abs(twoDimension-approaching) < 0.05 {
t.Fatalf("2D separation %.6f arcsec is indistinguishable from the 3D one %.6f", twoDimension, approaching)
}
fmt.Printf("同一时刻角距:三维 rv=-100 为 %.4f\",二维为 %.4f\",三维 rv=+100 为 %.4f\"\n",
approaching, twoDimension, receding)
}
func TestStarCoordinatePathDistanceAcceptsLightYears(t *testing.T) {
// 光年与等价视差必须进入同一套路径几何,不能一个当无穷远。
byParallax := occStar3DTestCoordinate(224.366667, -21.415556)
byParallax.ParallaxMas = 173
byLightYear := byParallax
byLightYear.ParallaxMas = 0
byLightYear.DistanceLightYear = (1000.0 / 173) * tools.AstronomicalUnitKilometers * (648000 / math.Pi) / tools.LightYearKilometers
tt := occultationTimeToTT(time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC))
fromParallax := starOccultationEphemerisStateAt(tt, byParallax)
fromLightYear := starOccultationEphemerisStateAt(tt, byLightYear)
if fromParallax.starDistanceKM <= 0 {
t.Fatal("parallax input should yield a finite path distance")
}
// 两种等价输入只允许差一个浮点往返的量级。
if relative := math.Abs(fromLightYear.starDistanceKM-fromParallax.starDistanceKM) / fromParallax.starDistanceKM; relative > 1e-12 {
t.Fatalf("path distance = %g km from light-years, want %g km from the equivalent parallax (relative %.3g)",
fromLightYear.starDistanceKM, fromParallax.starDistanceKM, relative)
}
kept := newStarOccultationLocalEphemeris(tt, byLightYear)
if relative := math.Abs(kept.starDistanceKM()-fromParallax.starDistanceKM) / fromParallax.starDistanceKM; relative > 1e-12 {
t.Fatalf("local ephemeris distance = %g km, want %g km (relative %.3g)", kept.starDistanceKM(), fromParallax.starDistanceKM, relative)
}
}
func TestStarCoordinatePathGeometryUsesPropagatedDistance(t *testing.T) {
// 路径与本地星历必须用当日的距离,而不是历元距离。1 pc、径向 -100 km/s,1000 年后
// 距离缩短约 10%,两种口径给出的路径距离必须能区分开。
star := occStar3DTestCoordinate(189.1975, -5.831944444444)
star.ParallaxMas = 1000
star.RadialVelocityKmPerSecond = -100
epochDistanceKM := 206264806.247 / star.ParallaxMas * occultationPathAstronomicalUnitKM
for _, years := range []float64{26, 1000, 3000} {
tt := occultationTimeToTT(star.Epoch) + years*365.25
state := starOccultationEphemerisStateAt(tt, star)
if !state.valid {
t.Fatalf("ephemeris state invalid at %.0f years", years)
}
_, _, distanceAU := starApparentRaDecDistanceGeocentric(tt, star)
want := distanceAU * occultationPathAstronomicalUnitKM
if math.Abs(state.starDistanceKM-want) > want*1e-12 {
t.Fatalf("path distance at %.0f years = %g km, want the propagated %g km", years, state.starDistanceKM, want)
}
if years >= 1000 && math.Abs(state.starDistanceKM-epochDistanceKM) < want*0.01 {
t.Fatalf("path distance at %.0f years matches the epoch distance, so the radial term is still dropped", years)
}
if got := newStarOccultationLocalEphemeris(tt, star).starDistanceKM(); math.Abs(got-want) > want*1e-12 {
t.Fatalf("local ephemeris distance at %.0f years = %g km, want %g km", years, got, want)
}
}
// 径向速度为零时两种口径必须重合,否则说明引入了与运动无关的偏移。
star.RadialVelocityKmPerSecond = 0
still := starOccultationEphemerisStateAt(occultationTimeToTT(star.Epoch)+1000*365.25, star)
if math.Abs(still.starDistanceKM-epochDistanceKM) > epochDistanceKM*1e-9 {
t.Fatalf("path distance without radial motion = %g km, want the epoch %g km", still.starDistanceKM, epochDistanceKM)
}
}
// TestStarCoordinateOptimizedEphemerisKeepsFiniteDistance 固定优化分支(密集星历)也带当日距离:
// 节点按每个采样时刻的推进距离装配,状态回读若丢掉它,有限距离会悄悄退化成无穷远框架,
// 而且 exact 与 optimized 两条分支会在同一颗带视差恒星上给出不同的帧。
func TestStarCoordinateOptimizedEphemerisKeepsFiniteDistance(t *testing.T) {
star := occStar3DTestCoordinate(189.1975, -5.831944444444)
star.ProperMotionRACosDecMasPerYear = -28
star.ProperMotionDecMasPerYear = -18
star.ParallaxMas = 768.5
star.RadialVelocityKmPerSecond = -30
cst := time.FixedZone("CST", 8*3600)
start := time.Date(2025, 6, 5, 0, 0, 0, 0, cst)
center := occultationTimeToTT(start.Add(12 * time.Hour))
_, _, distanceAU := starApparentRaDecDistanceGeocentric(center, star)
want := distanceAU * occultationPathAstronomicalUnitKM
cache := newStarOccultationEventCache(star)
cache.preparePathEphemeris(center, OccultationPathAlgorithmOptimized)
if cache.local == nil || !cache.local.dense {
t.Fatalf("dense ephemeris not selected, the optimized branch is not exercised")
}
if got := cache.local.starDistanceKM(); math.Abs(got-want) > want*1e-9 {
t.Fatalf("optimized ephemeris distance = %g km, want the propagated %g km", got, want)
}
state, ok := cache.local.stateAt(center)
if !ok {
t.Fatal("optimized state unavailable")
}
if math.Abs(state.starDistanceKM-want) > want*1e-6 {
t.Fatalf("optimized state distance = %g km, want the propagated %g km", state.starDistanceKM, want)
}
denseFrame, ok := starOccultationPathFrameFromState(state)
if !ok {
t.Fatal("optimized frame unavailable")
}
exactFrame, ok := starOccultationPathFrameFromState(starOccultationEphemerisStateAt(center, star))
if !ok {
t.Fatal("exact frame unavailable")
}
if angle := occultationPathNorm(occultationPathCross(denseFrame.axis, exactFrame.axis)) * 180 / math.Pi * 3600 * 1000; angle > 1e-6 {
t.Fatalf("optimized and exact frame axes differ by %.3e mas, want only the dense table's own error", angle)
}
// 距离未知时仍按 0 上报,几何保持无穷远,不要退化成"1 km 处"的假视差。
unknown := star
unknown.ParallaxMas = 0
unknownCache := newStarOccultationEventCache(unknown)
unknownCache.preparePathEphemeris(center, OccultationPathAlgorithmOptimized)
if unknownCache.local == nil {
t.Fatal("local ephemeris missing for the distance-free star")
}
if got := unknownCache.local.starDistanceKM(); got != 0 {
t.Fatalf("distance-free ephemeris distance = %g, want 0", got)
}
if state, ok := unknownCache.local.stateAt(center); !ok || state.starDistanceKM != 0 {
t.Fatalf("distance-free state distance = %g (ok=%v), want 0", state.starDistanceKM, ok)
}
// 路径层:两条分支必须给出同一条路径。
options := OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 200}
end := start.Add(24 * time.Hour)
exactOptions, optimizedOptions := options, options
exactOptions.Algorithm = OccultationPathAlgorithmExact
optimizedOptions.Algorithm = OccultationPathAlgorithmOptimized
exactPaths, err := FindStarOccultationPaths(start, end, star, exactOptions)
if err != nil || len(exactPaths) == 0 {
t.Fatalf("exact path: %v (paths=%d)", err, len(exactPaths))
}
optimizedPaths, err := FindStarOccultationPaths(start, end, star, optimizedOptions)
if err != nil || len(optimizedPaths) == 0 {
t.Fatalf("optimized path: %v (paths=%d)", err, len(optimizedPaths))
}
exact, optimized := exactPaths[0], optimizedPaths[0]
for _, moment := range []struct {
name string
exact, optimiz time.Time
}{
{"start", exact.Start.Time, optimized.Start.Time},
{"greatest", exact.Greatest.Time, optimized.Greatest.Time},
{"end", exact.End.Time, optimized.End.Time},
} {
if delta := math.Abs(moment.exact.Sub(moment.optimiz).Seconds()); delta > 1e-3 {
t.Fatalf("%s differs by %.6f s between the two ephemeris branches", moment.name, delta)
}
}
if delta := math.Abs(exact.Greatest.WidthKM - optimized.Greatest.WidthKM); delta > 1e-3 {
t.Fatalf("greatest width differs by %.6f km between the two ephemeris branches", delta)
}
}
+3 -3
View File
@@ -102,7 +102,7 @@ func TestStarOccultationApparentPlaceCorrections(t *testing.T) {
t.Fatalf("apparent place = %.9f %.9f, want near 189.527817 -5.973401", gotRA, gotDec) t.Fatalf("apparent place = %.9f %.9f, want near 189.527817 -5.973401", gotRA, gotDec)
} }
years := (tt - Date2JDE(star.Epoch.UTC())) / 365.25 years := (tt - Date2JD(star.Epoch.UTC())) / 365.25
meanRA := star.RA + years*star.ProperMotionRACosDecMasPerYear/(3600000*math.Cos(star.Dec*math.Pi/180)) meanRA := star.RA + years*star.ProperMotionRACosDecMasPerYear/(3600000*math.Cos(star.Dec*math.Pi/180))
meanDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000 meanDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000
meanRA, meanDec = Precess(meanRA, meanDec, 2451545, tt) meanRA, meanDec = Precess(meanRA, meanDec, 2451545, tt)
@@ -226,7 +226,7 @@ func TestRefineOccultationPathWidthsBoundsSmoothInterpolationError(t *testing.T)
func TestOccultationPathCachedEarthRotationMatchesDirectGeometry(t *testing.T) { func TestOccultationPathCachedEarthRotationMatchesDirectGeometry(t *testing.T) {
tt := occultationTimeToTT(time.Date(2025, time.June, 5, 12, 2, 6, 0, time.UTC)) tt := occultationTimeToTT(time.Date(2025, time.June, 5, 12, 2, 6, 0, time.UTC))
vector := occultationPathVector{x: 4123.5, y: -2789.25, z: 3950.75} vector := occultationPathVector{x: 4123.5, y: -2789.25, z: 3950.75}
angle := ApparentSiderealTime(TD2UT(tt, false)) * 15 * math.Pi / 180 angle := ApparentSiderealTime(TT2UT1(tt)) * 15 * math.Pi / 180
want := occultationPathVector{ want := occultationPathVector{
x: math.Cos(angle)*vector.x + math.Sin(angle)*vector.y, x: math.Cos(angle)*vector.x + math.Sin(angle)*vector.y,
y: -math.Sin(angle)*vector.x + math.Cos(angle)*vector.y, y: -math.Sin(angle)*vector.x + math.Cos(angle)*vector.y,
@@ -257,7 +257,7 @@ func TestOccultationPathCachedMoonAndSiderealMatchDirectPoint(t *testing.T) {
direct := occultationPathPointFromVector(tt, vector, 1234.5, time.UTC) direct := occultationPathPointFromVector(tt, vector, 1234.5, time.UTC)
cached := occultationPathPointFromVectorWithMoonSidereal( cached := occultationPathPointFromVectorWithMoonSidereal(
tt, vector, 1234.5, frame.moon, ApparentSiderealTime(TD2UT(tt, false))*15, time.UTC, tt, vector, 1234.5, frame.moon, ApparentSiderealTime(TT2UT1(tt))*15, time.UTC,
) )
if !cached.Time.Equal(direct.Time) { if !cached.Time.Equal(direct.Time) {
t.Fatalf("cached point time = %v, want %v", cached.Time, direct.Time) t.Fatalf("cached point time = %v, want %v", cached.Time, direct.Time)
+32 -3
View File
@@ -991,7 +991,12 @@ func occultationStationEnvelopeJacobian(
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1] longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
evaluation := cache.evaluation(tt) evaluation := cache.evaluation(tt)
residual, ok := model.residual(evaluation, longitude, latitude) // 点源目标(恒星)没有盘面半径项,接触度量可用解析速率;有限盘面行星保持中心差分。
residualAt := model.residual
if evaluation.center.targetRadiusKM <= 0 {
residualAt = model.residualWithRate
}
residual, ok := residualAt(evaluation, longitude, latitude)
if !ok { if !ok {
return [2]float64{}, [2][3]float64{}, false return [2]float64{}, [2][3]float64{}, false
} }
@@ -1002,7 +1007,7 @@ func occultationStationEnvelopeJacobian(
shifted[column] += steps[column] shifted[column] += steps[column]
shiftedTT := referenceTT + shifted[2]/occultationStationEnvelopeTimeScale shiftedTT := referenceTT + shifted[2]/occultationStationEnvelopeTimeScale
shiftedEvaluation := cache.evaluation(shiftedTT) shiftedEvaluation := cache.evaluation(shiftedTT)
shiftedResidual, shiftedOK := model.residual( shiftedResidual, shiftedOK := residualAt(
shiftedEvaluation, normalizeLongitude(shifted[0]), shifted[1], shiftedEvaluation, normalizeLongitude(shifted[0]), shifted[1],
) )
if !shiftedOK { if !shiftedOK {
@@ -1028,6 +1033,30 @@ func (model occultationStationEnvelopeModel) residual(
return [2]float64{state.contactMetric, derivative}, state.valid && finite(derivative) return [2]float64{state.contactMetric, derivative}, state.valid && finite(derivative)
} }
// residualWithRate 与 residual 取值完全相同,但把时间导数换成解析速率:每列只需一次站心几何求值,
// 不再为前后时刻各求一次状态。接受判据仍走 residual 的精确中心差分。
func (model occultationStationEnvelopeModel) residualWithRate(
evaluation occultationRiseSetEvaluation,
longitude, latitude float64,
) ([2]float64, bool) {
state := evaluation.center.stateAt(longitude, latitude)
if !state.valid {
return [2]float64{}, false
}
if model.kind == occultationStationVisibilityEnvelope {
rate := evaluation.moonAltitudeRateAt(longitude, latitude)
if !finite(rate) {
return [2]float64{}, false
}
return [2]float64{state.moonAltitude, rate}, true
}
rate := evaluation.contactRateAt(longitude, latitude)
if !finite(rate) {
return [2]float64{}, false
}
return [2]float64{state.contactMetric, rate}, true
}
func (model occultationStationEnvelopeModel) valueTolerance() float64 { func (model occultationStationEnvelopeModel) valueTolerance() float64 {
if model.kind == occultationStationVisibilityEnvelope { if model.kind == occultationStationVisibilityEnvelope {
return occultationStationHorizonResidualToleranceDeg return occultationStationHorizonResidualToleranceDeg
@@ -1530,7 +1559,7 @@ func occultationStationOracleSampleAt(
) occultationStationBoundarySample { ) occultationStationBoundarySample {
rotation := occultationPathEarthRotationAt(tt) rotation := occultationPathEarthRotationAt(tt)
inertial := occultationStationInverseEarthRotation(fixed, rotation) inertial := occultationStationInverseEarthRotation(fixed, rotation)
longitude, latitude := occultationPathGeodeticWithSidereal(inertial, ApparentSiderealTime(TD2UT(tt, false))*15) longitude, latitude := occultationPathGeodeticWithSidereal(inertial, ApparentSiderealTime(TT2UT1(tt))*15)
return occultationStationBoundarySample{ return occultationStationBoundarySample{
point: OccultationPathPoint{ point: OccultationPathPoint{
Time: occultationTTToLocation(tt, location), Time: occultationTTToLocation(tt, location),

Some files were not shown because too many files have changed in this diff Show More